[13.11.2023] - Удалена логика beforeReporters, к RemoteReporter теперь применяются filters и extensions
Раньше метод logger.addBeforeReporter использовался для добавления клиентского RemoteReporter, для отправки логов в сервис loggly без базовой фильтрации по уровню (которая настраивается через методы logger.setLevel и logger.enable).
Теперь обработка RemoteReporter встроена в @tinkoff/logger и он работает как обычный reporter, но без базовой фильтрации по уровню логов (но в RemoteReporter всегда была своя фильтрация).
Метод logger.addBeforeReporter теперь под капотом вызывает logger.addReporter.
Поведение, когда и какой лог отправляется в loggly, должно остаться без изменений.
Ломающее изменение, теперь к RemoteReporter применяются фильтры и расширения, это может повлиять на отправку и структуру логов если у вас есть кастомные filters и extensions.
ASYNC_LOCAL_STORAGE_TOKEN интегрирован с LoggerModule и дает удобную возможность обогатить серверные логи юзер-специфичной информацией
@tramvai/cli теперь поддерживает baseUrl и paths из tsconfig.json, убедитесь что ваш tsconfig содержит актуальную конфигурацию алиасов и удалите опцию из tramvai.json
Если вы использовали опцию svgo.plugins в tramvai.json, необходимо сменить формат конфигурации, например если было:
{
"svgo": {
"plugins": [
{
"collapseGroups": false
}
]
}
}Надо заменить на:
{
"svgo": {
"plugins": [
{
"name": "collapseGroups",
"active": false
}
]
}
}Начиная с 2-ой версии, из-за использования fastify, нам необходима поддержка префикса node: при импорте модулей в случае использования библиотеки @tramvai/test-unit. Поддержка префикса есть в jest, но начинается только с версии >=27.1.1, поэтому в случае ошибок при миграции на более свежую версию, может потребоваться обновить и jest в зависимости от вашей текущей версии.
Если вы используете другой тест-раннер, то убедитесь что он поддерживает импорты с перфиксом node: для стандартных модулей Node.js.
Теперь tramvai поддерживает работоспособность для nodejs минимум 16 версии. С указанной версии это делается официально, но скорее всего что-то не будет работать уже в предыдущих релизах на более старых версиях nodejs.
Для обновления:
- заменить образы в ci с nodejs на версию >=16 при использовании tramvai
- при прогоне тестов playwright версия образа playwright должна быть больше
1.17.0
[14.03.2023] v2.79.7 - Изменена логика загрузки переменных из env.development.js и env.js при использовании tramvai static
Команда tramvai static теперь не загружает env.development.js и env.js файлы по-умолчанию. Это сделано потому, что сборка приложения для tramvai static – это продакшн сборка, и если загружать для нее указанные файлы это будет неявным образом влиять на работу самой команды. Если все еще нужно использовать эти файлы при выполнении команды, используйте переменную DANGEROUS_UNSAFE_ENV_FILES=true.
Конфиг полностью переработан, чтобы упростить работу с ним как для пользователей, так и для разработчиков самого tramvai.
Миграция на новый конфиг должна произойти автоматически при обновлении на последние версии tramvai. В случае каких-то проблем или несостыковок в конфиге после миграции используйте возможности IDE по валидации JSON схемы чтобы определить неправильные настройки и вообще доступные настройки и их опции
- Поменяйте настройку
commands.serve.configurations.experiments.transpilationна значениеswc - Запустите сборку и проверьте вывод cli на наличие ворнингов:
- неподдерживаемое свойство
alias- cli теперь умеет работать с tsconfig-paths, поэтому вы теперь можете просто убрать конфиг alias из tramvai.json и задать конфиг в вашемtsconfig.json(скорее всего у вас уже даже задан этот конфиг если вы использовали алиасы) - другие неподдерживаемые свойства - их аналога нет, т.к. это legacy настройки, которые уже не рекомендуются к использованию и желательно от них отказаться
- неподдерживаемое свойство
Необходимо заменить в tramvai.json и Dockerfile вашего приложения все названия старого форка mountebank-fork на новый @sotqa/mountebank-fork
Granular chunking - режим разделения кода, когда создается множество мелких shared чанков для минимального итогового размера кода на отдельных страницах.
Подробнее про конфигурацию режима в гайде Bundle Optimization
Раньше под капотом в @tramvai/react-query использовался react-query@3, теперь же используемой версией становится @tanstack/react-query@4.
Со стороны tramvai мы постарались сохранить все те интерфейсы, которые использовались для задания query. Однако некоторое поведение по умолчанию, возвращаемые результаты и кеширование изменилось в соответствии с изменениями в самой либе.
Для полного понимания, что изменилось при обновлении советую обратиться к документации также учитывайте:
- т.к. поменялось название библиотеки, то все внутренние импорты в tramvai поменялись. Проблемы могут быть только если вы каким-то образом использовали оригинальную библиотеку. В таком случае желательно перейти полностью на аналогичные импорты из
@tramvai/react-query - для формирования query/mutation key всё ещё можно использовать простые строковые значения - tramvai автоматически преобразует их в массив
- обратите внимание что пропали некоторые статусы запроса
- обратите внимание что теперь нельзя использовать undefined в качестве результата запроса
- изменилась логика работы при отсутствии сети. Это может влиять на поведение запросов
- проверьте также другие изменения в changelog если вы используете некоторые редкие фичи react-query
Библиотека express больше не используется в @tramvai/module-server (под капотом используется fastify) и также больше не используется слой обратной совместимости, который позволяет использовать старые токены, завязанные на express.
Подробнее про изменения:
- убраны токены
REQUEST,RESPONSEкоторые предоставляли сущности req, res изexpress. Вместо них предполагается использовать абстракцииREQUEST_MANAGER_TOKENиRESPONSE_MANAGER_TOKEN - больше недоступны токены вида
WEB_APP_*, которые предоставляли доступ к инстансуexpressи позволяли подписаться на разные этапы инициализации сервера, чтобы добавить свою логику кexpress. Полноценной замены нет, в целом не предполагается использование таких сущностей и большая необходимость в них (тем не менее, в tramvai, на самом деле, теперь используются аналогичные токены дляfastify, но мы предпочитаем считать их внутренними деталями реализации, которые недоступны пользователям для использования) - papi также ушло от поддержки
express- рефакторинг papi
Для большинства приложений не ожидается каких-либо ломающих изменений, кроме случаев:
- активного использования papi - в этом случае смотри миграцию для papi
- использование токенов, упомянутых выше, которые больше недоступны - при возможности перейти на доступные абстракции
Теперь Tramvai заточен под Node 14+ и React 18+. И гарантируется работа именно с этими версиями. Пожалуйста обновите версии у себя в репозитории
Смигрировал с initialState который мы закидывали как строку, на прокидывания JSON, для того, чтобы уменьшить количества обходов initialState и пересоздавания строки. Если вы напрямую брали строку window.initialState, то теперь этой переменной не будет и нужно доставать данные из токена INITIAL_APP_STATE_TOKEN
- изменён интерфейс
createPapiMethod:- улучшена типизация
- под капотом используется fastify
- нельзя получить доступ к
req, resзапроса и нужно использовать абстракции из tramvai: REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN (они поставляются автоматически первым аргументом при вызове) - deps теперь можно получить через
this.depsв хендлере - специфичный логер доступен для хендлера через
this.log - удалены многие опции, пока оставлена только опция
timeout
- файловые апи (которые создаются как отдельные файлы в специальной папке которая указывается через tramvai.json в
application.commands.build.options.serverApiDir) теперь тоже должны использоватьcreatePapiMethodдля создания papi (только для них не нужно указыватьpathпараметр т.к. он выводится автоматически) - в backward-compatibility модулях больше не инициализируются старые legacy papi
- Вместо того, чтобы получать параметры запроса из
reqиспользуйте параметры, которые передаются в качестве первого аргументаhandler. Прокидываются общие параметры, которые могут понадобится и тайпинги подскажут что именно доступно. Если что всегда можно использоватьrequestManagerполе, чтобы получить доступ к абстракции REQUEST_MANAGER_TOKEN - для формирования ответа вместо использования
resвозвращайте ответ явно через return в функцииhandler. Чтобы проставить дополнительные параметры запроса можно использоватьresponseManagerиз параметров вызова, которые представляет собой RESPONSE_MANAGER_TOKEN - чтобы получить доступ к
depsиспользуйтеthis.depsвнутриhandler - для логгирования вместо самостоятельно использования
LOGGER_TOKENиспользуйтеthis.logвнутриhandler
Как было:
const provider = {
provide: SERVER_MODULE_PAPI_PRIVATE_ROUTE,
multi: true,
useFactory: () => {
return createPapiMethod({
method: 'post',
path: '/debug-http-request',
async handler({ interceptor, req }) {
const { delay = 10000 } = req.body;
if (delay) {
interceptor.setDelay(delay);
} else {
interceptor.setIntercept(true);
}
return interceptor.getStatus();
},
deps: {
interceptor: INTERCEPTOR_TOKEN,
},
});
},
};Как стало:
const provider = {
provide: SERVER_MODULE_PAPI_PRIVATE_ROUTE,
multi: true,
useFactory: () => {
return createPapiMethod({
method: 'post',
path: '/debug-http-request',
async handler({ body }) {
const { delay = 10000 } = body;
const { interceptor } = this.deps;
this.log.info({
event: 'delay',
delay,
});
if (delay) {
interceptor.setDelay(delay);
} else {
interceptor.setIntercept(true);
}
return interceptor.getStatus();
},
deps: {
interceptor: INTERCEPTOR_TOKEN,
},
});
},
};- вместо экспорта функции делайте
export default createPapiMethod(...) - определение интерфейса аналогично комментариям про интерфейс выше. Единственное отличие - не надо задавать
pathопцию papi т.к. она будет выведена автоматически по пути к файлу - если раньше вы использовали
rootDepsиmapRootDepsто теперь можно использовать только deps и для того, чтобы задать зависимости со скоупомSINGLETONнужно их инициализировать на уровне самого приложения.
Как было:
import { Request, Response } from '@tramvai/papi';
import { CREATE_CACHE_TOKEN } from '@tramvai/module-common';
export const rootDeps = {
createCache: CREATE_CACHE_TOKEN,
};
export const mapRootDeps = ({ createCache }: typeof rootDeps) => {
return {
cache: createCache('memory'),
};
};
export default (req: Request, res: Response, { cache }: ReturnType<typeof mapRootDeps>) => {
const {
body: { a, b },
method,
} = req;
if (method !== 'POST') {
throw new Error('only post methods');
}
if (!a || !b) {
return {
error: true,
message: 'body parameters a and b should be set',
};
}
const key = `${a},${b}`;
if (cache.has(key)) {
return { error: false, fromCache: true, result: cache.get(key) };
}
const result = +a + +b;
cache.set(key, result);
return { error: false, fromCache: false, result };
};Как стало:
import { createPapiMethod } from '@tramvai/papi';
// создаём токен отдельно и провайдим его на уровне приложения
import { PAPI_CACHE_TOKEN } from '../tokens';
export default createPapiMethod({
async handler({ body, requestManager }) {
const { cache } = this.deps;
const method = requestManager.getMethod();
const { a, b } = body;
if (method !== 'POST') {
throw new Error('only post methods');
}
if (!a || !b) {
return {
error: true,
message: 'body parameters a and b should be set',
};
}
const key = `${a},${b}`;
if (cache.has(key)) {
return { error: false, fromCache: true, result: cache.get(key) };
}
const result = +a + +b;
cache.set(key, result);
return { error: false, fromCache: false, result };
},
deps: {
cache: PAPI_CACHE_TOKEN,
},
});Во 2.16.0 версии трамвая удалены следующие библиотеки, связанные с legacy роутингом:
- @tramvai/module-route
- @tramvai/tokens-route
Перед переходом на 2.x.x мажорку, рекомендуем сначала провести миграцию на новые модули роутинга:
@tramvai/module-routeнеобходимо заменить на@tramvai/module-router- Миграция базового модуля@tramvai/tokens-routeнеобходимо заменить на@tramvai/tokens-router
Общий гайд по типизации в tramvai приложениях
Новый хелпер declareAction приходит на смену старому createAction. Старый хелпер помечен как deprecated и не рекомендуется к использованию.
Преимущества нового хелпера:
- улучшения типизации - теперь можно лучше контролировать список аргументов, которые получает экшен: можно указывать несколько аргументов, если аргументов нет, то это теперь нормально обрабатывается при вызове, опциональные аргументы теперь работают правильно при вызове
- функция для логики экшена теперь требует указания только аргументов, которые принимает функции. Нет необходимости указывать аргументы, которые раньше проставлялись при вызове (context, deps) или игнорировать их
- все дополнительные возможности для экшенов теперь передаются через
this(deps, dispatch, getState и т.п.), поэтому для их использования потребуется указывать полноценнуюfunction, а не стрелочную функцию - добавился новый параметр
conditionsFailResultкоторый позволяет определить как должен вести себя экшен при вызове, еслиconditionsдля него не выполняются. По умолчанию, эта настройка ведёт себя также как и раньше - экшен просто резолвится с undefined без фактического выполнения. Теперь также можно указать, что должна быть сгенерирована соответствующая ошибка.
Важно declareAction не совместим со старым ConsumerContext, что может создавать проблемы при расхождении версий tramvai - особенно это актуально для child-app. В этом случае миграция на новую версию tramvai должна быть выполнена сначала в рутовых приложениях и уже когда все перейдут на tramvai@2, child-app смогут использовать declareAction в своём коде
Миграция с createAction должна быть довольно простой, нужно только поменять способ обращения к deps и context, а вместо стрелочной использовать обычную функцию
Было:
export const fetchPokemonAction = createAction({
name: 'fetchPokemon',
fn: async (context, payload: string, deps) => {
const { name } = deps.pageService.getCurrentRoute().params;
const pokemonResponse = await deps.pokeapiHttpClient.get<Pokemon>(
`/pokemon/${payload || name}`
);
context.dispatch(pokemonLoadedEvent(pokemonResponse.payload));
},
deps: {
pokeapiHttpClient: POKEAPI_HTTP_CLIENT,
pageService: PAGE_SERVICE_TOKEN,
},
conditions: {
always: true,
},
});Стало:
export const fetchPokemonAction = declareAction({
name: 'fetchPokemon',
async fn(payload: string) {
// заменили на обычную функцию и оставили только параметры самого экшена
// заменили deps на this.deps
const { name } = this.deps.pageService.getCurrentRoute().params;
const pokemonResponse = await this.deps.pokeapiHttpClient.get<Pokemon>(
`/pokemon/${paylod || name}`
);
// заменили context.dispatch на this.dispatch
this.dispatch(pokemonLoadedEvent(pokemonResponse.payload));
},
deps: {
pokeapiHttpClient: POKEAPI_HTTP_CLIENT,
pageService: PAGE_SERVICE_TOKEN,
},
conditions: {
always: true,
},
});
Примерный вариант рецепта миграции (для jscodeshift)
Пример команды для запуска:
# Из требований: иметь установленный jscodeshift/typescript в проекте
# transformer.ts - файл с кодом трансформера (код ниже)
# ./src/actions/*.ts - какие файлы заденет миграция
# Подробнее ознакомиться можно в документации jscodeshift
node node_modules/.bin/jscodeshift -t ./transformer.ts --extensions=ts --parser=ts ./src/actions/*.tsimport { API, FileInfo } from 'jscodeshift';
// Миграция createAction -> declareAction
// Текущие проблемы: добавляются пробелы перед и после метода fn, при замене поля объекта на метод объекта
function orderTypeParams(j, typeParams) {
const anyTypeNode = j.tsAnyKeyword();
if (!typeParams || typeParams.length < 1)
return j.tsTypeParameterInstantiation([anyTypeNode, anyTypeNode, anyTypeNode]);
if (typeParams.length === 1)
return j.tsTypeParameterInstantiation([anyTypeNode, typeParams[0], anyTypeNode]);
if (typeParams.length === 2)
return j.tsTypeParameterInstantiation([
j.tsTupleType([typeParams[1]]),
typeParams[0],
anyTypeNode,
]);
return j.tsTypeParameterInstantiation([
j.tsTupleType([typeParams[1]]),
typeParams[0],
typeParams[2],
]);
}
function normalizeFnBody({ j, pathToFn, contextName, contextNames, depsName, depsNames }) {
const body = j.withParser('tsx')(pathToFn.body);
body
.find(j.Identifier, (node) => {
if (contextName ? node.name === contextName : contextNames.indexOf(node.name) > -1)
return true;
if (depsName ? node.name === depsName : depsNames.indexOf(node.name) > -1) return true;
return false;
})
.replaceWith((p) => {
if (
j.MemberExpression.check(p.parentPath.value) &&
p.parentPath.value.object.name !== p.value.name
)
return p.value;
if (p.value.name === contextName) return j.thisExpression();
if (contextNames.indexOf(p.value.name) > -1)
return j.memberExpression(j.thisExpression(), p.value);
return j.memberExpression(
j.memberExpression(j.thisExpression(), j.identifier('deps')),
p.value
);
});
// Здесь, так как прокидываем только одну node в парсер в начале функции,
// то и вернется точно только одна node
return body.nodes()[0];
}
function replaceFnMethod(j, p, isMethod = false) {
const resultPath = isMethod ? p.value : p.value.value;
const [ctx, payload, deps] = resultPath.params;
const contextName = ctx && ctx.name;
const contextNames: string[] = [];
if (!contextName && ctx) {
ctx.properties &&
ctx.properties.forEach((el) =>
typeof el.key.name === 'string' ? contextNames.push(el.key.name) : null
);
}
const depsName = deps && deps.name;
const depsNames: string[] = [];
if (!depsName && deps) {
deps.properties &&
deps.properties.forEach((el) =>
typeof el.key.name === 'string' ? depsNames.push(el.key.name) : null
);
}
const body = normalizeFnBody({
j,
pathToFn: resultPath,
contextNames,
contextName,
depsName,
depsNames,
});
return j.objectMethod.from({
kind: 'method',
key: p.value.key,
params: payload ? [payload] : [],
body: j.BlockStatement.check(resultPath.body)
? body
: j.blockStatement([j.returnStatement(body)]),
computed: p.value.computed,
async: resultPath.async,
});
}
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j.withParser('tsx')(file.source);
root
.find(j.Identifier, (node) => node.name === 'createAction')
.replaceWith((p) => {
p.parentPath.value.typeParameters = orderTypeParams(
j,
p.parentPath.value.typeParameters && p.parentPath.value.typeParameters.params
);
return j.identifier('declareAction');
});
root
.find(j.CallExpression, (p) => p.callee.name === 'declareAction')
.find(j.ObjectMethod, (p) => p.key && p.key.name === 'fn')
.replaceWith((p) => replaceFnMethod(j, p, true));
root
.find(j.CallExpression, (p) => p.callee.name === 'declareAction')
.find(
j.ObjectProperty,
(p) => p.value.type === 'ArrowFunctionExpression' && p.key && p.key.name === 'fn'
)
.replaceWith((p) => replaceFnMethod(j, p));
return root.toSource();
}Это в основном внутренняя сущность, которая позволяет теперь отслеживать контекст исполнения разной логики и использовать AbortController для отмены выполнения кода, который по какой-то причине стал неактуален. Раньше же такой код всегда выполнялся и тратил ресурсы.
Применение Execution Context Manager на данный момент:
- при возникновении ошибки при выполнении линий CommandLineRunner вызывается соответствующий
abortController.abort()что позволяет отменить выполнение других функций, которые работают в этих линиях (точнее говоря там, где правильно используетсяabortSignalиз контекста выполнения - а это экшены, запросы и явное использование в других функциях) - в страничных экшенах на сервере - abort будет вызван при превышении лимита выполнения экшенов (хотя те экшены которые будут в процессе выполнения, всё равно продолжат исполняться, но они уже не смогут вызвать новые экшены или выполнять запросы после таймаута)
- при выполнении запросов в рамках commandLine - если выполнение линий было аобртнуто, то все соответствующие запросы в рамках этой линий также будут аборнуты
- Явно внутри экшенов для отмены выполнения текущего экшена - выполняется через вызов
this.abortController.abort()внутри функции изdeclareAction
Явная миграция не требуется.
Можно только использовать новые возможности в плане работы с abortSignal чтобы избегать выполнения лишних действий:
- для commandLineRunner
- для actions
@tramvai/react-query теперь использует внутри себя declareAction вместе с conditionsFailResult: 'reject', что означает, что результатом query которая не может быть выполнена из-за проверки conditions теперь будет ошибка, а не пустые данные как раньше.
Также при использовании авторизационных ролей в качестве conditions, если эти роли поменялись на клиенте, то все активные query, зависящие от ролей, будут перевызваны.
Общий гайд по типизации в tramvai приложениях
Раньше, при использовании useFactory внутри provider, не проверялся тип токена и результата, возвращаемого фабрикой. Например, такая конструкция не выдавала ошибок:
const provider = provide({
provide: NUMBER_TOKEN,
useFactory: () => 'some string',
});И приходилось вручную писать тип токена как результат вызова фабрики:
const provider = provide({
provide: NUMBER_TOKEN,
useFactory: (): typeof NUMBER_TOKEN => 1000,
});Теперь происходит автоматическое сравнение типов результата вызова фабрики и токена. Это изменение может вызвать ошибки проверки типов в вашем приложении, для исправления ошибок, придерживайтесь нескольких правил:
- Не указывайте тип результата вызова функции для
useFactory - Не указывайте типа для аргумента
depsуuseFactory - Не указывайте массив типов для
multiтокенов
Раньше при создании токена с типом string, при получении этого токена из DI, тип полученного значения выводился как any. Примеры проблемных кейсов:
const provider = provide({
provide: NUMBER_TOKEN,
useFactory: ({ fakeNumber }) => {
// тип fakeNumber выводится как `any`, нет ошибок компиляции
return fakeNumber;
},
deps: {
fakeNumber: STRING_TOKEN,
},
});const Component = () => {
const fakeNumber = useDi(STRING_TOKEN);
// тип fakeNumber выводится как `any`, нет ошибок компиляции
return <>{fakeNumber.toFixed()}</>;
};Теперь во всех кейсах корректно выводится тип токена и его значения. При появлении ошибок тайпчекинга в приложении, убедитесь в корректной работе с проблемными токенами.
Раньше приходилось указывать массив значения для multi токена:
const MULTI_TOKEN = createToken<number[]>('multi token', { multi: true });При использовании этого токена для создания провайдера, приходилось вручную получать тип элемента массива:
const provider = provide({
provide: MULTI_TOKEN,
useFactory: (): typeof NUMBER_TOKEN[number] => 1000,
});Сейчас корректные типы выводятся автоматически, и для создания значения, и для работы с зависимостями:
// создаем два мульти токена с одинаковыми типами
const MULTI_TOKEN = createToken<number>('multi token', { multi: true });
const ANOTHER_MULTI_TOKEN = createToken<number>('another multi token', { multi: true });
const provider = provide({
provide: MULTI_TOKEN,
// проверяется, что useFactory возвращает число
// тип deps.anotherNumbers выводится как массив чисел
useFactory: (deps) => {
return deps.anotherNumbers[0] ?? 1000;
},
deps: {
anotherNumbers: ANOTHER_MULTI_TOKEN,
},
});Для решения ошибок с тайпчекингом при использовании multi токенов, рекомендуется несколько шагов:
- Отрефакторить токены, что бы в типах не было массивов, если только они там действительно не нужны
- Убрать ручной вывод типов для провайдеров, такие конструкции как
: typeof MULTI_TOKEN[number]
С этого дня 1.x.x мажорная версия tramvai переходит в состояние Supported, и будет получать только критичные патчи, до 27 декабря 2022г. При этом, в latest теге в npm остается именно 1.x.x версия, до того как 2.x.x мажорка стабилизируется.
2.x.x мажорная версия публикуется с тегом prerelease, и на данный момент не содержит breaking changes, но они планируются в ближайшем будущем.
В этой версии в зависимостях трамвай пакетов обновили все пакеты @tinkoff/request-* на мажорную версию. Что изменилось:
Перешли на lru-cache-nano, облегченный форк lru-cache и на сборку через @tramvai/build По итогу, это немного (2-3kb gzip) уменьшает размер клиентского бандла.
После обновления, стоит убедиться что у вас нет в клиентском коде более старых версий библиотек @tinkoff/request-* - дубликаты могут попасть из транзитивных зависимостей, либо могут быть явно указаны у вас в package.json.
С этого релиза минимально необходимая версии React - >=16.14.0