diff --git a/package.json b/package.json index 93079de3..2fce64e6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "agent" ], "scripts": { - "postinstall": "patch-package && node scripts/install-brand-assets.mjs && install-electron --no", + "postinstall": "patch-package && node scripts/install-plugin-compatibility.mjs && node scripts/install-brand-assets.mjs && install-electron --no", "dev": "electron-vite dev", "build": "electron-vite build", "icons:generate": "node scripts/generate-app-icons.mjs", diff --git a/scripts/install-plugin-compatibility.mjs b/scripts/install-plugin-compatibility.mjs new file mode 100644 index 00000000..a729518a --- /dev/null +++ b/scripts/install-plugin-compatibility.mjs @@ -0,0 +1,52 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const frontendDirectory = path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-web-frontend', + 'dist' +) +const indexPath = path.join(frontendDirectory, 'index.html') + +// rc.6 plugins registered `settings.plugin.item` as a list slot using `id`. +// rc.7+ made it a keyed slot that requires `options.key`, so legacy bundles +// fail to load with "keyed slot \"settings.plugin.item\" requires options.key". +// DSH Desktop cannot ship a fork of the Harness frontend, so we shim the +// minified SlotCore served to the browser: when the slot is `settings.plugin.item` +// and the registrant only has `id`, copy `id` into `key`. All other keyed +// slots stay strict. +const original = 'const u=l.spec,c=n.priority??0' +const compatible = + 'const u=l.spec;n.name==="settings.plugin.item"&&u.kind==="keyed"&&n.key===void 0&&n.id!==void 0&&(n={...n,key:n.id});const c=n.priority??0' + +const index = await readFile(indexPath, 'utf8') +const assetUrl = index.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1] +if (assetUrl === undefined) { + throw new Error( + 'Could not install plugin compatibility: DSH frontend asset was not found in dist/index.html' + ) +} + +const assetPath = path.join(frontendDirectory, assetUrl) +const asset = await readFile(assetPath, 'utf8') + +if (asset.includes(compatible)) { + console.log( + `Legacy plugin compatibility already installed: ${path.relative(projectRoot, assetPath)}` + ) +} else { + const occurrences = asset.split(original).length - 1 + if (occurrences !== 1) { + throw new Error( + `Could not install plugin compatibility in ${path.relative(projectRoot, assetPath)}: expected exactly one SlotCore registration site, found ${occurrences}. The DSH frontend bundle shape has changed; update scripts/install-plugin-compatibility.mjs.` + ) + } + await writeFile(assetPath, asset.replace(original, compatible)) + console.log( + `Installed legacy plugin compatibility: ${path.relative(projectRoot, assetPath)}` + ) +} diff --git a/test/plugin-slot-compatibility.test.ts b/test/plugin-slot-compatibility.test.ts new file mode 100644 index 00000000..7a3d47da --- /dev/null +++ b/test/plugin-slot-compatibility.test.ts @@ -0,0 +1,46 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +const projectRoot = path.resolve(import.meta.dirname, '..') + +const SHIM_FRAGMENT = + 'n.name==="settings.plugin.item"&&u.kind==="keyed"&&n.key===void 0&&n.id!==void 0&&(n={...n,key:n.id})' + +const FRONTEND_DIST = path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-web-frontend', + 'dist' +) + +describe('legacy plugin settings slot compatibility', () => { + it('runs the postinstall shim and ships the new step in package.json', async () => { + const packageJson = JSON.parse( + await readFile(path.join(projectRoot, 'package.json'), 'utf8') + ) as { scripts: { postinstall: string } } + const installer = await readFile( + path.join(projectRoot, 'scripts', 'install-plugin-compatibility.mjs'), + 'utf8' + ) + + expect(packageJson.scripts.postinstall).toContain( + 'node scripts/install-plugin-compatibility.mjs' + ) + expect(installer).toContain( + 'const u=l.spec;n.name==="settings.plugin.item"&&u.kind==="keyed"&&n.key===void 0&&n.id!==void 0&&(n={...n,key:n.id});const c=n.priority??0' + ) + expect(installer).toContain('const u=l.spec,c=n.priority??0') + }) + + it('patches the SlotCore bundle actually served by the desktop app', async () => { + const indexHtml = await readFile(path.join(FRONTEND_DIST, 'index.html'), 'utf8') + const asset = indexHtml.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1] + expect(asset).toBeDefined() + + const servedAsset = await readFile(path.join(FRONTEND_DIST, asset ?? ''), 'utf8') + expect(servedAsset).toContain(SHIM_FRAGMENT) + expect(servedAsset).not.toContain('const u=l.spec,c=n.priority??0') + }) +})