---
url: https://nsh.dev/ru/docs/compatibility
description: "Как Native Shell обеспечивает работу приложений при переходе между релизами: что гарантирует совместимый релиз, запись в nsh-compat, в которой хранятся все изменения этой гарантии, процесс выпуска релизов, а также как добавить, переименовать или изменить API."
---

# Обратная совместимость

Хост-приложения загружают приложения NSH как [подписанные пакеты](https://nsh.dev/translations/ru/docs/signed-packages.md) и запускают их как плагины, а выпущенное приложение, возможно, никогда не будет собрано заново: его команда ушла на другие задачи, исходный код лежит где-то в бизнес-процессе, а хост-приложение обновляет NSH по собственному графику. Поэтому Native Shell даёт каждому приложению обещание: **приложение, собранное для совместимого релиза, работает на каждом следующем релизе без изменений.** Если приложение перестало работать после обновления NSH, это ошибка в NSH.

На этой странице описано, в чём состоит обещание, как его соблюдение проверяется и как изменять NSH, не нарушая его.

<a id="what-an-app-can-rely-on"></a>

## На что может полагаться приложение

Обещание охватывает всё, к чему обращается собранное приложение:

| Часть | Почему это важно |
| --- | --- |
| Экспорты каждого модуля (`nsh`, `nsh/*`, `nsh/base`, стандартные модули) и их члены | Бандл импортирует их по имени: если имя пропало, приложение не запускается. |
| `nsh:rt`, функции, которые вызывает скомпилированный JSX | Каждый бандл содержит результат работы компилятора, которым он был собран. |
| Глобальные объекты (таймеры, `fetch`, `console`, ...) и их члены | Приложения вызывают их по имени. |
| Props элементов, события и их данные, ключи стилей и их значения | Приложения передают их по имени; переименованный prop игнорируется, и приложение работает неправильно. |
| Утилиты и варианты Tailwind | Классы разрешаются во время выполнения по строкам из бандла. |
| Поведение: значения по умолчанию, единицы измерения, данные событий, порядок обратных вызовов | Приложение полагается на то, что наблюдалось при его написании. |
| Объявления TypeScript (`.nsh/types`) | Исходный код старого приложения по-прежнему проходит проверку типов, когда его команда исправляет ошибку. |
| Формат пакета и его подписи | Пакет, подписанный для более раннего релиза, по-прежнему проходит проверку. |

Правила, которые это обеспечивают:

1. **Всё, что поставлялось в релизе, остаётся.** Ни один модуль, экспорт, член, prop, событие, ключ данных события, ключ или значение стиля, утилита Tailwind, глобальный объект или функция `nsh:rt` не удаляется.
2. **При переименовании старое имя сохраняется** как псевдоним той же реализации, помеченный `@deprecated` с указанием нового имени, навсегда. Если экспорт переносится в другой модуль, он остаётся и в старом.
3. **Типы только расширяются.** Union получает новые варианты, функция получает перегрузки или завершающие необязательные параметры, параметр становится необязательным; то, что приложения передают (props, стили, опции), получает только необязательные члены. Значения, которые приложение читает, растут так же: `platform.os` получает платформу, а состояние или причина получает новый вариант. Вариант, который приложение уже видело, сохраняет своё значение, а неизвестное значение приложение обрабатывает через `default` в `switch`:

   ```tsx
   switch (platform.os) {
     case "ios":
     case "android":
       return <MobileLayout />;
     default:
       // The desktops, the web, and platforms NSH adds later.
       return <WideLayout />;
   }
   ```
4. **Поведение сохраняется.** Новое поведение включается явно: через новый prop, опцию или значение.
5. **`nsh:rt` только растёт**, а каждая функция сохраняет свою сигнатуру и смысл.
6. **Форматы читаются всегда.** Пакеты, сертификаты, списки отзыва и `app.json` более ранних релизов читаются более поздними; пакет указывает минимальную среду выполнения, которая ему нужна (`nsh publish --min-runtime`).
7. **Безопасность — единственное исключение.** Возможность, которую нельзя сделать безопасной, закрывается, вносится в список с указанием причины и упоминается в примечаниях к релизу.

Обещание действует с первого релиза, записанного как совместимый, и сохраняется навсегда, что бы ни говорил номер версии. Между релизами API, который есть только в `main`, ещё может измениться.

<a id="how-it-is-enforced"></a>

## Как это проверяется

То, что заморожено релизами, хранится в отдельном репозитории, [longbridge/nsh-compat](https://github.com/longbridge/nsh-compat), независимо от кода NSH. Изменение NSH проверяется по нему и не может его редактировать: pull request, который переименовывает API, не может заодно переписать запись об этом API.

```
nsh                                       nsh-compat
├─ crates/          the code              ├─ releases/v<version>/   one per compatible release, never changed
└─ tests/compat/    apps that test the    │  ├─ runtime.txt         every export, global and nsh:rt function
   ├─ cases/        API being written     │  ├─ types/              the declarations apps build against
   └─ examples/                           │  ├─ tailwind.txt        every Tailwind class and what it sets
                                          │  └─ apps/               the test apps as that release built them
                                          ├─ closed.txt             security closures, each with its reason
                                          └─ src/                   the checks
```

Каждый pull request в NSH запускает проверки `nsh-compat` на своём checkout (workflow **Compat** или `cargo xtask compat` локально):

| Проверка | Что считается успехом |
| --- | --- |
| `checkout_apps_run` | Приложения из `tests/compat/`, собранные этим checkout, делают то, что написано в их сценариях, и каждая часть API, которую сценарий заявляет как проверяемую, действительно проверяется. |
| `nothing_released_is_removed` | Всё, что предлагал каждый записанный релиз (экспорты, члены и их виды, объявления, классы Tailwind и то, что они задают), по-прежнему предлагается в том же виде или расширенным. |
| `released_apps_run` | Приложения, собранные каждым записанным релизом его компилятором, запускаются на этой среде выполнения и делают то, что написано в их сценариях. |

Первая проверка тестирует API, который пишется сейчас; две другие удерживают каждый записанный релиз. Переименование выпущенного API завершается так:

```
v0.2.0: runtime [nsh] Platform: removed
Keep it: a rename keeps the old name as a @deprecated alias of the new one, a type only widens.
```

`nsh-compat` проверяется мейнтейнерами NSH, а его собственный CI запускает его на `main` NSH.

<a id="the-release-flow"></a>

## Процесс релиза

1. **Разработка.** API попадает в `main` вместе с кейсом в `tests/compat/`, сценарий которого его проверяет. Пока релиз не записал его, API можно переименовать или изменить; кейс меняется вместе с ним.
2. **Запись.** При релизе `cargo xtask compat record <version>` записывает `releases/v<version>/` в checkout репозитория `nsh-compat`: поверхность API, которую предлагает релиз, и каждое приложение из `tests/compat/`, собранное его компилятором, вместе со сценариями. Записывается только тот API, каждую часть которого проверяет сценарий.
3. **Заморозка.** Запись вливается через pull request в `nsh-compat`. С этого момента каждое изменение NSH проверяется по ней вместе со всеми более ранними записями.

<a id="developing-within-the-promise"></a>

## Разработка в рамках обещания

<a id="adding-an-api"></a>

### Добавление API

Каждый новый экспорт, глобальный объект, prop, событие или ключ стиля сопровождается кейсом:

1. Напишите модуль `tests/compat/cases/<domain>/<case>.tsx`, экспорт по умолчанию которого — компонент, использующий API и показывающий результат. Домены: `base`, `core`, `events`, `layout`, `paint`, `props`, `modules` и `web`, а также `android`, `macos` и `browser` для того, что определяется одной платформой или одной настройкой `nsh.json`.
2. Добавьте его в `App.tsx` домена, который показывает по одному кейсу за раз.
3. Допишите шаги в `script.txt` домена: `tap id:case-<case>`, затем шаги, проверяющие, что делает API, и шаг `covers <item>` для каждой добавляемой части.
4. Запустите `cargo xtask compat` (`NSH_COMPAT_APP=<domain>` запускает один домен).

```txt
# props-media-video
tap id:case-props-media-video
prop id:clip src https://example.com/clip.mp4
prop id:clip paused true
tap id:play
prop id:clip paused false
covers [props] src
covers [props] paused
```

Шаг `covers` проверяется: приложение должно использовать элемент, а для prop, события или ключа стиля удаление его из собранного приложения должно приводить к падению одного из шагов сценария. Элементы называются `[<module>] <export>`, `[globals] <name>`, `[props] <name>`, `[events] <name>`, `[styles] <name>`, собственные элементы компонента — `[props] <Component>.<name>`, а props платформы — `ios.<name>`, `android.<name>`, `desktop.<name>`, `web.<name>`.

<a id="renaming-or-moving-an-api"></a>

### Переименование или перенос API

Добавьте новое имя и сохраните старое как псевдоним той же реализации, объявленный `@deprecated` с новым именем. Обновите кейс и его сценарий под новое имя. API, который ещё не записан ни одним релизом, просто переименовывается.

<a id="changing-a-type"></a>

### Изменение типа

Расширяйте его: больше вариантов в union, больше перегрузок, завершающие необязательные параметры, необязательные новые члены того, что приложения передают. Union, который приложения читают, получает члены так же, каждый новый член соответствует новому случаю. Имена параметров можно менять свободно; их число, необязательность и типы остаются.

<a id="changing-behavior"></a>

### Изменение поведения

Сохраните старое поведение и добавьте новое за новым prop, опцией или значением. Исправление ошибки, меняющее то, что видит работающее приложение, тоже является изменением поведения: поместите его за явное включение или убедитесь, что ни одно приложение не может полагаться на старый результат. Добавьте для этого поведения шаг в кейс, чтобы оно оставалось неизменным.

<a id="changing-the-compiler"></a>

### Изменение компилятора

Ничего дополнительного: приложения каждого релиза остаются такими, какими их собрал компилятор этого релиза, а кейсы каждый раз собираются заново вашим. Функции, которые он помещает в бандлы (`nsh:rt`), только растут.

<a id="closing-an-api-for-security"></a>

### Закрытие API из соображений безопасности

Pull request в `nsh-compat` добавляет запись в его `closed.txt` с указанием причины (`<entry> -- <reason>`, запись в том виде, в каком её показывает проверка) и вливается первым; после этого изменение NSH, удаляющее API, проходит проверку. В примечаниях к релизу объясняется, почему это сделано.

<a id="scripts"></a>

## Сценарии

`script.txt` — это один шаг на строку. Шаги-действия: `tap <selector>`, `type <selector> <text>`, `key <selector> <key>`, `scroll <selector> <x> <y>`, `touch start|move|end|cancel`, `event <selector> <onEvent>
[<key>=<value>...]``, ``wait <ms>``, ``keyboard <height> <duration>``, ``pixelratio <ratio>`. Шаги-проверки: `expect <text>`, `absent <text>`, `frame <selector> <x> <y> <width> <height>`, `origin`, `prop` / `noprop`, `style` / `nostyle`, `listens`, `state`, `statusbar`, `log <text>`. Первые строки могут задавать экран: `viewport <w> <h>`, `platform <os> [desktop]`, `fontscale <scale>`. Селектор — это `id:<testID>`, `label:<label>`, `text:<text>` или `span:<text>`; props и стили записываются так, как их пишут приложения (`style id:card backgroundColor #3366ffff`). `NSH_COMPAT_DUMP=1` выводит, что каждый шаг добавляет на экран и удаляет с него, что помогает при написании сценария.

<a id="commands"></a>

## Команды

```sh
cargo xtask compat                      # the checks against this checkout
NSH_COMPAT_APP=props cargo xtask compat -v
cargo xtask compat record 0.2.0         # at a release: record it in nsh-compat, then open a pull request there
```

`cargo xtask compat` хранит checkout репозитория `nsh-compat` в `target/nsh-compat/` и обновляет его при каждом запуске; checkout с собственными изменениями используется как есть, поэтому изменение проверок можно испытать на NSH до его pull request.
