Pull to refresh

Мой опыт перевода typescript проекта на ESM

Reading time7 min
Views8.1K

Доколе

Все любят, чтобы инструменты просто работали. Не 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 оптимизация.

Для системы с регулярной пользовательской активностью эта разница в потреблении памяти, думаю, растворится в общем объёме. Но что есть то есть.

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments24

Articles