diff --git a/README.md b/README.md index c161988e..4cbbb185 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ See [example folder](example) for a minimal example. ### Including extra files -All files from `package/include` will be included in the final build file. See [Exclude/Include](https://serverless.com/framework/docs/providers/aws/guide/packaging#exclude--include) +All files from `package/patterns` will be included in the final build file. See [Patterns](https://serverless.com/framework/docs/providers/aws/guide/packaging#patterns). + +Include/exclude is deprecated, but still supported. ### External Dependencies diff --git a/package.json b/package.json index bd9eb330..7c524b23 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@types/jest": "^26.0.14", "@types/node": "^12.12.38", "@types/ramda": "^0.27.6", - "@types/serverless": "^1.78.18", + "@types/serverless": "^1.78.25", "@typescript-eslint/eslint-plugin": "^4.2.0", "@typescript-eslint/parser": "^4.2.0", "eslint": "^7.9.0", diff --git a/src/index.ts b/src/index.ts index 3f4236ea..3709bb23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { build, BuildResult, BuildOptions } from 'esbuild'; import * as fs from 'fs-extra'; import * as globby from 'globby'; import * as path from 'path'; -import { mergeRight } from 'ramda'; +import { concat, mergeRight } from 'ramda'; import * as Serverless from 'serverless'; import * as Plugin from 'serverless/classes/Plugin'; import * as chokidar from 'chokidar'; @@ -170,17 +170,30 @@ export class EsbuildPlugin implements Plugin { fs.mkdirpSync(this.buildDirPath); fs.mkdirpSync(path.join(this.workDirPath, SERVERLESS_FOLDER)); // exclude serverless-esbuild + this.serverless.service.package = { + ...(this.serverless.service.package || {}), + patterns: [ + ...new Set([ + ...(this.serverless.service.package?.include || []), + ...(this.serverless.service.package?.exclude || []).map(concat('!')), + ...(this.serverless.service.package?.patterns || []), + '!node_modules/serverless-esbuild', + ]), + ] + }; + for (const fnName in this.functions) { const fn = this.serverless.service.getFunction(fnName); - fn.package = fn.package || { - exclude: [], - include: [], + fn.package = { + ...(fn.package || {}), + patterns: [ + ...new Set([ + ...(fn.package?.include || []), + ...(fn.package?.exclude || []).map(concat('!')), + ...(fn.package?.patterns || []), + ]), + ] }; - - // Add plugin to excluded packages or an empty array if exclude is undefined - fn.package.exclude = [ - ...new Set([...(fn.package.exclude || []), 'node_modules/serverless-esbuild']), - ]; } } @@ -221,13 +234,13 @@ export class EsbuildPlugin implements Plugin { }); } - /** Link or copy extras such as node_modules or package.include definitions */ + /** Link or copy extras such as node_modules or package.patterns definitions */ async copyExtras() { const { service } = this.serverless; - // include any "extras" from the "include" section - if (service.package.include && service.package.include.length > 0) { - const files = await globby(service.package.include); + // include any "extras" from the "patterns" section + if (service.package.patterns.length > 0) { + const files = await globby(service.package.patterns); for (const filename of files) { const destFileName = path.resolve(path.join(this.buildDirPath, filename)); @@ -238,7 +251,28 @@ export class EsbuildPlugin implements Plugin { } if (!fs.existsSync(destFileName)) { - fs.copySync(path.resolve(filename), path.resolve(path.join(this.buildDirPath, filename))); + fs.copySync(path.resolve(filename), destFileName); + } + } + } + + // include any "extras" from the individual function "patterns" section + for (const fnName in this.functions) { + const fn = this.serverless.service.getFunction(fnName); + if (fn.package.patterns.length === 0) { + continue; + } + const files = await globby(fn.package.patterns); + for (const filename of files) { + const destFileName = path.resolve(path.join(this.buildDirPath, `__only_${fn.name}`, filename)); + const dirname = path.dirname(destFileName); + + if (!fs.existsSync(dirname)) { + fs.mkdirpSync(dirname); + } + + if (!fs.existsSync(destFileName)) { + fs.copySync(path.resolve(filename), destFileName); } } } diff --git a/src/pack.ts b/src/pack.ts index 6ba764c4..8f6648ac 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -1,11 +1,12 @@ import * as fs from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; -import { intersection, isEmpty, path as get, without } from 'ramda'; +import { intersection, isEmpty, lensProp, map, over, path as get, pipe, reject, replace, test, without } from 'ramda'; import * as semver from 'semver'; import { EsbuildPlugin, SERVERLESS_FOLDER } from '.'; import { doSharePath, flatDep, getDepsFromBundle } from './helper'; import * as Packagers from './packagers'; +import { IFiles } from './types'; import { humanSize, zip } from './utils'; function setFunctionArtifactPath(this: EsbuildPlugin, func, artifactPath) { @@ -38,7 +39,7 @@ export async function pack(this: EsbuildPlugin) { ); // get a list of all path in build - const files: { localPath: string; rootPath: string }[] = glob + const files: IFiles = glob .sync('**', { cwd: this.buildDirPath, dot: true, @@ -57,8 +58,14 @@ export async function pack(this: EsbuildPlugin) { const zipName = `${this.serverless.service.service}.zip`; const artifactPath = path.join(this.workDirPath, SERVERLESS_FOLDER, zipName); + // remove prefixes from individual extra files + const filesPathList = pipe( + reject(test(/^__only_[^/]+$/)) as (x: IFiles) => IFiles, + map(over(lensProp('localPath'), replace(/^__only_[^/]+\//, ''))) + )(files); + const startZip = Date.now(); - await zip(artifactPath, files); + await zip(artifactPath, filesPathList); const { size } = fs.statSync(artifactPath); this.serverless.cli.log( @@ -107,10 +114,13 @@ export async function pack(this: EsbuildPlugin) { const artifactPath = path.join(this.workDirPath, SERVERLESS_FOLDER, zipName); // filter files - const filesPathList = files.filter(({ rootPath, localPath }) => { + const filesPathList = files.filter(({ localPath }) => { // exclude non individual files based on file path (and things that look derived, e.g. foo.js => foo.js.map) if (excludedFiles.find(p => localPath.startsWith(p))) return false; + // exclude files that belong to individual functions + if (localPath.startsWith('__only_') && !localPath.startsWith(`__only_${name}/`)) return false; + // exclude non whitelisted dependencies if (localPath.startsWith('node_modules')) { // if no externals is set or if the provider is google, we do not need any files from node_modules @@ -123,7 +133,9 @@ export async function pack(this: EsbuildPlugin) { } return true; - }); + }) + // remove prefix from individual function extra files + .map(({localPath, ...rest}) => ({ localPath: localPath.replace(`__only_${name}/`, ''), ...rest})); const startZip = Date.now(); await zip(artifactPath, filesPathList); diff --git a/src/types.ts b/src/types.ts index f550abad..042159c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,2 +1,8 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type JSONObject = any; + +export interface IFile { + readonly localPath: string + readonly rootPath: string +} +export type IFiles = readonly IFile[]; diff --git a/src/utils.ts b/src/utils.ts index 4f7de55b..96f8c8fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import * as childProcess from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import { join } from 'ramda'; +import { IFiles } from './types'; export class SpawnError extends Error { constructor(message: string, public stdout: string, public stderr: string) { @@ -91,7 +92,7 @@ export const humanSize = (size: number) => { return `${sanitized} ${['B', 'KB', 'MB', 'GB', 'TB'][i]}`; }; -export const zip = (zipPath: string, filesPathList: { rootPath: string; localPath: string }[]) => { +export const zip = (zipPath: string, filesPathList: IFiles) => { fs.mkdirpSync(path.dirname(zipPath)); const zip = archiver.create('zip');