Generate zip file bundle.

This commit is contained in:
Bryce Gibson
2021-09-18 17:45:15 +10:00
parent fc09a7d903
commit ba3b91eaf1
5 changed files with 547 additions and 161 deletions
+6 -5
View File
@@ -136,7 +136,7 @@ export class NexeCompiler {
}
@bound
addResource(absoluteFileName: string, content?: Buffer | string | File) {
addResource(absoluteFileName: string, content?: Buffer | string | File | null) {
return this.bundle.addResource(absoluteFileName, content)
}
@@ -291,19 +291,20 @@ export class NexeCompiler {
if (!this.options.mangle) {
return binary
}
const resources = this.bundle.renderIndex()
this.shims.unshift(wrap(`process.__nexe = ${JSON.stringify({ resources })};\n`))
const code = this.code(),
codeSize = Buffer.byteLength(code),
lengths = Buffer.from(Array(16))
const bundleBuffer = await this.bundle.toBuffer()
lengths.writeDoubleLE(codeSize, 0)
lengths.writeDoubleLE(this.bundle.size, 8)
lengths.writeDoubleLE(bundleBuffer.length, 8)
return new (MultiStream as any)([
binary,
toStream(code),
this.bundle.toStream(),
toStream(bundleBuffer),
toStream(Buffer.concat([Buffer.from('<nexe~~sentinel>'), lengths])),
])
}
+21 -131
View File
@@ -5,155 +5,45 @@ import { Readable } from 'stream'
import { argv } from '../options'
import { File } from 'resolve-dependencies'
import MultiStream = require('multistream')
const archiver: any = require('archiver')
const highland: any = require('highland')
const lstat = promisify(fs.lstat),
realpath = promisify(fs.realpath),
createReadStream = fs.createReadStream,
stat = promisify(fs.stat)
export type MultiStreams = (Readable | (() => Readable))[]
function makeRelative(cwd: string, path: string) {
return './' + relative(cwd, path)
}
function sortEntry(a: [string, File], b: [string, File]) {
return a[0] > b[0] ? 1 : -1
function makeRelativeToZip(cwd: string, path: string) {
return '/snapshot/' + relative(cwd, path)
}
export function toStream(content: Buffer | string) {
const readable = new Readable({
read() {
this.push(content)
this.push(null)
},
})
return readable
return highland([content])
}
async function createFile(absoluteFileName: string) {
const stats = await lstat(absoluteFileName),
file: File = {
size: stats.size,
moduleType: 'commonjs',
contents: '',
absPath: absoluteFileName,
deps: {},
}
if (stats.isSymbolicLink()) {
file.size = stats.size
const [realPath, realStat] = await Promise.all([
realpath(absoluteFileName),
stat(absoluteFileName),
])
file.realPath = realPath
file.realSize = realStat.size
}
return file
}
export class Bundle {
size = 0
cwd: string
rendered = false
private offset = 0
private index: { [key: string]: [number, number] } = {}
private files: { [key: string]: File } = {}
streams: MultiStreams = []
files = new Set<string>()
zip: any
constructor({ cwd }: { cwd: string } = { cwd: process.cwd() }) {
this.cwd = cwd
this.zip = archiver('zip')
}
get list() {
return Object.keys(this.files)
return Array.from(this.files)
}
public async addResource(
absoluteFileName: string,
content?: File | Buffer | string
): Promise<number> {
if (this.files[absoluteFileName]) {
return this.size
}
if (typeof content === 'string' || Buffer.isBuffer(content)) {
this.files[absoluteFileName] = {
size: Buffer.byteLength(content),
moduleType: 'commonjs',
contents: content as any, //todo type is wrong here... should allow buffer
deps: {},
absPath: absoluteFileName,
}
} else if (content) {
this.files[content.absPath] = content
} else {
this.files[absoluteFileName] = {
absPath: absoluteFileName,
moduleType: 'commonjs',
contents: '',
deps: {},
size: 0,
}
this.files[absoluteFileName] = await createFile(absoluteFileName)
}
return (this.size += this.files[absoluteFileName].size)
}
/**
* De-dupe files by absolute path, partition by symlink/real
* Iterate over real, add entries
* Iterate over symlinks, add symlinks
*/
renderIndex() {
if (this.rendered) {
throw new Error('Bundle index already rendered')
}
const files = Object.entries(this.files),
realFiles: [string, File][] = [],
symLinks: [string, File][] = []
for (const entry of files) {
if (entry[1].realPath) {
symLinks.push(entry)
public addResource(absoluteFileName: string, content?: File | Buffer | string | null) {
const destPath = makeRelativeToZip(this.cwd, absoluteFileName)
if (!this.files.has(destPath)) {
if (content == null) {
this.zip.file(absoluteFileName, { name: destPath })
} else {
realFiles.push(entry)
this.zip.append(content, { name: destPath })
}
}
realFiles.sort(sortEntry)
symLinks.sort(sortEntry)
for (const [absPath, file] of realFiles) {
this.addEntry(absPath, file)
}
for (const [absPath, file] of symLinks) {
this.addEntry(file.realPath as string, file)
this.addEntry(absPath, file, file.realPath)
}
this.rendered = true
return this.index
}
/**
* Add a stream if needed and an entry with the required offset and size
* Ensure the calling order of this method is idempotent (eg, while iterating a sorted set)
* @param entryPath
* @param file
* @param useEntry
*/
addEntry(entryPath: string, file: File, useEntry?: string) {
const existingName = useEntry && makeRelative(this.cwd, useEntry),
name = makeRelative(this.cwd, entryPath),
size = file.realSize ?? file.size,
existingEntry = this.index[existingName ?? name]
this.index[name] = existingEntry || [this.offset, size]
if (!existingEntry) {
this.streams.push(() =>
file.contents ? toStream(file.contents) : createReadStream(file.absPath)
)
this.offset += size
this.files.add(destPath)
}
}
toStream() {
return new (MultiStream as any)(this.streams)
public async toBuffer(): Promise<Buffer> {
this.zip.finalize()
return await new Promise((resolve) =>
highland(this.zip).toArray((arr: Array<Buffer>) => resolve(Buffer.concat(arr)))
)
}
}
+7 -4
View File
@@ -61,9 +61,11 @@ export default async function bundle(compiler: NexeCompiler, next: any) {
compiler.entrypoint = './' + relative(cwd, input)
}
const { files, warnings } = resolveFiles(
const step = compiler.log.step('Resolving dependencies...')
const { files, warnings } = await resolveFiles(
input,
...Object.keys(compiler.bundle.list).filter((x) => x.endsWith('.js')),
...Object.keys(compiler.bundle.list).filter((x) => x.endsWith('.js') || x.endsWith('.mjs')),
{ cwd, expand: 'variable', loadContent: false }
)
@@ -76,8 +78,9 @@ export default async function bundle(compiler: NexeCompiler, next: any) {
//TODO: warnings.forEach((x) => console.log(x))
await Promise.all(
Object.entries(files).map(([key, file]) => {
return compiler.addResource(key, file)
Object.entries(files).map(([key]) => {
step.log(`Including dependency: ${key}`)
return compiler.addResource(key)
})
)