<j/>jtsx-loaderДокументация

Быстрый старт

JSX и TSX в Node.js без React. Одна HTML-фабрика для сервера и браузера.

Требования: Node.js ≥20.16 · ESM

01Установка

Терминал
npm install jtsx-loader
npm pkg set type=module
Терминал
mkdir my-jtsx-app
cd my-jtsx-app
npm init -y
npm pkg set type=module
npm install jtsx-loader

02Первый компонент

Создайте Page.jsx, Counter.jsx и app.js в одной папке.

Page.jsx
import Counter from './Counter.jsx';

export default ({ title }) => <main>
    <h1>{title}</h1>
    <Counter count={3} />
</main>;
Counter.jsx
export default ({
    count,
    label = 'One component. Two environments.',
    buttonLabel = 'Add one',
}) => (
    <div class="client-counter">
        <p>{label}</p>
        <output aria-live="polite">{count}</output>
        <button type="button" data-increment>
            {buttonLabel}
        </button>
    </div>
);
app.js
import 'jtsx-loader';

// Registration completes before this dynamic import.
const { default: Page } = await import('./Page.jsx');
const html = String(Page({ title: 'Hello JSX' }));
console.log(html);
HTML
<main><h1>Hello JSX</h1><div class="client-counter"><p>One component. Two environments.</p><output aria-live="polite">3</output><button type="button" data-increment="true">Add one</button></div></main>

03Запуск

Терминал
node app.js

HTML появится в терминале. Чтобы открыть страницу в браузере, подключите Express или Fastify.

Нужны Node.js ≥20.16 и ESM ("type": "module" в package.json). В Node.js loader подключает .jsx/.tsx через импорт и преобразует синтаксис с помощью esbuild. Для браузера компоненты собираются в обычный JavaScript.

02 / 13

Запуск без --import

Регистрация загрузчика может находиться прямо в стартовом JS-файле.

Обычный Node.js должен сначала узнать, как загружать JSX/TSX. Регистрация подключает эту обработку один раз на процесс. Выберите один способ: регистрация в app.js перед динамическим импортом, отдельный bootstrap.js или флаг --import при запуске.

Импорт "jtsx-loader" регистрирует загрузчик; из того же модуля можно импортировать raw и renderToString. Для явной регистрации используйте register.js, как в примере ниже. После регистрации загружайте .jsx/.tsx через await import(). Статический import шаблона в том же стартовом файле выполняется слишком рано — даже если написан ниже регистрации.

bootstrap.js — небольшой стартовый файл: он регистрирует загрузчик и только затем запускает server.js. Поэтому в server.js уже можно писать import Page from "./Page.jsx". Код server.js приведён в разделе «Express и Fastify». Для функций без регистрации используйте jtsx-loader/runtime.js; этот путь нужен внутри jtsx.config.js, чтобы избежать цикла загрузки.

bootstrap.js
import 'jtsx-loader/register.js';
await import('./server.js');
bash
node bootstrap.js
# Or the original preload path:
node --import jtsx-loader server.js
03 / 13

Фабрика в браузере

Один JSX-компонент для фронтенда и бэкенда.

Для примера нужны Counter.jsx из быстрого старта и три файла ниже: client.jsx, build-client.mjs и index.html в одной папке. Установите esbuild, запустите сборку и раздавайте эту папку через ваш локальный HTTP-сервер. Откройте index.html по HTTP: кнопка обновляет счётчик. После изменения JSX повторите сборку и обновите страницу.

Counter.jsx из быстрого старта уже отрендерен на сервере прямо ниже. Клиентская сборка импортирует тот же файл и обновляет его в браузере. Нажмите кнопку: счётчик меняется без запроса к серверу и без React.

Импортируйте фабрику из jtsx-loader/browser.js. Этот вход не читает Node-конфиг, не регистрирует loader hooks и не требует Node-полифиллов. Он экспортирует _jsx, _jsxFragment, _jsxUtils, raw, renderToString и createFactory(options). Корневой импорт также выбирает browser.js в сборщиках с поддержкой browser condition; явный путь удобен для предсказуемой настройки.

