Доколе
Все любят, чтобы инструменты просто работали. Не 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 оптимизация.
Для системы с регулярной пользовательской активностью эта разница в потреблении памяти, думаю, растворится в общем объёме. Но что есть то есть.