diff --git a/build/office-engine-resolution.mjs b/build/office-engine-resolution.mjs index a0d1a33ed..713f59832 100644 --- a/build/office-engine-resolution.mjs +++ b/build/office-engine-resolution.mjs @@ -26,8 +26,12 @@ export function packagedArchiveRoot(dshEntryPath) { * Hooks apply to this thread only. */ export function registerOfficeEngineResolution(dshEntryPath) { - const archive = packagedArchiveRoot(dshEntryPath) - if (archive === undefined) return undefined + const entryArchive = packagedArchiveRoot(dshEntryPath) + if (entryArchive === undefined) return undefined + // Compare canonical paths on both sides, as upstream does: resolved URLs are + // realpath'd below, and the entry path can differ in spelling (a symlinked + // or junctioned install directory, Windows 8.3 names or drive-letter case). + const archive = realpathSync(entryArchive) const engines = join(archive, 'node_modules', '@deepseek-ai', 'libreoffice-kit-') const source = pathToFileURL(engines).href const destination = pathToFileURL(join(`${archive}.unpacked`, 'node_modules', '@deepseek-ai', 'libreoffice-kit-')).href diff --git a/test/office-engine-resolution.test.ts b/test/office-engine-resolution.test.ts index c5128dd0e..8d9a83c39 100644 --- a/test/office-engine-resolution.test.ts +++ b/test/office-engine-resolution.test.ts @@ -1,8 +1,8 @@ import { spawnSync } from 'node:child_process' -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { pathToFileURL } from 'node:url' import { afterEach, describe, expect, it } from 'vitest' import { packagedArchiveRoot, registerOfficeEngineResolution } from '../build/office-engine-resolution.mjs' @@ -63,6 +63,18 @@ describe('Office engine resolution', () => { expect(resolveEngine(archive, entry)).toBe(join(`${archive}.unpacked`, 'node_modules', ENGINE, 'package.json')) }) + it('matches when the entry path names the install directory through a link', async () => { + // Resolved URLs are canonical; the entry path may not be (a symlinked or + // junctioned install, Windows 8.3 names). Without canonicalizing the + // archive the hook never matched and the engine stayed inside app.asar. + const { archive } = await packagedLayout() + const linked = join(dirname(dirname(archive)), 'Linked Resources') + await symlink(dirname(archive), linked, process.platform === 'win32' ? 'junction' : 'dir') + const linkedArchive = join(linked, 'app.asar') + const entry = join(linkedArchive, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js') + expect(resolveEngine(linkedArchive, entry)).toBe(join(`${archive}.unpacked`, 'node_modules', ENGINE, 'package.json')) + }) + it('installs nothing outside an app.asar installation', () => { expect(registerOfficeEngineResolution('/repo/node_modules/@deepseek-ai/dsh/lib/bin.js')).toBeUndefined() })