@netlify/zip-it-and-ship-it 15.3.4 → 15.4.0
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.
- package/dist/feature_flags.d.ts +2 -0
- package/dist/feature_flags.js +1 -0
- package/dist/runtimes/container/archive.d.ts +9 -0
- package/dist/runtimes/container/archive.js +100 -0
- package/dist/runtimes/container/index.d.ts +3 -0
- package/dist/runtimes/container/index.js +57 -0
- package/dist/runtimes/index.js +2 -1
- package/dist/runtimes/runtime.d.ts +1 -0
- package/dist/runtimes/runtime.js +1 -0
- package/package.json +2 -2
package/dist/feature_flags.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const defaultFlags: {
|
|
|
7
7
|
readonly zisi_esbuild_fail_double_glob: false;
|
|
8
8
|
readonly zisi_add_instrumentation_loader: true;
|
|
9
9
|
readonly zisi_netlify_play: false;
|
|
10
|
+
readonly zisi_container_functions: false;
|
|
10
11
|
};
|
|
11
12
|
export type FeatureFlags = Partial<Record<keyof typeof defaultFlags, boolean>>;
|
|
12
13
|
export declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
@@ -18,4 +19,5 @@ export declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
|
18
19
|
readonly zisi_esbuild_fail_double_glob: false;
|
|
19
20
|
readonly zisi_add_instrumentation_loader: true;
|
|
20
21
|
readonly zisi_netlify_play: false;
|
|
22
|
+
readonly zisi_container_functions: false;
|
|
21
23
|
}) => FeatureFlags;
|
package/dist/feature_flags.js
CHANGED
|
@@ -17,6 +17,7 @@ export const defaultFlags = {
|
|
|
17
17
|
// Adds the `___netlify-telemetry.mjs` file to the function bundle.
|
|
18
18
|
zisi_add_instrumentation_loader: true,
|
|
19
19
|
zisi_netlify_play: false,
|
|
20
|
+
zisi_container_functions: false,
|
|
20
21
|
};
|
|
21
22
|
// List of supported flags and their default value.
|
|
22
23
|
export const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports whether the file at `path` is a container image archive, in either
|
|
3
|
+
* the OCI or the Docker flavor.
|
|
4
|
+
*
|
|
5
|
+
* The check reads tar headers rather than trusting the extension, since a
|
|
6
|
+
* `.tar` in a functions directory could be anything. Only header blocks are
|
|
7
|
+
* read, never entry contents, so the cost does not scale with image size.
|
|
8
|
+
*/
|
|
9
|
+
export declare const isImageArchive: (path: string) => Promise<boolean>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { open } from 'fs/promises';
|
|
2
|
+
// A tar header block is 512 bytes, and file contents are padded to the same
|
|
3
|
+
// boundary.
|
|
4
|
+
const BLOCK_SIZE = 512;
|
|
5
|
+
// The entry name occupies the first 100 bytes of a header.
|
|
6
|
+
const NAME_LENGTH = 100;
|
|
7
|
+
// The entry size is a 12-byte octal string starting at offset 124.
|
|
8
|
+
const SIZE_OFFSET = 124;
|
|
9
|
+
const SIZE_LENGTH = 12;
|
|
10
|
+
// The header checksum is an 8-byte octal string starting at offset 148.
|
|
11
|
+
const CHECKSUM_OFFSET = 148;
|
|
12
|
+
const CHECKSUM_LENGTH = 8;
|
|
13
|
+
// How many entries to inspect before giving up.
|
|
14
|
+
const MAX_ENTRIES = 4096;
|
|
15
|
+
// An OCI archive is a tar of an OCI layout. Always carries this marker file.
|
|
16
|
+
const OCI_LAYOUT_MARKER = 'oci-layout';
|
|
17
|
+
// A Docker archive declares its images in a root manifest.
|
|
18
|
+
const DOCKER_MANIFEST_MARKER = 'manifest.json';
|
|
19
|
+
/**
|
|
20
|
+
* Reports whether the file at `path` is a container image archive, in either
|
|
21
|
+
* the OCI or the Docker flavor.
|
|
22
|
+
*
|
|
23
|
+
* The check reads tar headers rather than trusting the extension, since a
|
|
24
|
+
* `.tar` in a functions directory could be anything. Only header blocks are
|
|
25
|
+
* read, never entry contents, so the cost does not scale with image size.
|
|
26
|
+
*/
|
|
27
|
+
export const isImageArchive = async (path) => {
|
|
28
|
+
let handle;
|
|
29
|
+
try {
|
|
30
|
+
handle = await open(path, 'r');
|
|
31
|
+
const header = Buffer.alloc(BLOCK_SIZE);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
for (let entry = 0; entry < MAX_ENTRIES; entry++) {
|
|
34
|
+
const { bytesRead } = await handle.read(header, 0, BLOCK_SIZE, offset);
|
|
35
|
+
if (bytesRead < BLOCK_SIZE) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const name = readName(header);
|
|
39
|
+
// Two consecutive empty blocks terminate a tar archive; a single empty
|
|
40
|
+
// name means we have run out of entries to look at.
|
|
41
|
+
if (name === '') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// A tar header checksums its own bytes, so a file whose leading bytes
|
|
45
|
+
// merely imitate a marker name fails here.
|
|
46
|
+
if (!hasValidChecksum(header)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (name === OCI_LAYOUT_MARKER || name === DOCKER_MANIFEST_MARKER) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const size = readSize(header);
|
|
53
|
+
if (size === undefined) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// Skip the header and the entry's contents, rounded up to a block.
|
|
57
|
+
offset += BLOCK_SIZE + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Unreadable, a directory, or not a tar at all: not an image archive.
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await handle?.close();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const readName = (header) => {
|
|
70
|
+
const raw = header.subarray(0, NAME_LENGTH);
|
|
71
|
+
const end = raw.indexOf(0);
|
|
72
|
+
return raw.subarray(0, end === -1 ? NAME_LENGTH : end).toString('utf8');
|
|
73
|
+
};
|
|
74
|
+
// The checksum field holds the sum of all header bytes with the field itself
|
|
75
|
+
// read as spaces.
|
|
76
|
+
const hasValidChecksum = (header) => {
|
|
77
|
+
const raw = header.subarray(CHECKSUM_OFFSET, CHECKSUM_OFFSET + CHECKSUM_LENGTH).toString('utf8');
|
|
78
|
+
const expected = parseInt(raw.replace(/\0/g, '').trim(), 8);
|
|
79
|
+
if (Number.isNaN(expected)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
let unsigned = 0;
|
|
83
|
+
let signed = 0;
|
|
84
|
+
for (let index = 0; index < BLOCK_SIZE; index++) {
|
|
85
|
+
const inChecksumField = index >= CHECKSUM_OFFSET && index < CHECKSUM_OFFSET + CHECKSUM_LENGTH;
|
|
86
|
+
const byte = inChecksumField ? 0x20 : header[index];
|
|
87
|
+
unsigned += byte;
|
|
88
|
+
signed += byte < 0x80 ? byte : byte - 0x100;
|
|
89
|
+
}
|
|
90
|
+
return expected === unsigned || expected === signed;
|
|
91
|
+
};
|
|
92
|
+
const readSize = (header) => {
|
|
93
|
+
const raw = header.subarray(SIZE_OFFSET, SIZE_OFFSET + SIZE_LENGTH).toString('utf8');
|
|
94
|
+
const octal = raw.replace(/\0/g, '').trim();
|
|
95
|
+
if (octal === '') {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const size = parseInt(octal, 8);
|
|
99
|
+
return Number.isNaN(size) || size < 0 ? undefined : size;
|
|
100
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { basename, dirname, extname } from 'path';
|
|
2
|
+
import { copyFile } from 'copy-file';
|
|
3
|
+
import { cachedLstat } from '../../utils/fs.js';
|
|
4
|
+
import getInternalValue from '../../utils/get_internal_value.js';
|
|
5
|
+
import { nonNullable } from '../../utils/non_nullable.js';
|
|
6
|
+
import { RUNTIME } from '../runtime.js';
|
|
7
|
+
import { isImageArchive } from './archive.js';
|
|
8
|
+
// The extension a container image archive is expected to carry. Restricting by
|
|
9
|
+
// extension first keeps us from reading every file in the functions directory.
|
|
10
|
+
const ARCHIVE_EXTENSION = '.tar';
|
|
11
|
+
const findFunctionsInPaths = async function ({ cache, featureFlags, paths }) {
|
|
12
|
+
const functions = await Promise.all(paths.map((path) => findFunctionInPath({ cache, featureFlags, path })));
|
|
13
|
+
return functions.filter(nonNullable);
|
|
14
|
+
};
|
|
15
|
+
const findFunctionInPath = async function ({ cache, featureFlags, path }) {
|
|
16
|
+
if (!featureFlags.zisi_container_functions) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (extname(path) !== ARCHIVE_EXTENSION) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const stat = await cachedLstat(cache.lstatCache, path);
|
|
23
|
+
if (stat.isDirectory()) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!(await isImageArchive(path))) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return processArchive({ cache, path });
|
|
30
|
+
};
|
|
31
|
+
const processArchive = async ({ cache, path }) => {
|
|
32
|
+
const stat = await cachedLstat(cache.lstatCache, path);
|
|
33
|
+
return {
|
|
34
|
+
extension: ARCHIVE_EXTENSION,
|
|
35
|
+
filename: basename(path),
|
|
36
|
+
mainFile: path,
|
|
37
|
+
name: basename(path, ARCHIVE_EXTENSION),
|
|
38
|
+
srcDir: dirname(path),
|
|
39
|
+
srcPath: path,
|
|
40
|
+
stat,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const zipFunction = async function ({ config, destFolder, filename, srcPath, isInternal }) {
|
|
44
|
+
const destPath = `${destFolder}/${filename}`;
|
|
45
|
+
await copyFile(srcPath, destPath);
|
|
46
|
+
return {
|
|
47
|
+
config,
|
|
48
|
+
displayName: config.name,
|
|
49
|
+
generator: config.generator ?? getInternalValue(isInternal),
|
|
50
|
+
path: destPath,
|
|
51
|
+
// A container starts from the entrypoint recorded in its own image config.
|
|
52
|
+
// There is no entry file for the platform to call into.
|
|
53
|
+
entryFilename: '',
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const runtime = { findFunctionsInPaths, findFunctionInPath, name: RUNTIME.CONTAINER, zipFunction };
|
|
57
|
+
export default runtime;
|
package/dist/runtimes/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { extname, basename } from 'path';
|
|
|
2
2
|
import { getConfigForFunction } from '../config.js';
|
|
3
3
|
import { defaultFlags } from '../feature_flags.js';
|
|
4
4
|
import { FunctionBundlingUserError } from '../utils/error.js';
|
|
5
|
+
import containerRuntime from './container/index.js';
|
|
5
6
|
import goRuntime from './go/index.js';
|
|
6
7
|
import jsRuntime from './node/index.js';
|
|
7
8
|
import { ENTRY_FILE_NAME } from './node/utils/entry_file.js';
|
|
@@ -43,7 +44,7 @@ const findFunctionsInRuntime = async function ({ cache, dedupe = false, featureF
|
|
|
43
44
|
// The order of this array determines the priority of the runtimes. If a path
|
|
44
45
|
// is used by the first time, it won't be made available to the subsequent
|
|
45
46
|
// runtimes.
|
|
46
|
-
const RUNTIMES = [jsRuntime, goRuntime, rustRuntime];
|
|
47
|
+
const RUNTIMES = [containerRuntime, jsRuntime, goRuntime, rustRuntime];
|
|
47
48
|
/**
|
|
48
49
|
* Gets a list of functions found in a list of paths.
|
|
49
50
|
*/
|
|
@@ -11,6 +11,7 @@ import { Logger } from '../utils/logger.js';
|
|
|
11
11
|
import type { BundlerReason, NodeBundlerName } from './node/bundlers/types.js';
|
|
12
12
|
import type { StaticAnalysisResult } from './node/in_source_config/index.js';
|
|
13
13
|
export declare const RUNTIME: {
|
|
14
|
+
readonly CONTAINER: "container";
|
|
14
15
|
readonly GO: "go";
|
|
15
16
|
readonly JAVASCRIPT: "js";
|
|
16
17
|
readonly RUST: "rs";
|
package/dist/runtimes/runtime.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/zip-it-and-ship-it",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.4.0",
|
|
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": "aaee524cfed2fc082e6cb7268ce7230071b39068"
|
|
104
104
|
}
|