From 51a91e17ae54de92aeb4a0badb1e3c0451478e47 Mon Sep 17 00:00:00 2001 From: citizenl Date: Thu, 20 Aug 2026 18:28:07 +0800 Subject: [PATCH 1/3] fix(market): expose packaged pnpm services --- build/dsh-desktop.patch.yml | 1 + .../dsh-desktop-market-installer/index.js | 311 +++++++++++------- test/market-installer.test.js | 96 +++++- 3 files changed, 288 insertions(+), 120 deletions(-) diff --git a/build/dsh-desktop.patch.yml b/build/dsh-desktop.patch.yml index b3af2e59..ecda68f4 100644 --- a/build/dsh-desktop.patch.yml +++ b/build/dsh-desktop.patch.yml @@ -13,6 +13,7 @@ # The desktop shell has the real lifecycle authority, so keep that behavior off # whenever the optional plugin is present in the composed profile. - id: dsh-market + inject: [desktopProfiles] config: profile: web allowRestart: false diff --git a/packages/dsh-desktop-market-installer/index.js b/packages/dsh-desktop-market-installer/index.js index 0bd7728a..44c9b0e6 100644 --- a/packages/dsh-desktop-market-installer/index.js +++ b/packages/dsh-desktop-market-installer/index.js @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs' import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { homedir } from 'node:os' -import { delimiter, dirname, join, resolve } from 'node:path' +import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { SIDELINE_MARKER } from './pnpm-runner.mjs' @@ -20,7 +20,7 @@ const OPERATION_TIMEOUT_MS = 15 * 60 * 1000 const MAX_LOG_BYTES = 32 * 1024 export const name = 'dsh-desktop-market-installer' -export const inject = ['webServer'] +export const inject = [] function dshHome() { return process.env.DSH_HOME || join(homedir(), '.dsh') @@ -257,6 +257,140 @@ export async function ensurePnpmShim(home = dshHome()) { return directory } +function processPath(environment) { + return ( + (process.platform === 'win32' ? environment.Path : environment.PATH) ?? + environment.PATH ?? + environment.Path ?? + '' + ) +} + +export function buildPnpmEnvironment( + binDirectory, + environment = process.env, + executablePath = process.execPath +) { + const result = { ...environment } + for (const key of Object.keys(result)) { + if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete result[key] + } + + const seen = new Set() + const paths = [binDirectory, dirname(executablePath), ...processPath(environment).split(delimiter)] + .map((entry) => entry.trim()) + .filter((entry) => { + if (!entry) return false + const identity = process.platform === 'win32' ? entry.toLowerCase() : entry + if (seen.has(identity)) return false + seen.add(identity) + return true + }) + const value = paths.join(delimiter) + result.PATH = value + if (process.platform === 'win32') result.Path = value + result.CI = 'true' + result.NO_COLOR = '1' + result.PNPM_CONFIG_CHILD_CONCURRENCY = '1' + result.PNPM_CONFIG_PACKAGE_IMPORT_METHOD = 'clone-or-copy' + result.PNPM_CONFIG_SIDE_EFFECTS_CACHE = 'false' + return result +} + +export function createDesktopProfilesService(home = dshHome()) { + const current = Object.freeze({ + name: MARKET_PROFILE, + dir: profileDirectory(home) + }) + return Object.freeze({ + current, + list: () => [current], + select: async (name) => { + if (name !== MARKET_PROFILE) { + throw new Error(`DSH Desktop only exposes the ${MARKET_PROFILE} profile.`) + } + } + }) +} + +function validatePluginOperation(args, invokingDir) { + if (!Array.isArray(args) || args.length === 0) { + throw new Error('Desktop pnpm requires at least one plugin argument.') + } + if (args.some((argument) => typeof argument !== 'string' || !argument || argument.includes('\0'))) { + throw new Error('Desktop pnpm arguments must be non-empty strings without NUL.') + } + if (typeof invokingDir !== 'string' || !isAbsolute(invokingDir) || invokingDir.includes('\0')) { + throw new Error('Desktop pnpm requires an absolute invoking directory without NUL.') + } +} + +export function createDesktopPnpmService(options) { + const { + binDirectory, + dshEntryPath = resolveDshEntry(), + executablePath = process.execPath, + environment = process.env, + spawnProcess = spawn + } = options + let active + let closed = false + + const runPlugin = (args, invokingDir, signal) => { + validatePluginOperation(args, invokingDir) + if (closed) throw new Error('The DSH Desktop pnpm service has been disposed.') + if (signal?.aborted) throw signal.reason ?? new Error('The package operation was aborted.') + if (active) throw new Error('Another desktop pnpm operation is already running.') + + const child = spawnProcess( + executablePath, + [dshEntryPath, 'plugin', '--profile', MARKET_PROFILE, ...args], + { + cwd: invokingDir, + env: buildPnpmEnvironment(binDirectory, environment, executablePath), + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + detached: process.platform !== 'win32' + } + ) + const cancel = () => killProcessTree(child) + const done = new Promise((resolveDone, rejectDone) => { + child.once('error', rejectDone) + child.once('close', (exitCode, exitSignal) => { + resolveDone({ exitCode, signal: exitSignal }) + }) + }) + const handle = { + stdout: child.stdout, + stderr: child.stderr, + done, + cancel + } + active = handle + + const abort = () => cancel() + signal?.addEventListener('abort', abort, { once: true }) + if (signal?.aborted) abort() + const release = () => { + signal?.removeEventListener('abort', abort) + if (active === handle) active = undefined + } + void done.then(release, release) + return handle + } + + return Object.freeze({ + runPlugin, + async dispose() { + closed = true + const operation = active + if (!operation) return + operation.cancel() + await operation.done.catch(() => undefined) + } + }) +} + export function resolveDshEntry(argv = process.argv) { const entry = argv[1] if (!entry || !/[/\\]bin\.js$/u.test(entry)) { @@ -288,7 +422,7 @@ async function atomicWrite(path, contents) { } function killProcessTree(child) { - if (!child || child.exitCode !== null) return + if (!child || child.exitCode !== null || !child.pid) return if (process.platform === 'win32') { spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { windowsHide: true, @@ -303,16 +437,59 @@ function killProcessTree(child) { } } -export function apply(ctx) { +export async function apply(ctx) { const home = dshHome() const directory = profileDirectory(home) const manifestPath = join(directory, 'package.json') - let activeChild let operationPromise let phase = 'idle' let detail let restartRequired = false + // These generation-scoped services are the supported Desktop integration + // boundary consumed by dsh-market 1.6+. The market therefore never probes + // or provisions a system package manager and all mutations stay on the + // packaged Node/pnpm pair. + const binDirectory = await ensurePnpmShim(home) + const desktopProfiles = createDesktopProfilesService(home) + const desktopPnpm = createDesktopPnpmService({ binDirectory }) + ctx.provide('desktopProfiles', desktopProfiles) + ctx.provide('desktopPnpm', desktopPnpm) + ctx.effect(() => () => desktopPnpm.dispose(), 'dsh-desktop-market-installer: desktop pnpm') + + const runProfileCommand = async (args, action) => { + const handle = desktopPnpm.runPlugin(args, directory) + let output = '' + const append = (chunk) => { + output = `${output}${chunk.toString('utf8')}`.slice(-MAX_LOG_BYTES) + const lines = output.trim().split(/\r?\n/u) + detail = lines.at(-1)?.slice(0, 800) + } + handle.stdout.on('data', append) + handle.stderr.on('data', append) + + let timedOut = false + const timer = setTimeout(() => { + timedOut = true + handle.cancel() + }, OPERATION_TIMEOUT_MS) + + try { + const exit = await handle.done + if (timedOut) throw new Error(`${action} timed out after 15 minutes.`) + if (exit.exitCode !== 0) { + throw new Error( + detail || + `${action} exited with ${exit.signal ? `signal ${exit.signal}` : `code ${exit.exitCode}`}.` + ) + } + } finally { + clearTimeout(timer) + handle.stdout.off('data', append) + handle.stderr.off('data', append) + } + } + const status = async () => { const installation = await readMarketInstallation(home) if (phase === 'installing' || phase === 'uninstalling') { @@ -377,53 +554,11 @@ export function apply(ctx) { if (error?.code !== 'ENOENT') throw error } - const pathKey = process.platform === 'win32' ? 'Path' : 'PATH' - const envPath = process.env[pathKey] ?? process.env.PATH ?? process.env.Path ?? '' - const child = spawn(process.execPath, buildInstallArguments(), { - cwd: directory, - env: { - ...process.env, - PATH: envPath, - Path: envPath, - CI: 'true', - NO_COLOR: '1', - PNPM_CONFIG_CHILD_CONCURRENCY: '1', - PNPM_CONFIG_PACKAGE_IMPORT_METHOD: 'clone-or-copy', - PNPM_CONFIG_SIDE_EFFECTS_CACHE: 'false' - }, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - detached: process.platform !== 'win32' - }) - activeChild = child - - let output = '' - const append = (chunk) => { - output = `${output}${chunk.toString('utf8')}`.slice(-MAX_LOG_BYTES) - const lines = output.trim().split(/\r?\n/u) - detail = lines.at(-1)?.slice(0, 800) - } - child.stdout.on('data', append) - child.stderr.on('data', append) - - let timedOut = false - const timer = setTimeout(() => { - timedOut = true - killProcessTree(child) - }, OPERATION_TIMEOUT_MS) - try { - const exit = await new Promise((resolveExit, rejectExit) => { - child.once('error', rejectExit) - child.once('exit', (code, signal) => resolveExit({ code, signal })) - }) - if (timedOut) throw new Error('Installation timed out after 15 minutes.') - if (exit.code !== 0) { - throw new Error( - detail || - `The installer exited with ${exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`}.` - ) - } + await runProfileCommand( + ['add', '--save-exact', `${MARKET_PACKAGE}@${RECOMMENDED_MARKET_VERSION}`], + 'Installation' + ) const installed = await readMarketInstallation(home) if (!installed.installedVersion) { @@ -442,9 +577,6 @@ export function apply(ctx) { phase = 'error' detail = error instanceof Error ? error.message : String(error) ctx.logger.warn(error instanceof Error ? error : new Error(detail)) - } finally { - clearTimeout(timer) - if (activeChild === child) activeChild = undefined } } @@ -468,53 +600,8 @@ export function apply(ctx) { if (error?.code !== 'ENOENT') throw error } - const pathKey = process.platform === 'win32' ? 'Path' : 'PATH' - const envPath = process.env[pathKey] ?? process.env.PATH ?? process.env.Path ?? '' - const child = spawn(process.execPath, buildUninstallArguments(), { - cwd: directory, - env: { - ...process.env, - PATH: envPath, - Path: envPath, - CI: 'true', - NO_COLOR: '1', - PNPM_CONFIG_CHILD_CONCURRENCY: '1', - PNPM_CONFIG_PACKAGE_IMPORT_METHOD: 'clone-or-copy', - PNPM_CONFIG_SIDE_EFFECTS_CACHE: 'false' - }, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - detached: process.platform !== 'win32' - }) - activeChild = child - - let output = '' - const append = (chunk) => { - output = `${output}${chunk.toString('utf8')}`.slice(-MAX_LOG_BYTES) - const lines = output.trim().split(/\r?\n/u) - detail = lines.at(-1)?.slice(0, 800) - } - child.stdout.on('data', append) - child.stderr.on('data', append) - - let timedOut = false - const timer = setTimeout(() => { - timedOut = true - killProcessTree(child) - }, OPERATION_TIMEOUT_MS) - try { - const exit = await new Promise((resolveExit, rejectExit) => { - child.once('error', rejectExit) - child.once('exit', (code, signal) => resolveExit({ code, signal })) - }) - if (timedOut) throw new Error('Uninstallation timed out after 15 minutes.') - if (exit.code !== 0) { - throw new Error( - detail || - `The uninstaller exited with ${exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`}.` - ) - } + await runProfileCommand(['remove', MARKET_PACKAGE], 'Uninstallation') const removed = await readMarketInstallation(home) if (removed.dependency || removed.installedVersion) { @@ -533,14 +620,11 @@ export function apply(ctx) { phase = 'error' detail = error instanceof Error ? error.message : String(error) ctx.logger.warn(error instanceof Error ? error : new Error(detail)) - } finally { - clearTimeout(timer) - if (activeChild === child) activeChild = undefined } } - ctx.effect(() => { - const disposeStatus = ctx.webServer.register({ + ctx.inject(['webServer'], (webCtx) => webCtx.effect(() => { + const disposeStatus = webCtx.webServer.register({ kind: 'exact', path: STATUS_PATH, handler: async (req, res) => { @@ -551,7 +635,7 @@ export function apply(ctx) { sendJson(res, 200, await status()) } }) - const disposeInstall = ctx.webServer.register({ + const disposeInstall = webCtx.webServer.register({ kind: 'exact', path: INSTALL_PATH, handler: async (req, res) => { @@ -578,7 +662,7 @@ export function apply(ctx) { .catch((error) => { phase = 'error' detail = error instanceof Error ? error.message : String(error) - ctx.logger.warn(error instanceof Error ? error : new Error(detail)) + webCtx.logger.warn(error instanceof Error ? error : new Error(detail)) }) .finally(() => { operationPromise = undefined @@ -586,7 +670,7 @@ export function apply(ctx) { sendJson(res, 202, await status()) } }) - const disposeUninstall = ctx.webServer.register({ + const disposeUninstall = webCtx.webServer.register({ kind: 'exact', path: UNINSTALL_PATH, handler: async (req, res) => { @@ -615,7 +699,7 @@ export function apply(ctx) { .catch((error) => { phase = 'error' detail = error instanceof Error ? error.message : String(error) - ctx.logger.warn(error instanceof Error ? error : new Error(detail)) + webCtx.logger.warn(error instanceof Error ? error : new Error(detail)) }) .finally(() => { operationPromise = undefined @@ -628,14 +712,7 @@ export function apply(ctx) { disposeUninstall() disposeInstall() disposeStatus() - killProcessTree(activeChild) await operationPromise?.catch(() => undefined) } - }, 'dsh-desktop-market-installer: fixed package routes') - - ctx.effect(() => { - void ensurePnpmShim(home).catch((error) => { - ctx.logger.warn(error instanceof Error ? error : new Error(String(error))) - }) - }, 'dsh-desktop-market-installer: packaged pnpm shim') + }, 'dsh-desktop-market-installer: fixed package routes')) } diff --git a/test/market-installer.test.js b/test/market-installer.test.js index 82f9f2d3..8348543b 100644 --- a/test/market-installer.test.js +++ b/test/market-installer.test.js @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises' +import { mkdtemp, mkdir, readFile, realpath, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' @@ -8,9 +8,12 @@ import { RECOMMENDED_MARKET_VERSION, STATUS_PATH, UNINSTALL_PATH, + buildPnpmEnvironment, buildInstallArguments, buildUninstallArguments, cleanStaleTemporaryDirectories, + createDesktopPnpmService, + createDesktopProfilesService, ensurePnpmShim, isTrustedRequest, readMarketInstallation, @@ -105,7 +108,7 @@ describe('desktop plugin market installer', () => { expect(existsSync(validDir)).toBe(true) }) - it('cleans the leftovers inside a package’s own node_modules', async () => { + it('cleans the leftovers inside a package\u2019s own node_modules', async () => { // A replaced dependency of a dependency stages under the dependent, so a // sweep that stops at the top level leaves one copy behind per attempt. const home = await mkdtemp(join(tmpdir(), 'dsh-market-clean-nested-')) @@ -132,7 +135,7 @@ describe('desktop plugin market installer', () => { // Node's recursive `rm` reports success and removes nothing under such a // path on Windows. A profile lives under the user's home, so one non-ASCII // character in the account name used to disable this sweep entirely. - const home = join(await mkdtemp(join(tmpdir(), 'dsh-market-unicode-')), '数据项素') + const home = join(await mkdtemp(join(tmpdir(), 'dsh-market-unicode-')), '\u6570\u636e\u9879\u7d20') const nodeModules = join(home, 'profiles', 'web', 'node_modules') const stale = join(nodeModules, 'dshmarket_tmp_7408_13', 'lib') await mkdir(stale, { recursive: true }) @@ -144,6 +147,92 @@ describe('desktop plugin market installer', () => { expect(existsSync(join(nodeModules, 'dshmarket_tmp_7408_13'))).toBe(false) }) + it('exposes the active Desktop profile without inferring it from argv', async () => { + const home = join('C:\\Users\\tester', 'AppData', 'Roaming', 'dsh-desktop', 'harness') + const profiles = createDesktopProfilesService(home) + + expect(profiles.current).toEqual({ + name: 'web', + dir: join(home, 'profiles', 'web') + }) + expect(profiles.list()).toEqual([profiles.current]) + await expect(profiles.select('web')).resolves.toBeUndefined() + await expect(profiles.select('other')).rejects.toThrow('only exposes the web profile') + }) + + it('runs plugin mutations through one packaged pnpm operation boundary', async () => { + const root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-desktop-pnpm-service-'))) + const binDirectory = join(root, '.desktop-bin') + const fakeDshEntry = join(root, 'fake-dsh.mjs') + await mkdir(binDirectory, { recursive: true }) + await writeFile( + fakeDshEntry, + [ + "process.stdout.write(JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd(), path: process.env.PATH }))", + "await new Promise((resolve) => setTimeout(resolve, Number(process.env.DSH_DESKTOP_TEST_DELAY_MS ?? '0')))" + ].join('\n'), + 'utf8' + ) + + const environment = { + ...process.env, + DSH_DESKTOP_TEST_DELAY_MS: '80', + ELECTRON_RUN_AS_NODE: '1' + } + const service = createDesktopPnpmService({ + binDirectory, + dshEntryPath: fakeDshEntry, + executablePath: process.execPath, + environment + }) + const handle = service.runPlugin(['remove', 'example-plugin'], root) + expect(() => service.runPlugin(['install'], root)).toThrow( + 'Another desktop pnpm operation is already running.' + ) + + let stdout = '' + handle.stdout.on('data', (chunk) => { + stdout += chunk.toString('utf8') + }) + await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null }) + const invocation = JSON.parse(stdout) + expect(invocation.args).toEqual([ + 'plugin', + '--profile', + 'web', + 'remove', + 'example-plugin' + ]) + expect(invocation.cwd).toBe(root) + expect(invocation.path.split(process.platform === 'win32' ? ';' : ':')[0]).toBe( + binDirectory + ) + expect(buildPnpmEnvironment(binDirectory, environment)).not.toHaveProperty( + 'ELECTRON_RUN_AS_NODE' + ) + + const next = service.runPlugin(['install'], root) + await expect(next.done).resolves.toEqual({ exitCode: 0, signal: null }) + await service.dispose() + expect(() => service.runPlugin(['install'], root)).toThrow('has been disposed') + }) + + it('rejects a package operation that was already aborted', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-desktop-pnpm-abort-')) + const controller = new AbortController() + controller.abort(new Error('cancelled before start')) + const service = createDesktopPnpmService({ + binDirectory: join(root, '.desktop-bin'), + dshEntryPath: join(root, 'unused-dsh-entry.mjs'), + executablePath: process.execPath + }) + + expect(() => service.runPlugin(['install'], root, controller.signal)).toThrow( + 'cancelled before start' + ) + await service.dispose() + }) + it('reports both the requested dependency and installed package version', async () => { const home = await mkdtemp(join(tmpdir(), 'dsh-market-status-')) const profile = join(home, 'profiles', 'web') @@ -210,6 +299,7 @@ describe('desktop plugin market installer', () => { '只会移除 dsh-market。通过插件市场安装的其他插件将继续保留。' ) expect(desktopPatch).toContain('name: dsh-desktop-market-installer') + expect(desktopPatch).toContain('inject: [desktopProfiles]') expect(desktopPatch).toContain('allowRestart: false') expect(preload).toContain("restartHarness: (): Promise<{ ok: boolean }>") }) From 73ebcaec5cb3f9aa8be032956dcae866ed775488 Mon Sep 17 00:00:00 2001 From: yaojin3616 Date: Fri, 21 Aug 2026 17:28:12 +0800 Subject: [PATCH 2/3] fix(market): optimize pnpm worker concurrency and configure Windows PR CI --- packages/dsh-desktop-market-installer/index.js | 9 ++++++++- src/main/runtime/harness-runtime.ts | 4 ++++ src/main/runtime/profile-plugin-command.ts | 7 +++++++ test/market-installer.test.js | 9 ++++++--- test/release.test.ts | 3 +++ test/runtime.test.ts | 4 ++++ 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/dsh-desktop-market-installer/index.js b/packages/dsh-desktop-market-installer/index.js index 44c9b0e6..e42829ba 100644 --- a/packages/dsh-desktop-market-installer/index.js +++ b/packages/dsh-desktop-market-installer/index.js @@ -291,6 +291,10 @@ export function buildPnpmEnvironment( if (process.platform === 'win32') result.Path = value result.CI = 'true' result.NO_COLOR = '1' + result.PNPM_MAX_WORKERS = '1' + result.npm_config_child_concurrency = '1' + result.npm_config_package_import_method = 'clone-or-copy' + result.npm_config_side_effects_cache = 'false' result.PNPM_CONFIG_CHILD_CONCURRENCY = '1' result.PNPM_CONFIG_PACKAGE_IMPORT_METHOD = 'clone-or-copy' result.PNPM_CONFIG_SIDE_EFFECTS_CACHE = 'false' @@ -331,7 +335,8 @@ export function createDesktopPnpmService(options) { dshEntryPath = resolveDshEntry(), executablePath = process.execPath, environment = process.env, - spawnProcess = spawn + spawnProcess = spawn, + home = dshHome() } = options let active let closed = false @@ -342,6 +347,8 @@ export function createDesktopPnpmService(options) { if (signal?.aborted) throw signal.reason ?? new Error('The package operation was aborted.') if (active) throw new Error('Another desktop pnpm operation is already running.') + void cleanStaleTemporaryDirectories(home).catch(() => undefined) + const child = spawnProcess( executablePath, [dshEntryPath, 'plugin', '--profile', MARKET_PROFILE, ...args], diff --git a/src/main/runtime/harness-runtime.ts b/src/main/runtime/harness-runtime.ts index 9a7647ec..a92fe04e 100644 --- a/src/main/runtime/harness-runtime.ts +++ b/src/main/runtime/harness-runtime.ts @@ -62,6 +62,10 @@ export function buildHarnessSpawnOptions( ...parentEnvironment, DSH_HOME: dshHome, NO_COLOR: '1', + PNPM_MAX_WORKERS: '1', + npm_config_child_concurrency: '1', + npm_config_package_import_method: 'clone-or-copy', + npm_config_side_effects_cache: 'false', PNPM_CONFIG_CHILD_CONCURRENCY: '1', PNPM_CONFIG_PACKAGE_IMPORT_METHOD: 'clone-or-copy', PNPM_CONFIG_SIDE_EFFECTS_CACHE: 'false', diff --git a/src/main/runtime/profile-plugin-command.ts b/src/main/runtime/profile-plugin-command.ts index 2a7e7eb7..188116db 100644 --- a/src/main/runtime/profile-plugin-command.ts +++ b/src/main/runtime/profile-plugin-command.ts @@ -129,6 +129,13 @@ export function buildProfilePluginCommandEnvironment( result.DSH_HOME = result.DSH_HOME ?? '' result.CI = 'true' result.NO_COLOR = '1' + result.PNPM_MAX_WORKERS = '1' + result.npm_config_child_concurrency = '1' + result.npm_config_package_import_method = 'clone-or-copy' + result.npm_config_side_effects_cache = 'false' + result.PNPM_CONFIG_CHILD_CONCURRENCY = '1' + result.PNPM_CONFIG_PACKAGE_IMPORT_METHOD = 'clone-or-copy' + result.PNPM_CONFIG_SIDE_EFFECTS_CACHE = 'false' return result } diff --git a/test/market-installer.test.js b/test/market-installer.test.js index 8348543b..908ddf01 100644 --- a/test/market-installer.test.js +++ b/test/market-installer.test.js @@ -207,9 +207,12 @@ describe('desktop plugin market installer', () => { expect(invocation.path.split(process.platform === 'win32' ? ';' : ':')[0]).toBe( binDirectory ) - expect(buildPnpmEnvironment(binDirectory, environment)).not.toHaveProperty( - 'ELECTRON_RUN_AS_NODE' - ) + const pnpmEnv = buildPnpmEnvironment(binDirectory, environment) + expect(pnpmEnv).not.toHaveProperty('ELECTRON_RUN_AS_NODE') + expect(pnpmEnv.PNPM_MAX_WORKERS).toBe('1') + expect(pnpmEnv.npm_config_child_concurrency).toBe('1') + expect(pnpmEnv.npm_config_package_import_method).toBe('clone-or-copy') + expect(pnpmEnv.npm_config_side_effects_cache).toBe('false') const next = service.runPlugin(['install'], root) await expect(next.done).resolves.toEqual({ exitCode: 0, signal: null }) diff --git a/test/release.test.ts b/test/release.test.ts index 20665535..ad09d53d 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -283,6 +283,9 @@ describe('GitHub release contract', () => { expect(workflow).toMatch( /macos-intel:\r?\n\s+name: macOS Intel\r?\n(?:[\s\S]*?)runs-on: macos-15-intel\r?\n\s+steps:/ ) + expect(workflow).toMatch( + /windows-x64:\r?\n name: Windows x64\r?\n runs-on: windows-2022\r?\n steps:/ + ) }) it('routes the published download through the official website', async () => { diff --git a/test/runtime.test.ts b/test/runtime.test.ts index b5b944fd..a298e08f 100644 --- a/test/runtime.test.ts +++ b/test/runtime.test.ts @@ -144,6 +144,10 @@ describe('Harness launch contract', () => { PATH: '/usr/bin', DSH_HOME: '/Users/tester/Library/Application Support/dsh-desktop/harness', NO_COLOR: '1', + PNPM_MAX_WORKERS: '1', + npm_config_child_concurrency: '1', + npm_config_package_import_method: 'clone-or-copy', + npm_config_side_effects_cache: 'false', PNPM_CONFIG_CHILD_CONCURRENCY: '1', PNPM_CONFIG_PACKAGE_IMPORT_METHOD: 'clone-or-copy', PNPM_CONFIG_SIDE_EFFECTS_CACHE: 'false' From 78eea2a79a37de14353174b8b6ba2481c64f029b Mon Sep 17 00:00:00 2001 From: yaojin3616 Date: Fri, 21 Aug 2026 17:49:32 +0800 Subject: [PATCH 3/3] fix(market): install latest dshmarket version on demand --- packages/dsh-desktop-market-installer/client.js | 4 ++-- packages/dsh-desktop-market-installer/index.js | 5 ++--- test/market-installer.test.js | 5 ++--- test/release.test.ts | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/dsh-desktop-market-installer/client.js b/packages/dsh-desktop-market-installer/client.js index 2176637a..9632d880 100644 --- a/packages/dsh-desktop-market-installer/client.js +++ b/packages/dsh-desktop-market-installer/client.js @@ -501,7 +501,7 @@ window.__ModuleLoader__.load({ setStatus((current) => ({ ...current, phase: 'installing', - recommendedVersion: current?.recommendedVersion || '1.15.0' + recommendedVersion: current?.recommendedVersion || 'latest' })) try { const response = await fetch(INSTALL_PATH, { @@ -539,7 +539,7 @@ window.__ModuleLoader__.load({ const busy = phase === 'installing' const installed = phase === 'installed' const failed = phase === 'error' || phase === 'incomplete' || Boolean(error) - const version = status?.recommendedVersion || '1.15.0' + const version = status?.recommendedVersion || 'latest' return React.createElement( 'section', diff --git a/packages/dsh-desktop-market-installer/index.js b/packages/dsh-desktop-market-installer/index.js index e42829ba..eb642a11 100644 --- a/packages/dsh-desktop-market-installer/index.js +++ b/packages/dsh-desktop-market-installer/index.js @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import { SIDELINE_MARKER } from './pnpm-runner.mjs' import { removeTree } from './remove-tree.mjs' -export const RECOMMENDED_MARKET_VERSION = '1.15.0' +export const RECOMMENDED_MARKET_VERSION = 'latest' export const MARKET_PACKAGE = 'dshmarket' export const MARKET_PROFILE = 'web' export const STATUS_PATH = '/dsh-desktop/market-installer/status' @@ -413,7 +413,6 @@ export function buildInstallArguments(dshEntry = resolveDshEntry()) { '--profile', MARKET_PROFILE, 'add', - '--save-exact', `${MARKET_PACKAGE}@${RECOMMENDED_MARKET_VERSION}` ] } @@ -563,7 +562,7 @@ export async function apply(ctx) { try { await runProfileCommand( - ['add', '--save-exact', `${MARKET_PACKAGE}@${RECOMMENDED_MARKET_VERSION}`], + ['add', `${MARKET_PACKAGE}@${RECOMMENDED_MARKET_VERSION}`], 'Installation' ) diff --git a/test/market-installer.test.js b/test/market-installer.test.js index 908ddf01..888260d3 100644 --- a/test/market-installer.test.js +++ b/test/market-installer.test.js @@ -29,11 +29,10 @@ describe('desktop plugin market installer', () => { '--profile', 'web', 'add', - '--save-exact', - 'dshmarket@1.15.0' + 'dshmarket@latest' ]) expect(MARKET_PACKAGE).toBe('dshmarket') - expect(RECOMMENDED_MARKET_VERSION).toBe('1.15.0') + expect(RECOMMENDED_MARKET_VERSION).toBe('latest') expect(STATUS_PATH).toBe('/dsh-desktop/market-installer/status') expect(INSTALL_PATH).toBe('/dsh-desktop/market-installer/install') expect(UNINSTALL_PATH).toBe('/dsh-desktop/market-installer/uninstall') diff --git a/test/release.test.ts b/test/release.test.ts index ad09d53d..4f873d31 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -284,7 +284,7 @@ describe('GitHub release contract', () => { /macos-intel:\r?\n\s+name: macOS Intel\r?\n(?:[\s\S]*?)runs-on: macos-15-intel\r?\n\s+steps:/ ) expect(workflow).toMatch( - /windows-x64:\r?\n name: Windows x64\r?\n runs-on: windows-2022\r?\n steps:/ + /windows-x64:\r?\n\s+name: Windows x64\r?\n(?:[\s\S]*?)runs-on: windows-2022\r?\n\s+steps:/ ) })