Соберите JSX/TSX заранее: браузер сам не понимает JSX-синтаксис. В esbuild задайте jsxFactory, jsxFragment и inject, как в примере. React и ReactDOM устанавливать не нужно. esbuild работает только при сборке, в клиентский bundle попадают компоненты и фабрика.

Фабрика возвращает HTML, а состояние, DOM и события контролирует ваш код. Пример использует innerHTML и делегированный addEventListener на стабильном контейнере. Это повторный рендер, а не hydration или virtual DOM: замена содержимого сбрасывает состояние вложенного DOM и фокус. Для сложных обновлений выбирайте нужную стратегию работы с DOM.

Общие компоненты должны использовать только данные и универсальные импорты. Вызовы node:fs оставляйте на сервере, а document/window — в клиентском entry. Настройки браузерной фабрики задаются через createFactory(options), не через jtsx.config.js. Async-компоненты можно вызывать с явным await; для автоматического ожидания вложенных компонентов используйте browserAsync.js как inject и renderToString(await Page()). Прежние пути factory/jsxFactory.js и factory/asyncFactory.js также выбирают клиентские адаптеры при browser condition.

Один компонент. Две среды.

0
client.jsx
import { renderToString } from 'jtsx-loader/browser.js';
import Counter from './Counter.jsx';

const root = document.querySelector('#client-demo');
const ru = root.dataset.lang === 'ru';
let count = 0;

function render() {
    root.innerHTML = renderToString(
        <Counter
            count={count}
            label={
                ru
                    ? 'Один компонент. Две среды.'
                    : 'One component. Two environments.'
            }
            buttonLabel={ru ? 'Добавить один' : 'Add one'}
        />,
    );
}

// Keep the listener on the stable container when replacing its content.
root.addEventListener('click', (event) => {
    if (event.target.closest('[data-increment]')) {
        count += 1;
        render();
    }
});
render();
build-client.mjs
import { build } from 'esbuild';
import { fileURLToPath } from 'node:url';

await build({
    entryPoints: ['client.jsx'],
    bundle: true,
    platform: 'browser',
    format: 'esm',
    target: 'es2020',
    jsxFactory: '_jsx',
    jsxFragment: '_jsxFragment',
    inject: [fileURLToPath(import.meta.resolve('jtsx-loader/browser.js'))],
    outfile: 'dist/client.js',
});
bash
npm install --save-dev esbuild
node build-client.mjs
index.html
<div id="client-demo" data-lang="en"></div>
<script type="module" src="./dist/client.js"></script>
factory-config.js
import { createFactory } from 'jtsx-loader/browser.js';
export const { _jsx, _jsxFragment, _jsxUtils } = createFactory({
    rewriteReactAttrs: true,
});
// Use this module as the bundler inject entry instead of browser.js.
04 / 13

Компоненты и layouts

Обычные функции, props и композиция.

Компонент — функция, получающая объект props и возвращающая JSX. Вызов <Layout title="Catalog">...</Layout> передаёт title и вложенное содержимое children. Layout в примере ниже задаёт общую HTML-страницу, а компонент каталога заполняет её содержимое. Для готовой строки вызовите renderToString(await Page({ items })) в серверном коде.

Props передаются компоненту без экранирования: значения остаются данными до сериализации тега. children — [], одно значение или массив. Массивы разворачиваются рекурсивно без добавления пробелов. null, undefined и boolean пропускаются; 0 и bigint сохраняются. Пробелы задавайте явно.

Фрагменты не добавляют DOM-обёртку. Произвольный объект в children вызывает ошибку: выберите его поле или явно используйте JSON.stringify. JSX не устанавливает браузерные обработчики событий; клиентский JS подключается отдельно.

jsx
const Layout = ({ title, children }) => <html lang="en">
    <head><title>{title}</title></head>
    <body>{children}</body>
</html>;

export default ({ items }) => <Layout title="Catalog">
    <h1>Catalog</h1>
    <ul>{items.map(item => <li>{item.name}</li>)}</ul>
    {items.length === 0 && <p>No items</p>}
    <>{'A'}{' '}{'B'}</>
</Layout>;
05 / 13

Асинхронный рендер

Явный await или фабрика, ожидающая вложенные компоненты.

