@netlify/zip-it-and-ship-it 16.2.0 → 16.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Stats } from 'fs';
|
|
1
2
|
import { ArchiveFormat } from '../../../archive.js';
|
|
2
3
|
import type { FeatureFlags } from '../../../feature_flags.js';
|
|
3
4
|
import type { RuntimeCache } from '../../../utils/cache.js';
|
|
@@ -29,4 +30,13 @@ interface ZipNodeJsResult {
|
|
|
29
30
|
export declare const zipNodeJs: ({ archiveFormat, ...options }: ZipNodeParameters & {
|
|
30
31
|
archiveFormat: ArchiveFormat;
|
|
31
32
|
}) => Promise<ZipNodeJsResult>;
|
|
33
|
+
interface ArchiveEntry {
|
|
34
|
+
destPath: string;
|
|
35
|
+
srcFile: string;
|
|
36
|
+
stat: Stats;
|
|
37
|
+
}
|
|
38
|
+
export declare const resolveSymlinkedDestPaths: <T extends ArchiveEntry>(files: T[], { readTarget, rewrites, }?: {
|
|
39
|
+
readTarget?: (srcFile: string) => Promise<string>;
|
|
40
|
+
rewrites?: Map<string, string>;
|
|
41
|
+
}) => Promise<T[]>;
|
|
32
42
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { mkdir, readlink as readLink, rm, symlink, writeFile } from 'fs/promises';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import { basename, dirname, extname, join } from 'path';
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, posix } from 'path';
|
|
5
5
|
import { getPath as getV2APIPath } from '@netlify/serverless-functions-api';
|
|
6
6
|
import { copyFile } from 'copy-file';
|
|
7
7
|
import pMap from 'p-map';
|
|
@@ -66,19 +66,17 @@ const createDirectory = async function ({ aliases = new Map(), basePath, cache,
|
|
|
66
66
|
addBootstrapFile(srcFiles, aliases);
|
|
67
67
|
}
|
|
68
68
|
const symlinks = new Map();
|
|
69
|
+
const statted = await Promise.all([...new Set(srcFiles)].map(async (srcFile) => ({
|
|
70
|
+
srcFile,
|
|
71
|
+
stat: await cachedLstat(cache.lstatCache, srcFile),
|
|
72
|
+
destPath: normalizeFilePath({ commonPrefix: basePath, path: aliases.get(srcFile) || srcFile, userNamespace }),
|
|
73
|
+
})));
|
|
69
74
|
// Copying source files.
|
|
70
|
-
await pMap(
|
|
71
|
-
const
|
|
72
|
-
const normalizedDestPath = normalizeFilePath({
|
|
73
|
-
commonPrefix: basePath,
|
|
74
|
-
path: destPath,
|
|
75
|
-
userNamespace,
|
|
76
|
-
});
|
|
77
|
-
const absoluteDestPath = join(functionFolder, normalizedDestPath);
|
|
75
|
+
await pMap(await resolveSymlinkedDestPaths(statted, { rewrites }), async ({ srcFile, stat, destPath }) => {
|
|
76
|
+
const absoluteDestPath = join(functionFolder, destPath);
|
|
78
77
|
if (rewrites.has(srcFile)) {
|
|
79
78
|
return mkdirAndWriteFile(absoluteDestPath, rewrites.get(srcFile));
|
|
80
79
|
}
|
|
81
|
-
const stat = await cachedLstat(cache.lstatCache, srcFile);
|
|
82
80
|
// If the path is a symlink, find the link target and add the link to a
|
|
83
81
|
// `symlinks` map, which we'll later use to create the symlinks in the
|
|
84
82
|
// target directory. We can't do that right now because the target path
|
|
@@ -157,18 +155,15 @@ const createZipArchive = async function ({ aliases = new Map(), basePath, branch
|
|
|
157
155
|
}
|
|
158
156
|
const deduplicatedSrcFiles = [...new Set(srcFiles)];
|
|
159
157
|
const srcFilesInfos = await Promise.all(deduplicatedSrcFiles.map((file) => addStat(cache, file)));
|
|
158
|
+
const entries = await resolveSymlinkedDestPaths(srcFilesInfos.map(({ srcFile, stat }) => ({
|
|
159
|
+
srcFile,
|
|
160
|
+
stat,
|
|
161
|
+
destPath: normalizeFilePath({ commonPrefix: basePath, path: aliases.get(srcFile) || srcFile, userNamespace }),
|
|
162
|
+
})), { rewrites });
|
|
160
163
|
// We ensure this is not async, so that the archive's checksum is
|
|
161
164
|
// deterministic. Otherwise it depends on the order the files were added.
|
|
162
|
-
|
|
163
|
-
zipJsFile({
|
|
164
|
-
aliases,
|
|
165
|
-
archive,
|
|
166
|
-
commonPrefix: basePath,
|
|
167
|
-
rewrites,
|
|
168
|
-
srcFile,
|
|
169
|
-
stat,
|
|
170
|
-
userNamespace,
|
|
171
|
-
});
|
|
165
|
+
entries.forEach(({ srcFile, stat, destPath }) => {
|
|
166
|
+
zipJsFile({ archive, destPath, rewrites, srcFile, stat });
|
|
172
167
|
});
|
|
173
168
|
await endZip(archive, output);
|
|
174
169
|
return { path: destPath, entryFilename, bootstrapVersion };
|
|
@@ -183,17 +178,85 @@ const addEntryFileToZip = function (archive, { contents, filename }) {
|
|
|
183
178
|
const contentBuffer = Buffer.from(contents);
|
|
184
179
|
addZipContent(archive, contentBuffer, filename);
|
|
185
180
|
};
|
|
181
|
+
// A tracer that resolves a module through a symlinked directory reports the path
|
|
182
|
+
// it walked, not where the files really live. The archive then holds a link at
|
|
183
|
+
// `node_modules/<pkg>` *and* files under that same path, which cannot both exist
|
|
184
|
+
// once extracted — AWS Lambda rejects the whole bundle for it.
|
|
185
|
+
//
|
|
186
|
+
// Put those entries back on the path the link points at, which is the layout the
|
|
187
|
+
// package manager produced in the first place: files in the store, `node_modules
|
|
188
|
+
// /<pkg>` a link to them. Resolution then works the way it does locally.
|
|
189
|
+
//
|
|
190
|
+
// A link the archive cannot contain — absolute, or reaching above the bundle
|
|
191
|
+
// root — is left alone; rewriting onto it would move entries out of the bundle.
|
|
192
|
+
export const resolveSymlinkedDestPaths = async function (files, { readTarget = readLink, rewrites = new Map(), } = {}) {
|
|
193
|
+
const links = files.filter(({ stat }) => stat.isSymbolicLink());
|
|
194
|
+
if (links.length === 0) {
|
|
195
|
+
return files;
|
|
196
|
+
}
|
|
197
|
+
// Resolve in parallel, but record in `links` order: keying off whichever
|
|
198
|
+
// readlink finished first would make the output depend on IO timing, and the
|
|
199
|
+
// archive checksum has to be reproducible.
|
|
200
|
+
const targets = await Promise.all(links.map(({ srcFile }) => readTarget(srcFile)));
|
|
201
|
+
const realPaths = new Map();
|
|
202
|
+
links.forEach(({ destPath }, index) => {
|
|
203
|
+
const target = targets[index];
|
|
204
|
+
if (isAbsolute(target)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const realPath = posix.normalize(posix.join(posix.dirname(destPath), target));
|
|
208
|
+
if (realPath.startsWith('..')) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
realPaths.set(destPath, realPath);
|
|
212
|
+
});
|
|
213
|
+
if (realPaths.size === 0) {
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
// Longest first, so a link nested inside another wins over its parent.
|
|
217
|
+
const candidates = [...realPaths.entries()].sort(([a], [b]) => b.length - a.length);
|
|
218
|
+
// One rewrite can land under another link, so keep going until the path sits
|
|
219
|
+
// below none of them. Links that point at each other would otherwise spin
|
|
220
|
+
// here, so stop as soon as a path repeats.
|
|
221
|
+
const resolveDestPath = (destPath) => {
|
|
222
|
+
const visited = new Set([destPath]);
|
|
223
|
+
let current = destPath;
|
|
224
|
+
for (;;) {
|
|
225
|
+
const match = candidates.find(([link]) => current.startsWith(`${link}/`));
|
|
226
|
+
if (match === undefined) {
|
|
227
|
+
return current;
|
|
228
|
+
}
|
|
229
|
+
const next = match[1] + current.slice(match[0].length);
|
|
230
|
+
if (visited.has(next)) {
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
visited.add(next);
|
|
234
|
+
current = next;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const byDestPath = new Map();
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
const destPath = resolveDestPath(file.destPath);
|
|
240
|
+
const resolved = destPath === file.destPath ? file : { ...file, destPath };
|
|
241
|
+
const existing = byDestPath.get(resolved.destPath);
|
|
242
|
+
// Two spellings of one package can now collapse onto a single path. Keep
|
|
243
|
+
// the entry whose contents were rewritten — nft patches package.json files
|
|
244
|
+
// into `rewrites`, and the unpatched twin would ship the wrong manifest.
|
|
245
|
+
if (existing === undefined || (!rewrites.has(existing.srcFile) && rewrites.has(resolved.srcFile))) {
|
|
246
|
+
byDestPath.set(resolved.destPath, resolved);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return [...byDestPath.values()];
|
|
250
|
+
};
|
|
186
251
|
const addStat = async function (cache, srcFile) {
|
|
187
252
|
const stat = await cachedLstat(cache.lstatCache, srcFile);
|
|
188
253
|
return { srcFile, stat };
|
|
189
254
|
};
|
|
190
|
-
const zipJsFile = function ({
|
|
191
|
-
const destPath = aliases.get(srcFile) || srcFile;
|
|
192
|
-
const normalizedDestPath = normalizeFilePath({ commonPrefix, path: destPath, userNamespace });
|
|
255
|
+
const zipJsFile = function ({ archive, destPath, rewrites = new Map(), stat, srcFile, }) {
|
|
193
256
|
if (rewrites.has(srcFile)) {
|
|
194
|
-
addZipContent(archive, rewrites.get(srcFile),
|
|
257
|
+
addZipContent(archive, rewrites.get(srcFile), destPath);
|
|
195
258
|
}
|
|
196
259
|
else {
|
|
197
|
-
addZipFile(archive, srcFile,
|
|
260
|
+
addZipFile(archive, srcFile, destPath, stat);
|
|
198
261
|
}
|
|
199
262
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/zip-it-and-ship-it",
|
|
3
|
-
"version": "16.2.
|
|
3
|
+
"version": "16.2.1",
|
|
4
4
|
"description": "Zip it and ship it",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"type": "module",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"engines": {
|
|
101
101
|
"node": ">=22.12.0"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "d99b17e8a25b18687ce7b7657621dd22827304b7"
|
|
104
104
|
}
|