- .moon - конфигурация
планера задач. - build - итоговый
билдRK, генерируемый с помощьюmoon :build. - developers -
конфигурациипроектов Roodl для разработки RK в них. - packages -
пакеты, на которые разбит RK. Один пакет содержит один или нескольконод. - nodes -
нодыRK. Однанодавсегда состоит изReact-ноды/JS-нодыикомпоненты- неспосредственного кода. - shared - внутренние
утилиты,библиотеки, глобальнаятипизация, общая конфигурациябандлераи интеграция RK с Roodl. - package.json - есть в корне, в каждом
пакетеиноде. Зависимостибиблиотекустроены иерархично. Если что-то прописано на верхнем уровне, это можно импортировать на любом нижнем. - pnpm-workspace.yaml - конфигурация pmpm-workspace, за счет которой +
moon, все это превращяется в автоматизированныймонорепозитарий. - остальные - настраивают
git,prettier- автоформатирование кода,postcss- пост-обработчикCSSдляMantine,rolder-kit.code-workspace- настройкиVS Code.
moon :build- создает билд RK.moon названиеПакета:build- создает билд указанного пакета RK. Через пробел можно указать несколько пакетов.moon :clean- удаляет все node_modules. Рекомендовано запускать эту задачу и удалять.moon/cacheперед билдом нового релиза.moon :dev -- --env=developer=никРазработчика project=названиеПроекта- создает не сжатый билд RK и кладет его в указанный Roodl-проект. При изменении кода перезапускается и заменяет билд-файлы.moon названиеПакета:dev -- --env=developer=никРазработчика project=названиеПроекта- как команда выше, но для одного пакета.
Весь код ноды находится в папке nodes. packages только сктруктурирует ноды по пакетам (то что в итоге собирается в папки). Тут только импорт и минимальные параметры, которые нельзя указать для отдельных версий. В nodes и packages своя структура, следуй ей используя примеры.
Каждая нода имеет версию и лежит в отдельной папке с названием версии. Если это модульная нода, то рядом с версиями нужно положить папку modules. В этой папке модули повторяют структуру ноды, включая версионность (смотри Table). Cтруктура ноды, которой нужно следовать:
- package.json - минимально содержит три параметра:
{
"name": "@nodes/text-v2.0.0",
"main": "node/definition.ts",
"type": "module"
}К ним добавляются dependencies для библиотек и devDependencies для типов TypeScript. Важно их не путать, т.к. первые попадают в итоговый бандл, вторые нет.
- node - код для интеграции ноды с Roodl. Минимально здесь находится декларация ноды
definition. Для сложных нод, здесь могут быть дополнительные файлы. Смотри примерuseData, где проверяется схема и создается хранилище. - component - код разрабатываемой ноды. Минимально - это один входной файл, которые импортируется в
definition.
Декларация - это схема, определяющая базовые праметры, задающая входные/выходные порты ноды и их поведение. Для каждой версии ноды своя декларация.
Создание делкарации разбито на 2 части. Первая, верхенеуровневая задает параметры, которые нельзя указать на уровне версий и импортирует декларации версий, вторую часть. Нужно использовать наши функции - jsNode и reactNode. Они принимают три параметра:
-
Название ноды. Для js-нод c маленькой буквы, для react-нод с большой. Название будет видно в Roodl ровно как указано. Под капотом функция добавит системное название
rolder-kit.api-v1${nodeName}, отличив наши ноды от всех других.api-v1нужен, чтобы была возможность совместить несколько версий RK в одном проекте, когда поменялся наш способ интеграции с Roodl. Это так же создает контекст. Некоторые библиотеки не видят код из другого контекста. Из-за чего, смена api дело трудоемкое. -
Декларации версий. Это объект, где ключ - версия без хештега, значение - сама декларация.
-
Параметры. Это объект. Он один на все версии. Т.е. мы не можем менять их для каждой версии отдельно. При изменении треубется перезайти в проект. Параметры, все опциональны:
color. Цвет ноды. Только JS-ноды. TS предложит варианты. Общая логика такая. Визуальная нода - синий (не настраивается), работа с бекенд или интеграциями - зеленый, все остальное - фиолетовый. Серый не используем, чтобы разработчик легко отличал свои повторяемые функции от RK. Если не установить цвет, js-нода будет зеленой.allowChildren. Разрешить вставлять детей. Только React-ноды. Отключено по дефолту.docs. Ссылка на документацию. Если ссылка есть, в панеле параметров рядом с переименованием и удалением появится вопросик для перехода. Документация не отображается в браузере компонентов Roodl.
Декларация версии. Разбита на несколько частей для облегчения понимания процесса. О самом процессе ниже, здесь описание параметров:
-
hashTag. Добавляет к отображаемой версии ноды выбранный хеш-тег. -
module. Единственный обязятельный параметр.static. В теле до экспорта нужно добавить импорт текущей версии и добавить название импорта в значение ключаstatic. Такой вид импорта не выделяет код в отдельный файл и он будет загружаться вместе с приложением не зависимо от того используется нода или нет.dynamic. В ключ нужно передать динамичный импорт -module: { dynamic: import('../component/search') }для JS иmodule: { dynamic: lazy(() => import('../component/Stack')) }для React. Такой вид импорта выделит код в отдельный файл и он будет загружаться в момент первого использования ноды.
-
beforeNode:validate. Опционально. Функция для проверок до запуска всего остального. Принимаетmodel- мадель ноды, через которую можно дотянуться до иерархии нод и много чего проверить. Нужно верутьtrue- валидация прошла успешно,false- валидация не прошла, разработчику будет показано сообщениеNode validation failed.,string- валидация не прошла, разработчику будет показано твое сообщение. Нужно всегда возвращать свое сообщение, т.к. стандартное мало что подскажет разработчику.
-
inNode:inputs. У инпутов больше всего параметров и больше всего обработки. На уровне декларации инпут - это тоже декларация, т.е. это форма удобная для описания декларации и внутреннего применения вjsNodeиreactNode. Под капотом они преобразуются в нужнуюRoodlформу в стандартизованном виде. Задается массивом, в котором каждый инпут нужно получать через функцию-парсер декларации -getPortDef. Эта функция подскажет, что можно делать, а что нельзя. Порядок в массиве имеет значение, ровно как задан, так и будет отображен в редакторе. Больше нет возможности задавать инпуты по шаблонам кучей. Это сделано во избежании лишнего кода в каждой ноде. Есть два стандартных порта, которые автоматически добавляются в массив:version- порт с выбором версии, всегда первый иcustomProps- функция, которая возвращает любые кастомные настройки разработчка вRoodl, которые может использовать разработчик ноды, всегда последний. Декларация инпута, которую нужно подавать вgetPortDef(все параметры обязательны, кроме специально отмеченных):name. Название инпута. Будет переданно в функцию разрабатываемого модуля черезpropsс таким же названием.displayName. Отображаемое имя для разработчика. Пишем с большой буквы предложением, пример -Custom props.group. Название группы. Выбор из списка, т.е. названия стандартизованы, но можно выбратьCustom, тогда в параметре ниже можно указать любое название.customGroup. Опционально. Название кастомной группы, если выше выбранCustom.type. Тип инпута. На типах много завязано. Происходит несколько проверок, часть встроенные, часть разработчик может задать сам. Любые проверки выдают ошибки при их не прохождении. В отличии от предыдущей версии, больше не нужно перезагружать приложение, чтобы сбросить ошибку, достаточно подать новое корректное значение. Подробнее:- Литералы -
string,numberиboolean. Простые типы, не конвертируются, проверяются на соответсвие. При не соответствии типа разработчику будет показана соотвествующая ошибка. signal. Сигнал. Не проходит проверок. Сигнал - это простой тогл true/false. Когда сигнал подан - true и тут же false. Под капотом мы реагируем на true.object. Объект. Можно подавать только через подключение, в редакторе не доступен. Проверяется на то, что он объект.array. Массив. Если подан через подключение, проверяеться, что массив. Если через редактор, то это текст, т.к. параметры редактора сохраняются вproject.json, а массив может содержать функции. Преобразуется в массив с отлавливанием ошибок синтаксиса.proplist. Список текстовых праметров. Задается только через редактор. Под капотом - это массив текстов. Не проверяется.component. Путь к шаблонку компоненты. Обычный тектс, не проверяется.objectEval. Наш специальный тип. Это функция, которая принимает текущиеpropsи должна вернуть объект. Под капотом вRoodlподается как типarray, благодаря чему разработчик может устанавливать значение в редакторе, причем не как массив, а сразу функцией. Как и массив,Roodlсохранит функцию текстом. Преобразуется в функцию. Проверяется, что это функция, что нет синтаксических ошибок и что возвращаемое значение объект.funcEval. Наш специальный тип. Так же какobjectEval, но каждая нода определяет, что должно возвращаться. Пример - функции фильтрации вTable, которые должны возвращатьboolean. Проверяется на синтаксические ошибки.- Enum. Специальный тип для выбора из списка. Задается как массив объектов, с двума ключам
labelиvalue. Оба всегда текстовые.
- Литералы -
default. Опционально. Дефолтное значение. Можно устанавливать только для простых типов. Нельзя устанавливать комментарий с кодом и это просто нужно знать, нет автоматизации проверки.codeComment. Опционально. Для типовarray,objectEvalиfuncEvalможно указать комментарий. Такой комментарий будет виден в редакторе, но будет удален при конвертации. Хороший пример - указать закомментированный пример схемы, массива или функции. Тогда разработчику остается убрать скобки комментариев и код готов.visibleAt. Опционально. Видимость порта:editor- только в редакторе,connection- только через подключение,both- оба. Если не указать будетboth.tooltip. Опционально. Подсказка. Подается в виде текста, который может содержать стандартныеHTML-теги. Учти, текст изначально жирный. Пробовал выделять тектс косым, вставлять картинки, делать списки, смотреть кино. Все работает. Можно форматироватьJSON, смотри функциюvalidateFetchSchemeвuseDataдля примера.dependsOn. Опционально. Функция, делающая инпут зависимым. Принимает текущиеprops, должна вернутьtrue/false. При положительном возврате порт будет добавлен, иначе нет. Запускается после конвертации значений и присвоения дефолтов, что дает актуальныеprops.validate. Опционально. Функция для проверки значения. Заменяет устаревший параметрrequired. Принимает текущиеprops. Нужно верутьtrue- валидация прошла успешно,false- валидация не прошла, разработчику будет показано сообщениеInput "${inputDef.displayName}" is required.,string- валидация не прошла, разработчику будет показано твое сообщение.transform. Опционально. Функция преобразования порта. Преобразует изначальную декларацию в новую. Пример - когда тип указан списком и состав списка нужно менять в зависимости от значения других параметров. Принимает текущиеpropsи текущую декларацию инпута. Нужно вернуть новую декларацию инпута. Если в новой декларации будет заданdefault, он будет сконвертирван, если нужно и проверен на тип.
outputs. Задаются какinputs, поддерживая все параметры. Логично, что не все типы (кастомные и те, что имеют смысл только в редакторе) имеют смысл, нет смысла делать валидацию (это задача разработчика ноды), нет смысла в дефолте и подсказке. Но часто нужно указатьdependsOn, изредкаtransform. Значения никак не конверируются и не проверяются.
-
afterNode:transformPorts. Функция преобразования всех портов. Работает какtransformдля одного порта, но на вход получает все порты в объекте, с разделением наinputsиoutputs. Вернуть нужно объект с портами в таком же формате. Это заменяет устаревшуюaddNodePorts.validate. Функция для проверки всей ноды. Работает так же как одноименная на порту, запускается после проверки инпутов. Пример, когда может потребоваться - некоторые ноды требуют, чтобы их родитель был строго одного типа, тогда можно найти родителя вvalidateи вернуть ошибку разработчкиу. ТакAuthпроверяет, что он находится вData.triggerOnInputs. Только для JS-нод. Функция, указывающая список инпутов, на подачу которых нужно запускать реактивную js-функцию. Принимает текущиеprops, нужно вернуть список с названиями портов. Список не проверяется. Подробнее зачем это нужно ниже.getInspectInfo. Функция для отображения полезной информации над нодой. Принимает текущиеpropsи значения выходных портов. Нужно вернуть массив, в котором объекты - это раздел с отображаемой информацией. Вот пример, где первая строка простой текст, имитирующий заголовок, воторая массив:
getInspectInfo(p, outProps) { if (p.fields) return [ { type: 'text', value: 'Search fields' }, { type: 'value', value: p.fields }, ]; else return []; },
-
beforeComponent:initialize. Функция для одноразового запуска любого кода до первого запуска JS-ноды или первого рендера React-ноды. Принимаетprops, их можно мутировать, т.к. самой компоненты еще нет. Код может быть асинхронным или нет, ну самinitializeвсегда асинхронный. Эта функция обеспечивает важный сценарий, когда нужно гарантировать не пустой возврат при первом запуске, как это сделано в нодахitemиnode.
-
disableCustomProps: Параметр отключения встроенного инпутаcustomProps.
inNode: запускается при каждом изменении инпутов. Здесь запускаются все обработки для каждого порта.afterNode: запускается при каждом изменении инпутов после их обработки.beforeComponent: запускается после всех*nodeи перед запуском (монтированием) компоненты.
nodeTime. Этот режим существует только в момент разработки. Все*nodeэтапы очередности работают только в нем. Иными словами, все валидации и транформации существуют только тут. Так сделано в соотвествии с принципом максимально приблежать проверки к моменту разработки, а не в момент использования.runTime. Этот режим существует всегда. Если мы находимся в редакторе, запускаются валидации и здесь, чтобы отловить ошибки, когда значения инпутов прилетают с подключений.
- Значение прилетело с подключения:
- Мы в редакторе:
- Проверка типа.
- Проверка функцией
validate. - Передача значения в компоненту.
- Приложение задеплоено:
- Передача значения в компоненту.
- Мы в редакторе:
- Значение прилетело с редактора:
- Мы в редакторе:
- Конвертация значения, если нужно.
- Проверка типа.
- Проверка функцией
validate. - Передача значения в компоненту.
- Приложение задеплоено.
- Конвертация значения, если нужно.
- Передача значения в компоненту.
- Мы в редакторе:
Можно разделить код на 2 этапа запуска:
- До регистрации ноды в Roodl. Если посмотреть
index.htmlв любом проекте Roodl с RK, то станет понятно, что каждый пакет RK загружается вместе с самим Roodl и с React. Это означает, что еще нет глобальных объектов. Для нас важны 2 -Noodl(есть, но урезанный) иR. - После регистрации ноды в Roodl. Здесь нет никаких ограничений, все глобальные объекты доступны. Разработчик в Roodl имеет доступ только к этой области. Но может указывать код в
Head Code.
Вот пример:
import '@shared/types-v0.1.0';
import { defineNode } from '@noodl/noodl-sdk';
import search from './src/search';
import useData from './src/useData';
const jsPackages = [search, useData];
Noodl.defineModule({ nodes: jsPackages.map((i) => defineNode(i)) });Здесь видно, что используется defineModule из глобально заданного Noodl, но не используется defineNode. Если вывести Noodl в запущенном проекте, то мы увидим большую структуру, включая defineNode. Но если вывести Noodl (или window.Noodl) в теле этого примера, то там будет минимальная труктура, включающая пару параметров и defineModule.
Еще один пример. Везде, где можно стараюсь брать библиотеки из R.libs, чтобы не увеличивать бандл. В useData используется валидация схемы через библиотеку valibot. Для нее нужно задать схему, которая по сути просто куча функций. Схема называется FetchScheme и расположена внтури validateFetchScheme, которую запускает validate с декларации порта fetchScheme. Если расположить схему FetchScheme в корне тела файла, то библиотеку valibot из R.libs не удастся использовать, т.к. FetchScheme запускает функции valibot сразу при импорте, а R просто еще нет. Поэтому, FetchScheme лежит внутри validateFetchScheme, которая запускается сразу после регистрации ноды.
Есть глоабльная и локальная типизации.
Глоабльная задается пакетом @shared/types-v0.1.0. Определяет глобальные переменные - R, Noodl и log, а так же экспортирует некоторые глобальные типы - NoodlNode, Item, DbClass и другие. Для использования глобальных переменных нужно задать такой импорт - import '@shared/types-v0.1.0'. Для импорта конкретного типа данных такой - import type { Item } from '@shared/types-v0.1.0'. Первое не перекрывает второе, но можно совмещать.
Локальная обычно устанавливается в файле с декларацией - definition.ts
import type { BaseReactProps } from '@shared/node-v1.0.0';
export type Props = BaseReactProps;BaseReactProps или BaseJsProps - это минимальная типизация любой компоненты. Ткни через control в какой то ноже, чтобы изучить.
Чтобы добавить свои типы, нужно расширить базовый:
import { BaseJsProps } from '@shared/node-v1.0.0';
import type { Item } from '@shared/types-v0.1.0';
import type { IFuseOptions } from 'fuse.js';
export type Props = BaseJsProps & {
fields?: string[];
minMatchCharLength?: number;
searchString?: string;
customOptions?: IFuseOptions<Item>;
items?: Item[];
};Решил описать это, т.к. есть не мало различий, который приводят к неверной локальной типизации.
- Если порт валидируется
validate, то типу можно доверять. - Если
visibleAtравенeditor, то типу можно доверять. - Если валидации нет и
visibleAtне равенeditor, то всегда опционално и типу можно доверять.
React-нода - это всегда одна React-компонента, которая является точкой входа. Дальше, сколько угодно.
- Исползьуемые библиотки должны поддерживать React 18.
- Экспорт должен быть всегда дефолтным.
forwardRefобязателен.
Для принятия сигнала внутри React-компоненты нужно использовать хук useImperativeHandle с такими параметрами:
- Первым параметром нужно передать
ref, который подаетforwardRefвоторым параметром послеprops. - Вторым параметром должна быть функция без параметров, которая должна вернуть объект с ключями-функциями. Каждый ключ-функция - это сигнал, его название должно совпадать с входным сигналом. На вход эта функция передает текущие props (можно брать и через зависимости, но так удобнее). Возвращать ничего не нужно.
- Третий параметр - зависимости. Если до хука
useImperativeHandleесть какието константы, состояния или функции, их можно перечислить массивом. Так они передаются в ключи-функции. Это нужно, т.к. эти функции выполняются родительской React-нодой, а не текущей.
Вот как может выклядеть компонента с сигналами:
export default forwardRef((props: Props, ref) => {
const [selectedItem, setSelectedItem] = useState<Item | undefined>();
useImperativeHandle(
ref,
() => ({
setSelectedItem: (p: Props) => (p.selectedItem ? setSelectedItem(p.selectedItem) : setSelectedItem(undefined)),
resetSelectedItem: () => setSelectedItem(undefined),
}),
[]
);
return <SomeComponent />;
});Если компонент несколько, нужно поднять ref до верхней компоненты через useRef, иначе придется повторять useImperativeHandle в каждой. Смотри пример в Table, где хранилище из дочерней компонентой поднимается в родительскую.
Как это работает. ref - это своего рода ссылка на DOM-ноду. В React его используют как константу (в теле React-компоненты не может быть переменных). forwardRef передает ref от родителя (или наоборот? :) ). useImperativeHandle добовляет в этот ref ключи функции. В результате, родительская компонента получает эти функции и может выполнять. Наша интеграция использует ref, ищет там эти функции по названию сигналов и запускает.
Если React-нода - это одна React-компонента, то JS-нода - это объект с одной или несколькими функциями ключами.
- Должен быть дефолтный экспорт, даже если это пустой объект.
- Название ключей должны совпадать с названием входных сигналов или называться
reactive. - В делкарации должен быть объявлен соотвествующий порт-сигнал и/или
triggerOnInputs, выдающий список портов, на подачу которых будет запускатьсяreactive. - Функции могут быть асинхронными, но нужно учитывать, что пока не завершилась асинхронная функция, повторный запуск этой или других ключей-функций не возможен.
Раз JS-нода это объект с функциями, то можно передать его в константу и экспортировать эту константу. Тогда внтури функций объекта можно запускать другие функции этого же объекта. Такой подходи называют Фабрикой.
- Классический вариант. Название функций должно совпадать с названиями сигналов. Сигнал будет просто запускать такую функцию. Функции
reactiveне должно быть.triggerOnInputsдолжен отсутстовавть. - Реактивный вариант. Функция должна называться
reactive, сигналов не должно быть. В декларации нодыtriggerOnInputsдолжен вернуть список инпутов. СамаtriggerOnInputsпринимает текущиеprops. Функция запустится всякий раз при обновлении значений инпутов из списка. - Комбинированный вариант. Повторяем первые два. Имеет смысл в сценариях, когда основное поведение задано через реактивную функцию, а сигналы служат для каких-то отдельных задач. Редкая история, чаще всего хватает первых двух. Что точно не верно - это пытаться делать одно и тоже и через сигнал и реактивно.
Классический или сигналы.
export default {
sendData: (p: Props) => {
// Какой то код
},
setServerState: (p: Props) => {
// Какой то код
},
};Реактивный вариант.
export default {
reactive: (p: Props) => {
// Какой то код
},
};Комбинированный вариант.
export default {
reactive: (p: Props) => {
// Какой то код
},
sendData: (p: Props) => {
// Какой то код
},
};Фабрика.
const myFabric = {
reactive: (p: Props) => {
// Какой то код
},
sendData: async (p: Props) => {
// Какой то код
},
someExtraUsefulFunc: async (p: Props) => {
const result = await myFabric.sendData({ ...p, someNewProp: 'value' });
if (result) doSomething;
},
};
export default myFabric;