Если компонент загружает данные из API или файла, объявите его async. Для одного такого компонента достаточно явного await; менять конфиг не нужно. Если хотите писать вложенные <Child /> без await в каждом месте, подключите async-фабрику через jtsx.config.js. Это два альтернативных способа, показанных ниже.

Стандартная фабрика синхронна: для async-компонента внутри шаблона используйте {await Child()}. Если Promise попал в children, стандартный рендер сообщит, что его нужно дождаться. Альтернатива — asyncFactory.js в конфиге ниже.

Async-фабрика возвращает Promise с готовой JSX-разметкой, разрешает вложенные массивы и Promise параллельно, сохраняя порядок. Ошибки вложенных компонентов попадают во внешний await. Значения атрибутов и __raw/__escape автоматически не ожидаются: разрешайте их заранее.

jsx
const Child = async () => <b>Ready</b>;
export default async () => <main>{await Child()}</main>;
jtsx.config.js
export default {
    injectFactory: true,
    importFactory: "import { _jsx, _jsxFragment, _jsxUtils } from 'jtsx-loader/factory/asyncFactory.js';",
};
async.jsx
const Child = async () => <b>{await Promise.resolve('<Ready>')}</b>;

export default () => <main>
    <Child />
    {[Promise.resolve('A'), ['B', null, false, 0]]}
</main>;
app-async.js
import { renderToString } from 'jtsx-loader';
const { default: Page } = await import('./async.jsx');
console.log(renderToString(await Page()));
bash
node app-async.js
06 / 13

Атрибуты и CSS

Нативные HTML-имена и явные правила сериализации.

Используйте обычный class прямо в JSX/TSX: <div class="card">. className не нужен — это работает без дополнительных настроек и на сервере, и в браузере. Для совместимости className тоже поддерживается и всегда преобразуется в class.

Для других React-имён по умолчанию выводится предупреждение; rewriteReactAttrs: true включает замену. style принимает строку или объект с нативными CSS-именами. style={null} пропускается.

true сериализуется как "true", false пропускается, null становится "null", undefined создаёт атрибут без значения. Поэтому aria-expanded={false} нужно записывать строкой "false". Функции в атрибутах пропускаются с предупреждением.

attributeParser выбирается по префиксу до двоеточия и возвращает доверенный HTML-фрагмент целиком. Он обязан сам экранировать значения. Имя атрибута берите из кода шаблона.

jsx
<label class="field" for="name">Name</label>
<input id="name" value={'"<&'} disabled={true} />
<div style={{ color: 'red', '--gap': '8px' }} />
jtsx.config.js
import { escapeHtml } from 'jtsx-loader/factory/jsxUtils.js';
export default {
    attributeParser: {
        ac: (name, value) =>
            `data-${name.replaceAll(':', '-')}="${escapeHtml(value)}"`,
    },
};
07 / 13

Конфигурация

jtsx.config.js

Для базового запуска конфиг не нужен. Если хотите изменить настройки, создайте jtsx.config.js рядом с package.json и экспортируйте объект через export default. Запускайте Node из этой папки. После изменения конфига перезапустите процесс: ?reload предназначен для шаблонов, а не для повторной настройки загрузчика.

Конфиг читается из process.cwd(), а не из папки шаблона. Отсутствующий файл допустим. Ошибка существующего файла вызывает предупреждение и fallback к defaults; JTSX_STRICT_CONFIG=1 завершает запуск с ошибкой. Конфиг может выполняться в разных контекстах Node: избегайте побочных эффектов.

При esbuildTransformConfig задайте injectFactory: true либо импортируйте фабрику вручную. Не сочетайте ручной импорт тех же имён с принудительной инъекцией. esbuildTransformConfig поддерживает настройки transform, включая minify и target.

Конфигурация
escapeChildrentrueЭкранировать строки, возвращать JSX-обёртку
escapeAttributestrueЭкранировать обычные значения атрибутов
injectFactory'legacy'Инъекция без esbuildTransformConfig; true — всегда, false — вручную
importFactoryfactory/jsxFactory.jsСтрока импорта _jsx, _jsxFragment, _jsxUtils
esbuildTransformConfignullНастройки esbuild transform
attributeParser{}Обработчики по префиксам атрибутов
disableAttrWarningsfalseОтключить предупреждения React-имён
rewriteReactAttrsfalseЗаменять React-имена нативными
jtsx.config.js
export default {
    injectFactory: true,
    esbuildTransformConfig: { minify: true },
};
powershell
$env:JTSX_STRICT_CONFIG = "1"
node app.js
08 / 13

