Движок OneScript — открытой реализации языка 1С/BSL, переписанный
на TypeScript. Наша целевая платформа — браузер: движок исполняет скрипты в
собственном формате .osjs и код из <one-script> в браузере (Node.js — вспомогательная
среда для локальной отладки и тестов). Нужны Node.js >= 20 и pnpm. Полный справочник по
функциям, объектам и перечислениям — docs/reference.md.
Форматы файлов. .osjs — наш формат: скрипты для OneScriptJS, браузерная
стандартная библиотека и наши расширения диалекта (например, Асинх/Ждать).
.os — чужой формат: файлы ванильного C#-OneScript для бэкенда, у них свои линтеры и
инструменты. Движок по-прежнему исполняет .os: грамматика BSL у нас общая, а эталонный
корпус reference/OneScript/tests/*.os — внутренняя проверка поведения, а не продуктовое
обещание заменить C#-движок. Блокировать .os на входе не нужно, переименовывать его
тоже не нужно. В одном проекте файлы обоих форматов спокойно соседствуют — расширение
однозначно указывает, чей это файл. Решение и границы —
specs/013-osjs-format/spec.md.
- Язык — переменные, условия, циклы, процедуры/функции (
Знач, по ссылке), исключения,Перейти/метки,?(),Новый, аннотации,#-препроцессор, строки|. - Модули —
#Использовать "<файл>": модуль-синглтон или класс (ПриСозданииОбъекта,Новый); кеш наrun(), защита от циклов; Node — файлы, браузер —fetch. - Коллекции и данные —
Массив,Структура,Соответствие,СписокЗначений,ТаблицаЗначений,ДеревоЗначений, фиксированные варианты; числа, строки,Дата,Null/Неопределено,Тип/ТипЗнч, перечисления, UUID,ДвоичныеДанные. - Stdlib — строки, математика,
Формат, JSON,РегулярноеВыражение, ГСЧ,asserts(Ожидаем.Что(…)),ЗагрузитьСценарийИзСтроки,ТекущийСценарий. - HTTP —
HTTPСоединение/HTTPЗапрос/HTTPОтвет: GET/POST/PUT/DELETE/PATCH/HEAD, произвольный метод, basic-auth, кодировки тела; вызовы синхронные. - Задания и async —
ФоновыеЗадания/ФоновоеЗадание,Приостановить(кооперативная модель),Асинх/Ждатьи непрозрачноеОбещание. - События и браузер — BSL-события (
ДобавитьОбработчик,ВызватьСобытие,ЭтотОбъект);<one-script>,Документ,data-os-*,Событие. - Node.js и тесты — CLI
pnpm oscript, библиотека (run,runScript), файловые функции; xUnit-раннерrunTests.
pnpm install
pnpm build:browser # dist/onescript.js
pnpm dev # http://localhost:5173/examples/index.html<script src="/onescript.js"></script>
<one-script>
имя = "Никита";
сообщить("Привет, " + имя);
</one-script>Сообщить() пишет в console.log; исходник <one-script> скрыт стилем движка, код
выполняется после разбора документа.
pnpm build
pnpm oscript script.osjs
echo 'сообщить("Привет из stdin");' | pnpm oscript<div id="контейнер"></div>
<one-script>
Карточка = Документ.СоздатьЭлемент("div");
Карточка.Класс = "card";
Заголовок = Документ.СоздатьЭлемент("h3");
Заголовок.Текст = "Собрано из BSL";
Заголовок.ДобавитьВ(Карточка);
Карточка.ДобавитьВ(Документ.ПолучитьЭлемент("контейнер"));
</one-script>Документ: ПолучитьЭлемент, СоздатьЭлемент, Запросить, ЗапроситьВсе,
ТелоКорпуса, Заголовок. Элемент: Текст, HTML, Значение, Идентификатор,
Класс, ИмяТега, Стиль, Родитель, Дети; методы ДобавитьВ, Добавить,
Удалить, УстановитьАтрибут/ПолучитьАтрибут, ДобавитьКласс/УбратьКласс,
НаСобытие/СнятьСобытие, Запросить. Стиль — CSS-свойства и
Установить/Получить/Удалить.
Декларативно — атрибут data-os-<событие>; код исполняется в области видимости своего
<one-script> (по умолчанию первого на странице, data-os-script="id" — указанного).
Из BSL — метод НаСобытие:
<button id="кнопка1" data-os-click="Счёт = Счёт + 1; Вывод.Текст = Строка(Счёт);">+1</button>
<button id="кнопка2">+10</button>
<output id="вывод">0</output>
<script src="/onescript.js"></script>
<one-script>
Перем Счёт;
Счёт = 0;
Вывод = Документ.ПолучитьЭлемент("вывод");
Документ.ПолучитьЭлемент("кнопка2").НаСобытие("Нажатие",
"Счёт = Счёт + 10; Вывод.Текст = Строка(Счёт);");
</one-script>Поддерживаются data-os-click, -dblclick, -input, -change, -mouseenter,
-mouseleave, -keydown, -submit и русские алиасы (data-os-нажатие).
Объект Событие появляется в момент DOM-события: движок создаёт его заново на каждое
событие и кладёт локальной переменной в область видимости именно этого inline-фрагмента
(код атрибута исполняется как отдельный фрагмент в дочерней области модуля). Поэтому
Тип, Цель, Отменено, Отменить(), ОстановитьРаспространение() видны в строке
атрибута, но не видны в вызываемых процедурах — передайте нужное аргументом:
Отправить(Событие.Тип) или весь объект Отправить(Событие). Живёт объект только пока
выполняется обработчик (при Ждать/Приостановить — до его завершения). Переменная
между вызовами — через Перем. Живые примеры:
counter.html, delegation.html,
form.html.
Один обработчик может обслуживать много элементов: повесьте data-os-* на контейнер —
клики всплывают, а Событие.Цель — именно нажатый элемент (не контейнер). Вложенная
разметка даёт целью внутренний <span>, поэтому до кнопки поднимаются по Родитель:
<div id="панель" data-os-click="Нажали(Событие.Цель);">
<button type="button" data-choice="Отлично">Отлично</button>
<button type="button" data-choice="Хорошо"><span>Хорошо</span></button>
</div>Процедура Нажали(Знач Цель)
Кнопка = Цель;
Пока Кнопка <> Неопределено И Кнопка.ИмяТега <> "button" Цикл
Кнопка = Кнопка.Родитель;
КонецЦикла;
Если Кнопка = Неопределено Тогда
Возврат;
КонецЕсли;
Сообщить("Нажато: " + Кнопка.Текст + " (" + Строка(Кнопка.ПолучитьАтрибут("data-choice")) + ")");
КонецПроцедурыУ элемента доступны Идентификатор, ИмяТега, Текст, HTML, Класс,
ПолучитьАтрибут("data-*"), ЕстьКласс, Родитель, Дети. Событие.Цель — это
event.target; отдельного currentTarget нет, поэтому контейнер определяйте через
Родитель или передавайте его в код явно. Альтернативы: свой код у каждой кнопки
(data-os-click="Нажали('b1')") или шаблон, вшивающий id в строку НаСобытие (так
сделано в todo.html). Живой пример —
delegation.html.
Процедура Обработчик() Экспорт
Сообщить("событие поймано");
КонецПроцедуры
Источник = ЗагрузитьСценарийИзСтроки(
"Процедура Сгенерировать(Имя) Экспорт
| ВызватьСобытие(Имя);
|КонецПроцедуры");
ДобавитьОбработчик Источник.МоеСобытие, ЭтотОбъект.Обработчик;
Источник.Сгенерировать("МоеСобытие"); // событие пойманоДобавитьОбработчик/УдалитьОбработчик (AddHandler/RemoveHandler) принимают источник,
имя события и обработчик; ВызватьСобытие/RaiseEvent вызывает подписчиков синхронно в
порядке подписки, повторная подписка той же пары не дублирует вызов. ЭтотОбъект/
ThisObject — объект-модуль; источником может быть Сценарий, загруженный через
ЗагрузитьСценарий или ЗагрузитьСценарийИзСтроки.
// lib/Модуль.osjs
Перем Поле Экспорт;
Функция Сложить(А, Б) Экспорт
Возврат А + Б;
КонецФункции
Поле = 10;Используется как #Использовать "lib/Модуль.osjs" + Модуль.Сложить(2, 3) → 5,
Модуль.Поле → 10. Класс — модуль с ПриСозданииОбъекта, экземпляры через Новый:
// lib/Приветствие.osjs
Перем Обращение Экспорт;
Процедура ПриСозданииОбъекта(НовоеИмя)
Обращение = "Привет, " + НовоеИмя;
КонецПроцедуры
Функция Текст() Экспорт
Возврат Обращение;
КонецФункции#Использовать "lib/Приветствие.osjs"
Сообщить(Новый Приветствие("Никита").Текст()); // Привет, НикитаПорядок секций: #Использовать → Перем → методы → код; импорт после кода — ошибка.
Импорт по имени библиотеки (#Использовать asserts, lib.config, OSCRIPT_LIB) не
поддерживается — только файлы; #Использовать asserts — no-op, Ожидаем уже доступен.
// Адрес мок-API: pnpm dev — :5173, pnpm demo — :4173; для реального сервера — свой.
Соединение = Новый HTTPСоединение("http://localhost:5173");
Запрос = Новый HTTPЗапрос("/api/demo");
Ответ = Соединение.Получить(Запрос);
Если Ответ.КодСостояния = 200 Тогда
Данные = ПрочитатьJSON(Ответ.ПолучитьТелоКакСтроку());
Сообщить(Данные.message);
КонецЕсли;HTTPСоединение: Получить, ОтправитьДляОбработки, Записать, Удалить, Изменить,
ПолучитьЗаголовки, ВызватьHTTPМетод; свойства Сервер, Порт, Пользователь,
Пароль, Таймаут, РазрешитьАвтоматическоеПеренаправление. HTTPЗапрос —
АдресРесурса, Заголовки, УстановитьТелоИзСтроки, УстановитьТелоИзДвоичныхДанных;
HTTPОтвет — КодСостояния, Заголовки, ПолучитьТелоКакСтроку,
ПолучитьТелоКакДвоичныеДанные, Закрыть.
Вызовы синхронные, как в эталоне. В браузере это XMLHttpRequest: нужен CORS, часть
заголовков браузер запрещает (молча игнорируются), ошибка сети/CORS — исключение. Прокси,
произвольные SSL-сертификаты, файловое тело и потоки не поддерживаются. В Node каждый
запрос выполняет дочерний процесс с fetch (~100 мс).
Функция Посчитать(Знач Пауза) Экспорт
Приостановить(Пауза);
Возврат 42;
КонецФункции
Параметры = Новый Массив; // аргументы задания — элементы Массива
Параметры.Добавить(20);
Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "Посчитать", Параметры);
Сообщить("запущено: " + Строка(Задание.Состояние));
Задание.ОжидатьЗавершения();
Сообщить("результат: " + Строка(Задание.Результат));Вывод: запущено: НеВыполнялось, затем результат: 42 — задание стартует в ближайшем
кванте, поэтому сразу после запуска состояние ещё НеВыполнялось. Осторожно:
Новый Массив(20) — это Массив из 20 пустых элементов, а не один параметр 20;
элементы Параметры передаются методу позиционными аргументами. Есть Очистить,
ОжидатьВсе, ОжидатьЛюбое, ОжидатьЗавершенияЗадач, ПолучитьФоновыеЗадания,
ПолучитьТекущее; у задания — Состояние, Результат, ИнформацияОбОшибке,
ОжидатьЗавершения(Таймаут?). Модель кооперативная: логические потоки в одном
JS-потоке с общим состоянием модуля; настоящего параллелизма CPU, отмены и приоритетов
нет, точки переключения — ожидания и Приостановить, поэтому страница не замирает.
Асинх Функция Посчитать(Знач Пауза) Экспорт
Приостановить(Пауза);
Возврат 42;
КонецФункции
Обещание = Посчитать(20); // сразу возвращает Обещание
Сообщить("результат: " + Строка(Ждать Обещание)); // результат: 42Вызов async-метода сразу возвращает Обещание; Ждать даёт результат или бросает
исходную ошибку в точке ожидания, повторное ожидание кэшируется. Необработанные ошибки
уходят в console.error/stderr, скрипт продолжается. Хост дожидается async-тела и всех
Обещаний (flushAsync), поэтому <one-script> и CLI не теряют отложенный вывод.
Ждать разрешён и в теле модуля. Отмены, таймаутов и КогдаВсе/КогдаЛюбой нет.
import { run, runScript, createGlobalContext, version } from 'onescriptjs';
await runScript('сообщить("Привет, мир");');
await run('сообщить(2 + 3);', createGlobalContext());
console.log(version());Основные публичные экспорты: run, runScript, createGlobalContext, runTests,
BslTestContext, version, ONESCRIPT_VERSION, Interpreter, GlobalContext, Scope,
tokenize, parse, preprocess (и типы RunOptions, TestResult, RuntimeEnvironment,
GlobalFunction, TypeConstructor, HttpTransport, Value, BslObject).
run/runScript возвращают MaybePromise<void>: для скриптов с ожиданием нужен await.
createGlobalContext() — только stdlib. Node-контекст с файловыми функциями и чтением
модулей с диска (createNodeGlobalContext) не экспортируется — это возможности CLI.
Для импортов в run() можно передать RunOptions (path, basePath, modules,
resolve, loadSource): так CLI предзагружает дерево #Использовать.
pnpm oscript script.osjs # выполнить файл
echo 'сообщить("hi");' | pnpm oscript # программа из stdin
pnpm oscript --help # -h: справка
pnpm oscript --version # -v: версия движкаCLI исполняет файл с Node-контекстом: доступны файловые функции и импорты с диска.
Импорты #Использовать разрешаются относительно входного файла, а файловые функции
(ЧтениеТекста, ЗаписьТекста, ЗагрузитьСценарий) — относительно текущего рабочего
каталога процесса. Код возврата — 1 при ошибке исполнения.
ОбъединитьПути, ЧтениеТекста, ЗаписьТекста, ЗагрузитьСценарий — только Node/CLI.
ЗагрузитьСценарийИзСтроки, ТекущийСценарий, СтартовыйСценарий — stdlib, доступны
везде. xUnitFor1C-совместимый раннер запускается из библиотеки:
import { createGlobalContext, runTests } from 'onescriptjs';
const source = `Перем юТест;
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
юТест = ЮнитТестирование;
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("ТестСуммы");
Возврат ВсеТесты;
КонецФункции
Процедура ТестСуммы() Экспорт
юТест.ПроверитьРавенство(4, 2 + 2);
КонецПроцедуры`;
const results = runTests(source, createGlobalContext());
// [{ name: 'ТестСуммы', passed: true }]runTests возвращает TestResult[] (name, passed, error?) и не бросает исключение
на упавшем тесте. Утверждения: ПроверитьРавенство, ПроверитьНеРавенство, Проверить,
ПроверитьИстина/ПроверитьЛожь, ПроверитьБольше/ПроверитьМеньше,
ПроверитьВхождение, ПроверитьТип, ПроверитьЗаполнено. Сервисные методы:
ТестПровален, ИмяВременногоФайла (имя временного файла), ПодробныеОписанияОшибок.
Библиотека asserts доступна глобально:
Ожидаем.Что(2 + 2).Равно(4) (матчеры Равно, Больше, Меньше, ЭтоНе,
ИмеетДлину, Существует, Содержит, …). Раннер синхронный: тесты с Приостановить
не поддерживаются.
Товары = Новый ТаблицаЗначений;
Товары.Колонки.Добавить("Товар");
Товары.Колонки.Добавить("Цена");
Товары.Колонки.Добавить("Количество");
Строка = Товары.Добавить();
Строка.Товар = "Кофе";
Строка.Цена = 450.5;
Строка.Количество = 2;
Строка = Товары.Добавить();
Строка.Товар = "Чай";
Строка.Цена = 300;
Строка.Количество = 3;
Сообщить(СтрШаблон("Позиций: %1, сумма прайса: %2", Товары.Количество(), Формат(Товары.Итог("Цена"), "ЧДЦ=2; ЧН=0")));
Для Каждого Строка Из Товары Цикл
Сообщить(СтрШаблон("%1 — %2 × %3", Строка.Товар, Формат(Строка.Цена, "ЧДЦ=2"), Строка.Количество));
КонецЦикла;
Данные = Новый Структура("Имя, Возраст", "Никита", 38);
JSON = ЗаписатьJSON(Данные);
Сообщить(JSON);
Сообщить(ПрочитатьJSON(JSON).Имя);Вывод: Позиций: 2, сумма прайса: 750.50, строки Кофе — 450.50 × 2 и
Чай — 300.00 × 3, JSON {"имя":"Никита","возраст":38}, затем Никита. Ключи
Структуры нечувствительны к регистру, поэтому в JSON они в нижнем регистре.
// Мок GET /api/demo: pnpm dev — http://localhost:5173, pnpm demo — http://localhost:4173.
Соединение = Новый HTTPСоединение("http://localhost:5173");
Запрос = Новый HTTPЗапрос("/api/demo");
Ответ = Соединение.Получить(Запрос);
Данные = ПрочитатьJSON(Ответ.ПолучитьТелоКакСтроку());
Контейнер = Документ.ПолучитьЭлемент("demo-list");
Контейнер.HTML = "";
Для Каждого Элемент Из Данные.items Цикл
Пункт = Документ.СоздатьЭлемент("li");
Пункт.Текст = СтрШаблон("%1 — %2", Элемент.id, Элемент.name);
Пункт.ДобавитьВ(Контейнер);
КонецЦикла;Адрес сервера задайте строкой под свой режим/сервер; в BSL-коде location недоступен
(он не входит в стандартную библиотеку) — как в examples/http.html,
origin можно подставить из обычного JS в поле ввода. Запрос стоит обернуть в
Попытка/Исключение, чтобы показать ошибку сети.
<form id="форма" data-os-submit="Событие.Отменить(); Отправить(Событие.Тип);">
<label for="имя">Ваше имя</label>
<input id="имя" name="имя" autocomplete="name" />
<button type="submit">Поздороваться</button>
</form>
<p id="результат" role="status" aria-live="polite">Введите имя.</p>
<script src="/onescript.js"></script>
<one-script>
Перем ПолеИмени;
Перем Результат;
Процедура Отправить(Знач ТипСобытия)
Имя = СокрЛП(ПолеИмени.Значение);
Результат.Текст = "Привет, " + Имя + "! Событие: " + ТипСобытия + ".";
КонецПроцедуры
ПолеИмени = Документ.ПолучитьЭлемент("имя");
Результат = Документ.ПолучитьЭлемент("результат");
</one-script>Событие.Отменить() отменяет отправку формы, а его тип передаётся в процедуру аргументом.
Объект создаётся движком на каждое DOM-событие и живёт только на время выполнения
inline-кода; можно передать и весь объект — Отправить(Событие).
Живой пример — examples/form.html.
// lib/Строки.osjs
Функция Приветствие(Имя) Экспорт
Возврат "Привет, " + Имя + "!";
КонецФункции// script.osjs
#Использовать "lib/Строки.osjs"
Файл = ОбъединитьПути(ТекущийСценарий().Каталог, "out.txt");
ЗаписьТекста(Файл, "строка из BSL");
Сообщить(Строки.Приветствие("мир"));
Сообщить("прочитано: " + ЧтениеТекста(Файл));pnpm oscript script.osjs
# Привет, мир!
# прочитано: строка из BSLОткрываются через pnpm dev (http://localhost:5173/examples/<страница>.html) или
pnpm demo (http://localhost:4173); мок-бэкенд GET /api/demo и POST /api/echo
работает в обоих режимах.
| Страница | Что демонстрирует |
|---|---|
hello.html |
приёмочный пример из task.md: переменные, условия, Сообщить |
counter.html |
счётчик на data-os-click, переменная через Перем |
delegation.html |
один обработчик на контейнере: Событие.Цель и много кнопок |
input.html |
зеркалирование ввода: data-os-input и процедура модуля |
hover.html |
data-os-mouseenter/mouseleave, классы элемента |
dom.html |
создание DOM-дерева из BSL через Документ |
collections.html |
ТаблицаЗначений, Структура, сортировка, Итог, DOM-таблица |
strings-json.html |
строки, Формат, ЗаписатьJSON/ПрочитатьJSON |
form.html |
форма, data-os-submit, Событие.Отменить() |
todo.html |
todo-приложение: таблица, НаСобытие, добавление/удаление |
quotes.html |
экранирование кавычек "" в строковых литералах |
modules.html |
#Использовать: модуль и класс из examples/lib/ |
http.html |
HTTPСоединение GET/POST к мок-API, JSON-ответ |
jobs.html |
фоновые задания и Приостановить, живая страница |
async.html |
Асинх/Ждать и Обещание |
pnpm install # зависимости
pnpm typecheck # проверка типов
pnpm test # юнит-тесты (Vitest)
pnpm check:docs # сверить публичный API из src/ со справочником
pnpm test:watch # тесты в watch-режиме
pnpm build:browser # dist/onescript.js
pnpm build:node # dist/node/ (Node-библиотека)
pnpm build:cli # dist/cli.mjs
pnpm build # полная сборка
pnpm dev # dev-сервер примеров, http://localhost:5173
pnpm demo # сборка + примеры в dist/ + preview, http://localhost:4173pnpm dev требует предварительного pnpm build:browser: сервер отдаёт уже собранный
dist/onescript.js по пути /onescript.js. Правили движок (src/) — пересоберите бандл;
правили HTML — достаточно обновить страницу. pnpm demo выполняет vite build, копирует
examples/*.html и examples/lib/ в dist/ (scripts/copy-examples.mjs) и поднимает
preview с тем же мок-API.
Меняли stdlib или язык — в том же изменении обновите
docs/reference.md: справочник должен отражать текущий src/
(правило в AGENTS.md). Проверить синхронизацию:
pnpm check:docs (падает, если имя API из src/ не упомянуто в справочнике).
- Нет отмены async-операций и фоновых заданий, таймаутов у
Ждать/async,КогдаВсе/КогдаЛюбой; у фоновых заданий таймаутОжидатьЗавершения/ОжидатьВсе/ОжидатьЛюбоеесть. - Фоновые задания кооперативные: нет настоящих потоков, приоритетов и изоляции данных.
- HTTP без прокси, произвольных SSL-сертификатов, файловых тел и потоков; в браузере — CORS и запрещённые заголовки.
#Использоватьимпортирует только файлы: нет поиска библиотек по имени, версий,lib.config/OSCRIPT_LIB;#Использовать asserts— no-op.- xUnit-раннер синхронный; тесты с приостановкой не поддерживаются.
- Аннотации разбираются, но рефлексия по ним (
Рефлектор) не реализована. - Объектные
ЗаписьJSON/ЧтениеJSON,ТекстовыйДокумент,ЧасовойПояс,ХешированиеДанных,ЗначениеЗаполнено,XMLСтрокаи часть форматов дат — вне scope. events.osждётПодключитьСценарий; приёмка в реальном браузере (Playwright) — спека.
Подробный статус и известные пробелы — docs/status.md.
docs/reference.md— справочник API: функции с параметрами и возвратом, объекты с полями и методами, перечисления.AGENTS.md— инструкции для агентов, команды и конвенции.docs/architecture.md— карта «C# → TypeScript», слои движка.docs/bsl-surface.md— синтаксис BSL и поверхность stdlib.docs/status.md— что реализовано и что осталось.specs/— спецификации незавершённых работ.task.md— исходное ТЗ и приёмочный HTML-пример.
MIT.