Доколе
Все любят, чтобы инструменты просто работали. Не works simple, но just works.
А ES6 модули где-то рядом. Нодовские релиз ноуты рапортуют что их поддержка всё стабильней и стабильней, Андрей Мелихов пишет чат на чистой ноде (хорошо ему) с использованием модулей, Axel Rauschmayer пишет отличнейший пост про использование нативных модулей с тайпскриптом...
Пишет то он пишет, и в статье всё гладко. А надо попробовать самому. Но как-то всё находишь для себя оправдания чтобы не начинать. Ведь как оно всегда бывает. Делаешь всё по туториалу, но получаешь ошибки и вместо налаженного процесса получаешь копание в кишках стек-трейсов. Да зачем мне всё это, меня и здесь неплохо кормят.
И вот уже Rauschmayer строит на основе предыдущего проекта монорепозиторий. Надо же иметь моральное право мне и самому высказываться на данную тему. Доколе!.. Ну и что с того что час ночи? Все равно не спишь а пялишься в потолок. Вставай и иди пробуй.
Мы будем жить теперь по-новому
Итак, что принципиально меняется в настройках нашего проекта.
1) в package.json меняем
"main": "index.js" // или что там у вас за точка входа
на
"module": "index.js",
"type": "module"
2) в tsconfig.json проставляем значение
"module": "es2020", // ну или es2015, ну или ESNext
"allowSyntheticDefaultImports": true // если вы этого еще не сделали
Всё, теперь не отвертимся.
Протокол затыков
Естественно за основу возьмём туториал Rauschmayer'а. Ну а чо. Надо экономить мыслетопливо. Собственно помимо правки package.json и tsconfig.json он указывает на необходимость некоторой автоматизации. Дело в том, что в выходных файлах после компиляции тайпскрипта импорты указываются без расширения файла, а для ES6 модуля это указание обязательно. Так что после компиляции нужно пробежаться по полученным файлам, определить что импорт идет из другого файла а не из npm пакета и добавить расширение файла в импорт.
Алекс предоставляет нам регулярку для нахождения таких импортов. Респект и уважуха. Сам бы я её полдня составлял. Терпеть не могу регулярки. Напишите в комментариях если вы от них тащитесь. Но только исполнение этой регулярки падает с ошибкой. Ну я же говорил - всё как всегда. Дело в том что символ / который используется в пути к файлу, вотспринимается интерпретатором как конец регулярного выражения, так что его надо ескейпить. Но тут к Алексу и претензий то больших нет, я его прекрасно понимаю. Напишите в комментариях если вы тащитесь от регулярок.
Итак, ескейпим слэш, и hello world вроде работает. Но не реальный проект. Мы же пишем "тот самый, настоящий бэкенд на nodejs (с)". А предложенная регулярка игнорирует файлы с точками в названии, обозванные в соответствии с принятой в Nest.js конвенцией. Такие как app.module.ts. Ну что ж, с этим мы справимся.
Что ещё можно было бы добавить. Получившаяся в итоге регулярка (конечно же будет приведена в конце статьи) расчитана на одиночные кавычки. Если у вас на проекте приняты двойные кавычки, то регулярка исправляется в одно движение. Если же у вас разброд и шатание, то страдайте. Можете написать универсальный регексп с сохранением символа кавычки в переменную, но эту радость я оставляю вам. А лучше всё же прикрутите линтер.
Не импортом единым
Многим описанных мер будет достаточно. Но в моём конкретном случае нет. Дело в том, что я очень люблю добавлять файл index.ts в папочку с однородными файлами. В индексе экспортировать всё содержание папки, и в остальных местах в импорте указывать путь только для папки а не до конкретного файла. Банальный пример - папка utils (никогда не добавляйте в свой проект папку utils).
Во-первых, с такой практикой нужно будет патчить ещё и строчки с экспортами. Но это конечно не проблема, просто помните об этом. А во-вторых, импорт из папки с индексом (на примере utils) будет преобразован в '../utils.js'
, а такого файла у нас конечно не будет. Можно в ts файлах писать import {} from '../utils/index'
но это лажа. Я хотел писать меньше букв, а получил вот это. К тому же автоподгруженные с помощью IDE импорты руками править надо будет...
Короче, скрипт по патчингу выходных файлов усложняется. Теперь, когда мы нашли новый импорт, нужно понять на что он ссылается - на файл или на папку. И если оказалось что на папку, то переделывать ссылку на файл index.js внутри неё.
U Can Touch This
Ну что ж. Теперь похоже проект дошёл до состояния когда он "just works". Я создал репозиторий с минимальным примером описанной инфраструктуры. Можете выполнить в нём команду build и посмотреть какие сформировались импорты в папочке dist. Скриптец для патчинга скомпилированных файлов лежит в папочке buildtools. В данном скрипте, собственно, и можно найти приснопамятную регулярку.
Что мне нравится, что я могу просто выполнить команду node dist/index.js
и всё заработает. Дело в том что это не первая моя попытка затащить в проект ES6 модули. И не вторая. Когда я где-то пол года назад пробовал, то указание type module давало возможность обозвать файл с расширением js только для входной точки проекта. А остальные файлы которые импортятся, всё равно были обязаны иметь расширение mjs. И запуск команды node всё равно нужно было производить с флагом. Тогда я решил что для затаскивания ES6 модулей нужно совершить неоправданно много телодвижений. А теперь рррас и всё :)
Ах да. Ещё не поздно указать что я использовал ноду версии 14.17.4 в своих экспериментах?
Из неожиданных эффектов - скрипты в папочке buildtools (а на реальном проекте в отличии приведённого минимально примера у меня их больше одного) оказывается тоже нужно теперь писать с использованием ES6 модулей. А я думал что это касается только моего приложения...
Пока нерешенные проблемы
Как всегда, есть нюансы. Вы знаете анекдот про нюанс?
Первое. На проде статика у меня раздаётся nginx'ом. А при локальной разработке приложение раздаёт статику само себе - чтобы поменьше контейнеров запускать. Чтобы не затаскивать раздачу статики на прод, я добавил соответствующие пакеты в devDependencies а в коде приложения всё организовал приблизительно следующим образом:
import { DynamicModule, Module, Type } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import path from 'path';
import { PromoModule } from './promo/promo.module';
const imports: Array<DynamicModule | Type<any>> = [
PromoModule,
ConfigModule.forRoot(configOptions),
TypeOrmModule.forRootAsync(dbConfig),
];
if (process.env.NODE_ENV === 'dev') {
const NestStatic = require('@nestjs/serve-static');
imports.push(
NestStatic.ServeStaticModule.forRoot({
rootPath: path.join(process.cwd(), 'static'),
}),
);
}
@Module({imports})
export class AppModule {}
Теперь же я не могу использовать require, а что касается импорта то
An import declaration can only be used in a namespace or module. ts (1232)
А чтобы эта строчка с импортом не упала на верхнем уровне файла на проде, нужно добавлять @nestjs/serve-static и сопутствующие пакеты в основные зависимости, чего мне не хочется.
Второе. Возможно вы решите что я параноик, но мне очень не хотелось добавлять конфиг подключения к БД в несколько файлов. Я в своё время наелся проблем с тем, что в одном месте конфиг обновили, а в другом забыли. Так что в корень проекта я положил вот такой ormconfig.js:
const dotenv = require('dotenv');
const {error, parsed} = dotenv.config();
if (error !== undefined || parsed === undefined) {
throw error;
}
const options = {
APP_HOST: parsed.APP_HOST,
APP_PORT: parsed.APP_PORT,
ADMIN_PASS: parsed.ADMIN_PASS,
type: parsed.DB_TYPE,
host: parsed.DB_HOST,
port: parseInt(parsed.DB_PORT, 10),
username: parsed.DB_USER,
password: parsed.DB_PASS,
database: parsed.DB_NAME,
autoLoadEntities: false,
synchronize: parsed.DB_SYNC === 'true',
logging: parsed.DB_LOG === 'true',
timezone: parsed.DB_TIME,
migrations: [
`./dist/promo/dal/migrations/*`
],
cli: {
"migrationsDir": `src/promo/dal/migrations`,
},
};
module.exports = options;
Данный конфиг необходим мне в первую очередь для создания и выполнения миграций. А приложение получает конфиг следующим образом:
import { ConfigFactory } from '@nestjs/config';
import path from 'path';
import { validateConfig } from './config_validator';
import { IAppConfig } from './typings';
export const configFactory: ConfigFactory<IAppConfig> = () => {
const ormconfig = require(path.join(process.cwd(), 'ormconfig'));
const config: IAppConfig = {
APP_HOST: ormconfig.APP_HOST,
APP_PORT: parseInt(ormconfig.APP_PORT, 10),
ADMIN_PASS: ormconfig.ADMIN_PASS,
DB_CONFIG: {
type: ormconfig.type,
host: ormconfig.host,
port: ormconfig.port,
username: ormconfig.username,
password: ormconfig.password,
database: ormconfig.database,
synchronize: ormconfig.synchronize,
logging: ormconfig.logging,
timezone: ormconfig.timezone,
},
};
validateConfig(config);
return config;
};
Мне и самому данный подход не вполне нравится, т.к. фактически служебный файл узкоспециализированной тулзы typeorm что-то знает про ADMIN_PASS и APP_PORT. Но зато значения присваиваются 1 раз. Но я сейчас не об этом.
В первую очередь я хочу сказать о том, что опять же я теряю возможность подгрузить ormconfig.js при помощи require. Можно перенести данный файл из корня проекта в src и переделать его в ts, а при запуске скрипта миграции переопределять стандартный путь до конфигурационного файла. А можно не выпендриваться и отдельно прочитать переменные окружения внутри приложения и в ormconfig.js. И то и другое всё равно не сработает т.к. typeorm игнорирует конфиг в формате js, который не содержит commonjs экспорта (а я такой теперь написать не могу) и кладёт/ищет файлы миграций не туда куда я хочу, а прямо в корне проекта.
Счастлив ли я?
Ну хорошо, описанные выше проблемы в принципе не блокируют мне выкатку в прод приложения с ES6 модулями. Раздача статики на проде не нужна и её можно просто выпилить. Переменные окружения можно нормально прочитать в самом приложении, а новые миграции не предвидятся.
Мои импорты стильные-модные-молодёжные, и когда я читаю (часто ли это нужно) скомпиллированный js код то он максимально похож на исходный и не содержит всяких шумных полифиллов. Но только я до сих пор не понимаю, что же я приобретаю, кроме того что я в тренде и на хайпе?
Мой код по синтаксису соответствует тому как это должно быть в браузере? Да плевать на это. Я в принципе пишу на языке который не исполняется в браузере, а во что оно там скомпилируется - не так важно. Люди вообще всё обфусцируют через вебпак и ничего, не страдают от нечитаемости выходного файла. Появилась возможность шарить код между фронтом и бэком? У меня такой необходимости не возникало. Расскажите если у вас были такие кейсы.
Мне вот всё любопытно посмотреть, не упадёт ли потребление памяти после внедрения ES6 модулей, но до этого пока руки не дошли. В любом случае не думаю что это будут значения которые что-то кардинально поменяют (по крайней мере если у вас не 1500 микросервисов).
Так что пока у меня сменанные чувства. Наконец-то всё нормально заработало. И чо?
А что ты?
Напишите, как вы относитесь к возможности писать ES6 модули в ноде, какой профит от этого получаете и какие эмоции это вызывает у вас. Будет интересно узнать мнение умных людей :)
UPD (30.09.2021)
Давно хотел выкатить то приложение, на котором тренировался, на прод с ES6 модулями. Да что-то руки не доходили. И вот дошли. Приложение очень маленькое, потому что нишевое. За несколько месяцев в нём зарегистрировалось всего 6 пользователей. Так что всё потребление памяти уходит на поддержание своего собственного пассивного состояния.
И вот всё же руки дошли. С commonjs модулями приложение потребляло 49 мегабайт оперативки, с ES6 модулями потребляет 75. Кроме импортов в приложении ничего не поменялось. Приложение провисело 2 дня. Возможно конечно через месяц работы пройдёт какая-нибудь JIT оптимизация.
Для системы с регулярной пользовательской активностью эта разница в потреблении памяти, думаю, растворится в общем объёме. Но что есть то есть.
Комментарии (24)
mytecor
01.08.2021 21:07+1Все проблемы можно решить используя
nodemon --loader ts-node/esm -e ts -q --no-warnings
Ну и вписать в tsconfig
"moduleResolution": "node", "esModuleInterop": true,
Единственное, что пока нужно указывать в импортах расширения .js
muturgan Автор
01.08.2021 21:08Вы такой командой приложение и на проде планируете запускать?
Akuma
01.08.2021 21:15Использую ts-node на проде(transpile only). Вообще отлично. Оверхед в виде +10сек на старт скрипта(и то речь про тяжелые вещи), который происходит только при выкате новой версии и +50Мб памяти вообще не волнует. Специально на выходных тестировал tsc против ts-node. Не стоит оно того.
Зато офигеть как удобно работать напрямую с TS файлами. Это при том, что у меня половина кода используется и на клиенте и на сервере. Чисто для сервера берите не задумываясь.
А ещё тесты сразу на TS (jest) - тоже удобно
muturgan Автор
01.08.2021 21:26Ну что ж, это ваш подход и вы имеете на него полное право)
Но я предпочитаю затаскивать в проект как можно меньше прослоек.
Хотя о чем это я. Ведь мой скрипт из buildtools и есть аналогичная прослойка. Правда у меня над ней есть контроль.Akuma
01.08.2021 21:37На самом деле им много кто пользуется, судя по репозиторию.
На счёт прослойки вы правы. Но я точно так же не смотрю что там сбилдилось в JS, так что получается аналогично. Просто транспиляция происходит на лету.
Современный JS это одни прослойки ))) Без них уже давно ничего не работает.
muturgan Автор
01.08.2021 21:54Да я сам им пользуюсь! При локальной разработке.
Но по поводу "не смотрю во что сбилдилось" вы абсолютно правы.
Так что получается сам тайпскрипт является такой нежелательной прослойкой.
От неё я правда пока отказаться не готов.Akuma
01.08.2021 22:05-1Deno ещё смотрел. Но они чёт слишком радикально отрезали «старые» возможности и там половина npm не работает, судя по докам.
mytecor
01.08.2021 23:45почему нет? данная команда просто заменяет встроенный лодер на тс
таким образом получаем небольшую задержку на этапе импортов, но потом нода работает с обычной скоростью (подробнее тут)
рядом можно разместить сборку обычным tsc ничего не меняя, однако в таком случае ломаются ссылочные импорты через # и придётся городить костыль
muturgan Автор
02.08.2021 11:02На самом деле я задал неправильный вопрос. Какой командой вы стартуете приложение на проде и отличается ли она от команды node это тема отдельной дискуссии.
ведь моя проблема не в том что я не могу заимпортить какие-то модули. с этим проблем нет.
раньше я использовал require и мог подгрузить пакет @nestjs/serve-staticвнутри блока if и делать это только при локальной разработке. сейчасмой мой новый классный импорт обязан быть на верхнем уровне файла и мне придётся переносить @nestjs/serve-staticиз дев в основные зависимости. Как эту проблему решает то что мы запускаем приложение с использованием ts-node лоадера?ormconfig.js обязан быть ES6 модулем как и все остальные файлы в проекте. И теперь, когда я вызываю команду по генерации миграции, typeorm игнорирует данный конфиг и кладёт файл с новой миграцией в корень проекта вместо той папочки которая указана в конфиге. Как эту проблему решает то что мы запускаем приложение с использованием ts-node лоадера?
Извиняюсь, если в тексте статьи я недостаточно четко сформулировал проблемы.
mxmvshnvsk
02.08.2021 12:52+1Почему не использовать динамический импорт?
muturgan Автор
02.08.2021 17:25как полезно бывает взглянуть на проблему незамыленным глазом.
динамический импорт хорошо подходит, спасибо!
mytecor
02.08.2021 19:42Нодемон вполне может работать и в продакшне
И да, статику не следует отдавать нодой, для этого есть nginx
muturgan Автор
02.08.2021 20:53вы невнимательно читали статью либо коментарии
на проде статика отдается при помощи nginx
а при локальной разработке, чтобы поднимать меньше контейнеров
динамически добавляется ещё один nest.js модуль для раздачи статики
lorus
02.08.2021 08:25+2А почему вы не используете бандлер? Он бы решил проблемы с импортами без всяких регулярных выражений.
muturgan Автор
02.08.2021 10:25Замечание резонное.
Наверно по инерции.
Я считаю в бандлерах для бэка нет необходимости - это для клиента важно сколько будет весить сборка и сколькими чанками она приедет. На бэке же я могу себе позволить повторить исходную файловую структуру. И даже проще потом разбираться будет.Моя идея была следующая (возможно я на ней мало акцентировался) - раньше у меня всё "всего лишь" работало. Интересно, если я сейчас затащу в проект ES6 модули, продолжит ли у меня всё "всего лишь" работать?
Оказалось что нет. Нужно что-то с этим делать. Но что? Затаскивать в проект лишнюю зависимость, работу которой я не контролирую мне не хотелось (возможно вы заметили что у меня бзик по поводу уменьшения зависимостей). Так что наверно я просто не стал выходить из зоны комфорта))
khegay
02.08.2021 09:43А вы смотрели в сторону joi и @nestjs/config для конфига?
ConfigModule.forRoot({ validationSchema: Joi.object({ PORT: Joi.number().required(), JWT_SECRET: Joi.string().required(), JWT_EXPIRATION_TIME: Joi.number().required(), DB_URL: Joi.string().required(), }), isGlobal: true })
muturgan Автор
02.08.2021 10:01Смотрел.
если вы в принципе про наличие валидации конфига, то она у меня есть - перед ретюрном есть вызов validateConfig(config). Что там происходит - в рамках данной статьи неважно.А по поводу joi - после моего issue в опции
ConfigModule
добавили параметр validate с кастомной функцией валидации. Раньше была только возможность передать validationSchema со схемой joiЛично мне кажется странной идея затаскивать в проект ещё одну зависимость (которая за собой так же тянет кучу зависимостей) ради одного вызова при старте приложения, если у меня все дто-шки валидируются через class-validator
RiverFlow
02.08.2021 14:42-2А какой профит у import перед require ?
Опуская декор - остаётся возможность
"частичного импорта" это жы так крутааа, не тянуть всю либу а только "нужные классы".
Но вот вам интересная мысль: что если плюнуть на "частичный импорт руками" и испортить все :
import * as kissmyass from '/zhopasruchkoy'
А потом запускать какую-нибудь приблуду к веб-паку которая будет включать в реальный бандл только реально импортируемые вещи ?
Импорт руками это такое же щасте как ручная подсветка кода или проставки парных скобок.
Но в итоге "частичный" импорт выливается а полотенце "import" ов, иногда просто идиотических размеров!!!
И тут вопрос а зачем тогда import если " import * " это и есть require !?
Буду искренне благодарен за предметное объяснение (без воды и пены) и да, аргумент "патамушта тайпскрипт" - не аргумент, тайпскриптеров итак скоро пойдут подъезды мыть чтобы искупить все то гавно которым они засрали мир современной веб-разработки.
И у меня сильное подозрение что весь этот исход на import - одно из таких говн
muturgan Автор
02.08.2021 17:45Просто интересно было бы услышать ваше мнение)
А чем вам ts так не угодил?
muturgan Автор
02.08.2021 17:35Я конечно понимаю что тришейкинг заботит очень многих. Но не меня, т.к. на стороне сервера размер бандла не является проблемой.
Но в целом вопросы, которые вы задаёте, возникают и у меня.
Вот я потратил некоторое количество усилий, поправил некоторое количество нестыковок, натянул сову на глобус, и теперь у меня ES6 модули!
А профит то какой?Было прикольно этим заниматься, но на вопрос о профите надо найти ответ. Потому что этот вопрос, например, задаст команда, если ей предложить затащить это счастье на основной рабочий проект. А там уже кодовая база сильно больше и зависимости хитроумней. Так что для миграции нужно будет потратитьв разы больше усилий. И усилия эти обосновать пока не получается.
Так что личто я комментарий заплюсовал.
eshimischi
02.08.2021 18:01+1Откуда открыл ссылку только что убей не помню, но вот вместе с комментариями на тему статьи Gist Github ESM
aamonster
А вместо слэша в регэкспах другой символ нельзя? sed умеет (доводилось пользоваться, чтобы не усложнять скрипты кучей эскейпов), может, и в js можно?
Upd: вроде если регеэкспы создавать через конструктор – эскейпить не надо.