Express и Fastify

HTML-строка на границе HTTP.

Выберите один сервер. Для Express сохраните server.js ниже и bootstrap.js из раздела «Запуск без --import» рядом с Page.jsx и Counter.jsx. Установите express, запустите node bootstrap.js и откройте http://localhost:3000/. Для Fastify используйте отдельный fastify.js, установите fastify и запустите node fastify.js; bootstrap в этом примере не нужен.

При запросе сервер вызывает Page с данными, ждёт результат и превращает его в HTML-строку через renderToString(). Передавайте в res.send()/reply.send() именно эту строку: JSX-объект фреймворк может принять за JSON. Обработчики ниже возвращают 404 для неизвестного пути и 500 при ошибке рендера.

bash
npm install express
node bootstrap.js
server.js
import express from 'express';
import { renderToString } from 'jtsx-loader/runtime.js';
import Page from './Page.jsx';

const app = express();
app.get('/', async (req, res, next) => {
    try {
        const html = renderToString(await Page({ title: 'Hello' }));
        res.type('html').send(html);
    } catch (error) {
        next(error);
    }
});
app.use((req, res) => res.status(404).send('Not found'));
app.use((error, req, res, next) => {
    console.error(error);
    if (res.headersSent) return next(error);
    res.status(500).send('Internal server error');
});
app.listen(3000);
fastify.js
import Fastify from 'fastify';
import { renderToString } from 'jtsx-loader';
const { default: Page } = await import('./Page.jsx');
const app = Fastify({ logger: true });
app.get('/', async (request, reply) => {
    const html = renderToString(await Page({ title: 'Hello' }));
    return reply.type('text/html').send(html);
});
app.setNotFoundHandler((request, reply) => reply.code(404).send('Not found'));
app.setErrorHandler((error, request, reply) => {
    request.log.error(error);
    reply.code(500).send('Internal server error');
});
await app.listen({ port: 3000 });
bash
npm install fastify
node fastify.js
09 / 13

Статическая генерация

Рендер в файл без HTTP-сервера.

Сохраните static.js рядом с Page.jsx и Counter.jsx из быстрого старта. Команда node static.js создаст dist/index.html и завершится. Откройте этот файл или разместите dist на статическом хостинге. После изменения шаблона запустите генерацию заново: уже записанный HTML сам не обновляется.

Этот пример не требует --import. Не подавляйте ошибки сборки: недоступные данные или ошибка шаблона должны приводить к ненулевому коду выхода. Создание каталога и запись выполняются явно. Для полной страницы добавьте html/head/body в Page.

static.js
import { mkdir, writeFile } from 'node:fs/promises';
import { renderToString } from 'jtsx-loader';

const { default: Page } = await import('./Page.jsx');
await mkdir('dist', { recursive: true });
const html = '<!doctype html>\n' + renderToString(await Page({ title: 'Hello' }));
await writeFile('dist/index.html', html);
bash
node static.js
10 / 13

JSX / TSX

Транспиляция через esbuild, без проверки типов.

Загрузчик обрабатывает .jsx и .tsx. Обычные .ts он передаёт Node; возможность выполнить их зависит от версии Node. Импортируйте файлы с расширением. Проверку типов запускайте отдельно; готовый JSX namespace и полные типы props пока не поставляются. Не считайте успешный запуск TSX проверкой TypeScript.

Card.tsx
type Props = { title: string; count: number };
export default ({ title, count }: Props) => <section>
    <h1>{title}</h1>
    <p>{count}</p>
</section>;
11 / 13

Разработка и диагностика

Как увидеть изменения шаблонов и найти причину ошибки.

Node.js сохраняет результат импорта модуля в памяти. Если сервер продолжает работать, повторный обычный import того же пути возвращает уже загруженный модуль. Изменение файла на диске и обновление вкладки браузера сами по себе не обновляют этот модуль.

Есть два способа увидеть изменения: перезапустить процесс Node.js или повторно импортировать шаблон с ?reload в том же процессе. Автоматическое обновление вкладки браузера — отдельная задача; сам jtsx-loader не следит за файлами и не отправляет браузеру сигнал live reload.

