Repository: netlify/zip-it-and-ship-it (js)

Issue:
Symlinks in Functions do not always work
Symlinks in Functions do not always work due to two reasons:
  - Zipping does not always keep them as symlinks. Instead we should archive the symlink target file, not the symlink itself.
  - When there is no `package.json`, the absolute path is used to target files inside the zip archive. `resolve()` resolves symlinks with `realpath`, but this creates issues with absolute paths not having the same root directory.

Pull request description:
Fix using symlinks in Functions
Fixes #106.

Symlinks in Functions do not always work due to two reasons:
  - Zipping does not always keep them as symlinks. Instead we should archive the symlink target file, not the symlink itself.
  - When there is no `package.json`, the absolute path is used to target files inside the zip archive. `resolve()` resolves symlinks with `realpath`, but this creates issues with absolute paths not having the same root directory.

This PR fixes those problems and adds a regression test.

This PR also fixes CI tests on macOS.

Patch:
diff --git a/src/fixtures/symlinks/function/function.js b/src/fixtures/symlinks/function/function.js
new file mode 100644
index 0000000..368bd62
--- /dev/null
+++ b/src/fixtures/symlinks/function/function.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line node/no-missing-require
+module.exports = require('./file')
diff --git a/src/fixtures/symlinks/function/target.js b/src/fixtures/symlinks/function/target.js
new file mode 100644
index 0000000..33c1891
--- /dev/null
+++ b/src/fixtures/symlinks/function/target.js
@@ -0,0 +1,1 @@
+module.exports = true
diff --git a/src/node.js b/src/node.js
index 0b6d63d..2bae10d 100644
--- a/src/node.js
+++ b/src/node.js
@@ -1,4 +1,4 @@
-const { lstat } = require('fs')
+const { stat } = require('fs')
 const { dirname, normalize, sep } = require('path')
 
 const commonPathPrefix = require('common-path-prefix')
@@ -11,7 +11,7 @@ const { startZip, addZipFile, addZipContent, endZip } = require('./archive')
 const { getDependencies } = require('./dependencies')
 
 const pGlob = promisify(glob)
-const pLstat = promisify(lstat)
+const pStat = promisify(stat)
 
 // Zip a Node.js function file
 const zipNodeJs = async function(srcPath, srcDir, destPath, filename, handler, stat) {
@@ -63,7 +63,7 @@ const addEntryFile = function(commonPrefix, archive, filename, handler) {
 
 const zipJsFile = async function(file, commonPrefix, archive) {
   const filename = normalizeFilePath(file, commonPrefix)
-  const stat = await pLstat(file)
+  const stat = await pStat(file)
   addZipFile(archive, file, filename, stat)
 }
 
diff --git a/src/resolve.js b/src/resolve.js
index 24562b1..0036f64 100644
--- a/src/resolve.js
+++ b/src/resolve.js
@@ -6,7 +6,7 @@ const resolve = require('resolve')
 //   https://github.com/browserify/resolve/issues/151#issuecomment-368210310
 const resolveLocation = function(location, basedir) {
   return new Promise((success, reject) => {
-    resolve(location, { basedir }, (error, resolvedLocation) => {
+    resolve(location, { basedir, preserveSymlinks: true }, (error, resolvedLocation) => {
       if (error) {
         return reject(error)
       }


Test patch:
diff --git a/src/main.test.js b/src/main.test.js
index 5254467..571c45b 100644
--- a/src/main.test.js
+++ b/src/main.test.js
@@ -1,4 +1,4 @@
-const { readFile, chmod } = require('fs')
+const { readFile, chmod, symlink, unlink } = require('fs')
 const { tmpdir } = require('os')
 const { platform } = require('process')
 
@@ -16,6 +16,8 @@ const { zipNode, zipFixture, unzipFiles, zipCheckFunctions, FIXTURES_DIR } = req
 
 const pReadFile = promisify(readFile)
 const pChmod = promisify(chmod)
+const pSymlink = promisify(symlink)
+const pUnlink = promisify(unlink)
 
 test.after(async () => {
   await del(`${tmpdir()}/zip-it-test*`, { force: true })
@@ -118,6 +120,26 @@ test('Can require local files in the parent directories', async t => {
   await zipNode(t, 'local-parent-require')
 })
 
+// Need to create symlinks dynamically because they sometimes get lost when
+// committed on Windows
+if (platform !== 'win32') {
+  test('Can require symlinks', async t => {
+    const symlinkDir = `${FIXTURES_DIR}/symlinks/function`
+    const symlinkFile = `${symlinkDir}/file.js`
+    const targetFile = `${symlinkDir}/target.js`
+
+    if (!(await pathExists(symlinkFile))) {
+      await pSymlink(targetFile, symlinkFile)
+    }
+
+    try {
+      await zipNode(t, 'symlinks')
+    } finally {
+      await pUnlink(symlinkFile)
+    }
+  })
+}
+
 test('Can target a directory with a handler with the same name', async t => {
   await zipNode(t, 'directory-handler')
 })