@juxtapose/core 0.5.0 → 0.6.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 (45) hide show
  1. package/dist/browser/acquire.d.ts +5 -1
  2. package/dist/browser/acquire.js +14 -3
  3. package/dist/browser/acquire.js.map +1 -1
  4. package/dist/browser/browser.d.ts +1 -1
  5. package/dist/browser/daemon/client.d.ts +145 -0
  6. package/dist/browser/daemon/client.js +764 -0
  7. package/dist/browser/daemon/client.js.map +1 -0
  8. package/dist/browser/daemon/state.d.ts +99 -0
  9. package/dist/browser/daemon/state.js +297 -0
  10. package/dist/browser/daemon/state.js.map +1 -0
  11. package/dist/browser/daemon/supervisor-main.d.ts +1 -0
  12. package/dist/browser/daemon/supervisor-main.js +54 -0
  13. package/dist/browser/daemon/supervisor-main.js.map +1 -0
  14. package/dist/browser/daemon/supervisor.d.ts +37 -0
  15. package/dist/browser/daemon/supervisor.js +777 -0
  16. package/dist/browser/daemon/supervisor.js.map +1 -0
  17. package/dist/{capture/launch.js → browser/flags.js} +1 -1
  18. package/dist/browser/flags.js.map +1 -0
  19. package/dist/browser/index.d.ts +7 -2
  20. package/dist/browser/index.js +6 -2
  21. package/dist/browser/index.js.map +1 -1
  22. package/dist/browser/launch.js +4 -15
  23. package/dist/browser/launch.js.map +1 -1
  24. package/dist/browser/page.d.ts +1 -1
  25. package/dist/browser/private-dir.d.ts +7 -0
  26. package/dist/browser/private-dir.js +24 -0
  27. package/dist/browser/private-dir.js.map +1 -0
  28. package/dist/browser/resolve.d.ts +6 -0
  29. package/dist/browser/resolve.js +11 -0
  30. package/dist/browser/resolve.js.map +1 -1
  31. package/dist/browser/transport.d.ts +12 -0
  32. package/dist/browser/transport.js +26 -1
  33. package/dist/browser/transport.js.map +1 -1
  34. package/dist/browser/types.d.ts +4 -0
  35. package/dist/browser/types.js +2 -0
  36. package/dist/browser/types.js.map +1 -0
  37. package/dist/index.d.ts +5 -3
  38. package/dist/index.js +5 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/types.d.ts +2 -4
  41. package/dist/types.js +0 -2
  42. package/dist/types.js.map +1 -1
  43. package/package.json +2 -1
  44. package/dist/capture/launch.js.map +0 -1
  45. /package/dist/{capture/launch.d.ts → browser/flags.d.ts} +0 -0
@@ -1,11 +1,15 @@
1
1
  import { Browser } from './browser.js';
2
+ import { connectWarmBrowser } from './daemon/client.js';
2
3
  import type { SpawnFn } from './launch.js';
3
4
  export interface AcquireOptions {
4
5
  /** `--chrome <path>`; wins over JUX_CHROME and the caches. */
5
6
  chrome?: string;
6
7
  env?: NodeJS.ProcessEnv;
8
+ /** Fresh launches only; the daemon spawns its own Chrome. */
7
9
  spawn?: SpawnFn;
8
- /** PR 2: `--no-daemon`. Accepted now so callers need no change later. */
10
+ /** `--no-daemon`. */
9
11
  noDaemon?: boolean;
12
+ /** Test seam. */
13
+ connectWarm?: typeof connectWarmBrowser;
10
14
  }
11
15
  export declare function acquireBrowser(opts?: AcquireOptions): Promise<Browser>;
@@ -2,14 +2,25 @@
2
2
  //
3
3
  // The one place that decides where a Browser comes from. Every command and
4
4
  // the review server call this; PR 2 adds the warm daemon here and nowhere
5
- // else, so no caller changes when the policy does.
5
+ // else, so no caller changes when the policy does (Ruling 3).
6
6
  import { Browser } from './browser.js';
7
- import { noBrowserMessage, resolveChrome } from './resolve.js';
7
+ import { connectWarmBrowser, daemonEnabled, daemonIdleMinutes } from './daemon/client.js';
8
+ import { daemonSupported } from './daemon/state.js';
9
+ import { juxStateDir, noBrowserMessage, resolveChrome } from './resolve.js';
8
10
  export async function acquireBrowser(opts = {}) {
9
11
  const env = opts.env ?? process.env;
10
12
  const resolved = resolveChrome({ flag: opts.chrome, env });
11
13
  if (!resolved)
12
14
  throw new Error(noBrowserMessage());
13
- return Browser.launch({ executablePath: resolved.path, spawn: opts.spawn });
15
+ // Ruling 22: no daemon on Windows; CI and the opt-outs get PR 1's path exactly.
16
+ if (!daemonSupported() || !daemonEnabled(env, opts.noDaemon))
17
+ return Browser.launch({ executablePath: resolved.path, spawn: opts.spawn });
18
+ const { browser } = await (opts.connectWarm ?? connectWarmBrowser)({
19
+ executablePath: resolved.path,
20
+ stateDir: juxStateDir({ env }),
21
+ env,
22
+ idleMinutes: daemonIdleMinutes(env),
23
+ });
24
+ return browser;
14
25
  }