Где писать ?reload

Добавьте ?reload в конец пути внутри динамического import: await import("./Page.jsx?reload"). На диске файл по-прежнему называется Page.jsx. Это параметр импорта, который обрабатывает загрузчик jtsx-loader, а не флаг команды Node и не параметр адреса страницы в браузере. Перед импортом загрузчик должен быть зарегистрирован.

Используйте именно ?reload без значения. Каждый такой вызов создаёт новый экземпляр шаблона и его ESM-зависимостей, включая вложенные JSX/TSX-компоненты и JS-модули. Добавлять ?reload к каждому дочернему импорту не нужно: он распространяется от родительского шаблона. Это не механизм очистки CommonJS require.cache.

Импорт должен выполняться заново при каждом обновлении. Если один раз сохранить Page в переменную, повторный Page(props) продолжит вызывать старую функцию. В reload.js ниже импорт находится внутри renderPage(), поэтому каждый вызов берёт актуальный шаблон.

reload.js
import { renderToString } from 'jtsx-loader';

// Development only: import again for each render, including nested components.
export async function renderPage(props) {
    const { default: Page } = await import('./Page.jsx?reload');
    return renderToString(await Page(props));
}

Проверить обновление вложенного компонента

Сохраните reload.js и dev-server.js рядом с Page.jsx и Counter.jsx из быстрого старта. Запустите node dev-server.js и откройте http://localhost:3000/. Измените текст в Counter.jsx, сохраните файл и обновите страницу. Ответ должен содержать новый текст без перезапуска сервера, даже если Page.jsx не менялся. Сервер использует встроенный node:http, Express не нужен.

Это пример для разработки. Новые импорты не удаляют старые экземпляры из памяти, а зависимости могут повторно выполнять код при загрузке. Для долгих сессий перезапускайте процесс; для production используйте обычные импорты. После изменения jtsx.config.js также перезапустите Node.js.

dev-server.js
import { createServer } from 'node:http';
import { renderPage } from './reload.js';

export const server = createServer(async (request, response) => {
    if (request.url !== '/') {
        response.writeHead(404).end('Not found');
        return;
    }
    try {
        const html = await renderPage({ title: 'Live JSX' });
        response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        response.end(html);
    } catch (error) {
        console.error(error);
        response.writeHead(500).end('Render failed; check the terminal');
    }
}).listen(Number(process.env.PORT || 3000), '127.0.0.1', () => {
    console.log('Development server: http://localhost:' + server.address().port);
});
bash
node dev-server.js

Автоматический перезапуск вашего сервера

Если сохранять процесс не требуется, используйте nodemon с обычным bootstrap.js из примера Express. Он следит за указанными расширениями и перезапускает Node после сохранения файла. ?reload в этом варианте не нужен. После перезапуска обновите страницу браузера; nodemon сам вкладку не обновляет.

bash
npm install --save-dev nodemon
npx nodemon --watch . --ext js,mjs,cjs,json,jsx,tsx --ignore dist/ --ignore build/ bootstrap.js

Запуск самого сайта документации

Следующие команды относятся к отдельной папке jtsx-docs, а не к проекту, в котором установлен пакет. После npm install запустите npm run dev и откройте http://localhost:3001/ru. Сервер перезапускается при изменении исходников; страницу браузера обновляйте отдельно. Для запуска без наблюдения используйте npm start.

npm run build собирает весь сайт в build/: обе языковые страницы, стили, иконки и клиентский пример. Версия в документации берётся из установленного пакета jtsx-loader при сборке. Раздавайте build/ как корень сайта с поддержкой index.html в каталогах. npm start -- --write-html по-прежнему сохраняет только запрошенные страницы. Исходники JSX по HTTP не раздаются.

Сайт устанавливает jtsx-loader из npm registry и не меняет его версию при сборке. Версия зависимости зафиксирована в package.json и package-lock.json; соседняя папка с исходниками модуля не требуется. Для воспроизводимой установки используйте npm ci. После обновления зависимости проверьте сайт командой npm test и повторите npm run build.

bash
npm install
npm run dev

Частые ошибки и способы исправления

