serverless-esbuild
Version:
Serverless plugin for zero-config JavaScript and TypeScript code bundling using extremely fast esbuild
286 lines • 15.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.nodeExternalsPluginUtilsPath = nodeExternalsPluginUtilsPath;
exports.packExternalModules = packExternalModules;
const assert_1 = __importDefault(require("assert"));
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const R = __importStar(require("ramda"));
const packagers_1 = require("./packagers");
const utils_1 = require("./utils");
const helper_1 = require("./helper");
function rebaseFileReferences(pathToPackageRoot, moduleVersion) {
if (/^(?:file:[^/]{2}|\.\/|\.\.\/)/.test(moduleVersion)) {
const filePath = R.replace(/^file:/, '', moduleVersion);
return R.replace(/\\/g, '/', `${R.startsWith('file:', moduleVersion) ? 'file:' : ''}${pathToPackageRoot}/${filePath}`);
}
return moduleVersion;
}
/**
* Add the given modules to a package json's dependencies.
*/
function addModulesToPackageJson(externalModules, packageJson, pathToPackageRoot) {
R.forEach((externalModule) => {
const splitModule = R.split('@', externalModule);
// If we have a scoped module we have to re-add the @
if (R.startsWith('@', externalModule)) {
splitModule.splice(0, 1);
splitModule[0] = `@${splitModule[0]}`;
}
const dependencyName = R.head(splitModule);
if (!dependencyName) {
return;
}
// We have to rebase file references to the target package.json
const moduleVersion = rebaseFileReferences(pathToPackageRoot, R.join('@', R.tail(splitModule)));
// eslint-disable-next-line no-param-reassign
packageJson.dependencies = packageJson.dependencies || {};
// eslint-disable-next-line no-param-reassign
packageJson.dependencies[dependencyName] = moduleVersion;
}, externalModules);
}
/**
* Resolve the needed versions of production dependencies for external modules.
* @this - The active plugin instance
*/
function getProdModules(externalModules, packageJsonPath, rootPackageJsonPath) {
const packageJson = this.serverless.utils.readFileSync(packageJsonPath);
// only process the module stated in dependencies section
if (!packageJson.dependencies) {
return [];
}
const prodModules = [];
// Get versions of all transient modules
// eslint-disable-next-line max-statements
R.forEach((externalModule) => {
// (1) If not present in Dev Dependencies or Dependencies
if (!packageJson.dependencies?.[externalModule.external] &&
!packageJson.devDependencies?.[externalModule.external]) {
this.log.debug(`INFO: Runtime dependency '${externalModule.external}' not found in dependencies or devDependencies. It has been excluded automatically.`);
return;
}
// (2) If present in Dev Dependencies
if (!packageJson.dependencies?.[externalModule.external] &&
packageJson.devDependencies?.[externalModule.external]) {
// To minimize the chance of breaking setups we whitelist packages available on AWS here. These are due to the previously missing check
// most likely set in devDependencies and should not lead to an error now.
const ignoredDevDependencies = ['aws-sdk'];
if (!R.includes(externalModule.external, ignoredDevDependencies)) {
// Runtime dependency found in devDependencies but not forcefully excluded
this.log.error(`ERROR: Runtime dependency '${externalModule.external}' found in devDependencies.`);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Serverless typings (as of v3.0.2) are incorrect
throw new this.serverless.classes.Error(`Serverless-webpack dependency error: ${externalModule.external}.`);
}
this.log.debug(`INFO: Runtime dependency '${externalModule.external}' found in devDependencies. It has been excluded automatically.`);
return;
}
// (3) otherwise let's get the version
// get module package - either from root or local node_modules - will be used for version and peer deps
const rootModulePackagePath = path_1.default.join(path_1.default.dirname(rootPackageJsonPath), 'node_modules', externalModule.external, 'package.json');
const localModulePackagePath = path_1.default.join(process.cwd(), path_1.default.dirname(packageJsonPath), 'node_modules', externalModule.external, 'package.json');
// eslint-disable-next-line no-nested-ternary
const modulePackagePath = fs_extra_1.default.pathExistsSync(localModulePackagePath)
? localModulePackagePath
: fs_extra_1.default.pathExistsSync(rootModulePackagePath)
? rootModulePackagePath
: null;
const modulePackage = modulePackagePath ? require(modulePackagePath) : {};
// Get version
const moduleVersion = packageJson.dependencies?.[externalModule.external] || modulePackage.version;
// add dep with version if we have it - versionless otherwise
prodModules.push(moduleVersion ? `${externalModule.external}@${moduleVersion}` : externalModule.external);
// Check if the module has any peer dependencies and include them too
try {
// find peer dependencies but remove optional ones and excluded ones
const peerDependencies = modulePackage.peerDependencies;
const optionalPeerDependencies = Object.keys(R.pickBy((val) => val.optional, modulePackage.peerDependenciesMeta || {}));
(0, assert_1.default)(this.buildOptions, 'buildOptions not defined');
const peerDependenciesWithoutOptionals = R.omit([...optionalPeerDependencies, ...this.buildOptions.exclude], peerDependencies);
if (!R.isEmpty(peerDependenciesWithoutOptionals)) {
this.log.debug(`Adding explicit non-optionals peers for dependency ${externalModule.external}`);
const peerModules = getProdModules.call(this, R.compose(R.map(([external]) => ({ external })), R.toPairs)(peerDependenciesWithoutOptionals), packageJsonPath, rootPackageJsonPath);
Array.prototype.push.apply(prodModules, peerModules);
}
}
catch (error) {
this.log.warning(`WARNING: Could not check for peer dependencies of ${externalModule.external}`);
}
}, externalModules);
return prodModules;
}
function nodeExternalsPluginUtilsPath() {
try {
const resolvedPackage = require.resolve('esbuild-node-externals/dist/utils', {
paths: [process.cwd()],
});
return resolvedPackage;
}
catch {
// No-op
}
}
/**
* We need a performant algorithm to install the packages for each single
* function (in case we package individually).
* (1) We fetch ALL packages needed by ALL functions in a first step
* and use this as a base npm checkout. The checkout will be done to a
* separate temporary directory with a package.json that contains everything.
* (2) For each single compile we copy the whole node_modules to the compile
* directory and create a (function) compile specific package.json and store
* it in the compile directory. Now we start npm again there, and npm will just
* remove the superfluous packages and optimize the remaining dependencies.
* This will utilize the npm cache at its best and give us the needed results
* and performance.
*/
// eslint-disable-next-line max-statements
async function packExternalModules() {
(0, assert_1.default)(this.buildOptions, 'buildOptions not defined');
const upperPackageJson = (0, utils_1.findUp)('package.json');
const { plugins } = this;
if (plugins && plugins.map((plugin) => plugin.name).includes('node-externals')) {
const utilsPath = nodeExternalsPluginUtilsPath();
if (utilsPath) {
const { findDependencies, findPackagePaths, createAllowPredicate, } = require(utilsPath);
this.buildOptions.external = findDependencies({
packagePaths: findPackagePaths(),
dependencies: true,
devDependencies: false,
peerDependencies: false,
optionalDependencies: false,
allowWorkspaces: false,
allowPredicate: createAllowPredicate(this.buildOptions.nodeExternals?.allowList ?? []),
});
}
}
const externals = Array.isArray(this.buildOptions.external) &&
this.buildOptions.exclude !== '*' &&
!this.buildOptions.exclude.includes('*')
? R.without(this.buildOptions.exclude, this.buildOptions.external)
: [];
if (!externals.length) {
return;
}
// Read plugin configuration
// get the root package.json by looking up until we hit a lockfile
// if this is a yarn workspace, it will be the monorepo package.json
const rootPackageJsonPath = path_1.default.join((0, utils_1.findProjectRoot)() || '', './package.json');
// get the local package.json by looking up until we hit a package.json file
// if this is *not* a yarn workspace, it will be the same as rootPackageJsonPath
const packageJsonPath = this.buildOptions.packagePath ||
(upperPackageJson && path_1.default.relative(process.cwd(), path_1.default.join(upperPackageJson, './package.json')));
(0, assert_1.default)(packageJsonPath, 'packageJsonPath is not defined');
// Determine and create packager
const packager = await packagers_1.getPackager.call(this, this.buildOptions.packager, this.buildOptions.packagerOptions);
// Fetch needed original package.json sections
const sectionNames = packager.copyPackageSectionNames;
// Get scripts from packager options
const packagerScripts = typeof this.buildOptions.packagerOptions?.scripts !== 'undefined'
? (Array.isArray(this.buildOptions.packagerOptions.scripts)
? this.buildOptions.packagerOptions.scripts
: [this.buildOptions.packagerOptions.scripts]).reduce((scripts, script, index) => {
// eslint-disable-next-line no-param-reassign
scripts[`script${index}`] = script;
return scripts;
}, {})
: {};
const rootPackageJson = this.serverless.utils.readFileSync(rootPackageJsonPath);
const isWorkspace = !!rootPackageJson.workspaces;
const packageJson = isWorkspace
? (packageJsonPath && this.serverless.utils.readFileSync(packageJsonPath)) || {}
: rootPackageJson;
const packageSections = R.pick(sectionNames, packageJson);
if (!R.isEmpty(packageSections)) {
this.log.debug(`Using package.json sections ${R.join(', ', R.keys(packageSections))}`);
}
// Get first level dependency graph
this.log.debug(`Fetch dependency graph from ${packageJson}`);
// (1) Generate dependency composition
const externalModules = R.map((external) => ({ external }), externals);
const compositeModules = R.uniq(getProdModules.call(this, externalModules, packageJsonPath, rootPackageJsonPath));
if (R.isEmpty(compositeModules)) {
// The compiled code does not reference any external modules at all
this.log.warning('No external modules needed');
return;
}
// (1.a) Install all needed modules
const compositeModulePath = this.buildDirPath;
(0, helper_1.assertIsString)(compositeModulePath, 'compositeModulePath is not a string');
const compositePackageJson = path_1.default.join(compositeModulePath, 'package.json');
// (1.a.1) Create a package.json
const compositePackage = R.mergeRight({
name: this.serverless.service.service,
version: '1.0.0',
description: `Packaged externals for ${this.serverless.service.service}`,
private: true,
scripts: packagerScripts,
}, packageSections);
const relativePath = path_1.default.relative(compositeModulePath, path_1.default.dirname(packageJsonPath));
addModulesToPackageJson(compositeModules, compositePackage, relativePath);
this.serverless.utils.writeFileSync(compositePackageJson, JSON.stringify(compositePackage, null, 2));
// (1.a.2) Copy package-lock.json if it exists, to prevent unwanted upgrades
const packageLockPath = path_1.default.join(process.cwd(), path_1.default.dirname(packageJsonPath), packager.lockfileName);
const exists = await fs_extra_1.default.pathExists(packageLockPath);
if (exists) {
this.log.verbose('Package lock found - Using locked versions');
try {
let packageLockFile = this.serverless.utils.readFileSync(packageLockPath);
packageLockFile = packager.rebaseLockfile(relativePath, packageLockFile);
if (R.is(Object)(packageLockFile)) {
packageLockFile = JSON.stringify(packageLockFile, null, 2);
}
this.serverless.utils.writeFileSync(path_1.default.join(compositeModulePath, packager.lockfileName), packageLockFile);
}
catch (error) {
this.log.warning(`Warning: Could not read lock file${error instanceof Error ? `: ${error.message}` : ''}`);
}
}
// GOOGLE: Copy modules only if not google-cloud-functions
// GCF Auto installs the package json
if (R.path(['service', 'provider', 'name'], this.serverless) === 'google') {
return;
}
const start = Date.now();
this.log.verbose(`Packing external modules: ${compositeModules.join(', ')}`);
const { installExtraArgs } = this.buildOptions;
await packager.install(compositeModulePath, installExtraArgs, exists);
this.log.debug(`Package took [${Date.now() - start} ms]`);
// Prune extraneous packages - removes not needed ones
const startPrune = Date.now();
await packager.prune(compositeModulePath);
this.log.debug(`Prune: ${compositeModulePath} [${Date.now() - startPrune} ms]`);
(0, helper_1.assertIsString)(this.buildDirPath, 'buildDirPath is not a string');
// Run packager scripts
if (Object.keys(packagerScripts).length > 0) {
const startScripts = Date.now();
await packager.runScripts(this.buildDirPath, Object.keys(packagerScripts));
this.log.debug(`Packager scripts took [${Date.now() - startScripts} ms].\nExecuted scripts: ${Object.values(packagerScripts).map((script) => `\n ${script}`)}`);
}
}
//# sourceMappingURL=pack-externals.js.map