15
26
  //# sourceMappingURL=acquire.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"acquire.js","sourceRoot":"","sources":["../../src/browser/acquire.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,mDAAmD;AAEnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAW/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAuB,EAAE;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,QAAQ,GAAG,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACnD,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AAC9E,CAAC"}
1
+ {"version":3,"file":"acquire.js","sourceRoot":"","sources":["../../src/browser/acquire.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,8DAA8D;AAE9D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAc5E,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAuB,EAAE;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,QAAQ,GAAG,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC;QAC1D,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9E,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAC,CAAC;QACjE,cAAc,EAAE,QAAQ,CAAC,IAAI;QAC7B,QAAQ,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC;QAC9B,GAAG;QACH,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC;KACpC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { Viewport } from '../types.js';
1
+ import type { Viewport } from './types.js';
2
2
  import { CdpConnection } from './cdp.js';
3
3
  import type { SpawnFn } from './launch.js';
4
4
  import { type Page } from './page.js';
@@ -0,0 +1,145 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { Browser } from '../browser.js';
3
+ import { type DaemonPaths, type DaemonState } from './state.js';
4
+ export interface SpawnArgs {
5
+ executablePath: string;
6
+ args: string[];
7
+ stateDir: string;
8
+ key: string;
9
+ idleMinutes: number;
10
+ }
11
+ export interface WarmOptions {
12
+ executablePath: string;
13
+ args?: string[];
14
+ stateDir: string;
15
+ /** The caller's environment: the key is computed from it and the daemon runs with it. */
16
+ env?: NodeJS.ProcessEnv;
17
+ idleMinutes?: number;
18
+ /** Test seam: start a daemon for these args with this env. Default: a detached supervisor-main. */
19
+ spawnDaemon?: (args: SpawnArgs, env: NodeJS.ProcessEnv) => ChildProcess | void;
20
+ connectTimeoutMs?: number;
21
+ /** Test seam: see CommandReader. */
22
+ readCommand?: CommandReader;
23
+ }
24
+ /**
25
+ * The command line of a pid, '' when no such process exists, null when this
26
+ * machine gives no way to read it (no /proc and no ps).
27
+ */
28
+ export type CommandReader = (pid: number) => Promise<string | null>;
29
+ /**
30
+ * `CI` is the convention; these are the systems that do not set it (GitHub
31
+ * Actions does, but `CI` can be unset in a step's env; Jenkins sets BUILD_ID).
32
+ */
33
+ export declare const CI_ENV_VARS: readonly ["CI", "GITHUB_ACTIONS", "JENKINS_URL", "BUILD_ID", "TEAMCITY_VERSION", "BUILDKITE", "GITLAB_CI", "TF_BUILD", "CODEBUILD_BUILD_ID"];
34
+ /** Off with `--no-daemon`, JUX_NO_DAEMON (any truthy value), or any CI marker. */
35
+ export declare function daemonEnabled(env: NodeJS.ProcessEnv, flagNoDaemon?: boolean): boolean;
36
+ /**
37
+ * JUX_BROWSER_IDLE_MINUTES within [1, 35000], else the default (undefined).
38
+ * The upper bound is setTimeout's: 100000 minutes overflowed to a 1 ms idle
39
+ * timer and the daemon quit as soon as its last client left. Infinity used to
40
+ * pass too, became null in the spawn JSON, and every supervisor exited 1.
41
+ */
42
+ export declare function daemonIdleMinutes(env: NodeJS.ProcessEnv): number | undefined;
43
+ type StatusOptions = Pick<WarmOptions, 'executablePath' | 'stateDir' | 'args' | 'env' | 'readCommand'>;
44
+ /**
45
+ * A hash of `entry` and every file it reaches through relative imports. The
46
+ * supervisor that serves a client is whichever build spawned it; after a
47
+ * rebuild or an upgrade the supervisor's flags, allowlist or protocol can
48
+ * differ from this client's, and reusing it would render with the old code.
49
+ * A missing file hashes as missing (vitest runs the .ts sources, where the
50
+ * .js entry does not exist).
51
+ */
52
+ export declare function buildIdentity(entry: string): string;
53
+ /**
54
+ * `<@juxtapose/core version>+<build hash of the supervisor entry>`. Cached
55
+ * per process, but rechecked by stat on every call (about ten files): a
56
+ * Storybook addon or review server that stays up across a `pnpm build` kept
57
+ * the old key while spawning the new supervisor-main from disk, so it never
58
+ * shared a daemon with the CLI and ran a mixed build (PR #102 re-review N5).
59
+ */
60
+ export declare function juxIdentity(entry?: string): string;
61
+ /**
62
+ * The daemon key for these options. It covers everything a fresh launch
63
+ * would render with: the binary (path, size, mtime), the pin, the full Chrome
64
+ * command line jux would launch, the render env and this jux build. Keys that
65
+ * differ name different sockets, so a daemon from an older jux or other
66
+ * flags is never reused; it idles out.
67
+ */
68
+ export declare function daemonKeyFor(opts: Pick<WarmOptions, 'executablePath' | 'args' | 'env'>, jux?: string): string;
69
+ /**
70
+ * Start a detached supervisor-main with the caller's env: the key was computed
71
+ * from that env, so Chrome must render with it (TZ, LANG, fonts), not with
72
+ * whatever env this process happens to have. Detached in its own process
73
+ * group with stdio ignored: it outlives this jux, and a Ctrl-C to this
74
+ * terminal must not reach it.
75
+ *
76
+ * NODE_OPTIONS is not a render input and is dropped: VS Code's debug
77
+ * terminal (`--require …/bootloader.js`), `--inspect` or `--cpu-prof` would
78
+ * otherwise attach to a process that lives for hours, and `--cpu-prof` would
79
+ * write a profile into the project when it finally exits. The cwd is the
80
+ * state dir for the same reason: not the project that happened to start it.
81
+ */
82
+ export declare function spawnSupervisor(args: SpawnArgs, env: NodeJS.ProcessEnv, entry?: string): ChildProcess;
83
+ export declare const defaultReadCommand: CommandReader;
84
+ /**
85
+ * Remove the files of the daemon `s` describes, and nothing newer. The state
86
+ * goes only if it still names the same pid and start time: between our read
87
+ * and this call another client may have spawned a replacement that wrote its
88
+ * own (the review traced `jux browser stop` deleting a new daemon's files
89
+ * while the old one's Chrome took 5 s to close, orphaning the new one). The
90
+ * socket goes only if it is the path that state named and nothing listens on
91
+ * it, compared by inode so a socket bound meanwhile is left alone.
92
+ */
93
+ export declare function clearStaleState(paths: DaemonPaths, s: DaemonState): Promise<void>;
94
+ /**
95
+ * The warm daemon's browser, or a private one if the daemon cannot be had.
96
+ * Never rejects because of the daemon; a private launch failure rejects as in
97
+ * PR 1.
98
+ */
99
+ export declare function connectWarmBrowser(opts: WarmOptions): Promise<{
100
+ browser: Browser;
101
+ warm: boolean;
102
+ }>;
103
+ /**
104
+ * Start (or find) the daemon without a private fallback: `jux browser start`
105
+ * must not launch and close a whole Chrome just to report that the daemon
106
+ * could not start.
107
+ */
108
+ export declare function startDaemon(opts: WarmOptions): Promise<{
109
+ state: DaemonState;
110
+ } | {
111
+ error: string;
112
+ }>;
113
+ /** The running daemon for this fingerprint, or null. A dead daemon's files are cleared. */
114
+ export declare function daemonStatus(given: StatusOptions): Promise<DaemonState | null>;
115
+ type ListOptions = Pick<WarmOptions, 'stateDir' | 'readCommand'>;
116
+ /** Every live daemon in `stateDir`, whichever key; dead ones' files are cleared on the way. */
117
+ export declare function listDaemons(opts: ListOptions): Promise<DaemonState[]>;
118
+ /**
119
+ * What stopping one daemon did: `none` (nothing running, or the state named a
120
+ * pid that is not ours and nothing listened on its socket), `stopped` (its
121
+ * pid is gone), `unverified` (a live pid we could not confirm as ours:
122
+ * nothing is signalled or removed), or `running` (ours, signalled, and still
123
+ * alive after 2 s).
124
+ */
125
+ export type StopResult = {
126
+ outcome: 'none' | 'stopped';
127
+ } | {
128
+ outcome: 'unverified' | 'running';
129
+ pid: number;
130
+ reason: string;
131
+ };
132
+ /** Stop the daemon for this fingerprint only. */
133
+ export declare function stopDaemon(given: StatusOptions): Promise<StopResult>;
134
+ /** One daemon `stopAllDaemons` found, and what stopping it did. */
135
+ export type DaemonStop = {
136
+ key: string;
137
+ pid: number;
138
+ } & StopResult;
139
+ /**
140
+ * Stop every daemon in `stateDir`, whichever key it runs under, each through
141
+ * the same verification as `stopDaemon`. Entries whose outcome is `none`
142
+ * (stale files, reused pids) are cleared and left out.
143
+ */
144
+ export declare function stopAllDaemons(opts: ListOptions): Promise<DaemonStop[]>;
145
+ export {};