Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Install dependencies
run: yarn install --immutable
- name: Lint
run: yarn prettier:check
run: yarn lint
- name: Build
run: yarn build
- name: Test
Expand Down
9 changes: 9 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"trailingComma": "all",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"sortPackageJson": false,
"ignorePatterns": ["*.md", "*.yml", "*.yaml"]
}
10 changes: 10 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import"],
"categories": {
"correctness": "warn"
},
"options": {
"typeAware": true
}
}
6 changes: 0 additions & 6 deletions .prettierrc.json

This file was deleted.

22 changes: 16 additions & 6 deletions benchmark/profile-extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { generateFixture, FIXTURES } from './generate-fixtures.js';
import { createPackage, extractAll, listPackage, getRawHeader, uncache } from '../lib/asar.js';
import { createPackage, extractAll, uncache } from '../lib/asar.js';
import { readArchiveHeaderSync, readFilesystemSync } from '../lib/disk.js';

const config = FIXTURES.find((f) => f.name === 'many-small-files')!;
Expand All @@ -12,7 +12,9 @@ const archiveFile = path.join(tmpDir, 'test.asar');
await createPackage(fixtureDir, archiveFile);
const archiveSize = fs.statSync(archiveFile).size;

console.log(`=== Extract breakdown: ${config.fileCount} files, archive ${(archiveSize / 1024).toFixed(0)} KB ===\n`);
console.log(
`=== Extract breakdown: ${config.fileCount} files, archive ${(archiveSize / 1024).toFixed(0)} KB ===\n`,
);

