Быстрый старт
JSX и TSX в Node.js без React. Одна HTML-фабрика для сервера и браузера.
Требования: Node.js ≥20.16 · ESM
01Установка
02Первый компонент
Создайте Page.jsx, Counter.jsx и app.js в одной папке.
import Counter from './Counter.jsx';
export default ({ title }) => <main>
<h1>{title}</h1>
<Counter count={3} />
</main>;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>
);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);<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.jsHTML появится в терминале. Чтобы открыть страницу в браузере, подключите Express или Fastify.
Нужны Node.js ≥20.16 и ESM ("type": "module" в package.json). В Node.js loader подключает .jsx/.tsx через импорт и преобразует синтаксис с помощью esbuild. Для браузера компоненты собираются в обычный JavaScript.
Запуск без --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, чтобы избежать цикла загрузки.
import 'jtsx-loader/register.js';
await import('./server.js');node bootstrap.js
# Or the original preload path:
node --import jtsx-loader server.jsФабрика в браузере
Один 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.
Один компонент. Две среды.
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();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',
});npm install --save-dev esbuild
node build-client.mjs<div id="client-demo" data-lang="en"></div>
<script type="module" src="./dist/client.js"></script>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.Компоненты и 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 подключается отдельно.
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>;Асинхронный рендер
Явный await или фабрика, ожидающая вложенные компоненты.
Если компонент загружает данные из API или файла, объявите его async. Для одного такого компонента достаточно явного await; менять конфиг не нужно. Если хотите писать вложенные <Child /> без await в каждом месте, подключите async-фабрику через jtsx.config.js. Это два альтернативных способа, показанных ниже.
Стандартная фабрика синхронна: для async-компонента внутри шаблона используйте {await Child()}. Если Promise попал в children, стандартный рендер сообщит, что его нужно дождаться. Альтернатива — asyncFactory.js в конфиге ниже.
Async-фабрика возвращает Promise с готовой JSX-разметкой, разрешает вложенные массивы и Promise параллельно, сохраняя порядок. Ошибки вложенных компонентов попадают во внешний await. Значения атрибутов и __raw/__escape автоматически не ожидаются: разрешайте их заранее.
const Child = async () => <b>Ready</b>;
export default async () => <main>{await Child()}</main>;export default {
injectFactory: true,
importFactory: "import { _jsx, _jsxFragment, _jsxUtils } from 'jtsx-loader/factory/asyncFactory.js';",
};const Child = async () => <b>{await Promise.resolve('<Ready>')}</b>;
export default () => <main>
<Child />
{[Promise.resolve('A'), ['B', null, false, 0]]}
</main>;import { renderToString } from 'jtsx-loader';
const { default: Page } = await import('./async.jsx');
console.log(renderToString(await Page()));node app-async.jsАтрибуты и 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-фрагмент целиком. Он обязан сам экранировать значения. Имя атрибута берите из кода шаблона.
<label class="field" for="name">Name</label>
<input id="name" value={'"<&'} disabled={true} />
<div style={{ color: 'red', '--gap': '8px' }} />import { escapeHtml } from 'jtsx-loader/factory/jsxUtils.js';
export default {
attributeParser: {
ac: (name, value) =>
`data-${name.replaceAll(':', '-')}="${escapeHtml(value)}"`,
},
};Конфигурация
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.
| escapeChildren | true | Экранировать строки, возвращать JSX-обёртку |
|---|---|---|
| escapeAttributes | true | Экранировать обычные значения атрибутов |
| injectFactory | 'legacy' | Инъекция без esbuildTransformConfig; true — всегда, false — вручную |
| importFactory | factory/jsxFactory.js | Строка импорта _jsx, _jsxFragment, _jsxUtils |
| esbuildTransformConfig | null | Настройки esbuild transform |
| attributeParser | {} | Обработчики по префиксам атрибутов |
| disableAttrWarnings | false | Отключить предупреждения React-имён |
| rewriteReactAttrs | false | Заменять React-имена нативными |
export default {
injectFactory: true,
esbuildTransformConfig: { minify: true },
};$env:JTSX_STRICT_CONFIG = "1"
node app.jsExpress и 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 при ошибке рендера.
npm install express
node bootstrap.jsimport 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);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 });npm install fastify
node fastify.jsСтатическая генерация
Рендер в файл без HTTP-сервера.
Сохраните static.js рядом с Page.jsx и Counter.jsx из быстрого старта. Команда node static.js создаст dist/index.html и завершится. Откройте этот файл или разместите dist на статическом хостинге. После изменения шаблона запустите генерацию заново: уже записанный HTML сам не обновляется.
Этот пример не требует --import. Не подавляйте ошибки сборки: недоступные данные или ошибка шаблона должны приводить к ненулевому коду выхода. Создание каталога и запись выполняются явно. Для полной страницы добавьте html/head/body в Page.
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);node static.jsJSX / TSX
Транспиляция через esbuild, без проверки типов.
Загрузчик обрабатывает .jsx и .tsx. Обычные .ts он передаёт Node; возможность выполнить их зависит от версии Node. Импортируйте файлы с расширением. Проверку типов запускайте отдельно; готовый JSX namespace и полные типы props пока не поставляются. Не считайте успешный запуск TSX проверкой TypeScript.
type Props = { title: string; count: number };
export default ({ title, count }: Props) => <section>
<h1>{title}</h1>
<p>{count}</p>
</section>;Разработка и диагностика
Как увидеть изменения шаблонов и найти причину ошибки.
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(), поэтому каждый вызов берёт актуальный шаблон.
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.
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);
});node dev-server.jsАвтоматический перезапуск вашего сервера
Если сохранять процесс не требуется, используйте nodemon с обычным bootstrap.js из примера Express. Он следит за указанными расширениями и перезапускает Node после сохранения файла. ?reload в этом варианте не нужен. После перезапуска обновите страницу браузера; nodemon сам вкладку не обновляет.
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.
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 и повторите запуск. |
Экранирование и 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 безопасной.
import { raw } from 'jtsx-loader';
const text = '<b>Hello</b>';
const Text = () => <p>{text}</p>;
const Markup = () => <p>{raw(text)}</p>;<p><b>Hello</b></p>
<p><b>Hello</b></p>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>;Миграция с 0.1.15–0.1.18
С 0.1.15 можно обновиться напрямую. Версии 0.1.16–0.1.18 сохраняли прежнее поведение; текущий API меняет экранирование по умолчанию и тип результата JSX. Ниже — шаги перехода и различия между этапами.
- Замените передачу результата напрямую в HTTP/файл на renderToString(await Page(props)).
- Уберите ручной escapeHtml у обычных children и атрибутов. В attributeParser сохраните экранирование: его результат вставляется как готовый фрагмент атрибутов.
- Замените намеренную HTML-строку в children на raw(html).
- Не склеивайте дочерний JSX через join()/шаблонные строки до рендера.
- Учитывайте отсутствие неявных пробелов и сохранение нуля.
Устанавливать каждую промежуточную версию не нужно. При переходе с 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, если они зависели от этих ошибок.
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.
const Page = ({ html }) => <main>{html}</main>;
res.type('html').send(await Page({ html: '<b>Hello</b>' }));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.
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.
export default {
escapeChildren: false,
escapeAttributes: false,
};