Skip to content

Commit 815b892

Browse files
committed
fix(desktop): give pnpm deploy a workspace-relative staging target
pnpm joins its workspace root with the deploy target instead of resolving it, so an absolute path on another volume became D:\repo\C:\Users\... and the Windows job failed with ERR_PNPM_ENOENT. A relative target is correct whether pnpm joins or resolves, and cannot cross a drive letter, so the staging directory moves onto the repository's own volume.
1 parent 530dc19 commit 815b892

3 files changed

Lines changed: 46 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pymodel/pythinker-desktop': patch
3+
---
4+
5+
Stage the desktop Host closure inside the workspace so pnpm deploy resolves the target on Windows

apps/desktop/scripts/stage-runtime.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import { spawn } from 'node:child_process'
44
import { existsSync } from 'node:fs'
55
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
6-
import { tmpdir } from 'node:os'
7-
import { join, resolve, sep } from 'node:path'
6+
import { join, relative, resolve, sep } from 'node:path'
87
import { fileURLToPath } from 'node:url'
98

109
const desktopRoot = resolve(import.meta.dirname, '..')
@@ -14,6 +13,7 @@ const deployPackage = '@pymodel/pythinker-code'
1413
const entry = join(staging, 'node_modules/@pymodel/pythinker-code/dist/launcher.mjs')
1514
const frontend = join(staging, 'node_modules/@pymodel/pythinker-code/dist-web/index.html')
1615
const workspaceState = join(repositoryRoot, 'node_modules/.pnpm-workspace-state-v1.json')
16+
const stagingParent = join(repositoryRoot, 'node_modules', '.pythinker-desktop-staging')
1717

1818
/** Windows characters that make an argument unsafe to hand to `cmd.exe` unquoted. */
1919
const WINDOWS_UNSAFE_ARGUMENT = /[\s"&()<>^|]/u
@@ -42,6 +42,21 @@ export function packageManagerInvocation(platform: string, command: string, args
4242
}
4343
}
4444

45+
/**
46+
* Express a deploy target the way pnpm accepts it.
47+
*
48+
* pnpm joins its workspace root with the deploy target rather than resolving
49+
* it, so an absolute path on another volume produces a concatenated,
50+
* non-existent directory such as `D:\repo\C:\Users\…`. A workspace-relative
51+
* target is correct whether pnpm joins or resolves.
52+
* @param workspaceRoot - The pnpm workspace root, and the child process's cwd.
53+
* @param target - The absolute staging directory.
54+
* @returns The target expressed relative to the workspace root.
55+
*/
56+
export function deployTargetArgument(workspaceRoot: string, target: string): string {
57+
return relative(workspaceRoot, target)
58+
}
59+
4560
async function run(command: string, args: readonly string[]): Promise<void> {
4661
const invocation = packageManagerInvocation(process.platform, command, args)
4762
await new Promise<void>((accept, reject) => {
@@ -96,7 +111,8 @@ async function deploy(target: string): Promise<void> {
96111
try {
97112
await run('pnpm', [
98113
'--config.verify-deps-before-run=false', '--filter', deployPackage, 'deploy', '--legacy', '--prod',
99-
'--config.node-linker=hoisted', '--config.auto-install-peers=false', '--config.link-workspace-packages=true', target,
114+
'--config.node-linker=hoisted', '--config.auto-install-peers=false', '--config.link-workspace-packages=true',
115+
deployTargetArgument(repositoryRoot, target),
100116
])
101117
} finally {
102118
if (savedWorkspaceState === undefined) await rm(workspaceState, { force: true })
@@ -105,7 +121,8 @@ async function deploy(target: string): Promise<void> {
105121
}
106122

107123
async function main(): Promise<void> {
108-
const deployed = await mkdtemp(join(tmpdir(), 'pythinker-desktop-runtime-'))
124+
await mkdir(stagingParent, { recursive: true })
125+
const deployed = await mkdtemp(join(stagingParent, 'runtime-'))
109126
try {
110127
await deploy(deployed)
111128
await rm(join(staging, 'node_modules'), { recursive: true, force: true })

apps/desktop/tests/stage-runtime.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { isAbsolute, join } from 'node:path'
12
import { describe, expect, it } from 'vitest'
2-
import { packageManagerInvocation } from '../scripts/stage-runtime'
3+
import { deployTargetArgument, packageManagerInvocation } from '../scripts/stage-runtime'
34

45
describe('package manager invocation', () => {
56
it('leaves non-Windows invocations untouched', () => {
@@ -31,3 +32,21 @@ describe('package manager invocation', () => {
3132
.toEqual(['"deploy&verify"'])
3233
})
3334
})
35+
36+
describe('deploy target argument', () => {
37+
it('is relative to the workspace root', () => {
38+
expect(deployTargetArgument('/repo', '/repo/node_modules/.pythinker-desktop-staging/runtime-abc123'))
39+
.toBe(join('node_modules', '.pythinker-desktop-staging', 'runtime-abc123'))
40+
})
41+
42+
it('is never absolute', () => {
43+
expect(isAbsolute(deployTargetArgument('/repo', '/repo/node_modules/.pythinker-desktop-staging/runtime-abc123')))
44+
.toBe(false)
45+
})
46+
47+
it('never returns the target unchanged', () => {
48+
const target = '/repo/node_modules/.pythinker-desktop-staging/runtime-abc123'
49+
50+
expect(deployTargetArgument('/repo', target)).not.toBe(target)
51+
})
52+
})

0 commit comments

Comments
 (0)