Доколе

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


  1. aamonster
    01.08.2021 18:31
    +1

    А вместо слэша в регэкспах другой символ нельзя? sed умеет (доводилось пользоваться, чтобы не усложнять скрипты кучей эскейпов), может, и в js можно?

    Upd: вроде если регеэкспы создавать через конструктор – эскейпить не надо.


  1. mytecor
    01.08.2021 21:07
    +1

    Все проблемы можно решить используя

    nodemon --loader ts-node/esm -e ts -q --no-warnings

    Ну и вписать в tsconfig

    "moduleResolution": "node",
    "esModuleInterop": true,

    Единственное, что пока нужно указывать в импортах расширения .js


    1. muturgan Автор
      01.08.2021 21:08

      Вы такой командой приложение и на проде планируете запускать?


      1. Akuma
        01.08.2021 21:15

        Использую ts-node на проде(transpile only). Вообще отлично. Оверхед в виде +10сек на старт скрипта(и то речь про тяжелые вещи), который происходит только при выкате новой версии и +50Мб памяти вообще не волнует. Специально на выходных тестировал tsc против ts-node. Не стоит оно того.

        Зато офигеть как удобно работать напрямую с TS файлами. Это при том, что у меня половина кода используется и на клиенте и на сервере. Чисто для сервера берите не задумываясь.

        А ещё тесты сразу на TS (jest) - тоже удобно


        1. muturgan Автор
          01.08.2021 21:26

          Ну что ж, это ваш подход и вы имеете на него полное право)
          Но я предпочитаю затаскивать в проект как можно меньше прослоек.
          Хотя о чем это я. Ведь мой скрипт из buildtools и есть аналогичная прослойка. Правда у меня над ней есть контроль.


          1. Akuma
            01.08.2021 21:37

            На самом деле им много кто пользуется, судя по репозиторию.

            На счёт прослойки вы правы. Но я точно так же не смотрю что там сбилдилось в JS, так что получается аналогично. Просто транспиляция происходит на лету.

            Современный JS это одни прослойки ))) Без них уже давно ничего не работает.


            1. muturgan Автор
              01.08.2021 21:54

              Да я сам им пользуюсь! При локальной разработке.
              Но по поводу "не смотрю во что сбилдилось" вы абсолютно правы.
              Так что получается сам тайпскрипт является такой нежелательной прослойкой.
              От неё я правда пока отказаться не готов.


              1. Akuma
                01.08.2021 22:05
                -1

                Deno ещё смотрел. Но они чёт слишком радикально отрезали «старые» возможности и там половина npm не работает, судя по докам.


      1. mytecor
        01.08.2021 23:45

        почему нет? данная команда просто заменяет встроенный лодер на тс

        таким образом получаем небольшую задержку на этапе импортов, но потом нода работает с обычной скоростью (подробнее тут)

        рядом можно разместить сборку обычным tsc ничего не меняя, однако в таком случае ломаются ссылочные импорты через # и придётся городить костыль


        1. muturgan Автор
          02.08.2021 11:02

          На самом деле я задал неправильный вопрос. Какой командой вы стартуете приложение на проде и отличается ли она от команды node это тема отдельной дискуссии.

          ведь моя проблема не в том что я не могу заимпортить какие-то модули. с этим проблем нет.
          раньше я использовал require и мог подгрузить пакет @nestjs/serve-staticвнутри блока if и делать это только при локальной разработке. сейчасмой мой новый классный импорт обязан быть на верхнем уровне файла и мне придётся переносить @nestjs/serve-staticиз дев в основные зависимости. Как эту проблему решает то что мы запускаем приложение с использованием ts-node лоадера?

          ormconfig.js обязан быть ES6 модулем как и все остальные файлы в проекте. И теперь, когда я вызываю команду по генерации миграции, typeorm игнорирует данный конфиг и кладёт файл с новой миграцией в корень проекта вместо той папочки которая указана в конфиге. Как эту проблему решает то что мы запускаем приложение с использованием ts-node лоадера?

          Извиняюсь, если в тексте статьи я недостаточно четко сформулировал проблемы.


          1. mxmvshnvsk
            02.08.2021 12:52
            +1

            Почему не использовать динамический импорт?


            1. muturgan Автор
              02.08.2021 17:25

              как полезно бывает взглянуть на проблему незамыленным глазом.
              динамический импорт хорошо подходит, спасибо!


          1. eshimischi
            02.08.2021 15:14

            Добавлю статьи на тему дин импортинга: Один Два


          1. mytecor
            02.08.2021 19:42

            Нодемон вполне может работать и в продакшне

            И да, статику не следует отдавать нодой, для этого есть nginx


            1. muturgan Автор
              02.08.2021 20:53

              вы невнимательно читали статью либо коментарии
              на проде статика отдается при помощи nginx
              а при локальной разработке, чтобы поднимать меньше контейнеров
              динамически добавляется ещё один nest.js модуль для раздачи статики


              1. mytecor
                03.08.2021 02:28
                +1

                действительно, не вникал в суть


  1. lorus
    02.08.2021 08:25
    +2

    А почему вы не используете бандлер? Он бы решил проблемы с импортами без всяких регулярных выражений.


    1. muturgan Автор
      02.08.2021 10:25

      Замечание резонное.
      Наверно по инерции.
      Я считаю в бандлерах для бэка нет необходимости - это для клиента важно сколько будет весить сборка и сколькими чанками она приедет. На бэке же я могу себе позволить повторить исходную файловую структуру. И даже проще потом разбираться будет.

      Моя идея была следующая (возможно я на ней мало акцентировался) - раньше у меня всё "всего лишь" работало. Интересно, если я сейчас затащу в проект ES6 модули, продолжит ли у меня всё "всего лишь" работать?

      Оказалось что нет. Нужно что-то с этим делать. Но что? Затаскивать в проект лишнюю зависимость, работу которой я не контролирую мне не хотелось (возможно вы заметили что у меня бзик по поводу уменьшения зависимостей). Так что наверно я просто не стал выходить из зоны комфорта))


  1. 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
    })
    


    1. muturgan Автор
      02.08.2021 10:01

      Смотрел.
      если вы в принципе про наличие валидации конфига, то она у меня есть - перед ретюрном есть вызов validateConfig(config). Что там происходит - в рамках данной статьи неважно.

      А по поводу joi - после моего issue в опцииConfigModule добавили параметр validate с кастомной функцией валидации. Раньше была только возможность передать validationSchema со схемой joi

      Лично мне кажется странной идея затаскивать в проект ещё одну зависимость (которая за собой так же тянет кучу зависимостей) ради одного вызова при старте приложения, если у меня все дто-шки валидируются через class-validator


  1. RiverFlow
    02.08.2021 14:42
    -2

    А какой профит у import перед require ?

    Опуская декор - остаётся возможность

    "частичного импорта" это жы так крутааа, не тянуть всю либу а только "нужные классы".

    Но вот вам интересная мысль: что если плюнуть на "частичный импорт руками" и испортить все :

    import * as kissmyass from '/zhopasruchkoy'

    А потом запускать какую-нибудь приблуду к веб-паку которая будет включать в реальный бандл только реально импортируемые вещи ?

    Импорт руками это такое же щасте как ручная подсветка кода или проставки парных скобок.

    Но в итоге "частичный" импорт выливается а полотенце "import" ов, иногда просто идиотических размеров!!!

    И тут вопрос а зачем тогда import если " import * " это и есть require !?

    Буду искренне благодарен за предметное объяснение (без воды и пены) и да, аргумент "патамушта тайпскрипт" - не аргумент, тайпскриптеров итак скоро пойдут подъезды мыть чтобы искупить все то гавно которым они засрали мир современной веб-разработки.

    И у меня сильное подозрение что весь этот исход на import - одно из таких говн


    1. muturgan Автор
      02.08.2021 17:45

      Просто интересно было бы услышать ваше мнение)
      А чем вам ts так не угодил?


  1. muturgan Автор
    02.08.2021 17:35

    Я конечно понимаю что тришейкинг заботит очень многих. Но не меня, т.к. на стороне сервера размер бандла не является проблемой.

    Но в целом вопросы, которые вы задаёте, возникают и у меня.
    Вот я потратил некоторое количество усилий, поправил некоторое количество нестыковок, натянул сову на глобус, и теперь у меня ES6 модули!
    А профит то какой?

    Было прикольно этим заниматься, но на вопрос о профите надо найти ответ. Потому что этот вопрос, например, задаст команда, если ей предложить затащить это счастье на основной рабочий проект. А там уже кодовая база сильно больше и зависимости хитроумней. Так что для миграции нужно будет потратитьв разы больше усилий. И усилия эти обосновать пока не получается.

    Так что личто я комментарий заплюсовал.


  1. eshimischi
    02.08.2021 18:01
    +1

    Откуда открыл ссылку только что убей не помню, но вот вместе с комментариями на тему статьи Gist Github ESM