@tilewise/workbench-client 0.1.1 → 0.1.2
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 +9 -0
- package/README.md +45 -16
- package/dist/vite/builds.d.ts +8 -6
- package/dist/vite/builds.js +10 -7
- package/dist/vite/setup.d.ts +38 -0
- package/dist/vite/setup.js +65 -0
- package/dist/vite/workbenchLocal.d.ts +12 -6
- package/dist/vite/workbenchLocal.js +55 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 · 2026-11-07 06:11 +04:00
|
|
4
|
+
|
|
5
|
+
- feat(workbench-client): read the fake API from workbench.json (25e1ad9)
|
|
6
|
+
- Answer `/api` from the fake API that `viewers.proto.api` in the repository's `workbench.json` declares, from `viewers.proto.folder`, when `workbenchLocal()` is given no `api` option; the option still wins
|
|
7
|
+
- Read `workbench.json` on each request, so an edit to it counts without restarting Vite
|
|
8
|
+
- Build each other commit with the fake API its own `workbench.json` declares, else the checkout's
|
|
9
|
+
- Serve the checkout's setup at `GET /__workbench/setup`, for a workbench without the GitHub App
|
|
10
|
+
- Describe `workbench.json` in the README's local builds, workspace and fake API sections
|
|
11
|
+
|
|
3
12
|
## 0.1.1 · 2026-11-06 01:07 +04:00
|
|
4
13
|
|
|
5
14
|
- feat(workbench-client): build apps in a workspace folder and answer each build's fake API (fc6dba7)
|
package/README.md
CHANGED
|
@@ -174,6 +174,12 @@ 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 reads `workbench.json`, at the repository's top, where the
|
|
178
|
+
repository declares its setup: the fake API comes from it (below), and
|
|
179
|
+
`/__workbench/setup` hands the checkout's to the viewer, as `{ name, viewers }`
|
|
180
|
+
(404 without the file, 422 when it isn't a setup), for a workbench that can't
|
|
181
|
+
read it from GitHub.
|
|
182
|
+
|
|
177
183
|
Only the viewer may ask: from its origin (`WORKBENCH_URL`, or a viewer on
|
|
178
184
|
localhost) with the token in the link the dev server logs. `pnpm dev` opens
|
|
179
185
|
that link; without `WORKBENCH_URL` the dev server opens no viewer.
|
|
@@ -184,28 +190,51 @@ The app may live in a folder of a pnpm workspace, such as `apps/app/`, with
|
|
|
184
190
|
its dev server's root there. Each commit is built from that folder, or from
|
|
185
191
|
the top for a commit from before the app moved there. A workspace commit
|
|
186
192
|
installs its own packages (`pnpm install --frozen-lockfile --prefer-offline`),
|
|
187
|
-
so its packages link to that commit's, never the checkout's.
|
|
193
|
+
so its packages link to that commit's, never the checkout's. The folder is
|
|
194
|
+
the dev server's root, whatever `workbench.json` says, and a local build
|
|
195
|
+
installs and builds as above, without Proto's `install` and `build`.
|
|
188
196
|
|
|
189
197
|
### A fake API
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
Declare it in `workbench.json`, at the repository's top, as Proto's `api`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"name": "Aitems",
|
|
204
|
+
"viewers": {
|
|
205
|
+
"proto": { "folder": "apps/app", "api": "../fake-api/src/index.ts" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
193
208
|
```
|
|
194
209
|
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`/bags/b1`.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
- **
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
`api` is the fake API's module, from Proto's `folder`: the app's folder from
|
|
211
|
+
the top, or the top when it is `''` or left out. Its default export is a
|
|
212
|
+
fetch handler: a function from `Request` to `Response`, or an object with
|
|
213
|
+
`fetch`, as a Hono app is. The app asks it at `/api`, and the handler is
|
|
214
|
+
asked without that path, so `/api/bags/b1` reaches it as `/bags/b1`. With no
|
|
215
|
+
file, a file that isn't a setup, or no `api` in it, there is no fake API and
|
|
216
|
+
`/api` is the app's own.
|
|
217
|
+
|
|
218
|
+
- **Live:** the dev server answers it at `/api`, from its source. Vite loads
|
|
219
|
+
it again when it changes, and the file is read on each request, so
|
|
220
|
+
declaring the API or moving it needs no restart.
|
|
221
|
+
- **Builds:** each build bundles the fake API its commit's own
|
|
222
|
+
`workbench.json` declares (the checkout's, for a commit with no file of its
|
|
223
|
+
own), with every package inside it, into `.builds/<sha>/__api/`, and
|
|
224
|
+
answers it under the build, at `/__workbench/b/<sha>/api/…`. An old build
|
|
225
|
+
asks the API it was built with, whatever the checkout's has become. The app
|
|
226
|
+
asks `` `${import.meta.env.BASE_URL}api/…` ``, which is right in both.
|
|
208
227
|
- Each build's API runs in a worker of its own: loaded on its first request
|
|
209
228
|
and stopped after 30 minutes with nothing asked, so a visit's data lasts
|
|
210
229
|
until then. The worker gets no environment variables, and the bundle
|
|
211
230
|
itself is never served.
|
|
231
|
+
|
|
232
|
+
The `api` option overrides the file, for Live and for a commit with no file
|
|
233
|
+
of its own:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
workbenchLocal({ base: 'develop', api: { entry: '../fake-api/src/index.ts', path: '/api' } });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`entry` is the module from the app's folder, the dev server's root; `path` is
|
|
240
|
+
where the app asks it, `/api` unless set.
|
package/dist/vite/builds.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export interface Build {
|
|
|
8
8
|
/** The end of the log, when it failed. */
|
|
9
9
|
error?: string;
|
|
10
10
|
}
|
|
11
|
-
/** A fake API beside the app: its module
|
|
11
|
+
/** A fake API beside the app: where its module is, and where the app asks it. */
|
|
12
12
|
export interface ApiOption {
|
|
13
|
-
|
|
13
|
+
/** Its module in a tree of the repository: `top` is the tree's top, `app` the app's folder in it. */
|
|
14
|
+
entry: (top: string, app: string) => string;
|
|
14
15
|
path: string;
|
|
15
16
|
}
|
|
16
17
|
/** A build's own fake API: its bundle, and where the build asks it. */
|
|
@@ -30,14 +31,15 @@ export declare function lockOf(dir: string): Promise<{
|
|
|
30
31
|
*
|
|
31
32
|
* The app may live in a folder of the repository, such as a pnpm workspace's
|
|
32
33
|
* `apps/app/`: each commit is built from that folder, or from the top for a
|
|
33
|
-
* commit from before the app moved there.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* commit from before the app moved there. A commit's fake API is bundled too,
|
|
35
|
+
* into `.builds/<sha>/__api/`, so its build asks its own API, whatever the
|
|
36
|
+
* checkout's API has become since: the one the commit's own `workbench.json`
|
|
37
|
+
* declares, else `api`, the checkout's, when the commit has it.
|
|
36
38
|
*/
|
|
37
39
|
export declare function createBuilds(options: {
|
|
38
40
|
root: () => string;
|
|
39
41
|
git: Git;
|
|
40
|
-
api?: () => ApiOption | null
|
|
42
|
+
api?: () => Promise<ApiOption | null>;
|
|
41
43
|
}): {
|
|
42
44
|
apiOf: (sha: string) => Promise<BuiltApi | null>;
|
|
43
45
|
buildOf: (sha: string) => Promise<Build>;
|
package/dist/vite/builds.js
CHANGED
|
@@ -2,11 +2,12 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { appendFile, lstat, mkdir, readdir, readFile, rename, rm, rmdir, symlink, unlink, writeFile, } from 'node:fs/promises';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import { dirname, join, relative
|
|
5
|
+
import { dirname, join, relative } from 'node:path';
|
|
6
6
|
import { appFolder } from './appFolder.js';
|
|
7
7
|
import { bundleApi } from './bundleApi.js';
|
|
8
8
|
import { buildEnvironment } from './environment.js';
|
|
9
9
|
import { isWorkspace } from './isWorkspace.js';
|
|
10
|
+
import { setupApi } from './setup.js';
|
|
10
11
|
/** The Vite this plugin runs in, for a commit that has none of its own installed. */
|
|
11
12
|
const ownVite = () => {
|
|
12
13
|
return join(dirname(createRequire(import.meta.url).resolve('vite/package.json')), 'bin', 'vite.js');
|
|
@@ -29,9 +30,10 @@ export async function lockOf(dir) {
|
|
|
29
30
|
*
|
|
30
31
|
* The app may live in a folder of the repository, such as a pnpm workspace's
|
|
31
32
|
* `apps/app/`: each commit is built from that folder, or from the top for a
|
|
32
|
-
* commit from before the app moved there.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* commit from before the app moved there. A commit's fake API is bundled too,
|
|
34
|
+
* into `.builds/<sha>/__api/`, so its build asks its own API, whatever the
|
|
35
|
+
* checkout's API has become since: the one the commit's own `workbench.json`
|
|
36
|
+
* declares, else `api`, the checkout's, when the commit has it.
|
|
35
37
|
*/
|
|
36
38
|
export function createBuilds(options) {
|
|
37
39
|
const states = new Map();
|
|
@@ -180,10 +182,11 @@ export function createBuilds(options) {
|
|
|
180
182
|
'--emptyOutDir',
|
|
181
183
|
];
|
|
182
184
|
await step(sha, log, process.execPath, args, app, 'vite build');
|
|
183
|
-
// Its own fake API, when the commit has one inside the worktree
|
|
184
|
-
|
|
185
|
+
// Its own fake API, when the commit has one inside the worktree: the one its own
|
|
186
|
+
// workbench.json declares, else the checkout's.
|
|
187
|
+
const api = (await setupApi(work)) ?? (await options.api?.()) ?? null;
|
|
185
188
|
let built = null;
|
|
186
|
-
const entry = api ?
|
|
189
|
+
const entry = api ? api.entry(work, app) : null;
|
|
187
190
|
if (api && entry && !relative(work, entry).startsWith('..')) {
|
|
188
191
|
const file = await bundleApi((more, cwd) => step(sha, log, process.execPath, [vite, ...more], cwd, 'API bundle'), app, entry, join(temporary, '__api'));
|
|
189
192
|
built = file ? { file: `__api/${file}`, path: api.path } : null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** What a repository says about itself in `workbench.json` at its top. */
|
|
2
|
+
export interface Setup {
|
|
3
|
+
/** '' when the file gives none. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** In the file's order, each with its string settings. */
|
|
6
|
+
viewers: {
|
|
7
|
+
name: string;
|
|
8
|
+
options: Record<string, string>;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The `workbench.json` at `top`, read as the workbench reads it: an object,
|
|
13
|
+
* whose `viewers` is an object of viewers, each named in lower case and with
|
|
14
|
+
* an object of settings, of which only strings are kept, cut to 500
|
|
15
|
+
* characters. `missing` without the file, `invalid` when it isn't JSON or
|
|
16
|
+
* isn't a setup. Read each time, so an edit to it counts at once.
|
|
17
|
+
*
|
|
18
|
+
* ```json
|
|
19
|
+
* { "name": "Aitems", "viewers": { "proto": { "folder": "apps/app", "api": "../fake-api/src/index.ts" } } }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function readSetup(top: string): Promise<{
|
|
23
|
+
state: 'read';
|
|
24
|
+
setup: Setup;
|
|
25
|
+
} | {
|
|
26
|
+
state: 'missing' | 'invalid';
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* The fake API the `workbench.json` at `top` declares: Proto's `api`, its
|
|
30
|
+
* module from Proto's `folder` ('' or none for the top), answered at `/api`.
|
|
31
|
+
* `entry` places that module in any tree of the repository, this one or a
|
|
32
|
+
* build's worktree. Null when the file is missing or no setup, or declares no
|
|
33
|
+
* fake API.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setupApi(top: string): Promise<{
|
|
36
|
+
entry: (tree: string) => string;
|
|
37
|
+
path: string;
|
|
38
|
+
} | null>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
/** An object, as JSON has it: not null, not an array. */
|
|
4
|
+
const isObject = (value) => {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The `workbench.json` at `top`, read as the workbench reads it: an object,
|
|
9
|
+
* whose `viewers` is an object of viewers, each named in lower case and with
|
|
10
|
+
* an object of settings, of which only strings are kept, cut to 500
|
|
11
|
+
* characters. `missing` without the file, `invalid` when it isn't JSON or
|
|
12
|
+
* isn't a setup. Read each time, so an edit to it counts at once.
|
|
13
|
+
*
|
|
14
|
+
* ```json
|
|
15
|
+
* { "name": "Aitems", "viewers": { "proto": { "folder": "apps/app", "api": "../fake-api/src/index.ts" } } }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export async function readSetup(top) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await readFile(join(top, 'workbench.json'), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { state: 'missing' };
|
|
25
|
+
}
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(text);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { state: 'invalid' };
|
|
32
|
+
}
|
|
33
|
+
if (!isObject(raw) || (raw['viewers'] !== undefined && !isObject(raw['viewers']))) {
|
|
34
|
+
return { state: 'invalid' };
|
|
35
|
+
}
|
|
36
|
+
const name = typeof raw['name'] === 'string' ? raw['name'].trim().slice(0, 80) : '';
|
|
37
|
+
const viewers = [];
|
|
38
|
+
for (const [viewer, settings] of Object.entries(isObject(raw['viewers']) ? raw['viewers'] : {})) {
|
|
39
|
+
if (!/^[a-z][a-z0-9-]*$/.test(viewer) || !isObject(settings)) {
|
|
40
|
+
return { state: 'invalid' };
|
|
41
|
+
}
|
|
42
|
+
const options = {};
|
|
43
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
options[key] = value.slice(0, 500);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
viewers.push({ name: viewer, options });
|
|
49
|
+
}
|
|
50
|
+
return { state: 'read', setup: { name, viewers } };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The fake API the `workbench.json` at `top` declares: Proto's `api`, its
|
|
54
|
+
* module from Proto's `folder` ('' or none for the top), answered at `/api`.
|
|
55
|
+
* `entry` places that module in any tree of the repository, this one or a
|
|
56
|
+
* build's worktree. Null when the file is missing or no setup, or declares no
|
|
57
|
+
* fake API.
|
|
58
|
+
*/
|
|
59
|
+
export async function setupApi(top) {
|
|
60
|
+
const read = await readSetup(top);
|
|
61
|
+
const proto = read.state === 'read' ? read.setup.viewers.find((viewer) => viewer.name === 'proto') : null;
|
|
62
|
+
const api = proto?.options['api']?.trim() ?? '';
|
|
63
|
+
const folder = proto?.options['folder']?.trim() ?? '';
|
|
64
|
+
return api === '' ? null : { entry: (tree) => resolve(tree, folder, api), path: '/api' };
|
|
65
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Plugin } from 'vite';
|
|
|
4
4
|
* also answers the deployed viewer. It lists this machine's branches and
|
|
5
5
|
* commits, builds any commit, and checks a branch out.
|
|
6
6
|
*
|
|
7
|
+
* GET /__workbench/setup the checkout's workbench.json: { name, viewers }; 404 none, 422 no setup
|
|
7
8
|
* GET /__workbench/versions branches, tags, what is checked out, changed files
|
|
8
9
|
* GET /__workbench/commits?ref=<ref> that ref's history, newest first, each with its build
|
|
9
10
|
* POST /__workbench/builds/<sha> build it; asked again, says how it stands (?retry after a failure)
|
|
@@ -16,12 +17,17 @@ import type { Plugin } from 'vite';
|
|
|
16
17
|
* default branch unless set. Builds are served without the token, as a frame
|
|
17
18
|
* can't send a header. Without WORKBENCH_URL, the dev server opens no viewer.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* build
|
|
24
|
-
*
|
|
20
|
+
* A fake API beside the app is declared in `workbench.json` at the
|
|
21
|
+
* repository's top, as Proto's `api`: its module from Proto's `folder`, whose
|
|
22
|
+
* default export is a fetch handler, asked at `/api`. The dev server answers
|
|
23
|
+
* it there for Live, from its source, reading the file on each request; each
|
|
24
|
+
* build bundles the one its commit declares (the checkout's, for a commit with
|
|
25
|
+
* no file of its own) and answers it under the build, at
|
|
26
|
+
* `/__workbench/b/<sha>/api/…`. Either way the handler is asked without the
|
|
27
|
+
* path, so `/api/bags/b1` reaches it as `/bags/b1`.
|
|
28
|
+
*
|
|
29
|
+
* The `api` option overrides the file: `entry`, the module from the app's
|
|
30
|
+
* folder, and `path`, where the app asks it (`/api` unless set).
|
|
25
31
|
*/
|
|
26
32
|
export declare function workbenchLocal(options?: {
|
|
27
33
|
base?: string;
|
|
@@ -9,6 +9,7 @@ import { environment } from './environment.js';
|
|
|
9
9
|
import { sendResponse, toRequest } from './fetchBridge.js';
|
|
10
10
|
import { defaultBranch, gitIn } from './git.js';
|
|
11
11
|
import { sendJson, serveBuild } from './serveBuild.js';
|
|
12
|
+
import { readSetup, setupApi } from './setup.js';
|
|
12
13
|
import { commits, versions } from './versions.js';
|
|
13
14
|
const PREFIX = '/__workbench';
|
|
14
15
|
const SHA = /^[0-9a-f]{7,40}$/;
|
|
@@ -58,6 +59,7 @@ function open(url) {
|
|
|
58
59
|
* also answers the deployed viewer. It lists this machine's branches and
|
|
59
60
|
* commits, builds any commit, and checks a branch out.
|
|
60
61
|
*
|
|
62
|
+
* GET /__workbench/setup the checkout's workbench.json: { name, viewers }; 404 none, 422 no setup
|
|
61
63
|
* GET /__workbench/versions branches, tags, what is checked out, changed files
|
|
62
64
|
* GET /__workbench/commits?ref=<ref> that ref's history, newest first, each with its build
|
|
63
65
|
* POST /__workbench/builds/<sha> build it; asked again, says how it stands (?retry after a failure)
|
|
@@ -70,27 +72,46 @@ function open(url) {
|
|
|
70
72
|
* default branch unless set. Builds are served without the token, as a frame
|
|
71
73
|
* can't send a header. Without WORKBENCH_URL, the dev server opens no viewer.
|
|
72
74
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* build
|
|
78
|
-
*
|
|
75
|
+
* A fake API beside the app is declared in `workbench.json` at the
|
|
76
|
+
* repository's top, as Proto's `api`: its module from Proto's `folder`, whose
|
|
77
|
+
* default export is a fetch handler, asked at `/api`. The dev server answers
|
|
78
|
+
* it there for Live, from its source, reading the file on each request; each
|
|
79
|
+
* build bundles the one its commit declares (the checkout's, for a commit with
|
|
80
|
+
* no file of its own) and answers it under the build, at
|
|
81
|
+
* `/__workbench/b/<sha>/api/…`. Either way the handler is asked without the
|
|
82
|
+
* path, so `/api/bags/b1` reaches it as `/bags/b1`.
|
|
83
|
+
*
|
|
84
|
+
* The `api` option overrides the file: `entry`, the module from the app's
|
|
85
|
+
* folder, and `path`, where the app asks it (`/api` unless set).
|
|
79
86
|
*/
|
|
80
87
|
export function workbenchLocal(options = {}) {
|
|
81
88
|
let root = process.cwd();
|
|
82
89
|
let git = gitIn(root);
|
|
90
|
+
let top = null;
|
|
83
91
|
let server = null;
|
|
84
|
-
const
|
|
92
|
+
const given = options.api;
|
|
93
|
+
const option = given
|
|
85
94
|
? {
|
|
86
|
-
entry:
|
|
87
|
-
path: `/${(
|
|
95
|
+
entry: (_top, app) => resolve(app, given.entry),
|
|
96
|
+
path: `/${(given.path ?? '/api').replace(/^\/+|\/+$/g, '')}`,
|
|
88
97
|
}
|
|
89
98
|
: null;
|
|
99
|
+
/** The repository's top, where workbench.json is; null outside a repository. */
|
|
100
|
+
const topOf = () => {
|
|
101
|
+
return (top ??= git('rev-parse', '--show-toplevel').then((output) => output.trim() || null, () => null));
|
|
102
|
+
};
|
|
103
|
+
/** The checkout's fake API: the option, else the one its workbench.json declares now. */
|
|
104
|
+
const currentApi = async () => {
|
|
105
|
+
if (option) {
|
|
106
|
+
return option;
|
|
107
|
+
}
|
|
108
|
+
const at = await topOf();
|
|
109
|
+
return at === null ? null : setupApi(at);
|
|
110
|
+
};
|
|
90
111
|
const builds = createBuilds({
|
|
91
112
|
root: () => root,
|
|
92
113
|
git: (...args) => git(...args),
|
|
93
|
-
api:
|
|
114
|
+
api: currentApi,
|
|
94
115
|
});
|
|
95
116
|
const workers = createApiWorkers();
|
|
96
117
|
const base = async () => options.base ?? (await defaultBranch(git));
|
|
@@ -99,6 +120,16 @@ export function workbenchLocal(options = {}) {
|
|
|
99
120
|
if (request.method === 'GET' && path === '/versions') {
|
|
100
121
|
return sendJson(response, 200, await versions(git, await base(), builds.buildOf));
|
|
101
122
|
}
|
|
123
|
+
if (request.method === 'GET' && path === '/setup') {
|
|
124
|
+
const at = await topOf();
|
|
125
|
+
const read = at === null ? { state: 'missing' } : await readSetup(at);
|
|
126
|
+
if (read.state === 'read') {
|
|
127
|
+
return sendJson(response, 200, read.setup);
|
|
128
|
+
}
|
|
129
|
+
return read.state === 'missing'
|
|
130
|
+
? sendJson(response, 404, { error: 'No workbench.json' })
|
|
131
|
+
: sendJson(response, 422, { error: "workbench.json isn't a setup" });
|
|
132
|
+
}
|
|
102
133
|
if (request.method === 'GET' && path === '/commits') {
|
|
103
134
|
const ref = url.searchParams.get('ref') ?? 'HEAD';
|
|
104
135
|
if (!REF.test(ref)) {
|
|
@@ -146,13 +177,17 @@ export function workbenchLocal(options = {}) {
|
|
|
146
177
|
}
|
|
147
178
|
return true;
|
|
148
179
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The fake API for Live, from its source: Vite loads it again when it
|
|
182
|
+
* changes. Without one, the path is the app's, as if this were not here.
|
|
183
|
+
*/
|
|
184
|
+
async function answerLiveApi(request, response, next) {
|
|
151
185
|
try {
|
|
186
|
+
const api = await currentApi();
|
|
152
187
|
if (!api || !server) {
|
|
153
|
-
return
|
|
188
|
+
return next();
|
|
154
189
|
}
|
|
155
|
-
const module = await server.ssrLoadModule(
|
|
190
|
+
const module = await server.ssrLoadModule(api.entry((await topOf()) ?? root, root));
|
|
156
191
|
const asked = await toRequest(request, request.url ?? '/');
|
|
157
192
|
await sendResponse(response, await apiFetchOf(module)(asked));
|
|
158
193
|
}
|
|
@@ -203,6 +238,7 @@ export function workbenchLocal(options = {}) {
|
|
|
203
238
|
configResolved(config) {
|
|
204
239
|
root = config.root;
|
|
205
240
|
git = gitIn(root);
|
|
241
|
+
top = null;
|
|
206
242
|
},
|
|
207
243
|
configureServer(devServer) {
|
|
208
244
|
server = devServer;
|
|
@@ -216,12 +252,11 @@ export function workbenchLocal(options = {}) {
|
|
|
216
252
|
if (ours) {
|
|
217
253
|
stack.unshift(ours);
|
|
218
254
|
}
|
|
219
|
-
// Before Vite's own middleware, which would answer the path with the app's page.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
255
|
+
// Before Vite's own middleware, which would answer the path with the app's page. Always
|
|
256
|
+
// there without the option, so declaring the API in workbench.json needs no restart.
|
|
257
|
+
devServer.middlewares.use(option?.path ?? '/api', (request, response, next) => {
|
|
258
|
+
void answerLiveApi(request, response, next);
|
|
259
|
+
});
|
|
225
260
|
devServer.httpServer?.once('listening', () => {
|
|
226
261
|
void announce(devServer);
|
|
227
262
|
});
|
package/package.json
CHANGED