Skip to content
This repository was archived by the owner on May 22, 2024. It is now read-only.

Commit 8ebae9d

Browse files
author
Lukas Holzer
authored
fix: preserve symlinks from included files in the resulting zip archive (#1701)
* fix: preserve symlinks from included files in the resulting zip archive * chore: add the fix * chore: fix windows * chore: pr feedback from eduardo * chore: fix windows * chore: skip on windows
1 parent 8a3845d commit 8ebae9d

File tree

8 files changed

+928
-44
lines changed

8 files changed

+928
-44
lines changed

package-lock.json

Lines changed: 831 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@
102102
"@types/unixify": "1.0.2",
103103
"@types/yargs": "17.0.32",
104104
"@vitest/coverage-v8": "0.34.6",
105-
"adm-zip": "0.5.10",
106105
"browserslist": "4.22.2",
107106
"cardinal": "2.1.1",
108107
"cpy": "9.0.1",
108+
"decompress": "^4.2.1",
109109
"deepmerge": "4.3.1",
110110
"get-stream": "8.0.1",
111111
"husky": "8.0.3",

src/runtimes/node/utils/included_files.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ export const getPathsOfIncludedFiles = async (
5353
cwd: basePath,
5454
dot: true,
5555
ignore: excludePatterns,
56-
followSymbolicLinks: true,
57-
throwErrorOnBrokenSymbolicLink: true,
56+
onlyFiles: false,
57+
// get directories as well to get symlinked directories,
58+
// to filter the regular non symlinked directories out mark them with a slash at the end to filter them out.
59+
markDirectories: true,
60+
followSymbolicLinks: false,
5861
})
5962

60-
return { excludePatterns, paths: pathGroups.map(normalize) }
63+
// now filter the non symlinked directories out that got marked with a trailing slash
64+
return { excludePatterns, paths: pathGroups.filter((path) => !path.endsWith('/')).map(normalize) }
6165
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default async () =>
2+
new Response('<h1>Hello world</h1>', {
3+
headers: {
4+
'content-type': 'text/html',
5+
},
6+
})

tests/fixtures-esm/symlinked-included-files/node_modules/.pnpm/crazy-dep/package.json

Whitespace-only changes.

tests/fixtures-esm/symlinked-included-files/node_modules/crazy-dep

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/main.test.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { mkdir, readFile, chmod, symlink, writeFile, rm } from 'fs/promises'
1+
import { mkdir, readFile, rm, symlink, writeFile } from 'fs/promises'
22
import { dirname, isAbsolute, join, resolve } from 'path'
3-
import { arch, platform, version as nodeVersion } from 'process'
3+
import { arch, version as nodeVersion, platform } from 'process'
44

5-
import AdmZip from 'adm-zip'
65
import cpy from 'cpy'
6+
import decompress from 'decompress'
77
import merge from 'deepmerge'
88
import { execa, execaNode } from 'execa'
9+
import glob from 'fast-glob'
910
import isCI from 'is-ci'
1011
import { pathExists } from 'path-exists'
1112
import semver from 'semver'
@@ -22,15 +23,15 @@ import { shellUtils } from '../src/utils/shell.js'
2223
import type { ZipFunctionsOptions } from '../src/zip.js'
2324

2425
import {
26+
BINARY_PATH,
27+
FIXTURES_DIR,
28+
getBundlerNameFromOptions,
2529
getRequires,
26-
zipNode,
27-
zipFixture,
30+
importFunctionFile,
2831
unzipFiles,
2932
zipCheckFunctions,
30-
FIXTURES_DIR,
31-
BINARY_PATH,
32-
importFunctionFile,
33-
getBundlerNameFromOptions,
33+
zipFixture,
34+
zipNode,
3435
} from './helpers/main.js'
3536
import { computeSha1 } from './helpers/sha.js'
3637
import { allBundleConfigs, testMany } from './helpers/test_many.js'
@@ -40,8 +41,6 @@ import 'source-map-support/register'
4041

4142
vi.mock('../src/utils/shell.js', () => ({ shellUtils: { runCommand: vi.fn() } }))
4243

