Skip to content

About

Реализация OneScript на JS.

Resources

Stars

31 stars

Watchers

1 watching

Forks

Repository files navigation

OneScriptJS

Движок 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> скрыт стилем движка, код выполняется после разбора документа.

В Node.js

pnpm build
pnpm oscript script.osjs
echo 'сообщить("Привет из stdin");' | pnpm oscript

Браузер

DOM и объект Документ

<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.

BSL-события

Процедура Обработчик() Экспорт
  Сообщить("событие поймано");
КонецПроцедуры
Источник = ЗагрузитьСценарийИзСтроки(
  "Процедура Сгенерировать(Имя) Экспорт
  |  ВызватьСобытие(Имя);
  |КонецПроцедуры");
ДобавитьОбработчик Источник.МоеСобытие, ЭтотОбъект.Обработчик;
Источник.Сгенерировать("МоеСобытие"); // событие поймано

ДобавитьОбработчик/УдалитьОбработчик (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, Ожидаем уже доступен.

HTTP-клиент

// Адрес мок-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 не теряют отложенный вывод. Ждать разрешён и в теле модуля. Отмены, таймаутов и КогдаВсе/КогдаЛюбой нет.

Node.js

Библиотека

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 предзагружает дерево #Использовать.

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) (матчеры Равно, Больше, Меньше, ЭтоНе, ИмеетДлину, Существует, Содержит, …). Раннер синхронный: тесты с Приостановить не поддерживаются.

Практические рецепты

1. Коллекции, JSON и Формат (Node)

Товары = Новый ТаблицаЗначений;
Товары.Колонки.Добавить("Товар");
Товары.Колонки.Добавить("Цена");
Товары.Колонки.Добавить("Количество");
Строка = Товары.Добавить();
Строка.Товар = "Кофе";
Строка.Цена = 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 они в нижнем регистре.

2. HTTP → JSON → DOM-таблица (браузер)

// Мок 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 в поле ввода. Запрос стоит обернуть в Попытка/Исключение, чтобы показать ошибку сети.

3. Форма и Событие (браузер)

<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.

4. CLI-скрипт с модулем и файлами

// 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:4173

pnpm 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.

About

Реализация OneScript на JS.

Resources

Stars

31 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages