@tilewise/workbench-client 0.0.0-stage → 0.1.0

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +176 -2
  4. package/dist/blocks/ScreenScope.d.ts +11 -0
  5. package/dist/blocks/ScreenScope.js +11 -0
  6. package/dist/blocks/drawnBlocks.d.ts +11 -0
  7. package/dist/blocks/drawnBlocks.js +47 -0
  8. package/dist/blocks/screenContext.d.ts +2 -0
  9. package/dist/blocks/screenContext.js +3 -0
  10. package/dist/bridge/Bridge.d.ts +9 -0
  11. package/dist/bridge/Bridge.js +120 -0
  12. package/dist/bridge/device.d.ts +6 -0
  13. package/dist/bridge/device.js +28 -0
  14. package/dist/bridge/post.d.ts +5 -0
  15. package/dist/bridge/post.js +8 -0
  16. package/dist/bridge/readViewerMessage.d.ts +3 -0
  17. package/dist/bridge/readViewerMessage.js +44 -0
  18. package/dist/bridge/revealControl.d.ts +10 -0
  19. package/dist/bridge/revealControl.js +62 -0
  20. package/dist/bridge/toneAtTop.d.ts +3 -0
  21. package/dist/bridge/toneAtTop.js +23 -0
  22. package/dist/bridge/watchScreen.d.ts +10 -0
  23. package/dist/bridge/watchScreen.js +50 -0
  24. package/dist/bridge/wholeHeight.d.ts +8 -0
  25. package/dist/bridge/wholeHeight.js +16 -0
  26. package/dist/controls/paint.d.ts +8 -0
  27. package/dist/controls/paint.js +16 -0
  28. package/dist/controls/parseValue.d.ts +7 -0
  29. package/dist/controls/parseValue.js +34 -0
  30. package/dist/controls/resetControl.d.ts +3 -0
  31. package/dist/controls/resetControl.js +6 -0
  32. package/dist/controls/setControl.d.ts +3 -0
  33. package/dist/controls/setControl.js +6 -0
  34. package/dist/controls/store.d.ts +12 -0
  35. package/dist/controls/store.js +125 -0
  36. package/dist/controls/types.d.ts +111 -0
  37. package/dist/controls/types.js +6 -0
  38. package/dist/controls/useControl.d.ts +7 -0
  39. package/dist/controls/useControl.js +16 -0
  40. package/dist/define/buildManifest.d.ts +8 -0
  41. package/dist/define/buildManifest.js +51 -0
  42. package/dist/define/checkDefaults.d.ts +3 -0
  43. package/dist/define/checkDefaults.js +29 -0
  44. package/dist/define/defineApp.d.ts +24 -0
  45. package/dist/define/defineApp.js +92 -0
  46. package/dist/define/defineBlock.d.ts +12 -0
  47. package/dist/define/defineBlock.js +10 -0
  48. package/dist/define/defineScreen.d.ts +11 -0
  49. package/dist/define/defineScreen.js +10 -0
  50. package/dist/define/part.d.ts +23 -0
  51. package/dist/define/part.js +22 -0
  52. package/dist/define/registeredApp.d.ts +12 -0
  53. package/dist/define/registeredApp.js +5 -0
  54. package/dist/draw/boolean.d.ts +6 -0
  55. package/dist/draw/boolean.js +5 -0
  56. package/dist/draw/choose.d.ts +4 -0
  57. package/dist/draw/choose.js +4 -0
  58. package/dist/draw/color.d.ts +2 -0
  59. package/dist/draw/color.js +4 -0
  60. package/dist/draw/number.d.ts +5 -0
  61. package/dist/draw/number.js +5 -0
  62. package/dist/draw/text.d.ts +5 -0
  63. package/dist/draw/text.js +5 -0
  64. package/dist/draw/variant.d.ts +17 -0
  65. package/dist/draw/variant.js +4 -0
  66. package/dist/index.d.ts +18 -0
  67. package/dist/index.js +14 -0
  68. package/dist/protocol/protocol.d.ts +8 -0
  69. package/dist/protocol/protocol.js +8 -0
  70. package/dist/protocol/types.d.ts +133 -0
  71. package/dist/protocol/types.js +1 -0
  72. package/dist/vite/builds.d.ts +29 -0
  73. package/dist/vite/builds.js +196 -0
  74. package/dist/vite/checkout.d.ts +11 -0
  75. package/dist/vite/checkout.js +43 -0
  76. package/dist/vite/environment.d.ts +9 -0
  77. package/dist/vite/environment.js +10 -0
  78. package/dist/vite/git.d.ts +7 -0
  79. package/dist/vite/git.js +18 -0
  80. package/dist/vite/index.d.ts +1 -0
  81. package/dist/vite/index.js +1 -0
  82. package/dist/vite/serveBuild.d.ts +4 -0
  83. package/dist/vite/serveBuild.js +39 -0
  84. package/dist/vite/versions.d.ts +44 -0
  85. package/dist/vite/versions.js +122 -0
  86. package/dist/vite/workbenchLocal.d.ts +21 -0
  87. package/dist/vite/workbenchLocal.js +186 -0
  88. package/docs/protocol.md +58 -0
  89. package/package.json +60 -3
