|
1 | | -import { readdir } from "node:fs/promises"; |
2 | | -import { writeFile } from "node:fs/promises"; |
| 1 | +import { createGzip } from "node:zlib"; |
| 2 | +import { createWriteStream } from "node:fs"; |
| 3 | +import { readdir, readFile, stat, writeFile } from "node:fs/promises"; |
3 | 4 | import { basename, join, relative, resolve } from "node:path"; |
| 5 | +import { Readable } from "node:stream"; |
| 6 | +import { pipeline } from "node:stream/promises"; |
4 | 7 |
|
5 | 8 | export type ExtensionManifestRecord = Record<string, unknown>; |
6 | 9 |
|
@@ -136,3 +139,80 @@ export async function writeExtensionManifest( |
136 | 139 | ) { |
137 | 140 | await writeFile(manifestPath, `${stringifyManifest(manifest)}\n`); |
138 | 141 | } |
| 142 | + |
| 143 | +async function listPackageFiles(root: string) { |
| 144 | + const files: string[] = []; |
| 145 | + |
| 146 | + async function walk(directory: string) { |
| 147 | + for (const entry of await readdir(directory, { withFileTypes: true })) { |
| 148 | + if (entry.name === ".DS_Store") continue; |
| 149 | + |
| 150 | + const absolutePath = join(directory, entry.name); |
| 151 | + if (entry.isDirectory()) { |
| 152 | + await walk(absolutePath); |
| 153 | + } else if (entry.isFile()) { |
| 154 | + files.push(relative(root, absolutePath)); |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + await walk(root); |
| 160 | + return files.sort((a, b) => a.localeCompare(b)); |
| 161 | +} |
| 162 | + |
| 163 | +function writeOctalField(header: Buffer, offset: number, length: number, value: number) { |
| 164 | + const octal = value.toString(8).padStart(length - 1, "0"); |
| 165 | + header.write(octal, offset, length - 1, "ascii"); |
| 166 | + header[offset + length - 1] = 0; |
| 167 | +} |
| 168 | + |
| 169 | +function writeTarHeader(path: string, size: number, mode: number) { |
| 170 | + const header = Buffer.alloc(512, 0); |
| 171 | + const normalizedPath = path.replace(/\\/g, "/"); |
| 172 | + |
| 173 | + if (Buffer.byteLength(normalizedPath) > 100) { |
| 174 | + throw new Error(`Packaged extension path is too long for portable tar: ${normalizedPath}`); |
| 175 | + } |
| 176 | + |
| 177 | + header.write(normalizedPath, 0, 100, "utf8"); |
| 178 | + writeOctalField(header, 100, 8, mode & 0o777); |
| 179 | + writeOctalField(header, 108, 8, 0); |
| 180 | + writeOctalField(header, 116, 8, 0); |
| 181 | + writeOctalField(header, 124, 12, size); |
| 182 | + writeOctalField(header, 136, 12, 1577836800); |
| 183 | + header.fill(" ", 148, 156); |
| 184 | + header[156] = "0".charCodeAt(0); |
| 185 | + header.write("ustar", 257, 6, "ascii"); |
| 186 | + header.write("00", 263, 2, "ascii"); |
| 187 | + |
| 188 | + let checksum = 0; |
| 189 | + for (const byte of header) checksum += byte; |
| 190 | + const checksumText = checksum.toString(8).padStart(6, "0"); |
| 191 | + header.write(checksumText, 148, 6, "ascii"); |
| 192 | + header[154] = 0; |
| 193 | + header[155] = 0x20; |
| 194 | + |
| 195 | + return header; |
| 196 | +} |
| 197 | + |
| 198 | +export async function writeStableTarGz(root: string, packagePath: string) { |
| 199 | + const chunks: Buffer[] = []; |
| 200 | + |
| 201 | + for (const file of await listPackageFiles(root)) { |
| 202 | + const absolutePath = join(root, file); |
| 203 | + const fileStats = await stat(absolutePath); |
| 204 | + const contents = await readFile(absolutePath); |
| 205 | + chunks.push(writeTarHeader(file, contents.length, fileStats.mode)); |
| 206 | + chunks.push(contents); |
| 207 | + |
| 208 | + const padding = (512 - (contents.length % 512)) % 512; |
| 209 | + if (padding > 0) { |
| 210 | + chunks.push(Buffer.alloc(padding, 0)); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + chunks.push(Buffer.alloc(1024, 0)); |
| 215 | + const output = createWriteStream(packagePath); |
| 216 | + const gzip = createGzip({ level: 9 }); |
| 217 | + await pipeline(Readable.from(chunks), gzip, output); |
| 218 | +} |
0 commit comments