fix(books): downgrade archiver to v7 to restore zip export (#41)

archiver v8 switched to pure ESM with no default export and a
   completely different API, breaking every zip export request with
   TypeError: (0 , _archiver.default) is not a function. The NestJS
   SWC build produces CJS output, which cannot import the new ESM-only
   package at runtime.

   Downgrade to ~7.0.1 (the last CJS release) and add a Dependabot
   ignore rule for archiver major bumps so this cannot recur silently.

   Fixes #31
This commit is contained in:
neon
2026-05-18 15:48:46 -06:00
committed by GitHub
parent 38e901f3af
commit 1fc04f8af6
4 changed files with 12369 additions and 6664 deletions
+4
View File
@@ -14,6 +14,10 @@ updates:
- "npm"
commit-message:
prefix: "chore(deps)"
ignore:
# archiver v8 is ESM-only with a completely different API; pin to v7 until we migrate
- dependency-name: "archiver"
update-types: ["version-update:semver-major"]
groups:
production-dependencies:
dependency-type: "production"
+12272 -6663
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -64,7 +64,7 @@
"@nestjs/websockets": "^11.1.21",
"@parcel/watcher": "^2.5.6",
"@types/archiver": "^7.0.0",
"archiver": "^8.0.0",
"archiver": "~7.0.1",
"bcryptjs": "^3.0.3",
"cheerio": "^1.2.0",
"class-transformer": "^0.5.1",
@@ -559,6 +559,98 @@ describe('BookController', () => {
expect(archive.abort).toHaveBeenCalled();
});
it('aborts archive on raw "aborted" event', async () => {
const { controller, bookService } = makeController();
const { reply, emitRawEvent } = makeReply();
bookService.getExportFiles.mockResolvedValue({
files: [{ absolutePath: '/books/a.epub', zipPath: 'A.epub', sizeBytes: 10 }],
projectedBytes: 10,
bookCount: 1,
scope: 'primary',
});
const archive = {
pipe: vi.fn(),
file: vi.fn(),
on: vi.fn().mockReturnThis(),
abort: vi.fn(),
finalize: vi.fn().mockImplementation(() => {
emitRawEvent('aborted');
return Promise.reject(new Error('stream aborted'));
}),
};
(archiver as unknown as vi.Mock).mockReturnValueOnce(archive);
await expect(controller.exportBooks({ bookIds: [1], allFormats: false }, makeUser(), reply)).resolves.toBeUndefined();
expect(archive.abort).toHaveBeenCalled();
});
it('releases export slot when zip archive export fails', async () => {
const { controller, bookService } = makeController();
const { reply } = makeReply();
const release = vi.fn();
bookService.acquireExportSlot.mockReturnValueOnce(release);
bookService.getExportFiles.mockResolvedValue({
files: [{ absolutePath: '/books/a.epub', zipPath: 'A.epub', sizeBytes: 10 }],
projectedBytes: 10,
bookCount: 1,
scope: 'primary',
});
const archive = {
pipe: vi.fn(),
file: vi.fn(),
on: vi.fn().mockReturnThis(),
abort: vi.fn(),
finalize: vi.fn().mockRejectedValue(new Error('zip failure')),
};
(archiver as unknown as vi.Mock).mockReturnValueOnce(archive);
await expect(controller.exportBooks({ bookIds: [1], allFormats: false }, makeUser(), reply)).rejects.toThrow('zip failure');
expect(release).toHaveBeenCalledTimes(1);
});
it('archive warning event rejects the zip export', async () => {
const { controller, bookService } = makeController();
const { reply } = makeReply();
bookService.getExportFiles.mockResolvedValue({
files: [{ absolutePath: '/books/a.epub', zipPath: 'A.epub', sizeBytes: 10 }],
projectedBytes: 10,
bookCount: 1,
scope: 'primary',
});
const listeners: Record<string, (err: Error) => void> = {};
const archive = {
pipe: vi.fn(),
file: vi.fn(),
on: vi.fn().mockImplementation((event: string, cb: (err: Error) => void) => {
listeners[event] = cb;
return archive;
}),
abort: vi.fn(),
finalize: vi.fn().mockImplementation(() => {
void Promise.resolve().then(() => listeners['warning']?.(new Error('archive warning')));
return new Promise<void>(() => {});
}),
};
(archiver as unknown as vi.Mock).mockReturnValueOnce(archive);
await expect(controller.exportBooks({ bookIds: [1], allFormats: false }, makeUser(), reply)).rejects.toThrow('archive warning');
});
it('exportBooksDownload with scope=all passes through correctly', async () => {
const { controller, bookService } = makeController();
const { reply } = makeReply();
bookService.getExportFiles.mockResolvedValue({
files: [{ absolutePath: '/books/a.epub', zipPath: 'A.epub', sizeBytes: 10 }],
projectedBytes: 10,
bookCount: 1,
scope: 'all',
});
await controller.exportBooksDownload('5,6', 'all', makeUser(), reply);
expect(bookService.getExportFiles).toHaveBeenCalledWith([5, 6], expect.any(Object), 'all');
});
it('delegates book-level progress endpoint to service with current user id', async () => {
const { controller, bookService } = makeController();
const user = makeUser();