Skip to content

Latest commit

 

History

History
186 lines (166 loc) · 40.2 KB

js-modules.md

File metadata and controls

186 lines (166 loc) · 40.2 KB

JS modules

NSH ships two layers of modules. nsh holds the primitives every UI is made of, the environment and the capabilities, with nsh/mobile and nsh/desktop for what only mobile platforms or only the desktops have. nsh/base (the crate nsh-base) is the unstyled foundation every app's UI builds on: navigation stacks and pages (and their status bar), dialogs, safe areas, the system's alert dialog and the software keyboard. NSH provides no styled components: the look of an app is the app's, drawn from the primitives, or from Rust components in the app's own crate, whose UI set is a module of its own (app/native; docs/dev/components.md, tooling.md). Each export has one home. A UI set may serve a family of exports whose names only make sense together as a module under a path of its own specifier, as gpui-base's plot is a module of its crate: a submodule of the set (Registry::submodule, such as nsh/base/<family> of nsh/base), served, described and declared wherever the set is (Registry::sets: the hosts' module tables, the core's components, the toolchain's descriptions, one declaration file per module), so a list of UI sets names only the set.

Every module is native: nsh-js serves it (modules.rs: NSH's own modules by nsh_core::js::NATIVE_MODULES, then the registries: the runtime's nsh_core::runtime::registries() and the UI sets a host serves, nsh/base and the app's), and the bundler keeps imports of them as imports (plugin.rs; how apps are compiled: native-pipeline.md). A module can be both: the hand-written exports of nsh and nsh/mobile are joined by their registry's (nsh-js keeps the hand-written tables as internal sources the two serve, NSH_SOURCES and MOBILE_SOURCES, NativeModule::moved_to, declared in the module's own file). There is no JS file of NSH's and no internal module apps could reach.

Declarations: each export of a module of NSH's own carries its TypeScript declaration in its table (modules::Item: signal's signatures next to reactive::signal), each module its own interfaces (types); a registry's components and modules are declared from their specs (nsh_core::element::declarations, docs/dev/components.md), the styles, the host elements' props and events, the easings and the canvas methods from the core's tables. nsh-js's declarations.rs assembles one file per module (nsh/index.d.ts, nsh/base.d.ts, nsh/mobile.d.ts), nsh/types.d.ts (the element and JSX types nsh exports), nsh/jsx-runtime.d.ts and nsh/globals.d.ts, and tests check every module's exports against its declarations; the CLI writes them into .nsh/types (tooling.md). nsh has the reactive API, mount, the primitives every UI is made of (View, Text, Scroll, Input, Image, List, Canvas and its classes, WebView, Video), the environment, animation, gestures, the clipboard, the picker, storage, the host channel, keyboard shortcuts, what the embedding app registers, and the types. TSX imports mirror the Rust crates' layers: the primitives from nsh, as Rust takes them from nsh_core::prelude, the base components from nsh/base (nsh-base). The primitives' code is kept by area in internal sources nsh serves (NativeModule::also / moved_to: List in list.rs, Canvas and its classes in canvas.rs, Image in modules.rs, WebView and Video in platform_modules.rs); they are no modules apps import, and their declarations and types go into nsh's own file. Apps import a primitive's tag from nsh (native-pipeline.md): the table below. If and Each are globals.

Module Exports Implemented in (crates/nsh-js/src)
nsh signal, computed, effect, batch, untracked, cleanup, context, split, mount, ErrorBoundary (dom.rs: a memo of the children or the fallback, native-pipeline.md), resource (resource.rs: a callable object over four signals, its loads kept by nsh_core::resource::Runs); View, Text, Scroll (host elements as components: dom::host_component); View's selectable (texts that select together); padding and margin take env() (safe area, keyboard: styles.md "Environment variables"); Keyboard.dismiss() (a Rust module of nsh-core, element/keyboard.rs), safeArea(), safeAreaInsets(), keyboardFrame() (the environment's accessors, env.rs, re-exported by the same registry); List (list.rs), Canvas, Path2D, CanvasRenderingContext2D, CanvasGradient (canvas.rs), Image: the image element's component, with Image.getSize, Image.prefetch (promises the core resolves from the root's ImageInfo event, rejecting a URL network does not grant with ERR_ACCESS_DENIED in the call; the nsh registry's functions, components.rs), WebView, Video (platform_modules.rs); the refs (ViewRef, InputRef, ScrollRef, ListRef, WebViewRef, VideoRef, NativeComponentRef: declarations::REFS), Accessor, Signal, Context, Component, the types of styles, element props, events and JSX; Animation: createAnimatedValue, AnimatedValue, AnimatedNode, Animated, Easing, LayoutAnimation (animated.rs); The environment: windowSize(), colorScheme(), fontScale(), pixelRatio(), locale() (accessors), setColorScheme, platform (a constant: os, version, idiom, desktop, catalyst, select; desktop, the desktop idiom and select's desktop are Environment::desktop(), elements.md "Platform modules"); openUrl(url) (Rust, nsh_core::runtime's device.rs: a promise of { opened }, whether the URL opened outside the app, nsh_core::open_url; it rejects with a TypeError for a URL it never opens and with ERR_ACCESS_DENIED for a scheme openUrl does not grant; sandbox.md, embedding.md "The host channel") (env.rs + its registry); Accessibility: screenReaderEnabled(), reduceMotion() (accessors), announce(message), setAccessibilityFocus(node) (env.rs + its registry (nsh_core::runtime)); Gestures: PanResponder (touch.rs); The clipboard: Clipboard.writeText(text), Clipboard.readText() / hasText(), writeImage(src), readImage() / hasImage(), readFiles() (promises the core resolves from the root's Clipboard event; the has* reads do not read the content, so iOS shows no paste notice). readImage's url is, by contract, a URL that Image shows and fetch reads, nothing more (its scheme may change, to a native-backed URL); today images cross as base64 data: URIs (an image source and a fetch URL, so bytes stay native until JS asks for them), files as file: URLs one per line; the backends build both with nsh_core::platform::clipboard. An image over MAX_IMAGE_BYTES (48 MiB encoded) is left out of the answer, which carries its size (tooLarge; the core also measures a data: URI a backend encoded itself, the web's): readImage rejects with ERR_NSH_QUOTA_EXCEEDED (ClipboardRead::CheckedImage), the Rust API's clipboard_image answers None. UIPasteboard, ClipboardManager (text only), NSPasteboard, GDK, Win32, navigator.clipboard; on Android (System::os, checked in the call) writeImage and readImage reject with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM and hasImage resolves false without asking the platform; the clipboard capability, checked in each call first: without it every function rejects with ERR_ACCESS_DENIED (its registry (nsh_core::runtime)); The photo picker: Picker.pickImages({ multiple, limit, maxDimension, quality, format }): the system's picker for photos, resolving with PickedImages ({ file: File, size }; [] when cancelled). A Rust #[module] over nsh_core::picker: the core sends the root Command::PickImages with a checked PickRequest (or asks the embedder's PickerBackend, Core::set_picker / EngineConfig::picker), the platform answers with the root's Pick event, whose EventPayload::Picked carries the files' bytes, and the result becomes Files natively (JsHost::new_file: a Blob over the same ImageData, no copy and no base64). The picker capability, checked in the call (a pick it does not grant rejects with ERR_ACCESS_DENIED; options out of range reject with a RangeError) (its registry (nsh_core::runtime)); Storage: Storage.getItem / setItem / removeItem / keys / clear (async functions of the core; the entries live in memory in nsh_core::storage, loaded once from the platform's StorageBackend (EngineConfig::storage: nsh_engine::FileStorage::open in <EngineConfig::data_dir>/<app id> (the id travels with the app: app.json, the dev server, packages, Root::app_id), one per file in the process so a reload reads unflushed writes; a damaged file is set aside as storage.json.corrupt, an unreadable one leaves the changes in memory, never replacing it; localStorage under nsh:<app id>: on the web) and every change handed to it; 5 MB quota, setItem past it rejecting with ERR_NSH_QUOTA_EXCEEDED; no capability: the app's own data, kept where the host says) (its registry (nsh_core::runtime)); The host channel: host.call(name, args, { timeout }) (a task of the core (runtime/host.rs), racing the answer against the app's clock: 30 s unless timeout says, 0 none; a call that timed out drops its slot at once (Action::ForgetHostCalls), so Core::answer_host drops a late reply with a debug line; the root's Command::HostCall { id, name, args }, answered by the root's Host event, EventPayload::Json { type: "reply", id, ok, value?, error? } (an id that is not a whole number answers no call); it never throws, and rejects with nsh_core::host_channel's errors: at once with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when the environment has no host, with a TypeError when the name is not 1 to 128 of [A-Za-z0-9_.:-] without .. (access::valid_host_name, checked before the grants), with ERR_ACCESS_DENIED (permission "host", resource the name) when the name is not granted, with ERR_NSH_HOST_ERROR and the host's message when the host fails, and with ETIMEDOUT past the timeout; a timeout too large for the clock waits without limit, a negative or non-finite one rejects with a RangeError), host.available() (the platform environment or configured native SDK channel), host.on(event, listener) → () => void (JS listeners of the root's Host events of { type: "event", event, payload }, the core keeping the replies; a listener that throws leaves the next ones called, the first error thrown after them; events need no capability; a listener added while a component renders, or at a module's top level under hot module replacement, stops with it) (platform_modules.rs, host_channel.rs + its registry (nsh_core::runtime); SDK channel adds params and post); Keyboard shortcuts: keymap.bind(keystroke, handler, { when, repeat }) / keymap.bind({ keystroke: handler }): keyboard shortcuts. Keystrokes are parsed and checked in Rust (nsh_core::keymap: syntax, mod, reserved keystrokes, which ones text inputs keep); the whole table goes to the platform as the root's Prop::Keymap → MountItem::Keymap, which matches keys on the spot and reports the root's KeyCommand event (the binding's id). Bindings made under an owner go with it; a module's top-level ones are undone when hot module replacement runs it again (hot::undo_on_rerun) (keymap.rs); What the embedding app registers: nativeComponent(name, { props, events }): a component for a native component the embedding app registered; nativeModule(specifier): the exports of a UI set the embedding app gave its engines (EngineConfig::ui_sets), as one object made by the loader's code (modules::host_module, the same values an import of the set gets) and kept per specifier; only the host's sets, never nsh's own modules or the runtime's registries (platform_modules.rs, modules.rs) reactive.rs, dom.rs, modules.rs, env.rs, animated.rs, touch.rs, keymap.rs, platform_modules.rs, host_channel.rs + its registry (nsh_core::runtime)
nsh:rt what compiled JSX calls (element, insert, spread, cond, each, list, font, ...); apps never import it dom.rs, list.rs
(globals) If, Each (control flow; their tags compile to cond / each) dom.rs
nsh/mobile What iOS and Android have: isAvailable (true on iOS and Android), InputAccessoryView (iOS; nothing on Android), BackHandler (on("press", …) native: it returns the function that removes the listener, which goes with the owner it was added under, as keymap.bind's bindings do; exitApp Rust; Android; nothing on iOS), TabBar (a Rust component of the runtime, nsh_core::mobile; its root is the native component nsh/tab-bar (nsh_core::platform::TAB_BAR_COMPONENT), which the iOS (UITabBar, system_tab_bar.rs) and Android (Material 3's navigation bar, TabBarComponent.java) backends register themselves; items ({ title, icon }), selectedIndex, onChange({ index }); a hidden view elsewhere). The engine gives the core the runtime registries' components with the UI sets' (Registry::merged) platform_modules.rs + its registry
nsh/desktop isAvailable, titleBar() (the environment's title_bar), Menu.setApplicationMenu / Menu.popup (decoded natively into nsh_core::desktop::MenuItems, the items' functions kept by id, called on the root's onMenu), Window (Rust: setTitle, setTitleBar, minimize, toggleMaximize, toggleFullScreen, setFrame, setLevel, orderFront, close); all of it is Command::Desktop on the root, which mobile platforms ignore. Rust components have the same in nsh_core::element::desktop (menus' handlers kept by the core, ids from 2^31: element/menus.rs) desktop.rs + its registry
nsh/base The unstyled foundation of an app's UI. Navigation: NavStack, NavPage, createNavStack, Dialog (natives of nsh-js, Registry::native_values). Text input: Input (a host element made by nsh-js, env::ELEMENTS, Registry::native_values; its props' types InputProps and IOSInputProps declared beside it). Alerts: AlertDialog.show({ title, description, actions, presentation }) (a promise of { action }: the chosen action's id, required on every action, or null when closed without one; presentation: "actionSheet" is a list dialog on Android), the platform's own. A person's picture: Avatar (Rust; the fallback, the initials of name or the app's fallback slot, under the picture, which is transparent until the Image's own load event, so its load and error events drive AvatarStatus in the core with no JS and change the two parts only; fallbackDelay a timer of the component; an image role labelled with the name once, its fallback and picture accessibilityElementsHidden, decorative with accessibilityLabel=""; its src an ImageSrc, whose JsValue decodes and declares it with <Image>'s HostValue for ImageSource, so a Rust component's image prop takes what <Image>'s src does) and AvatarGroup (Rust; children_each places the first limit children, the rest stay out of the tree; the overflow slot while total is more, read as its content or overflowAccessibilityLabel; the group role). A press target: Button (Rust; no look of its own). A link: Link (Rust; the link role, a Tab stop that Return and Space press; href is data: a press reports it with onOpen, then openWith="system" (the default) opens a URL of another app or site through nsh_core::element::open_url (the embedding app first, the openUrl capability) while "app" leaves it to the handler; current is aria-current="page"; inside a <Text> an inline box, as every view there). A checked / unchecked / indeterminate control: Checkbox (Rust; its indicator is a part the app styles and fills with its mark, the indicator slot, checked: / indeterminate: / disabled: following the checkbox through Interactive::project_checked / project_indeterminate). A set of options the user picks one of: Radio in a RadioGroup (Rust; the group holds no value, each radio its checked, the app's indicator its children with group-checked: classes). An on / off control: Switch (Rust; its track and thumb are parts the app styles, checked: / disabled: following the switch through Interactive::project_checked). The tabs of a tab list: Tab in Tabs (Rust; the list holds no value, each tab its selected and onPress, the app shows the selected section's content; its indicator a part the app styles with indicatorClass / indicatorStyle, placed on the selected tab by the core, View::follow, and springing to the next natively). A button that is on or off: Toggle, alone or in a ToggleGroup (Rust; a button with accessibilityState.pressed, pressedStyle / aria-pressed:, the app's content its children with group-aria-pressed: classes; the group a toolbar that holds no value, one stop of the keyboard focus whose arrows only move the focus, and disabled for all its toggles; one pressed or several is the app's; inside a Toolbar a group of it). A bar of controls: Toolbar with ToolbarGroup and ToolbarSeparator (Rust; one stop of the keyboard focus over the Buttons, Toggles and Links inside it, at the item last left, its axis's arrows, Home and End moving the focus without pressing, disabled for all its items; a group with its accessibilityLabel, a separator outside the focus order; the look the app's). Menus: Menu, ContextMenu, MenuItem, MenuSeparator, Submenu (Rust, menu.rs; a popup on the desktops, a sheet on touch screens; a MenuItem with checked is checkable, menuitemcheckbox or with its role menuitemradio, the value the app's, onSelect the press, checked: / checkedStyle on the item and its indicator part (indicatorClass / indicatorStyle around the indicator slot), closeOnSelect whether a choice closes the menu). One value from a list of items: Select (Rust; items as data, the value the app's, value and onChange, or its own from defaultValue; a popup at the trigger on the desktops and a sheet on touch screens; the keyboard, typeahead, search and sections in listbox.rs; the highlighted row focus: through Interactive::project_focused, the chosen one selected:). One value or several from the same list: Combobox (Rust; value an array, multiple toggling and staying open, onConfirm on close, the app's query with onQueryChange, a footer slot). Dates and times: Calendar (Rust; an ISO date or [start, end] with range, onChange for a complete value only, numberOfMonths, firstDayOfWeek, disabledMatcher (DateMatcher: days of the week, { before, after }, { from, to }, or a function asked once per shown day in the next JS turn), yearRange, month and year views, the days one Tab stop, renderItem with CalendarItemState), TimeField (Rust; "HH:MM" / "HH:MM:SS", precision, hourCycle, segments over one native text field, renderSegment with TimeFieldSegmentState), DatePicker (Rust; Select's popup around a calendar, presets (DateRangePreset) and with timePrecision a time field, its value DatePickerValue in the HTML inputs' forms (date, datetime-local), every Calendar prop passed on, the trigger, timeLabel and clearButton slots). A region that expands and collapses: Collapsible (Rust; its children always, its content slot after them while open, kept while collapsed; with reveal a clip around it springs between zero and its height, a layout animation; the trigger and its expanded state are the app's). Stacked sections that expand and collapse: Accordion with AccordionItem, AccordionHeader, AccordionTrigger and AccordionPanel (Rust; the open items' values are the app's value, reported with onChange by a trigger's press, one at a time or several with multiple; the item hands its trigger and panel whether it is open through context; the root a group, the header a heading with level, the trigger a button with its expanded state and aria-expanded: styles, the panel a region hidden while closed, revealed as Collapsible's with reveal, kept with keepMounted). How far a task is: Progress (Rust; the progress bar role with the range 0 to 100 and its value, Accessible::accessibility_value, none while indeterminate; its track and indicator are parts the app styles, the indicator value percent of the track's width). A value or a range picked along a track: Slider (Rust; value a number or [start, end], controlled or from defaultValue, snapped to step from min, on a linear or logarithmic scale, along a horizontal or vertical axis; onChange per new value, onChangeEnd once per change; each thumb an adjustable element with its value, Tab stop, WAI-ARIA's slider keys and the screen readers' increment / decrement; its track, indicator, range and thumbs are parts the app styles). A number typed or stepped: NumberInput (Rust; value a number or null, controlled or from defaultValue; its field, nsh/base's Input, takes a number or the text on the way to one, other keyboards' digits read as ASCII, and reports the number it reads as with onChange; steps of step from its buttons, Up / Down and Page Up / Page Down while it edits, and the screen readers' increment / decrement keep the decimals and stop at min / max, to which leaving the field or Enter holds a typed value; the field a spin button with its value; the field, the buttons and their column are parts the app styles, the buttons' content its slots). One-time codes: OtpInput (Rust; one native field, transparent over the cells, owns the keyboard, paste, the number pad and the system's code autofill, and is the one text field screen readers get, Accessible::accessibility_elements_hidden on the cells; digits only up to length; the cell that takes the next digit focus: through Interactive::project_focused, with the app's blinking caret). Pages: Pagination (Rust; a navigation landmark of Previous, the pages pagination_items shows around page, ellipses, the app's children and Next, the current page selected and current: page; visiblePages + 2 places made once, each a page and an ellipsis view of which it shows one, so a page change makes no view). Documents: TextView (Rust; Markdown, HTML or blocks, docs/dev/text-view.md). Code: Editor (Rust; a code editor, its text controlled, docs/dev/editor.md), DiffView (Rust; the changes between two texts or of a patch, docs/dev/diff.md). The status bar: statusBar of NavPage and Dialog (a host prop of screens and modals; StatusBar, StatusBarForeground declared with the navigation) over nsh.json's "statusBar" nsh_base::registry() (crates/nsh-base); navigation.rs
nsh/base/plot The parts charts are built from, as gpui-base's plot, with no look of their own: Plot (Rust; the rows of data data: T[], generic over T so x / y are checked keys of it or functions of a row (RowAccessor), the point / linear / band scales of its axes, its size as their range, onHoverChange({ index }) for the row nearest the pointer, the hover's parts crossLineClass / dotClass / tooltipContent (placed by tooltipPlacement / tooltipOffset, kept inside the plot on the animator) and motion (appear, enter, exit, pointer, and update: radii and linear domains, the marks drawn through an animated transform, plot/tween.rs) on the platform's animator, interactive, the keyboard moving the same hover while it has the focus (plot/keys.rs), one element for screen readers with accessibilityValue and increment / decrement) and its marks Line, Area, Bar, Grid (Rust; a canvas each, drawn natively from the rows, painting with the paint properties fill / stroke / strokeWidth / strokeDasharray they inherit, currentColor where none is set: Painter::*_from_view, canvas.md; Line's dot part; a mark's own data / x and hoverDot part; Area's and Bar's background part, a CSS gradient inside its shapes: Painter::fill_background_from_view; Bar's stacks and side-by-side groups, its growth with the appear and its fading away from the hover's row as animated canvas numbers, its label part read with RowText), the radial marks Pie (a canvas per slice, the slice part, its classes sliceClass a string or a function of the slice's row; the plot's hover finds the slice under the pointer through the marks' hit tests, the slice lifting and the others fading on the animator; its label part read with RowText, a text outside the ring per slice, and the labelLine part, a leader line canvas per slice, both following their slice's motion and lift as animated numbers) Arc (one sector between two angles) and RadialLine (a series round the center, a radar: its radius a value of the linear y scale placed between innerRadius and outerRadius, its angle or the rows evenly round the turn; closed, its area and background part, its dot and hoverDot parts; the plot's hover finds the row nearest the pointer by angle through the same hit tests; its points numbers the animator moves as it appears and updates), with RadialGrid (rings at values of the y scale and spokes at the rows' angles), the Sankey (flows between the plot's rows: d3-sankey's layout ported from gpui-base, plot/sankey/layout.rs, run again only for new links, options or size; links read natively as Rows; a canvas per node and per ribbon, the node, link and linkBackground parts, their classes RowClass / SankeyLinkClass; the hovered node through the plot's hit tests, the other links fading on the animator; the nodeLabel part, views placed beside the nodes; motion.update as animated canvas numbers between two layouts), and PlotAxis (Rust; views along an edge, the ticks of the scale it carries: d3's nice values or rows, ticks / tickValues, their texts d3's default or the app's format(value, index), TickFormat, called in the next JS turn; each label centered on its tick and kept within the plot's ends by the layout's automatic margins, past the edge or inside it; its label, tick and line parts), and PlotLabel (Rust; texts at values of the scales, texts: PlotText[] read natively as JsObjects, placed by the layout and moved with the domains on the animator), and PlotAppearScope (Rust; a context its plots remember the appears that finished in, by their appearGeneration (number | string, AppearKey), so a list's rows made again show whole at once; a new id forgets, plot/appear.rs) nsh_base::plot::registry(), a submodule of nsh/base (crates/nsh-base/src/plot)
an app's UI set (app/native) The Rust components, modules and classes of the app's crate (#[native_module("app/native")]) the registry (components.rs, classes.rs)

nsh:hot (hot module replacement, hot.rs) is only imported by development bundles. The same loader also compiles the chunks of a split bundle (Screen.js: a name that is no native module, with code the host gave; chunks.rs, native-pipeline.md), which import() loads through the global __nsh_import.

  • Platform modules (nsh/mobile, nsh/desktop) are importable on every platform (one bundle runs everywhere); where an API does not apply its calls do nothing and its components render nothing. Their isAvailable and nsh's platform.os tell which. nsh/desktop's isAvailable (Os::is_desktop: macOS, Windows, Linux) says its window and menu APIs work; it is narrower than desktop behavior, platform.desktop (Environment::desktop(), which browsers in the desktop idiom have too: "One definition of desktop" in api-naming.md).
  • Platform-only props are grouped: ios={{…}} / android={{…}} / web={{…}} / desktop={{…}} (macOS, Windows, Linux, and browsers in the desktop idiom) on elements and components (IOSProps / AndroidProps / DesktopProps in types.d.ts, each extending its generated IOSHostProps / AndroidHostProps / DesktopHostProps, and extended per element with its own IOS…HostProps / Desktop…HostProps; a test holds every …HostProps to an element's props). The renderer (platform_props in dom.rs) sends only the current platform's object, each key as a prop of its own (keys removed from the object are cleared); the core knows them by those names. Android: ripple, elevation, accessibilityLiveRegion, Dialog statusBarTranslucent. iOS: backdropMaterial (the system material of a backdrop blur), input inputAccessoryViewID / textContentType / keyboardAppearance, Dialog supportedOrientations, WebView allowsBackForwardNavigationGestures. Desktop: windowDrag, WebView allowsBackForwardNavigationGestures (macOS' and Linux's trackpad swipes, Windows' touchscreen swipes; a system feature of several platforms names each: #[prop(ios = "x", desktop = "x")]). What an element means on every platform is a top-level prop, ignored where it does not apply (presentation, headerLargeTitle, headerBackTitle, AlertDialog's closeOnEscape / closeOnBackdropPress; api-naming.md).
  • A module's exports are made when a bundle first imports it; what must always happen (the root's direction, the themes) is the renderer's (dom.rs, setup), not a module's.

Component scope and project goals

NSH targets native views and interaction with app-owned visual design. Common controls have one cross-platform home in nsh/base: Switch owns behavior, state, focus and accessibility while the app styles its track, thumb and states. A system-styled version of an existing control does not justify a second platform-specific public component. Improve the shared component's behavior and customization surface instead.

Platform modules expose capabilities with platform-specific semantics. nsh/mobile's TabBar is a mobile navigation capability; nsh/base's Tabs switches tabbed content across platforms. Its presence does not establish a parallel catalog of system-styled form controls. Existing system-managed presentations (navigation headers and alerts) keep their documented behavior. An embedding app can register a particular native control when its product requires one.

For additions, classify the capability by role, keep shared state and event contracts coherent across targets, and verify input, accessibility, measurement and app styling on affected platforms. The public Goals and design principles describes this boundary for app developers. The discussion in PR #438 records the rationale.

Where new things go

Names follow gpui-base's, translated to TSX: before naming anything apps use, read api-naming.md (the conventions and the renames decided so far).

  • A primitive, the environment or a capability: nsh (or nsh/mobile / nsh/desktop when only mobile platforms or only the desktops have it), natives in crates/nsh-js listed in modules.rs and nsh_core::js::NATIVE_MODULES, each export with its declaration (Ts) and the module's interfaces in its types. Functions that only ask the platform (a command, a request answered by an event) are a Rust #[module] in crates/nsh-core/src/runtime/, registered in runtime::registries() under the module's specifier: their JS face and declarations come from the spec, with nothing written per function.
  • An unstyled base component (nsh-base is NSH's gpui-base, architecture.md "Layers"): behavior, state, focus, keyboard or accessibility a UI needs, or system UI (insets, the keyboard's avoidance, the status bar's setting, a dialog; controls such as a toggle, a slider or a popover as their behavior is written): a Rust component or #[module] in crates/nsh-base, registered by one of nsh_base::parts (core, or a part of its own for a large component), which nsh_base::registry() puts together (docs/dev/components.md). It never styles (no colors, paddings, radii, fonts or variants of its own), takes the app's styles, exposes its parts and reports changes of state the app owns.
  • A styled widget (a button with a look, a card, a list row): never in NSH, which has no gpui-component. Apps draw their own on nsh/base and the primitives, or ship them, as a third party does, as Rust components in a crate of their own (docs/dev/components.md).
  • Something only mobile platforms have (iOS, Android or both): nsh/mobile; only the desktops: nsh/desktop; or a prop in ios={{…}} / android={{…}} / desktop={{…}}.
  • Never a React Native compatibility wrapper. Something released that moves keeps its old name and module as a deprecated alias (compatibility.md); something unreleased simply moves.

Standard modules

The standard catalog is eleven bare specifiers, nsh_core::js::STANDARD_MODULES, shared by the bundler (which keeps their imports) and the loader: buffer, console, crypto, fs/promises, net, os, path, process, url, websocket and zlib. Only these names import: node:buffer and the Node modules outside the catalog (fs, events, assert, util, string_decoder, child_process, ...) are refused (tests: nsh-js tests/standard/imports.rs, which also checks that each module's runtime exports are its declared ones). The loader asks the module table first (typed registries own their names), then standard::load. zlib and crypto (with its globals, nsh_core::js::CRYPTO_GLOBALS) are features of nsh-js of those names, on by default and with nsh-engine's js (which every native platform enables); a runtime built without them (nsh-engine's js-core, the web runtime's std-zlib / std-crypto off) refuses to load them, and their code is not in its binary.

Module Where Exports
os nsh_core::runtime::os (#[module(flat, default)]) platform(), arch(), homedir(), EOL, default
fs/promises nsh_core::runtime::fs readFile, writeFile, readdir, exists, unlink, rmdir, mkdir, stat, lstat, rename, readlink, default; Stats and Dirent are #[class(interface)] (declared, no value)
process nsh_core::runtime::process, nsh-js standard/process.rs run, spawn, exit, platform, arch, nextTick, ChildProcess, default (also env / dataDir getters); the default object is the global process
net nsh_core::runtime::net connect, Socket, default
console nsh-js standard/console.rs default: the VM's console (log, info, debug, warn, error)
websocket nsh_core::runtime::websocket, nsh-js standard/websocket.rs (the global) connect, WebSocketConnection, default; the global WebSocket constructor
buffer, crypto, path, url, zlib LLRT, vendored by quickjs-jit-stdlib (standard/mod.rs) their LLRT exports, projected by standard/declarations.rs; Buffer and crypto are globals

process.platform, process.arch, os.platform() and os.arch() are Rust's std::env::consts values, as gpui-shell's are: "macos", "windows", "linux", "ios", "android"; "aarch64", "x86_64" (not Node's darwin / win32 / arm64 / x64); in WebAssembly they are empty strings, and platform.os (nsh's environment) is "web".

The typed modules' JS face and .nsh/types come from their specs (#[module], #[class]; docs/dev/rust-classes.md): grants and limits are checked in the call, and a function returning a promise rejects with every failure (its Result<Later<_>, JsError>'s Err rejects too: docs/dev/api-naming.md "Failures"), while a synchronous one (spawn, exit, ChildProcess.write, os.homedir) throws. A capability not granted is JsError::access_denied (ERR_ACCESS_DENIED, with permission fs / process / network and resource the path, command, "host:port" or URL), a platform without the feature codes::UNAVAILABLE, a used-up quota (operations or bytes in flight, children, sockets, WebSockets, pending writes, listeners) codes::QUOTA_EXCEEDED, a deadline codes::TIMED_OUT, a wrong use a TypeError / RangeError; I/O failures keep Node's codes. run resolves { code, signal, stdout, stderr } (code null and signal its name when a signal ended the child). The global WebSocket throws only for a wrong use (SyntaxError) or the shared quota: a URL not granted opens nothing and fires error and close (1006) a turn later (standard/websocket.rs refused), as a browser's refused connection does. The work runs as core tasks on the engine's tokio runtime (fs/promises on its blocking threads). A call's System carries the VM's Access, data directory, TrustedPaths, Quotas and per-VM state (Locals: process's children, killed at once and joined together when the VM goes), and the host's refuse_connections (EngineConfig::refuse_connections: a test's network), which net and both WebSocket APIs consult after the capability check (System::check_connection), failing with the host's error. process.spawn returns its ChildProcess in the call; a core task posts the child's output to the Callbacks on(..) keeps; nsh-js adds what needs the VM: the global process (the module's default object, one per VM) and nextTick, a JS job. websocket's URL, options, grant (Access::allows_websocket) and limits are checked in the call, the handshake uses the configured WebSocketTransport (browser WebSocket on web, the native transport elsewhere). On native hosts a task spawned per connection drives the socket (answering pings, keeping what arrives until it is read). Unread messages are back pressure: while 8 messages or 8 MiB wait, the native task does not poll the socket (TCP's flow control holds the peer back, and pings wait for reading to resume) and a host transport is paused (WebSocketTask::pause; the browser's page holds its socket's events, web/nsh.js), so a connection keeps at most one message of 8 MiB past the threshold. A clean close (the peer's close frame, or the app's close(code, reason) and the peer's reply) makes read() resolve null after the messages before it, with closeCode / closeReason the frame's (1005 without a code, 1006 for a connection that ended without one); only a failure rejects, with a code. Native TLS uses the host's installed crypto provider, or a connection-local ring provider, with WebPKI trust roots; opening a connection leaves the process-wide provider unchanged. Connections count under the VM's Quotas kind runtime::WEBSOCKETS, which the global WebSocket (nsh-js standard/websocket.rs, made on first use) shares. os.homedir() and process.dataDir are metadata and grant no file access. On WebAssembly, os.homedir() throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM and process.env is an empty object; both avoid std::env, whose accessors panic on wasm32-unknown-unknown.

QuickJS and the LLRT modules come through quickjs-jit and quickjs-jit-stdlib on the native targets. On the web, app code runs on the browser's JavaScript engine, and the browser host supplies the same modules (docs/dev/web.md "Standard modules in the browser"): buffer and path in JS, synchronous hashing and compression in Rust in the module, entropy from the browser's crypto.getRandomValues. All eleven catalog modules load on both, with Buffer and crypto globals. On the web, path uses POSIX separators and a virtual / working directory. The crates are published together at one version and pinned exactly (=0.14.1), with no git dependency or patch. standard/declarations.rs projects their actual exports from checksummed upstream assets in crates/nsh-js/third_party/llrt-types. zlib bounds each decompression (64 MiB, or the heap limit when smaller). NSH's WHATWG constructors (URL, Blob, streams, events) keep their identity. console declares the global console's type next to its implementation. The declared interfaces define the API: Node-shaped subsets compatible with gpui-shell's, not Node.js itself; where they differ (a symlink exists, listeners get the payload only, Stats' methods are on its prototype, the child environment and cwd rules), website/modules/standard-modules.md says so.

On web, System receives a BrowserFileSystem and BrowserWebSocket from the player. fs/promises checks capabilities and quotas in Rust, then sends app-relative operations to the browser's persistent OPFS namespace. Browser WebSocket I/O crosses the transport bridge while Rust retains grants, size/count bounds and deadlines; custom handshake headers are rejected explicitly. process.run / spawn and raw TCP remain callable exports that fail with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM in a browser. process.env stays empty there.