// 1. Header parsing
{
Expand All @@ -27,7 +29,9 @@ console.log(`=== Extract breakdown: ${config.fileCount} files, archive ${(archiv
const filesystem = readFilesystemSync(archiveFile);
const t = performance.now();
const files = filesystem.listFiles();
console.log(`listFiles: ${(performance.now() - t).toFixed(2)}ms (${files.length} entries)`);
console.log(
`listFiles: ${(performance.now() - t).toFixed(2)}ms (${files.length} entries)`,
);

// 3. getFile lookups
const followLinks = process.platform === 'win32';
Expand Down Expand Up @@ -63,7 +67,9 @@ console.log(`=== Extract breakdown: ${config.fileCount} files, archive ${(archiv
}
}
fs.closeSync(fd);
console.log(`readSync (individual): ${(performance.now() - t).toFixed(2)}ms (${totalRead} bytes)`);
console.log(
`readSync (individual): ${(performance.now() - t).toFixed(2)}ms (${totalRead} bytes)`,
);

// 5. Read all data in one shot
const t2 = performance.now();
Expand Down Expand Up @@ -139,13 +145,17 @@ console.log(`=== Extract breakdown: ${config.fileCount} files, archive ${(archiv

let t = performance.now();
for (const d of dirEntries) filesystem.insertDirectory(d, false);
console.log(`insertDirectory: ${(performance.now() - t).toFixed(1)}ms (${dirEntries.length})`);
console.log(
`insertDirectory: ${(performance.now() - t).toFixed(1)}ms (${dirEntries.length})`,
);

t = performance.now();
for (const f of fileEntries) {
await filesystem.insertFile(f, () => fs.createReadStream(f), false, metadata[f]);
}
console.log(`insertFile (all): ${(performance.now() - t).toFixed(1)}ms (${fileEntries.length})`);
console.log(
`insertFile (all): ${(performance.now() - t).toFixed(1)}ms (${fileEntries.length})`,
);
}

fs.rmSync(tmpDir, { recursive: true, force: true });
18 changes: 11 additions & 7 deletions benchmark/profile-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { generateFixture, formatBytes, FIXTURES } from './generate-fixtures.js';
import { createPackage, createPackageFromFiles, uncache } from '../lib/asar.js';
import { createPackage, createPackageFromFiles } from '../lib/asar.js';
import { crawl } from '../lib/crawlfs.js';

function getMemoryUsage() {
Expand All @@ -15,8 +15,8 @@ function printMem(label: string, before: NodeJS.MemoryUsage, after: NodeJS.Memor
const rssDelta = after.rss - before.rss;
console.log(
` ${label.padEnd(30)} ` +
`heap: ${formatBytes(after.heapUsed).padStart(10)} (Δ ${(heapDelta >= 0 ? '+' : '') + formatBytes(heapDelta)}) ` +
`rss: ${formatBytes(after.rss).padStart(10)} (Δ ${(rssDelta >= 0 ? '+' : '') + formatBytes(rssDelta)})`
`heap: ${formatBytes(after.heapUsed).padStart(10)} (Δ ${(heapDelta >= 0 ? '+' : '') + formatBytes(heapDelta)}) ` +
`rss: ${formatBytes(after.rss).padStart(10)} (Δ ${(rssDelta >= 0 ? '+' : '') + formatBytes(rssDelta)})`,
);
}

Expand Down Expand Up @@ -59,9 +59,11 @@ async function profileFixture(name: string) {
printMem('createPackage (after)', before, after);
console.log(
` ${'peak heap'.padEnd(30)} ${formatBytes(peakHeap).padStart(10)} (Δ +${formatBytes(peakHeap - before.heapUsed)}) ` +
`rss: ${formatBytes(peakRss).padStart(10)} (Δ +${formatBytes(peakRss - before.rss)})`
`rss: ${formatBytes(peakRss).padStart(10)} (Δ +${formatBytes(peakRss - before.rss)})`,
);
console.log(
` ${'data / peak-heap-delta'.padEnd(30)} ${((peakHeap - before.heapUsed) / dataSize).toFixed(2)}x data size`,
);
console.log(` ${'data / peak-heap-delta'.padEnd(30)} ${((peakHeap - before.heapUsed) / dataSize).toFixed(2)}x data size`);
}

// Measure: createPackageFromFiles (pre-crawled)
Expand Down Expand Up @@ -90,9 +92,11 @@ async function profileFixture(name: string) {
printMem('createPackageFromFiles (after)', before, after);
console.log(
` ${'peak heap'.padEnd(30)} ${formatBytes(peakHeap).padStart(10)} (Δ +${formatBytes(peakHeap - before.heapUsed)}) ` +
`rss: ${formatBytes(peakRss).padStart(10)} (Δ +${formatBytes(peakRss - before.rss)})`
`rss: ${formatBytes(peakRss).padStart(10)} (Δ +${formatBytes(peakRss - before.rss)})`,
);
console.log(
` ${'data / peak-heap-delta'.padEnd(30)} ${((peakHeap - before.heapUsed) / dataSize).toFixed(2)}x data size`,
);
console.log(` ${'data / peak-heap-delta'.padEnd(30)} ${((peakHeap - before.heapUsed) / dataSize).toFixed(2)}x data size`);
}

fs.rmSync(tmpDir, { recursive: true, force: true });
Expand Down
3 changes: 2 additions & 1 deletion benchmark/profile-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const config = FIXTURES.find((f) => f.name === 'many-small-files')!;
const fixtureDir = generateFixture(config);

// Get all files
const allFiles = fs.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
const allFiles = fs
.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
.filter((e) => e.isFile())
.map((e) => path.join(e.parentPath, e.name));

Expand Down
13 changes: 9 additions & 4 deletions benchmark/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path';
import os from 'node:os';
import { generateFixture, FIXTURES } from './generate-fixtures.js';
import { crawl } from '../lib/crawlfs.js';
import { createPackage, createPackageFromFiles, extractAll, uncache } from '../lib/asar.js';
import { createPackageFromFiles, extractAll, uncache } from '../lib/asar.js';

const config = FIXTURES.find((f) => f.name === 'many-small-files')!;
const fixtureDir = generateFixture(config);
Expand Down Expand Up @@ -48,7 +48,8 @@ console.log('=== Profiling many-small-files (10,000 x ~256B = 2.4MB) ===\n');
const { getFileIntegrity } = await import('../lib/integrity.js');

// Hash 10,000 small files via streams
const files = fs.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
const files = fs
.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
.filter((e) => e.isFile())
.map((e) => path.join(e.parentPath, e.name));

Expand Down Expand Up @@ -76,7 +77,8 @@ console.log('=== Profiling many-small-files (10,000 x ~256B = 2.4MB) ===\n');
// Profile: how much time is file I/O vs stream setup?
{
console.log('--- STREAM vs BUFFER READ ---');
const files = fs.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
const files = fs
.readdirSync(fixtureDir, { withFileTypes: true, recursive: true })
.filter((e) => e.isFile())
.map((e) => path.join(e.parentPath, e.name));

Expand All @@ -87,7 +89,10 @@ console.log('=== Profiling many-small-files (10,000 x ~256B = 2.4MB) ===\n');
const s = fs.createReadStream(file);
const chunks: Buffer[] = [];
s.on('data', (c) => chunks.push(c as Buffer));
s.on('end', () => { Buffer.concat(chunks); resolve(); });
s.on('end', () => {
Buffer.concat(chunks);
resolve();
});
s.on('error', reject);
});
}
Expand Down
27 changes: 9 additions & 18 deletions benchmark/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@ import {
} from '../lib/asar.js';
import { crawl } from '../lib/crawlfs.js';
import { getFileIntegrity } from '../lib/integrity.js';
import {
FIXTURES,
generateFixture,
cleanFixtures,
formatBytes,
type FixtureConfig,
} from './generate-fixtures.js';
import { FIXTURES, generateFixture, formatBytes, type FixtureConfig } from './generate-fixtures.js';

// ─── Benchmark harness ───────────────────────────────────────────────

Expand Down Expand Up @@ -85,9 +79,7 @@ async function benchmark(
}

function printResult(result: BenchmarkResult) {
const throughput = result.throughputMBps
? ` | ${result.throughputMBps.toFixed(1)} MB/s`
: '';
const throughput = result.throughputMBps ? ` | ${result.throughputMBps.toFixed(1)} MB/s` : '';
console.log(
` ${result.name.padEnd(40)} ` +
`avg=${result.avgMs.toFixed(2).padStart(9)}ms ` +
Expand Down Expand Up @@ -136,10 +128,7 @@ async function benchmarkCrawl(fixtureDir: string, config: FixtureConfig): Promis
);
}

async function benchmarkPack(
fixtureDir: string,
config: FixtureConfig,
): Promise<BenchmarkResult> {
async function benchmarkPack(fixtureDir: string, config: FixtureConfig): Promise<BenchmarkResult> {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asar-bench-pack-'));
const destFile = path.join(tmpDir, `${config.name}.asar`);
const dataSize = getDirectorySize(fixtureDir);
Expand Down Expand Up @@ -359,7 +348,11 @@ async function main() {
printSection('Generating fixtures');
const fixtureDirs = new Map<string, string>();
const activeFixtures = FIXTURES.filter(
(f) => !filterArg || f.name === filterArg || f.name.startsWith(filterArg + '-') || filterArg === 'all',
(f) =>
!filterArg ||
f.name === filterArg ||
f.name.startsWith(filterArg + '-') ||
filterArg === 'all',
);

for (const config of activeFixtures) {
Expand All @@ -368,9 +361,7 @@ async function main() {
const elapsed = (performance.now() - start).toFixed(1);
const totalSize = getDirectorySize(dir);
const fileCount = getFileCount(dir);
console.log(
` ${config.name}: ${fileCount} files, ${formatBytes(totalSize)} (${elapsed}ms)`,
);
console.log(` ${config.name}: ${fileCount} files, ${formatBytes(totalSize)} (${elapsed}ms)`);
fixtureDirs.set(config.name, dir);
}

Expand Down
85 changes: 48 additions & 37 deletions bin/asar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,35 @@
import fs from 'node:fs';
import path from 'node:path';

const splitVersion = function (version) { return version.split('.').map(function (part) { return Number(part) }) }
const requiredNodeVersion = splitVersion(packageJSON.engines.node.slice(2))
const actualNodeVersion = splitVersion(process.versions.node)
const splitVersion = function (version) {
return version.split('.').map(function (part) {
return Number(part);
});
};
const requiredNodeVersion = splitVersion(packageJSON.engines.node.slice(2));
const actualNodeVersion = splitVersion(process.versions.node);

if (actualNodeVersion[0] < requiredNodeVersion[0] || (actualNodeVersion[0] === requiredNodeVersion[0] && actualNodeVersion[1] < requiredNodeVersion[1])) {
console.error('CANNOT RUN WITH NODE ' + process.versions.node)
console.error('asar requires Node ' + packageJSON.engines.node + '.')
process.exit(1)
if (
actualNodeVersion[0] < requiredNodeVersion[0] ||
(actualNodeVersion[0] === requiredNodeVersion[0] && actualNodeVersion[1] < requiredNodeVersion[1])
) {
console.error('CANNOT RUN WITH NODE ' + process.versions.node);
console.error('asar requires Node ' + packageJSON.engines.node + '.');
process.exit(1);
}

program.version('v' + packageJSON.version)
.description('Manipulate asar archive files')
program.version('v' + packageJSON.version).description('Manipulate asar archive files');

program.command('pack <dir> <output>')
program
.command('pack <dir> <output>')
.alias('p')
.description('create asar archive')
.option('--ordering <file path>', 'path to a text file for ordering contents')
.option('--unpack <expression>', 'do not pack files matching glob <expression>')
.option('--unpack-dir <expression>', 'do not pack dirs matching glob <expression> or starting with literal <expression>')
.option(
'--unpack-dir <expression>',
'do not pack dirs matching glob <expression> or starting with literal <expression>',
)
.option('--exclude-hidden', 'exclude hidden files')
.action(function (dir, output, options) {
options = {
Expand All @@ -34,50 +44,51 @@
version: options.sv,
arch: options.sa,
builddir: options.sb,
dot: !options.excludeHidden
}
createPackageWithOptions(dir, output, options).catch(error => {
console.error(error)
process.exit(1)
})
})
dot: !options.excludeHidden,
};
createPackageWithOptions(dir, output, options).catch((error) => {
console.error(error);
process.exit(1);
});
});

program.command('list <archive>')
program
.command('list <archive>')
.alias('l')
.description('list files of asar archive')
.option('-i, --is-pack', 'each file in the asar is pack or unpack')
.action(function (archive, options) {
options = {
isPack: options.isPack
}
const files = listPackage(archive, options)
isPack: options.isPack,
};
const files = listPackage(archive, options);
for (const i in files) {

Check warning on line 65 in bin/asar.mjs

View workflow job for this annotation

GitHub Actions / Test (22.12.x, macos-latest)

typescript-eslint(no-for-in-array)

For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties.

Check warning on line 65 in bin/asar.mjs

View workflow job for this annotation

GitHub Actions / Test (22.12.x, windows-latest)

typescript-eslint(no-for-in-array)

For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties.

Check warning on line 65 in bin/asar.mjs

View workflow job for this annotation

GitHub Actions / Test (22.12.x, ubuntu-22.04)

typescript-eslint(no-for-in-array)

For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties.
console.log(files[i])
console.log(files[i]);
}
})
});

program.command('extract-file <archive> <filename>')
program
.command('extract-file <archive> <filename>')
.alias('ef')
.description('extract one file from archive')
.action(function (archive, filename) {
fs.writeFileSync(path.basename(filename),
extractFile(archive, filename))
})
fs.writeFileSync(path.basename(filename), extractFile(archive, filename));
});

program.command('extract <archive> <dest>')
program
.command('extract <archive> <dest>')
.alias('e')
.description('extract archive')
.action(function (archive, dest) {
extractAll(archive, dest)
})
extractAll(archive, dest);
});

program.command('*', { hidden: true})
.action(function (_cmd, args) {
console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', args[0])
})
program.command('*', { hidden: true }).action(function (_cmd, args) {
console.log("asar: '%s' is not an asar command. See 'asar --help'.", args[0]);
});

program.parse(process.argv)
program.parse(process.argv);

if (program.args.length === 0) {
program.help()
program.help();
}
Loading
Loading