@@ -0,0 +1,12 @@
1
+ import type { Control, Declared } from '../controls/types.js';
2
+ import { type Part } from './part.js';
3
+ /**
4
+ * Declares a block's controls, beside its component. They show in a section
5
+ * of their own, headed by `label`, on any screen while the block is drawn there.
6
+ */
7
+ export declare function defineBlock<const C extends readonly Control[]>(declaration: {
8
+ /** The block's component name. */
9
+ block: string;
10
+ label: string;
11
+ controls: C & NoInfer<Declared<C>>;
12
+ }): Part<C>;
@@ -0,0 +1,10 @@
1
+ import { checkDefaults } from './checkDefaults.js';
2
+ import { makePart } from './part.js';
3
+ /**
4
+ * Declares a block's controls, beside its component. They show in a section
5
+ * of their own, headed by `label`, on any screen while the block is drawn there.
6
+ */
7
+ export function defineBlock(declaration) {
8
+ checkDefaults(declaration.controls, `block "${declaration.block}"`);
9
+ return makePart({ level: 'block', block: declaration.block }, declaration.controls, declaration.label);
10
+ }
@@ -0,0 +1,11 @@
1
+ import type { Control, Declared } from '../controls/types.js';
2
+ import { type Part } from './part.js';
3
+ /**
4
+ * Declares a screen's controls, beside its component. They show in the form
5
+ * on that screen, in the order given.
6
+ */
7
+ export declare function defineScreen<const C extends readonly Control[]>(declaration: {
8
+ /** A screen id from the app's screens. */
9
+ screen: string;
10
+ controls: C & NoInfer<Declared<C>>;
11
+ }): Part<C>;
@@ -0,0 +1,10 @@
1
+ import { checkDefaults } from './checkDefaults.js';
2
+ import { makePart } from './part.js';
3
+ /**
4
+ * Declares a screen's controls, beside its component. They show in the form
5
+ * on that screen, in the order given.
6
+ */
7
+ export function defineScreen(declaration) {
8
+ checkDefaults(declaration.controls, `screen "${declaration.screen}"`);
9
+ return makePart({ level: 'screen', screen: declaration.screen }, declaration.controls);
10
+ }
@@ -0,0 +1,23 @@
1
+ import type { Control, Handle, Place } from '../controls/types.js';
2
+ /** Marks a part, so `defineApp` finds it among a module's exports. */
3
+ export declare const PART: unique symbol;
4
+ export interface PartInfo {
5
+ place: Place;
6
+ /** A block's heading in the form. */
7
+ label?: string;
8
+ controls: readonly Control[];
9
+ handles: readonly Handle[];
10
+ }
11
+ /**
12
+ * One `.workbench.ts` file's controls: the app's, a screen's or a block's.
13
+ * It holds one handle per control, under its id: `home.homepage`.
14
+ */
15
+ export type Part<C extends readonly Control[] = readonly Control[]> = {
16
+ readonly [K in C[number] as K['id']]: Handle<K>;
17
+ } & {
18
+ readonly [PART]: PartInfo;
19
+ };
20
+ export declare function makePart<C extends readonly Control[]>(place: Place, controls: C, label?: string): Part<C>;
21
+ export declare function isPart(value: unknown): value is Part;
22
+ /** Where a part sits, in words, for the messages the start-up checks throw. */
23
+ export declare function nameOf(info: PartInfo): string;
@@ -0,0 +1,22 @@
1
+ /** Marks a part, so `defineApp` finds it among a module's exports. */
2
+ export const PART = Symbol.for('@tilewise/workbench-client/part');
3
+ export function makePart(place, controls, label) {
4
+ const handles = controls.map((control) => ({ id: control.id, control, place }));
5
+ const info = { place, controls, handles, ...(label !== undefined && { label }) };
6
+ const byId = Object.fromEntries(handles.map((handle) => [handle.id, handle]));
7
+ return Object.assign(byId, { [PART]: info });
8
+ }
9
+ export function isPart(value) {
10
+ return typeof value === 'object' && value !== null && PART in value;
11
+ }
12
+ /** Where a part sits, in words, for the messages the start-up checks throw. */
13
+ export function nameOf(info) {
14
+ switch (info.place.level) {
15
+ case 'global':
16
+ return 'the app';
17
+ case 'screen':
18
+ return `screen "${info.place.screen}"`;
19
+ case 'block':
20
+ return `block "${info.place.block}"`;
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ import type { At, Manifest } from '../protocol/types.js';
2
+ /** The app `defineApp` declared, as the bridge needs it. */
3
+ export interface RegisteredApp {
4
+ manifest: Manifest;
5
+ /** The app's navigation, going by state id: false for a state it doesn't have. */
6
+ useNavigation: () => {
7
+ at: At;
8
+ go: (state: string) => boolean;
9
+ };
10
+ }
11
+ export declare function registerApp(app: RegisteredApp): void;
12
+ export declare const registeredApp: () => RegisteredApp | null;
@@ -0,0 +1,5 @@
1
+ let registered = null;
2
+ export function registerApp(app) {
3
+ registered = app;
4
+ }
5
+ export const registeredApp = () => registered;
@@ -0,0 +1,6 @@
1
+ import { type Case } from './choose.js';
2
+ /** What to draw for a boolean's value: `on` and `off`, both required. */
3
+ export declare function boolean<R>(value: boolean, cases: {
4
+ on: Case<true, R>;
5
+ off: Case<false, R>;
6
+ }): R;
@@ -0,0 +1,5 @@
1
+ import { choose } from './choose.js';
2
+ /** What to draw for a boolean's value: `on` and `off`, both required. */
3
+ export function boolean(value, cases) {
4
+ return value ? choose(cases.on, true) : choose(cases.off, false);
5
+ }
@@ -0,0 +1,4 @@
1
+ /** A case: something to draw, or a function that gets the value and returns it. */
2
+ export type Case<V, R> = R | ((value: V) => R);
3
+ /** The case's result for a value. */
4
+ export declare function choose<V, R>(found: Case<V, R> | undefined, value: V): R;
@@ -0,0 +1,4 @@
1
+ /** The case's result for a value. */
2
+ export function choose(found, value) {
3
+ return typeof found === 'function' ? found(value) : found;
4
+ }
@@ -0,0 +1,2 @@
1
+ /** A color's token as a CSS value, for a `style`: `color('lime')` is `'var(--lime)'`. */
2
+ export declare function color<T extends string>(token: T): `var(--${T})`;
@@ -0,0 +1,4 @@
1
+ /** A color's token as a CSS value, for a `style`: `color('lime')` is `'var(--lime)'`. */
2
+ export function color(token) {
3
+ return `var(--${token})`;
4
+ }
@@ -0,0 +1,5 @@
1
+ import { type Case } from './choose.js';
2
+ /** What to draw for a number: any exact numbers, and `other`, required, for the rest. */
3
+ export declare function number<R>(value: number, cases: Readonly<Record<number, Case<number, R>>> & {
4
+ other: Case<number, R>;
5
+ }): R;
@@ -0,0 +1,5 @@
1
+ import { choose } from './choose.js';
2
+ /** What to draw for a number: any exact numbers, and `other`, required, for the rest. */
3
+ export function number(value, cases) {
4
+ return choose(Object.hasOwn(cases, String(value)) ? cases[value] : cases.other, value);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { type Case } from './choose.js';
2
+ /** What to draw for a line of text: any exact lines, and `other`, required, for the rest. */
3
+ export declare function text<R>(value: string, cases: Readonly<Record<string, Case<string, R>>> & {
4
+ other: Case<string, R>;
5
+ }): R;
@@ -0,0 +1,5 @@
1
+ import { choose } from './choose.js';
2
+ /** What to draw for a line of text: any exact lines, and `other`, required, for the rest. */
3
+ export function text(value, cases) {
4
+ return choose(value !== 'other' && Object.hasOwn(cases, value) ? cases[value] : cases.other, value);
5
+ }
@@ -0,0 +1,17 @@
1
+ import { type Case } from './choose.js';
2
+ type EveryCase<V extends string, R> = {
3
+ [K in V]: Case<K, R>;
4
+ };
5
+ /** Some options, and `other` for the rest: its function gets them without the ones listed. */
6
+ type SomeCases<V extends string, L extends string, R> = {
7
+ [K in L]: K extends 'other' ? Case<Exclude<V, L>, R> : Case<K, R>;
8
+ } & {
9
+ other: unknown;
10
+ };
11
+ /**
12
+ * What to draw for a variant's value. A call without `other` lists every
13
+ * option, so a new option is a type error until every such call handles it.
14
+ */
15
+ export declare function variant<V extends string, R>(value: V, cases: EveryCase<V, R>): R;
16
+ export declare function variant<V extends string, L extends V | 'other', R>(value: V, cases: SomeCases<V, L, R>): R;
17
+ export {};
@@ -0,0 +1,4 @@
1
+ import { choose } from './choose.js';
2
+ export function variant(value, cases) {
3
+ return choose(Object.hasOwn(cases, value) ? cases[value] : cases['other'], value);
4
+ }
@@ -0,0 +1,18 @@
1
+ export { ScreenScope } from './blocks/ScreenScope.js';
2
+ export { Bridge } from './bridge/Bridge.js';
3
+ export { resetControl } from './controls/resetControl.js';
4
+ export { setControl } from './controls/setControl.js';
5
+ export type { BooleanControl, ColorControl, ColorOption, Control, Handle, Level, NumberControl, Option, Place, TextControl, Value, ValueOf, VariantControl, } from './controls/types.js';
6
+ export { useControl } from './controls/useControl.js';
7
+ export { defineApp } from './define/defineApp.js';
8
+ export { defineBlock } from './define/defineBlock.js';
9
+ export { defineScreen } from './define/defineScreen.js';
10
+ export type { Part } from './define/part.js';
11
+ export { boolean } from './draw/boolean.js';
12
+ export type { Case } from './draw/choose.js';
13
+ export { color } from './draw/color.js';
14
+ export { number } from './draw/number.js';
15
+ export { text } from './draw/text.js';
16
+ export { variant } from './draw/variant.js';
17
+ export { PROTOCOL } from './protocol/protocol.js';
18
+ export type { AppMessage, AppSays, At, DeviceMessage, Manifest, ManifestBlock, ManifestControl, Navigation, Screen, State, StateKind, Tone, ViewerMessage, ViewerSays, } from './protocol/types.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { ScreenScope } from './blocks/ScreenScope.js';
2
+ export { Bridge } from './bridge/Bridge.js';
3
+ export { resetControl } from './controls/resetControl.js';
4
+ export { setControl } from './controls/setControl.js';
5
+ export { useControl } from './controls/useControl.js';
6
+ export { defineApp } from './define/defineApp.js';
7
+ export { defineBlock } from './define/defineBlock.js';
8
+ export { defineScreen } from './define/defineScreen.js';
9
+ export { boolean } from './draw/boolean.js';
10
+ export { color } from './draw/color.js';
11
+ export { number } from './draw/number.js';
12
+ export { text } from './draw/text.js';
13
+ export { variant } from './draw/variant.js';
14
+ export { PROTOCOL } from './protocol/protocol.js';
@@ -0,0 +1,8 @@
1
+ /** The protocol this package speaks. */
2
+ export declare const PROTOCOL = 3;
3
+ /**
4
+ * Every message carries `source`, for every app, as builds from before the
5
+ * package send them. Each side ignores any message without the other's tag.
6
+ */
7
+ export declare const FROM_APP = "aitems";
8
+ export declare const FROM_VIEWER = "aitems-workbench";
@@ -0,0 +1,8 @@
1
+ /** The protocol this package speaks. */
2
+ export const PROTOCOL = 3;
3
+ /**
4
+ * Every message carries `source`, for every app, as builds from before the
5
+ * package send them. Each side ignores any message without the other's tag.
6
+ */
7
+ export const FROM_APP = 'aitems';
8
+ export const FROM_VIEWER = 'aitems-workbench';
@@ -0,0 +1,133 @@
1
+ import type { Control, Value } from '../controls/types.js';
2
+ /**
3
+ * Protocol 3: what the app and the viewer say to each other through
4
+ * `postMessage`, between the frame and the page around it. Any change to a
5
+ * message or to the manifest bumps the protocol, so the viewer can still read
6
+ * older builds.
7
+ */
8
+ export type StateKind = 'state' | 'sheet' | 'overlay';
9
+ /** One place on a screen. The app knows how to get there; the viewer only names it. */
10
+ export interface State {
11
+ /** Unique across the app, and never renamed. */
12
+ id: string;
13
+ label: string;
14
+ kind?: StateKind;
15
+ note?: string;
16
+ /** Only while the screen's `statesBy` control has one of these values. */
17
+ when?: readonly string[];
18
+ }
19
+ export interface Screen<S extends State = State> {
20
+ id: string;
21
+ label: string;
22
+ /** Two letters, for narrow lists. */
23
+ short: string;
24
+ /** The screen it sits under in the route tree. */
25
+ parent?: string;
26
+ /** The tab-bar tab it is the root of. */
27
+ tab?: string;
28
+ /** A heading that holds screens, with no states of its own. */
29
+ group?: boolean;
30
+ /** A page pushed from several screens. */
31
+ page?: boolean;
32
+ /** Where it opens from, in words. */
33
+ where?: string;
34
+ /** A control id: which states it has depends on that value. */
35
+ statesBy?: string;
36
+ /** The source files that draw it; a folder ends in `/`. */
37
+ files?: readonly string[];
38
+ states: readonly S[];
39
+ }
40
+ /** A control as declared, with where it sits. */
41
+ export type ManifestControl = Control & ({
42
+ level: 'global';
43
+ } | {
44
+ level: 'screen';
45
+ screen: string;
46
+ } | {
47
+ level: 'block';
48
+ block: string;
49
+ });
50
+ export interface ManifestBlock {
51
+ /** The component's name. */
52
+ id: string;
53
+ /** Its section's heading in the form. */
54
+ label: string;
55
+ /** Its controls' ids, in order. */
56
+ controls: string[];
57
+ }
58
+ /** Everything the app has, sent once with `hello`. A state goes without its jump. */
59
+ export interface Manifest {
60
+ /** Whose build this is, such as `aitems`. */
61
+ app: string;
62
+ screens: Screen[];
63
+ controls: ManifestControl[];
64
+ blocks: ManifestBlock[];
65
+ }
66
+ export type Tone = 'light' | 'dark';
67
+ /** What the app says. */
68
+ export type AppSays = {
69
+ type: 'hello';
70
+ protocol: 3;
71
+ manifest: Manifest;
72
+ } | {
73
+ type: 'at';
74
+ screen: string;
75
+ state: string | null;
76
+ } | {
77
+ type: 'blocks';
78
+ screen: string;
79
+ blocks: string[];
80
+ } | {
81
+ type: 'values';
82
+ values: Record<string, Value>;
83
+ } | {
84
+ type: 'tone';
85
+ top: Tone;
86
+ } | {
87
+ type: 'height';
88
+ height: number;
89
+ };
90
+ /** What the app says, as sent: tagged `source: 'aitems'`. */
91
+ export type AppMessage = {
92
+ source: 'aitems';
93
+ } & AppSays;
94
+ export interface DeviceMessage {
95
+ type: 'device';
96
+ /** What the status bar and the home indicator take, in px. */
97
+ insets: {
98
+ top: number;
99
+ bottom: number;
100
+ };
101
+ platform: 'ios' | 'android' | null;
102
+ /** Full page: no device, and the frame as tall as the open screen. */
103
+ whole: boolean;
104
+ }
105
+ /** What the viewer says. */
106
+ export type ViewerSays = {
107
+ type: 'hello';
108
+ } | DeviceMessage | {
109
+ type: 'go';
110
+ state: string;
111
+ } | {
112
+ type: 'set';
113
+ id: string;
114
+ value: Value;
115
+ } | {
116
+ type: 'reset';
117
+ id?: string;
118
+ };
119
+ /** What the viewer says, as sent: tagged `source: 'aitems-workbench'`. */
120
+ export type ViewerMessage = {
121
+ source: 'aitems-workbench';
122
+ } & ViewerSays;
123
+ /** Where the app is now. */
124
+ export interface At {
125
+ screen: string;
126
+ state: string | null;
127
+ }
128
+ /** How the package moves the app, and learns where it is. */
129
+ export interface Navigation<S extends State = State> {
130
+ /** Make the jump the state describes. */
131
+ go: (state: S) => void;
132
+ at: At;
133
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { Git } from './git.js';
2
+ export interface Build {
3
+ state: 'none' | 'queued' | 'building' | 'built' | 'failed';
4
+ /** What it is doing now, while building. */
5
+ step?: string;
6
+ /** 1 for a commit whose app talks to the viewer; 0 for one from before. Its hello says which protocol. */
7
+ protocol?: number;
8
+ /** The end of the log, when it failed. */
9
+ error?: string;
10
+ }
11
+ /** A tree's lockfile and the package manager it belongs to: pnpm, or npm for a commit from before a switch. */
12
+ export declare function lockOf(dir: string): Promise<{
13
+ manager: string;
14
+ text: string | null;
15
+ }>;
16
+ /**
17
+ * Builds any commit of the app, one at a time, in a git worktree under
18
+ * `.builds/work/`, into `.builds/<sha>/`, served under `/__workbench/b/<sha>/`.
19
+ * The checkout and its uncommitted changes are never touched.
20
+ */
21
+ export declare function createBuilds(options: {
22
+ root: () => string;
23
+ git: Git;
24
+ }): {
25
+ buildOf: (sha: string) => Promise<Build>;
26
+ requestBuild: (sha: string, retry?: boolean) => Promise<Build>;
27
+ settled: () => Promise<void>;
28
+ folder: () => string;
29
+ };
@@ -0,0 +1,196 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { appendFile, lstat, mkdir, readdir, readFile, rename, rm, rmdir, symlink, unlink, writeFile, } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { buildEnvironment } from './environment.js';
6
+ /** A tree's lockfile and the package manager it belongs to: pnpm, or npm for a commit from before a switch. */
7
+ export async function lockOf(dir) {
8
+ const pnpm = await readFile(join(dir, 'pnpm-lock.yaml'), 'utf8').catch(() => null);
9
+ if (pnpm !== null) {
10
+ return { manager: 'pnpm', text: pnpm };
11
+ }
12
+ return {
13
+ manager: 'npm',
14
+ text: await readFile(join(dir, 'package-lock.json'), 'utf8').catch(() => null),
15
+ };
16
+ }
17
+ /**
18
+ * Builds any commit of the app, one at a time, in a git worktree under
19
+ * `.builds/work/`, into `.builds/<sha>/`, served under `/__workbench/b/<sha>/`.
20
+ * The checkout and its uncommitted changes are never touched.
21
+ */
22
+ export function createBuilds(options) {
23
+ const states = new Map();
24
+ const queue = [];
25
+ let busy = false;
26
+ const folder = () => join(options.root(), '.builds');
27
+ async function buildOf(sha) {
28
+ const live = states.get(sha);
29
+ if (live) {
30
+ return live;
31
+ }
32
+ try {
33
+ const meta = JSON.parse(await readFile(join(folder(), sha, 'meta.json'), 'utf8'));
34
+ const build = meta.ok
35
+ ? { state: 'built', protocol: meta.protocol ?? 0 }
36
+ : { state: 'failed', ...(meta.error !== undefined && { error: meta.error }) };
37
+ states.set(sha, build);
38
+ return build;
39
+ }
40
+ catch {
41
+ return { state: 'none' };
42
+ }
43
+ }
44
+ async function requestBuild(sha, retry = false) {
45
+ const build = await buildOf(sha);
46
+ if (build.state === 'failed' && retry) {
47
+ states.delete(sha);
48
+ await rm(join(folder(), sha), { recursive: true, force: true });
49
+ }
50
+ else if (build.state !== 'none') {
51
+ return build;
52
+ }
53
+ const queued = { state: 'queued' };
54
+ states.set(sha, queued);
55
+ queue.push(sha);
56
+ void drain();
57
+ return queued;
58
+ }
59
+ /** Waits for the queue to empty, for the tests. */
60
+ async function settled() {
61
+ while (busy || queue.length > 0) {
62
+ await new Promise((resolve) => setTimeout(resolve, 20));
63
+ }
64
+ }
65
+ async function drain() {
66
+ if (busy) {
67
+ return;
68
+ }
69
+ busy = true;
70
+ try {
71
+ for (let sha = queue.shift(); sha; sha = queue.shift()) {
72
+ await build(sha);
73
+ }
74
+ }
75
+ finally {
76
+ busy = false;
77
+ }
78
+ }
79
+ /** Runs a command into the build's log, failing on a non-zero exit. */
80
+ const step = (sha, log, command, args, cwd, name) => {
81
+ return new Promise((resolve, reject) => {
82
+ // pnpm and npm are .cmd files on Windows, which only a shell starts; the arguments are ours.
83
+ const shell = process.platform === 'win32' && (command === 'pnpm' || command === 'npm');
84
+ const child = spawn(command, args, { cwd, shell, env: buildEnvironment() });
85
+ const write = (chunk) => {
86
+ void appendFile(log, chunk);
87
+ };
88
+ child.stdout.on('data', write);
89
+ child.stderr.on('data', write);
90
+ child.on('error', reject);
91
+ child.on('close', (code) => {
92
+ if (code === 0) {
93
+ resolve();
94
+ }
95
+ else {
96
+ reject(new Error(`${name} exited with ${code}`));
97
+ }
98
+ });
99
+ states.set(sha, { state: 'building', step: name });
100
+ });
101
+ };
102
+ async function build(sha) {
103
+ const root = options.root();
104
+ const work = join(folder(), 'work', sha);
105
+ const out = join(folder(), sha);
106
+ const log = join(folder(), `${sha}.log`);
107
+ await mkdir(join(folder(), 'work'), { recursive: true });
108
+ await writeFile(log, `Building ${sha}\n`);
109
+ states.set(sha, { state: 'building', step: 'checking out' });
110
+ try {
111
+ await dropWork(work);
112
+ await options.git('worktree', 'prune');
113
+ await options.git('worktree', 'add', '--detach', work, sha);
114
+ await addBasePath(work);
115
+ // The same lockfile as the checkout: borrow its node_modules. Otherwise the
116
+ // commit installs its own, with the package manager its lockfile belongs to.
117
+ const ours = await lockOf(root);
118
+ const theirs = await lockOf(work);
119
+ if (theirs.text !== null &&
120
+ theirs.text === ours.text &&
121
+ existsSync(join(root, 'node_modules'))) {
122
+ await symlink(join(root, 'node_modules'), join(work, 'node_modules'), 'junction');
123
+ }
124
+ else if (theirs.manager === 'pnpm') {
125
+ await step(sha, log, 'pnpm', ['install', '--frozen-lockfile'], work, 'pnpm install');
126
+ }
127
+ else {
128
+ await step(sha, log, 'npm', ['ci', '--no-audit', '--no-fund', '--loglevel=error'], work, 'npm ci');
129
+ }
130
+ const temporary = `${out}.tmp`;
131
+ await rm(temporary, { recursive: true, force: true });
132
+ const vite = join(work, 'node_modules', 'vite', 'bin', 'vite.js');
133
+ const args = [
134
+ vite,
135
+ 'build',
136
+ '--base',
137
+ `/__workbench/b/${sha}/`,
138
+ '--outDir',
139
+ temporary,
140
+ '--emptyOutDir',
141
+ ];
142
+ await step(sha, log, process.execPath, args, work, 'vite build');
143
+ const protocol = (await bundleText(temporary)).includes('aitems-workbench') ? 1 : 0;
144
+ await writeFile(join(temporary, 'meta.json'), JSON.stringify({ sha, ok: true, protocol, builtAt: new Date().toISOString() }));
145
+ await rm(out, { recursive: true, force: true });
146
+ await rename(temporary, out);
147
+ states.set(sha, { state: 'built', protocol });
148
+ }
149
+ catch (failure) {
150
+ const tail = (await readFile(log, 'utf8').catch(() => '')).split('\n').slice(-40).join('\n');
151
+ const error = `${failure.message}\n${tail}`.trim();
152
+ await mkdir(out, { recursive: true });
153
+ await writeFile(join(out, 'meta.json'), JSON.stringify({ sha, ok: false, error, builtAt: new Date().toISOString() }));
154
+ states.set(sha, { state: 'failed', error });
155
+ }
156
+ finally {
157
+ await dropWork(work);
158
+ }
159
+ }
160
+ /**
161
+ * Removes a build's worktree. Its node_modules may be a link to the
162
+ * checkout's: that link goes first, on its own, because removing the
163
+ * worktree with it in place follows it and empties the checkout's packages.
164
+ */
165
+ async function dropWork(work) {
166
+ const modules = join(work, 'node_modules');
167
+ if ((await lstat(modules).catch(() => null))?.isSymbolicLink()) {
168
+ await unlink(modules).catch(() => rmdir(modules));
169
+ }
170
+ await options
171
+ .git('worktree', 'remove', '--force', work)
172
+ .catch(() => rm(work, { recursive: true, force: true }));
173
+ }
174
+ return { buildOf, requestBuild, settled, folder };
175
+ }
176
+ /**
177
+ * A router made before the app was served under a base path matches nothing
178
+ * there; such a commit's TanStack router is given `import.meta.env.BASE_URL`.
179
+ */
180
+ async function addBasePath(work) {
181
+ const main = join(work, 'src', 'main.tsx');
182
+ if (!existsSync(main)) {
183
+ return;
184
+ }
185
+ const source = await readFile(main, 'utf8');
186
+ if (source.includes('createRouter({ routeTree })')) {
187
+ await writeFile(main, source.replace('createRouter({ routeTree })', 'createRouter({ routeTree, basepath: import.meta.env.BASE_URL })'));
188
+ }
189
+ }
190
+ async function bundleText(dir) {
191
+ const assets = join(dir, 'assets');
192
+ const files = existsSync(assets)
193
+ ? (await readdir(assets)).filter((file) => file.endsWith('.js'))
194
+ : [];
195
+ return (await Promise.all(files.map((file) => readFile(join(assets, file), 'utf8')))).join('\n');
196
+ }
@@ -0,0 +1,11 @@
1
+ import type { Git } from './git.js';
2
+ interface Answer {
3
+ status: number;
4
+ body: Record<string, unknown>;
5
+ }
6
+ /**
7
+ * Checks a branch out, refused while anything is uncommitted. When the
8
+ * lockfile changes, installs and restarts the dev server.
9
+ */
10
+ export declare function checkout(git: Git, root: string, branch: string, restart: () => void): Promise<Answer>;
11
+ export {};