Разработка и диагностика
ОшибкаПричинаДействие
ERR_UNKNOWN_FILE_EXTENSIONШаблон импортирован до регистрацииСначала import "jtsx-loader", затем await import("./Page.jsx"); либо запускайте server.js через bootstrap.js.
_jsx is not definedОтключена инъекция фабрикиВ jtsx.config.js задайте injectFactory: true или импортируйте фабрику вручную. Проверьте esbuildTransformConfig.
Теги видны как текстHTML передан обычной строкойНе преобразуйте дочерний JSX в строку. Для доверенной HTML-строки используйте raw(html); renderToString вызывайте перед отправкой всего результата.
[object Promise]Async-значение в старом режимеawait или asyncFactory
Вложенный компонент не обновляетсяСервер использует кешированный импортПовторите await import("./Page.jsx?reload") и используйте новый default, либо перезапустите процесс. Обновления браузера недостаточно.
ERR_MODULE_NOT_FOUNDНе найден пакет или импортируемый файлУстановите зависимость в этом проекте и проверьте путь, регистр букв и расширение .jsx/.tsx.
JTSX TRANSFORM ERRORОшибка синтаксиса шаблона или параметров преобразованияОткройте файл и строку из сообщения в терминале. Исправьте JSX/TSX или esbuildTransformConfig и повторите запуск.
12 / 13

Экранирование и raw()

Строки — текст. raw(value) — явное разрешение на вставку HTML.

По умолчанию текст и значения обычных атрибутов экранируются. JSX-результат хранит признак готовой разметки: вложенные компоненты не экранируются повторно. На границе HTTP, файла или лога вызывайте renderToString(await Page(props)). Не превращайте дочерний JSX в строку до вставки — признак разметки потеряется.

raw() возвращает неизменяемую обёртку и ничего не очищает. Используйте только доверенный или заранее очищенный HTML. В атрибуте raw() не отключает экранирование. __raw и __escape сохранены; __escape не нужно для обычного текста. Уже экранированные строки будут экранированы повторно.

Экранирование не проверяет URL-протоколы, имена динамических тегов/атрибутов, CSS и строковые event handlers. Не передавайте недоверенные структуры props целиком. Для script/style нужна контекстная обработка: raw() допустим для доверенного исходного кода, но не делает вставку пользовательского JavaScript безопасной.

jsx
import { raw } from 'jtsx-loader';

const text = '<b>Hello</b>';
const Text = () => <p>{text}</p>;
const Markup = () => <p>{raw(text)}</p>;
Результат
<p>&lt;b&gt;Hello&lt;&#x2F;b&gt;</p>
<p><b>Hello</b></p>
data.jsx
import { raw } from 'jtsx-loader/runtime.js';

// Escape HTML end tags before opting into raw JSON output.
export default ({ value }) => <script type="application/json">
    {raw(JSON.stringify(value).replaceAll('<', '\\u003c'))}
</script>;
13 / 13

Миграция с 0.1.15–0.1.18

С 0.1.15 можно обновиться напрямую. Версии 0.1.16–0.1.18 сохраняли прежнее поведение; текущий API меняет экранирование по умолчанию и тип результата JSX. Ниже — шаги перехода и различия между этапами.

  1. Замените передачу результата напрямую в HTTP/файл на renderToString(await Page(props)).
  2. Уберите ручной escapeHtml у обычных children и атрибутов. В attributeParser сохраните экранирование: его результат вставляется как готовый фрагмент атрибутов.
  3. Замените намеренную HTML-строку в children на raw(html).
  4. Не склеивайте дочерний JSX через join()/шаблонные строки до рендера.
  5. Учитывайте отсутствие неявных пробелов и сохранение нуля.

Устанавливать каждую промежуточную версию не нужно. При переходе с 0.1.15 проверьте оба этапа ниже; при переходе с 0.1.18 начните с нового контракта рендера.

В этом обновлении сохранены прежние deep-import пути, регистрация через --import, конфигурация, async-фабрика и ?reload. Полный changelog находится в репозитории.

0.1.15 → 0.1.16–0.1.18: совместимые дополнения

Эти версии оставляли строки children и обычные атрибуты неэкранированными, а результат JSX — строкой. Ограничение Node.js изменилось с >=20.16 <25 на >=20.16: API регистрации выбирается автоматически. Команда node --import jtsx-loader app.js и прежние deep-import пути продолжают работать.

