@tilewise/workbench-client 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +8 -0
- package/dist/vite/checkout.js +2 -22
- package/dist/vite/install.d.ts +5 -0
- package/dist/vite/install.js +30 -0
- package/dist/vite/pull.d.ts +16 -0
- package/dist/vite/pull.js +84 -0
- package/dist/vite/workbenchLocal.d.ts +2 -1
- package/dist/vite/workbenchLocal.js +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 · 2026-11-12 05:46 +04:00
|
|
4
|
+
|
|
5
|
+
- feat(workbench-client): pull a branch forward from the switcher (84ca115)
|
|
6
|
+
- Add /__workbench/pull: git fetch, then fast-forward a branch to its branch on GitHub, never merging or rebasing
|
|
7
|
+
- Move the checked-out branch's files too, refused while anything is uncommitted, and install when the lockfile changed
|
|
8
|
+
- Move any other branch's ref alone, refused when another worktree has it checked out
|
|
9
|
+
- Refuse a branch with commits GitHub doesn't have, or with no branch there, saying why
|
|
10
|
+
- Share Checkout's install step in install.ts
|
|
11
|
+
- Show Pull beside History on each branch behind GitHub's, with the result or the reason in the switcher's footer
|
|
12
|
+
- Ask for a newer client when the dev server has no Pull
|
|
13
|
+
|
|
3
14
|
## 0.2.0 · 2026-11-10 17:08 +04:00
|
|
4
15
|
|
|
5
16
|
- feat(workbench-client)!: serve the dev server over https by default, for Safari (4219956)
|
package/README.md
CHANGED
|
@@ -174,6 +174,14 @@ git-ignored `.builds/<sha>/`, and checks a branch out, refused while
|
|
|
174
174
|
anything is uncommitted. Builds are served under `/__workbench/b/<sha>/`, so
|
|
175
175
|
the app's router takes `basepath: import.meta.env.BASE_URL`.
|
|
176
176
|
|
|
177
|
+
It also pulls a branch, fast-forward only (`/__workbench/pull`): `git fetch`,
|
|
178
|
+
then the branch moves to its branch on GitHub, never merging or rebasing. The
|
|
179
|
+
checked-out branch moves its files too, refused while anything is
|
|
180
|
+
uncommitted, and installs at the repository's top when the lockfile changed,
|
|
181
|
+
then restarts the dev server. Any other branch moves alone, refused when
|
|
182
|
+
another worktree has it checked out. A branch with commits GitHub doesn't
|
|
183
|
+
have, or with no branch there, is refused, with the reason.
|
|
184
|
+
|
|
177
185
|
It also reads `workbench.json`, at the repository's top, where the
|
|
178
186
|
repository declares its setup: the fake API comes from it (below), and
|
|
179
187
|
`/__workbench/setup` hands the checkout's to the viewer, as `{ name, viewers }`
|
package/dist/vite/checkout.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { lockOf } from './builds.js';
|
|
2
|
+
import { installIfChanged } from './install.js';
|
|
3
3
|
/**
|
|
4
4
|
* Checks a branch out, refused while anything is uncommitted. When the
|
|
5
5
|
* lockfile changes, installs and restarts the dev server.
|
|
@@ -18,26 +18,6 @@ export async function checkout(git, root, branch, restart) {
|
|
|
18
18
|
}
|
|
19
19
|
const before = (await lockOf(root)).text;
|
|
20
20
|
await git('switch', branch);
|
|
21
|
-
const
|
|
22
|
-
const installed = before !== after.text;
|
|
23
|
-
if (installed) {
|
|
24
|
-
await new Promise((resolve, reject) => {
|
|
25
|
-
const args = after.manager === 'pnpm' ? ['install'] : ['install', '--no-audit', '--no-fund'];
|
|
26
|
-
const child = spawn(after.manager, args, {
|
|
27
|
-
cwd: root,
|
|
28
|
-
shell: process.platform === 'win32',
|
|
29
|
-
stdio: 'inherit',
|
|
30
|
-
});
|
|
31
|
-
child.on('close', (code) => {
|
|
32
|
-
if (code === 0) {
|
|
33
|
-
resolve();
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
reject(new Error(`${after.manager} install exited with ${code}`));
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
setTimeout(restart, 50);
|
|
41
|
-
}
|
|
21
|
+
const installed = await installIfChanged(root, before, restart);
|
|
42
22
|
return { status: 200, body: { current: branch, installed } };
|
|
43
23
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After the checkout moved: when its lockfile is no longer `before`, installs
|
|
3
|
+
* with its package manager and restarts the dev server. Whether it installed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function installIfChanged(root: string, before: string | null, restart: () => void): Promise<boolean>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lockOf } from './builds.js';
|
|
3
|
+
/**
|
|
4
|
+
* After the checkout moved: when its lockfile is no longer `before`, installs
|
|
5
|
+
* with its package manager and restarts the dev server. Whether it installed.
|
|
6
|
+
*/
|
|
7
|
+
export async function installIfChanged(root, before, restart) {
|
|
8
|
+
const after = await lockOf(root);
|
|
9
|
+
if (before === after.text) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const args = after.manager === 'pnpm' ? ['install'] : ['install', '--no-audit', '--no-fund'];
|
|
14
|
+
const child = spawn(after.manager, args, {
|
|
15
|
+
cwd: root,
|
|
16
|
+
shell: process.platform === 'win32',
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
});
|
|
19
|
+
child.on('close', (code) => {
|
|
20
|
+
if (code === 0) {
|
|
21
|
+
resolve();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
reject(new Error(`${after.manager} install exited with ${code}`));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
setTimeout(restart, 50);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Git } from './git.js';
|
|
2
|
+
import { installIfChanged } from './install.js';
|
|
3
|
+
interface Answer {
|
|
4
|
+
status: number;
|
|
5
|
+
body: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Brings a branch up to its branch on GitHub, fast-forward only: git fetch
|
|
9
|
+
* first, then the branch moves to what it fetched, never merging or
|
|
10
|
+
* rebasing. The checked-out branch moves its files too, refused while
|
|
11
|
+
* anything is uncommitted, and installs at `top` when the lockfile changed.
|
|
12
|
+
* Any other branch moves alone, its files untouched. A branch with commits
|
|
13
|
+
* GitHub doesn't have is refused, and so is one with no branch there.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pull(git: Git, top: string, branch: string, restart: () => void, install?: typeof installIfChanged): Promise<Answer>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { lockOf } from './builds.js';
|
|
2
|
+
import { refExists } from './git.js';
|
|
3
|
+
import { installIfChanged } from './install.js';
|
|
4
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? '' : 's'}`;
|
|
5
|
+
/**
|
|
6
|
+
* Brings a branch up to its branch on GitHub, fast-forward only: git fetch
|
|
7
|
+
* first, then the branch moves to what it fetched, never merging or
|
|
8
|
+
* rebasing. The checked-out branch moves its files too, refused while
|
|
9
|
+
* anything is uncommitted, and installs at `top` when the lockfile changed.
|
|
10
|
+
* Any other branch moves alone, its files untouched. A branch with commits
|
|
11
|
+
* GitHub doesn't have is refused, and so is one with no branch there.
|
|
12
|
+
*/
|
|
13
|
+
export async function pull(git, top, branch, restart, install = installIfChanged) {
|
|
14
|
+
const ref = `refs/heads/${branch}`;
|
|
15
|
+
if (!(await refExists(git, ref))) {
|
|
16
|
+
return { status: 404, body: { error: `No branch ${branch} on this computer` } };
|
|
17
|
+
}
|
|
18
|
+
const upstream = (await git('for-each-ref', '--format=%(upstream)', ref)).trim();
|
|
19
|
+
if (!upstream.startsWith('refs/remotes/origin/')) {
|
|
20
|
+
return {
|
|
21
|
+
status: 409,
|
|
22
|
+
body: { error: `${branch} has no branch on GitHub to pull from.`, reason: 'no-upstream' },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
await git('fetch', '--prune', 'origin');
|
|
26
|
+
if (!(await refExists(git, upstream))) {
|
|
27
|
+
return {
|
|
28
|
+
status: 409,
|
|
29
|
+
body: { error: `${branch}’s branch on GitHub is gone.`, reason: 'no-upstream' },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const [from = '', to = ''] = (await git('rev-parse', ref, upstream)).trim().split(/\s+/);
|
|
33
|
+
const [ahead = 0, behind = 0] = (await git('rev-list', '--left-right', '--count', `${from}...${to}`))
|
|
34
|
+
.trim()
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.map(Number);
|
|
37
|
+
if (!behind) {
|
|
38
|
+
return { status: 200, body: { branch, pulled: 0, installed: false } };
|
|
39
|
+
}
|
|
40
|
+
if (ahead) {
|
|
41
|
+
return {
|
|
42
|
+
status: 409,
|
|
43
|
+
body: {
|
|
44
|
+
error: `${branch} has ${plural(ahead, 'commit')} GitHub doesn’t, and GitHub ` +
|
|
45
|
+
`${plural(behind, 'commit')} it doesn’t. Pull only moves a branch forward: ` +
|
|
46
|
+
'merge or rebase it yourself.',
|
|
47
|
+
reason: 'diverged',
|
|
48
|
+
ahead,
|
|
49
|
+
behind,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const current = (await git('symbolic-ref', '--quiet', '--short', 'HEAD').catch(() => '')).trim();
|
|
54
|
+
if (current === branch) {
|
|
55
|
+
const changed = (await git('status', '--porcelain')).split('\n').filter(Boolean).length;
|
|
56
|
+
if (changed) {
|
|
57
|
+
return {
|
|
58
|
+
status: 409,
|
|
59
|
+
body: {
|
|
60
|
+
error: `You have ${plural(changed, 'changed file')}. Commit or stash ${changed === 1 ? 'it' : 'them'} first.`,
|
|
61
|
+
reason: 'changed',
|
|
62
|
+
changed,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const before = (await lockOf(top)).text;
|
|
67
|
+
await git('merge', '--ff-only', '--quiet', to);
|
|
68
|
+
const installed = await install(top, before, restart);
|
|
69
|
+
return { status: 200, body: { branch, pulled: behind, installed } };
|
|
70
|
+
}
|
|
71
|
+
// Checked out in another worktree, it would move under that worktree's files.
|
|
72
|
+
const worktrees = (await git('worktree', 'list', '--porcelain')).split('\n');
|
|
73
|
+
if (worktrees.includes(`branch ${ref}`)) {
|
|
74
|
+
return {
|
|
75
|
+
status: 409,
|
|
76
|
+
body: {
|
|
77
|
+
error: `${branch} is checked out in another worktree. Pull it there.`,
|
|
78
|
+
reason: 'worktree',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
await git('update-ref', '-m', 'pull: fast-forward', ref, to, from);
|
|
83
|
+
return { status: 200, body: { branch, pulled: behind, installed: false } };
|
|
84
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { Plugin } from 'vite';
|
|
|
2
2
|
/**
|
|
3
3
|
* The local builds service, as a Vite plugin: with it, the app's dev server
|
|
4
4
|
* also answers the deployed viewer. It lists this machine's branches and
|
|
5
|
-
* commits, builds any commit,
|
|
5
|
+
* commits, builds any commit, checks a branch out, and pulls one.
|
|
6
6
|
*
|
|
7
7
|
* GET /__workbench/connect { token, repo }: the token, for an allowed origin, which needs none
|
|
8
8
|
* GET /__workbench/setup the checkout's workbench.json: { name, viewers }; 404 none, 422 no setup
|
|
@@ -13,6 +13,7 @@ import type { Plugin } from 'vite';
|
|
|
13
13
|
* GET /__workbench/b/<sha>/… the build, for the viewer's frame
|
|
14
14
|
* POST /__workbench/checkout { branch }: refused while anything is uncommitted
|
|
15
15
|
* POST /__workbench/fetch git fetch, so push status is current
|
|
16
|
+
* POST /__workbench/pull { branch }: git fetch, then fast-forward it to GitHub's (pull.ts)
|
|
16
17
|
*
|
|
17
18
|
* `base` is the branch the others are compared with: the repository's
|
|
18
19
|
* default branch unless set. Builds are served without the token, as a frame
|
|
@@ -9,6 +9,7 @@ import { environment } from './environment.js';
|
|
|
9
9
|
import { sendResponse, toRequest } from './fetchBridge.js';
|
|
10
10
|
import { defaultBranch, gitIn, repositoryOf } from './git.js';
|
|
11
11
|
import { localhostCertificate, trustSteps } from './https.js';
|
|
12
|
+
import { pull } from './pull.js';
|
|
12
13
|
import { sendJson, serveBuild } from './serveBuild.js';
|
|
13
14
|
import { readSetup, setupApi } from './setup.js';
|
|
14
15
|
import { commits, versions } from './versions.js';
|
|
@@ -58,7 +59,7 @@ function open(url) {
|
|
|
58
59
|
/**
|
|
59
60
|
* The local builds service, as a Vite plugin: with it, the app's dev server
|
|
60
61
|
* also answers the deployed viewer. It lists this machine's branches and
|
|
61
|
-
* commits, builds any commit,
|
|
62
|
+
* commits, builds any commit, checks a branch out, and pulls one.
|
|
62
63
|
*
|
|
63
64
|
* GET /__workbench/connect { token, repo }: the token, for an allowed origin, which needs none
|
|
64
65
|
* GET /__workbench/setup the checkout's workbench.json: { name, viewers }; 404 none, 422 no setup
|
|
@@ -69,6 +70,7 @@ function open(url) {
|
|
|
69
70
|
* GET /__workbench/b/<sha>/… the build, for the viewer's frame
|
|
70
71
|
* POST /__workbench/checkout { branch }: refused while anything is uncommitted
|
|
71
72
|
* POST /__workbench/fetch git fetch, so push status is current
|
|
73
|
+
* POST /__workbench/pull { branch }: git fetch, then fast-forward it to GitHub's (pull.ts)
|
|
72
74
|
*
|
|
73
75
|
* `base` is the branch the others are compared with: the repository's
|
|
74
76
|
* default branch unless set. Builds are served without the token, as a frame
|
|
@@ -176,6 +178,15 @@ export function workbenchLocal(options = {}) {
|
|
|
176
178
|
await git('fetch', '--prune', 'origin');
|
|
177
179
|
return sendJson(response, 200, { ok: true });
|
|
178
180
|
}
|
|
181
|
+
if (request.method === 'POST' && path === '/pull') {
|
|
182
|
+
const branch = (await bodyOf(request))?.branch;
|
|
183
|
+
if (typeof branch !== 'string' || !REF.test(branch)) {
|
|
184
|
+
return sendJson(response, 400, { error: 'Bad branch' });
|
|
185
|
+
}
|
|
186
|
+
const top = (await topOf()) ?? root;
|
|
187
|
+
const result = await pull(git, top, branch, () => void server?.restart());
|
|
188
|
+
return sendJson(response, result.status, result.body);
|
|
189
|
+
}
|
|
179
190
|
return sendJson(response, 404, { error: 'Not found' });
|
|
180
191
|
}
|
|
181
192
|
/** A build's own fake API, asked at `rest`, the path under the build; false when it has none there. */
|
package/package.json
CHANGED