43-
const EXECUTABLE_PERMISSION = 0o755
44-
4544
const getZipChecksum = async function (opts: ZipFunctionsOptions) {
4645
const {
4746
files: [{ path }],
@@ -1754,16 +1753,6 @@ describe('zip-it-and-ship-it', () => {
17541753
const unzippedFile = `${unzippedFunctions[0].unzipPath}/bootstrap`
17551754
await expect(unzippedFile).toPathExist()
17561755

1757-
// The library we use for unzipping does not keep executable permissions.
1758-
// https://github.com/cthackers/adm-zip/issues/86
1759-
// However `chmod()` is not cross-platform
1760-
if (platform === 'linux') {
1761-
await chmod(unzippedFile, EXECUTABLE_PERMISSION)
1762-
1763-
const { stdout } = await execa(unzippedFile)
1764-
expect(stdout).toBe('Hello, world!')
1765-
}
1766-
17671756
const tcFile = `${unzippedFunctions[0].unzipPath}/netlify-toolchain`
17681757
await expect(tcFile).toPathExist()
17691758
const tc = await readFile(tcFile, 'utf8')
@@ -2853,7 +2842,7 @@ describe('zip-it-and-ship-it', () => {
28532842
})
28542843

28552844
testMany('only includes files once in a zip', [...allBundleConfigs], async (options) => {
2856-
const { files } = await zipFixture('local-require', {
2845+
const { files, tmpDir } = await zipFixture('local-require', {
28572846
length: 1,
28582847
opts: merge(options, {
28592848
basePath: join(FIXTURES_DIR, 'local-require'),
@@ -2866,9 +2855,11 @@ describe('zip-it-and-ship-it', () => {
28662855
}),
28672856
})
28682857

2869-
const zip = new AdmZip(files[0].path)
2858+
const unzipPath = join(tmpDir, 'unzipped')
2859+
2860+
await decompress(files[0].path, unzipPath)
28702861

2871-
const fileNames: string[] = zip.getEntries().map((entry) => entry.entryName)
2862+
const fileNames: string[] = await glob('**', { dot: true, cwd: unzipPath })
28722863
const duplicates = fileNames.filter((item, index) => fileNames.indexOf(item) !== index)
28732864
expect(duplicates).toHaveLength(0)
28742865
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { readdir } from 'fs/promises'
2+
import { platform } from 'os'
3+
import { join } from 'path'
4+
5+
import decompress from 'decompress'
6+
import { dir as getTmpDir } from 'tmp-promise'
7+
import { expect, test } from 'vitest'
8+
9+
import { ARCHIVE_FORMAT, zipFunction } from '../src/main.js'
10+
11+
import { FIXTURES_ESM_DIR } from './helpers/main.js'
12+
13+
/** Small helper function, reading a directory recursively and returning a record with the files and if it is a symlink or not */
14+
const readDirWithType = async (dir: string, readFiles?: Record<string, boolean>, parent = '') => {
15+
const files: Record<string, boolean> = readFiles || {}
16+
const dirents = await readdir(dir, { withFileTypes: true })
17+
18+
for (const dirent of dirents) {
19+
if (dirent.isDirectory()) {
20+
await readDirWithType(join(dir, dirent.name), files, dirent.name)
21+
} else {
22+
files[join(parent, dirent.name)] = dirent.isSymbolicLink()
23+
}
24+
}
25+
26+
return files
27+
}
28+
29+
test.skipIf(platform() === 'win32')('Symlinked directories from `includedFiles` are preserved', async () => {
30+
const { path: tmpDir } = await getTmpDir({ prefix: 'zip-it-test' })
31+
const basePath = join(FIXTURES_ESM_DIR, 'symlinked-included-files')
32+
const mainFile = join(basePath, 'function.mjs')
33+
34+
// assert on the source files
35+
expect(await readDirWithType(basePath)).toEqual({
36+
'function.mjs': false,
37+
[join('crazy-dep/package.json')]: false,
38+
[join('node_modules/crazy-dep')]: true,
39+
})
40+
41+
const result = await zipFunction(mainFile, tmpDir, {
42+
archiveFormat: ARCHIVE_FORMAT.ZIP,
43+
basePath,
44+
config: {
45+
'*': {
46+
includedFiles: ['**'],
47+
},
48+
},
49+
featureFlags: {},
50+
repositoryRoot: basePath,
51+
systemLog: console.log,
52+
debug: true,
53+
internalSrcFolder: undefined,
54+
})
55+
56+
const unzippedPath = join(tmpDir, 'extracted')
57+
await decompress(result!.path, unzippedPath)
58+
59+
// expect that the symlink for `node_modules/crazy-dep` is preserved
60+
expect(await readDirWithType(unzippedPath)).toEqual({
61+
'___netlify-bootstrap.mjs': false,
62+
'___netlify-entry-point.mjs': false,
63+
'function.mjs': false,
64+
[join('crazy-dep/package.json')]: false,
65+
[join('node_modules/crazy-dep')]: true,
66+
})
67+
})

0 commit comments

Comments
 (0)