Появился injectFactory: true | false | "legacy". Значение "legacy" сохраняет старое правило: фабрика внедряется только без esbuildTransformConfig. При настройке transform включите injectFactory: true либо оставьте ручной импорт фабрики; не сочетайте оба способа. escapeAttributes: true добавлен как опция, которая в 0.1.16–0.1.18 была выключена по умолчанию.

Добавлена отдельная factory/asyncFactory.js, выбираемая через importFactory. Она ожидает вложенные компоненты, Promise и массивы, сохраняет порядок и 0, пропускает null/undefined/boolean и не добавляет пробелы. Обычная фабрика осталась синхронной: обновление само по себе не включает ожидание вложенных async-компонентов.

Ошибочный существующий jtsx.config.js теперь вызывает предупреждение; внешняя переменная JTSX_STRICT_CONFIG=1 делает такую ошибку фатальной. Отсутствующий конфиг допустим. Исправлены обратные слеши в escapeHtml, null во фрагментах, style={null}, JSX/TSX-импорты с query/hash и позиции ошибок. Проверьте снимки HTML, если они зависели от этих ошибок.

async.config.js
export default {
    injectFactory: true,
    importFactory: "import { _jsx, _jsxFragment, _jsxUtils } from 'jtsx-loader/factory/asyncFactory.js';",
};

После 0.1.18: новый контракт рендера

escapeChildren и escapeAttributes теперь true по умолчанию. Нативные теги и фрагменты возвращают неизменяемый HTML-объект; async-фабрика — Promise<Html>. renderToString(await Page(props)) превращает результат в строку на границе HTTP/файла. Обычные строки он экранирует; Promise без await и произвольные объекты вызывают ошибку.

Массивы разворачиваются рекурсивно без неявных пробелов, null/undefined/boolean пропускаются, 0 и bigint сохраняются. Нужный пробел задавайте явно через {" "}. Оставляйте вложенный JSX объектом до финального рендера: join(), конкатенация и шаблонные строки теряют признак готовой разметки.

raw(html) предназначен для доверенного или заранее очищенного HTML и сам ничего не очищает. __raw и __escape сохранены; обычный текст больше не требует __escape. Примеры ниже — альтернативы для server.js, запущенного через node --import jtsx-loader server.js.

Было: 0.1.15–0.1.18
const Page = ({ html }) => <main>{html}</main>;
res.type('html').send(await Page({ html: '<b>Hello</b>' }));
Стало: текущий API
import { raw, renderToString } from 'jtsx-loader';
const Page = ({ html }) => <main>{raw(html)}</main>;
res.type('html').send(renderToString(await Page({ html: '<b>Hello</b>' })));

Новые точки входа

Прежний --import сохранён. Теперь корневой импорт также экспортирует raw и renderToString; register.js позволяет зарегистрировать loader из JS перед динамическим импортом server.js. Для helpers без регистрации используйте runtime.js, особенно внутри jtsx.config.js, чтобы избежать цикла загрузки.

Для общих серверных и клиентских компонентов добавлены browser.js и browserAsync.js. Браузерный JSX собирается заранее; настройки передаются в createFactory(options), Node-конфиг там не читается. При поддержке browser condition сборщик также выбирает браузерные адаптеры для прежних factory/jsxFactory.js и factory/asyncFactory.js.

bootstrap.js
import 'jtsx-loader/register.js';
await import('./server.js');

Поэтапный переход: старый режим

Добавьте настройки ниже в существующий jtsx.config.js, сохранив importFactory, attributeParser и остальные опции. Если в 0.1.16–0.1.18 уже включили escapeAttributes: true, оставьте его: для возврата строк достаточно escapeChildren: false.

В старом режиме отдавайте строки напрямую, без renderToString(): иначе разметка будет экранирована целиком. Синхронная фабрика сохраняет прежние пробелы и обработку falsy-значений; async-фабрика возвращает Promise<string>. Старый режим не защищает пользовательский текст автоматически; для новых проектов используйте новые defaults.

jtsx.config.js
export default {
    escapeChildren: false,
    escapeAttributes: false,
};