Недавно некоторые npm-пакеты, которые я использовал вместе с Serverless Framework, обновились до «чистого ESM». Это означало, что они больше не поддерживают старый синтаксис CommonJS require
, и мне нужно было выяснить, как заставить Webpack снова собрать все в пакет, чтобы развернуть его как лямбду CommonJS AWS.
Мой собственный код также использовал современные операторы импорта/экспорта в стиле ES6:
import { PrismaClient } from '@prisma/client';
import { map } from 'lodash';
import prettyMilliseconds from 'pretty-ms';
const prisma = new PrismaClient();
export const animals = async () => {
const startTime = new Date();
const frens = await prisma.animal.findMany();
return {
statusCode: 200,
body: JSON.stringify(
{
frens: map(frens, 'name').join(' & '),
runTime:
prettyMilliseconds(new Date() - startTime),
},
null,
2
),
};
};
Поскольку я использовал serverless-webpack вместе с помощником Prisma serverless-webpack-prisma, потребовалось немного дополнительных настроек конфигурации Webpack, чтобы все это использовало один и тот же синтаксис модуля без обычной страшной ошибки «Unexpected token export» или import error от Node.
Во-первых, мы указываем serverless-webpack включать модули npm в создаваемый им пучок в serverless.yml
:
custom:
webpack:
includeModules: true
Далее мы указываем Webpack создать цель CommonJS в webpack.config.js
. Я выбрал в качестве цели commonjs2
, но commonjs
тоже подойдет:
output: {
libraryTarget: 'commonjs2',
filename: '[name].js',
path: path.resolve(__dirname, '.webpack'),
}
Затем мы используем babel-loader с Webpack для транспиляции синтаксиса ES6+ (включая мой собственный код) в CommonJS.
Обычно вы говорите babel-loader исключить из транспиляции все, что находится в папке node_modules
, поскольку в противном случае это замедлит весь процесс пакетирования. В нашем случае, однако, мы хотим исключить только все, кроме тех пакетов npm, которые перешли на чистый ESM, потому что мы хотим, чтобы Babel все равно транспонировал их в CommonJS.
Мы могли бы создать сложное регулярное выражение для определения этого условия exclude
, но babel-loader-exclude-node-modules-except здесь очень кстати. Мы получаем удобный для чтения массив всех затронутых модулей:
const babelLoaderExcludeNodeModulesExcept
= require('babel-loader-exclude-node-modules-except');
// ...
module: {
rules: [
{
test: /.js$/,
exclude: babelLoaderExcludeNodeModulesExcept(
['pretty-ms', 'parse-ms']
),
loader: 'babel-loader',
},
],
}
Вы можете просто пропустить опцию exclude
, тогда все будет транспонировано, но этот способ лучше для производительности.
Я знал, что pretty-ms недавно перешел на чистый ESM, но важно также определить все его зависимости, которые тоже являются чистыми ESM (например, parse-ms), иначе мы столкнемся с этой ошибкой:
{
"errorMessage": "require() of ES Module /Users/joostschuur/Code/Personal/_Tests/serverless-prisma-esm/node_modules/parse-ms/index.js from /Users/joostschuur/Code/Personal/_Tests/serverless-prisma-esm/.webpack/service/src/handler.js not supported.nInstead change the require of index.js in /Users/joostschuur/Code/Personal/_Tests/serverless-prisma-esm/.webpack/service/src/handler.js to a dynamic import() which is available in all CommonJS modules."
}
Один из способов сделать это — просто продолжать добавлять новые модули в список, основываясь на их названии в сообщении об ошибке, пока вы не перестанете получать ошибку.
В конце концов, я пошел и автоматизировал и эту часть, используя webpack-node-module-types. Подробности в ответе на мой собственный вопрос на Stack Overflow.
Наконец, нам также нужно убедиться, что Webpack не игнорирует наши транспонированные пакеты через настройку externals. Даже если Babel транспонировал их, обычно используется webpack-node-externals для создания списка externals для нас. По умолчанию в него будет включено все из node_modules
. Поскольку внешние модули не включаются в пакет, это означает, что наши транспилированные чистые ESM-пакеты не будут использоваться.
Чтобы решить эту проблему, мы можем использовать опцию allowlist
, чтобы все же включить определенные пакеты в пакет. В этом случае используются версии CommonJS, созданные babel-loader.
Нам также нужно специально подобрать такие вещи, как formdata-polyfill/esm.min.js
, которые могут быть импортированы, но не смешивать date-fns
с da†e-fns-tz
, поэтому мы используем массив регулярных выражений для allowlist
.
const nodeExternals = require('webpack-node-externals');
const pureESMDependencies = ['pretty-ms', 'parse-ms'];
\ ....
externals: [nodeExternals({
allowlist: pureESMDependencies
.map((dep) => RegExp(`^${dep}(/.*)?$`))
})],
Собираем все вместе
В общем, вот полный webpack.config.js
с чистыми модулями ESM, определенными в многоразовом списке, и некоторыми другими необходимыми (хех) настройками:
const path = require('path');
const babelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except');
const nodeExternals = require('webpack-node-externals');
const slsw = require('serverless-webpack');
const pureESMModules = ['pretty-ms', 'parse-ms'];
module.exports = {
target: 'node',
stats: 'normal',
entry: slsw.lib.entries,
externals: [nodeExternals({
allowlist: pureESMDependencies
.map((dep) => RegExp(`^${dep}(/.*)?$`))
})],
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
optimization: { concatenateModules: false },
resolve: { extensions: ['.js'] },
module: {
rules: [
{
test: /.js$/,
exclude: babelLoaderExcludeNodeModulesExcept(pureESMModules),
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
},
],
},
output: {
libraryTarget: 'commonjs2',
filename: '[name].js',
path: path.resolve(__dirname, '.webpack'),
},
};
Это сопутствующий файл .babelrc
, хотя plugin-transform-runtime необязателен для того, что мы здесь делаем:
{
"plugins": ["@babel/plugin-transform-runtime"],
"presets": ["@babel/preset-env"]
}
После развертывания (или предварительного просмотра локально с помощью serverless-offline) наша конечная точка API теперь показывает ожидаемый результат с изображением наших милых морских млекопитающих frens:
{
"frens": "Bobby & Lola",
"runTime": "70ms"
}
Вы можете ознакомиться с полным прототипом на GitHub.
Путешествие стало наградой
Разбираться в этом было очень весело. Настолько весело, что я пишу здесь свой первый пост в блоге!
Изначально я был в тупике и фактически пытался сделать все наоборот, создав пакет ESM (теперь поддерживается AWS). Я даже написал половину поста на Stack Overflow с просьбой о помощи. В процессе я решил снова попробовать подход CommonJS и решил свою проблему еще до того, как отправил этот пост. Кому-нибудь это знакомо?
Для человека с СДВГ, как я, это был такой полезный опыт, когда все начало обретать смысл в моей голове. Как только я продумал некоторые взаимодействия между различными частями, такими как babel-loader и webpack-node-externals, мне вдруг пришла в голову потенциальная проблема, которая и привела меня к этому решению. Возможно, мне немного повезло, но я называю это победой!
Я призываю всех искать те моменты ясности, когда вы немного глубже понимаете тему, и пожинать плоды. Иногда все шаги на этом пути были частью процесса обучения, и мы не можем выбирать короткие пути.
LearnByVideo.dev
Новый побочный проект — LearnByVideo.dev. Он предназначен для разработчиков, которые могут находить обучающие видео по программированию и составлять плейлисты, доступные для совместного использования. Их можно использовать для организации новых навыков для изучения, не прибегая к десяткам игнорируемых вкладок браузера 🙂
В рамках этого я хотел перенести процесс обновления видео на выделенном сервере на использование долгоиграющих бессерверных функций по расписанию.
Сейчас это просто список последних видео на YouTube с более чем 750 каналов для разработчиков. Я проиндексировал уже более 140 000 видео и только что добавил аккуратную бесконечную прокрутку. Вы уже можете прокручивать его до бесконечности и получить представление обо всем удивительном бесплатном видеоконтенте, который можно изучать:
Следующим этапом будет классификация технологических стеков, сделанная по аналогии с другим моим сайд-проектом по поиску живых кодеров Twitch.
Если этот проект заинтересовал вас, пожалуйста, следите за @LearnByVideoDev или @joostschuur в Twitter.