Repository: netlify/zip-it-and-ship-it
Pull request #601 (2021-08-02 10:53:52)

feat: add support for manifest file
**- Summary**

Adds a new `manifest` property to `zipFunctions`. When set, it defines the path where a JSON-formatted file will be created, containing the list of all created functions as well as additional metadata properties.

This file can then be used by the CLI to know whether to reuse previously bundled functions as part of `build` and `deploy` commands.

**- Test plan**

New test added.

**- A picture of a cute animal (not mandatory but encouraged)**

![1501921785-baby-turtles-9](https://user-images.githubusercontent.com/4162329/127850779-f82869fa-70d5-4696-b3b7-b455a8209b07.jpg)


github-actions[bot]: ## ⏱ Benchmark results

*Comparing with e6490d61a256a27c70f3b602ec37e648d415b442*

### largeDepsEsbuild: 13.1s
 ⬆️ **17.28% increase** vs. e6490d61a256a27c70f3b602ec37e648d415b442
```
^           14s                                                                                           
│           ┌──┐                                                           13.3s                   13.1s  
│           |  |           12.5s           12.7s                   12.4s    ┌──┐   12.6s            ┌──┐  
│           |  |            ┌──┐            ┌──┐   12.2s            ┌──┐    |  |    ┌──┐    12s     |▒▒|  
│ ──────────┼──┼────────────┼──┼────────────┼──┼────┌──┐────────────┼──┼────┼──┼────┼──┼────┌──┐────|▒▒|──
│           |  |            |  |   10.6s    |  |    |  |            |  |    |  |    |  |    |  |    |▒▒|  
│   9.8s    |  |    9.6s    |  |    ┌──┐    |  |    |  |    10s     |  |    |  |    |  |    |  |    |▒▒|  
│   ┌──┐    |  |    ┌──┐    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
```
<details>
<summary>Legend</summary>

- T-30 (e6490d61a256a27c70f3b602ec37e648d415b442): 10.9s
- T-29 (15787efd3bfac84e0036e96082d071afa67c42c5): 11.3s
- T-28 (1ce8157a7351adbc6c6c751ddc8499e9080536b8): 12.9s
- T-27 (96c833514c1b10081dee2a1c236e05186c2ebe01): 13.2s
- T-26 (5d48d64bea0d963efa9dbe64925baef24002a9a4): 12.4s
- T-25 (bd7206ea53810a73ea9938f4e34e2826a142ae2d): 14.3s
- T-24 (69fdb7b0f312a6e007e6cedd6a40494772756eef): 11.4s
- T-23 (57a644f24c7bfd2c0ab1e19360465a3301157162): 13.8s
- T-22 (a9a9e610e1324bbcbee604561605dd12c8a7c4c8): 13.2s
- T-21 (8e299a25c7449b440f7a07849d73b8f184a3245c): 12.6s
- T-20 (cebf521a0a5e42ee0244108aa86367f74524f1be): 10.5s
- T-19 (b789589e534a4cc26c6a56636039b9a22f07a2e3): 12.9s
- T-18 (2e25a52b4d23522c64a01c78f7dba16291a085cf): 12.3s
- T-17 (fec9333941c4b8d0a6a648fd755ddecca07fc892): 12.1s
- T-16 (bd61ad00330f73d247dac787734abc9efeaba4b1): 12.7s
- T-15 (9c3d239efb5f53d53ada4c2d92db6f48b7401096): 13.9s
- T-14 (d790873c4cfb82de3f9d5cc64c2ecb45f4c4112f): 10.6s
- T-13 (c18abffd799ee05a0336a34fffd9a1d48e71c550): 10.9s
- T-12 (a0b35e4de9ee40eb9eed4fdd85a16796983fe154): 9.8s
- T-11 (f9084030bf84398023793983490c294b7b8ff483): 14s
- T-10 (b8eb5f8da62d48468c4aff35f2ed65b175bb14c7): 9.6s
- T-9 (cc6070c8f5a0b44163e3167be9b0beab9bf1c644): 12.5s
- T-8 (d1f77ee0caf96a19702d81abd85b8814ed62ab1c): 10.6s
- T-7 (2a11f56404dfdb6d92881751e9a21ed11f432ec8): 12.7s
- T-6 (30ec7f6a4e97007c2f5e0c40b85b196884acd10f): 12.2s
- T-5 (49c33d8209130930cdeed3b953ac874047cba15a): 10s
- T-4 (0bc0ff937f2bfc93d930c78ea4afb7ede53e30c1): 12.4s
- T-3 (69142468b5054449c51186dd64f79ec67ea7899b): 13.3s
- T-2 (d5392a0cb21fa9b712f5dd8c9d4703f6b080afbf): 12.6s
- T-1 (000ffff7ecbceeeb73e4b305859db4a1f8ff1e18): 12s
- **T (current commit): 13.1s**</details>
### largeDepsZisi: 1m 12.3s
 ⬆️ **18.31% increase** vs. e6490d61a256a27c70f3b602ec37e648d415b442
```
^         1m 15.1s                                                        1m 14.3s                        
│           ┌──┐                                                            ┌──┐                  1m 12.3s
│           |  |                          1m 8.7s                           |  |                    ┌──┐  
│           |  |          1m 4.1s           ┌──┐                   1m 4s    |  |  1m 5.3s 1m 3.6s   |▒▒|  
│ ──────────┼──┼────────────┌──┐────────────┼──┼──1m 1.5s───────────┌──┐────┼──┼────┌──┐────┌──┐────|▒▒|──
│           |  |            |  |   54.9s    |  |    ┌──┐            |  |    |  |    |  |    |  |    |▒▒|  
│  51.8s    |  |    51s     |  |    ┌──┐    |  |    |  |   52.8s    |  |    |  |    |  |    |  |    |▒▒|  
│   ┌──┐    |  |    ┌──┐    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
```
<details>
<summary>Legend</summary>

- T-30 (e6490d61a256a27c70f3b602ec37e648d415b442): 59s
- T-29 (15787efd3bfac84e0036e96082d071afa67c42c5): 55.9s
- T-28 (1ce8157a7351adbc6c6c751ddc8499e9080536b8): 1m 2.9s
- T-27 (96c833514c1b10081dee2a1c236e05186c2ebe01): 1m 3.6s
- T-26 (5d48d64bea0d963efa9dbe64925baef24002a9a4): 1m 2.1s
- T-25 (bd7206ea53810a73ea9938f4e34e2826a142ae2d): 1m 11.7s
- T-24 (69fdb7b0f312a6e007e6cedd6a40494772756eef): 54.7s
- T-23 (57a644f24c7bfd2c0ab1e19360465a3301157162): 1m 6.5s
- T-22 (a9a9e610e1324bbcbee604561605dd12c8a7c4c8): 1m 9.5s
- T-21 (8e299a25c7449b440f7a07849d73b8f184a3245c): 1m 3.8s
- T-20 (cebf521a0a5e42ee0244108aa86367f74524f1be): 51.2s
- T-19 (b789589e534a4cc26c6a56636039b9a22f07a2e3): 1m 8.3s
- T-18 (2e25a52b4d23522c64a01c78f7dba16291a085cf): 1m 3.4s
- T-17 (fec9333941c4b8d0a6a648fd755ddecca07fc892): 1m 4.1s
- T-16 (bd61ad00330f73d247dac787734abc9efeaba4b1): 1m 7.9s
- T-15 (9c3d239efb5f53d53ada4c2d92db6f48b7401096): 1m 17.1s
- T-14 (d790873c4cfb82de3f9d5cc64c2ecb45f4c4112f): 54.3s
- T-13 (c18abffd799ee05a0336a34fffd9a1d48e71c550): 58.4s
- T-12 (a0b35e4de9ee40eb9eed4fdd85a16796983fe154): 51.8s
- T-11 (f9084030bf84398023793983490c294b7b8ff483): 1m 15.1s
- T-10 (b8eb5f8da62d48468c4aff35f2ed65b175bb14c7): 51s
- T-9 (cc6070c8f5a0b44163e3167be9b0beab9bf1c644): 1m 4.1s
- T-8 (d1f77ee0caf96a19702d81abd85b8814ed62ab1c): 54.9s
- T-7 (2a11f56404dfdb6d92881751e9a21ed11f432ec8): 1m 8.7s
- T-6 (30ec7f6a4e97007c2f5e0c40b85b196884acd10f): 1m 1.5s
- T-5 (49c33d8209130930cdeed3b953ac874047cba15a): 52.8s
- T-4 (0bc0ff937f2bfc93d930c78ea4afb7ede53e30c1): 1m 4s
- T-3 (69142468b5054449c51186dd64f79ec67ea7899b): 1m 14.3s
- T-2 (d5392a0cb21fa9b712f5dd8c9d4703f6b080afbf): 1m 5.3s
- T-1 (000ffff7ecbceeeb73e4b305859db4a1f8ff1e18): 1m 3.6s
- **T (current commit): 1m 12.3s**</details>
<!--delta:benchmark@{}-->

Patch:
diff --git a/README.md b/README.md
index ff6baab..ad46964 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,22 @@ The following properties are accepted:
 
 See [feature flags](#feature-flags).
 
+#### `manifest`
+
+- _Type_: `string`
+- _Default value_: `undefined`
+
+Defines the path for a manifest file to be created with the results of the functions bundling. This file is a
+JSON-formatted string with the following properties:
+
+- `functions`: An array with the functions created, in the same format as returned by `zipFunctions`
+- `system.arch`: The operating system CPU architecture, as returned by
+  [`process.arch`](https://nodejs.org/api/process.html#process_process_arch)
+- `system.platform`: The operating system, as returned by
+  [`process.platform`](https://nodejs.org/api/process.html#process_process_platform)
+- `timestamp`: The timestamp (in milliseconds) at the time of the functions bundling process
+- `version`: The version of the manifest file (current version is `1`)
+
 #### `parallelLimit`
 
 - _Type_: `number`\
diff --git a/src/bin.js b/src/bin.js
index 2da4c5a..d477acf 100755
--- a/src/bin.js
+++ b/src/bin.js
@@ -35,6 +35,10 @@ const OPTIONS = {
     describe:
       'An object matching glob-like expressions to objects containing configuration properties. Whenever a function name matches one of the expressions, it inherits the configuration properties',
   },
+  manifest: {
+    string: true,
+    describe: 'If a manifest file is to be created, specifies its path',
+  },
   'parallel-limit': {
     number: true,
     describe: 'Maximum number of Functions to bundle at the same time',
diff --git a/src/manifest.js b/src/manifest.js
new file mode 100644
index 0000000..bdb56f5
--- /dev/null
+++ b/src/manifest.js
@@ -0,0 +1,22 @@
+const { resolve } = require('path')
+const { arch, platform } = require('process')
+
+const { writeFile } = require('./utils/fs')
+
+const MANIFEST_VERSION = 1
+
+const createManifest = async ({ functions, path }) => {
+  const formattedFunctions = functions.map(formatFunction)
+  const payload = {
+    functions: formattedFunctions,
+    system: { arch, platform },
+    timestamp: Date.now(),
+    version: MANIFEST_VERSION,
+  }
+
+  await writeFile(path, JSON.stringify(payload))
+}
+
+const formatFunction = (fn) => ({ ...fn, path: resolve(fn.path) })
+
+module.exports = { createManifest }
diff --git a/src/utils/fs.js b/src/utils/fs.js
index 294b8ee..aab49da 100644
--- a/src/utils/fs.js
+++ b/src/utils/fs.js
@@ -1,4 +1,4 @@
-const { lstat, readdir, readFile, unlink } = require('fs')
+const { lstat, readdir, readFile, unlink, writeFile } = require('fs')
 const { format, join, parse, resolve } = require('path')
 const { promisify } = require('util')
 
@@ -6,6 +6,7 @@ const pLstat = promisify(lstat)
 const pReaddir = promisify(readdir)
 const pReadFile = promisify(readFile)
 const pUnlink = promisify(unlink)
+const pWriteFile = promisify(writeFile)
 
 // This caches multiple FS calls to the same path. It creates a cache key with
 // the name of the function and the path (e.g. "readdir:/some/directory").
@@ -83,4 +84,5 @@ module.exports = {
   listFunctionsDirectory,
   resolveFunctionsDirectories,
   safeUnlink,
+  writeFile: pWriteFile,
 }
diff --git a/src/zip.js b/src/zip.js
index 4b9a522..7ba4356 100644
--- a/src/zip.js
+++ b/src/zip.js
@@ -4,6 +4,7 @@ const makeDir = require('make-dir')
 const pMap = require('p-map')
 
 const { getFlags } = require('./feature_flags')
+const { createManifest } = require('./manifest')
 const { getPluginsModulesPath } = require('./node_dependencies')
 const { getFunctionsFromPaths } = require('./runtimes')
 const { ARCHIVE_FORMAT_NONE, ARCHIVE_FORMAT_ZIP } = require('./utils/consts')
@@ -59,6 +60,7 @@ const zipFunctions = async function (
     basePath,
     config = {},
     featureFlags: inputFeatureFlags,
+    manifest,
     parallelLimit = DEFAULT_PARALLEL_LIMIT,
   } = {},
 ) {
@@ -75,7 +77,7 @@ const zipFunctions = async function (
     // source directories.
     getPluginsModulesPath(srcFolders[0]),
   ])
-  const zipped = await pMap(
+  const results = await pMap(
     functions.values(),
     async (func) => {
       const zipResult = await func.runtime.zipFunction({
@@ -100,7 +102,13 @@ const zipFunctions = async function (
       concurrency: parallelLimit,
     },
   )
-  return zipped.filter(Boolean).map(formatZipResult)
+  const formattedResults = results.filter(Boolean).map(formatZipResult)
+
+  if (manifest !== undefined) {
+    await createManifest({ functions: formattedResults, path: resolve(manifest) })
+  }
+
+  return formattedResults
 }
 
 const zipFunction = async function (


Test patch:
diff --git a/tests/main.js b/tests/main.js
index f069468..5cf5ad9 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -1,7 +1,7 @@
 const { readFile, chmod, symlink, unlink, rename, stat, writeFile } = require('fs')
 const { tmpdir } = require('os')
-const { dirname, join, normalize, resolve, sep } = require('path')
-const { env, platform } = require('process')
+const { dirname, isAbsolute, join, normalize, resolve, sep } = require('path')
+const { arch, env, platform } = require('process')
 const { promisify } = require('util')
 
 const test = require('ava')
@@ -1748,3 +1748,25 @@ if (platform !== 'win32') {
     )
   })
 }
+
+test('Creates a manifest file with the list of created functions if the `manifest` property is supplied', async (t) => {
+  const FUNCTIONS_COUNT = 6
+  const { path: tmpDir } = await getTmpDir({ prefix: 'zip-it-test' })
+  const manifestPath = join(tmpDir, 'manifest.json')
+  const { files } = await zipNode(t, 'many-functions', {
+    length: FUNCTIONS_COUNT,
+    opts: { manifest: manifestPath },
+  })
+
+  // eslint-disable-next-line import/no-dynamic-require, node/global-require
+  const manifest = require(manifestPath)
+
+  t.deepEqual(files, manifest.functions)
+  t.is(manifest.version, 1)
+  t.is(manifest.system.arch, arch)
+  t.is(manifest.system.platform, platform)
+  t.is(typeof manifest.timestamp, 'number')
+
+  // The `path` property of each function must be an absolute path.
+  manifest.functions.every(({ path }) => isAbsolute(path))
+})