@pix3/vite-plugin 0.0.0-stage → 2.0.0-alpha.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.
- package/LICENSE +201 -0
- package/README.md +38 -3
- package/dist/build/bundle-hooks.d.ts +23 -0
- package/dist/build/bundle-hooks.js +79 -0
- package/dist/build/classic-script.d.ts +11 -0
- package/dist/build/classic-script.js +46 -0
- package/dist/build/compress.d.ts +29 -0
- package/dist/build/compress.js +63 -0
- package/dist/build/dependency-imports.d.ts +53 -0
- package/dist/build/dependency-imports.js +239 -0
- package/dist/build/dev-manifest.d.ts +10 -0
- package/dist/build/dev-manifest.js +25 -0
- package/dist/build/editor-flush.d.ts +42 -0
- package/dist/build/editor-flush.js +91 -0
- package/dist/build/finish.d.ts +11 -0
- package/dist/build/finish.js +126 -0
- package/dist/build/output-plugins.d.ts +26 -0
- package/dist/build/output-plugins.js +88 -0
- package/dist/build/plan.d.ts +75 -0
- package/dist/build/plan.js +162 -0
- package/dist/build/player-modules.d.ts +99 -0
- package/dist/build/player-modules.js +204 -0
- package/dist/build/project-manifest.d.ts +57 -0
- package/dist/build/project-manifest.js +169 -0
- package/dist/build/record.d.ts +25 -0
- package/dist/build/record.js +14 -0
- package/dist/build/report.d.ts +114 -0
- package/dist/build/report.js +102 -0
- package/dist/build/scan.d.ts +84 -0
- package/dist/build/scan.js +375 -0
- package/dist/build/strip-decision.d.ts +27 -0
- package/dist/build/strip-decision.js +82 -0
- package/dist/build/strippable-runtime-modules.d.ts +85 -0
- package/dist/build/strippable-runtime-modules.js +309 -0
- package/dist/build/zip.d.ts +6 -0
- package/dist/build/zip.js +28 -0
- package/dist/client/host-files.d.ts +96 -0
- package/dist/client/host-files.js +169 -0
- package/dist/client/index.d.ts +218 -0
- package/dist/client/index.js +381 -0
- package/dist/dev-info.d.ts +55 -0
- package/dist/dev-info.js +116 -0
- package/dist/dev-server.d.ts +28 -0
- package/dist/dev-server.js +139 -0
- package/dist/editor-page.d.ts +31 -0
- package/dist/editor-page.js +74 -0
- package/dist/files/changeset.d.ts +84 -0
- package/dist/files/changeset.js +321 -0
- package/dist/files/content-type.d.ts +1 -0
- package/dist/files/content-type.js +56 -0
- package/dist/files/history.d.ts +67 -0
- package/dist/files/history.js +162 -0
- package/dist/files/paths.d.ts +41 -0
- package/dist/files/paths.js +144 -0
- package/dist/files/project-files.d.ts +208 -0
- package/dist/files/project-files.js +1062 -0
- package/dist/files/scan.d.ts +71 -0
- package/dist/files/scan.js +252 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +278 -0
- package/dist/module-paths.d.ts +13 -0
- package/dist/module-paths.js +32 -0
- package/dist/player/index.d.ts +31 -0
- package/dist/player/index.js +102 -0
- package/dist/server/editor-socket.d.ts +53 -0
- package/dist/server/editor-socket.js +134 -0
- package/dist/server/guard.d.ts +61 -0
- package/dist/server/guard.js +165 -0
- package/dist/server/http.d.ts +24 -0
- package/dist/server/http.js +70 -0
- package/dist/server/image-keys.d.ts +46 -0
- package/dist/server/image-keys.js +118 -0
- package/dist/server/image-proxy.d.ts +32 -0
- package/dist/server/image-proxy.js +124 -0
- package/dist/server/router.d.ts +54 -0
- package/dist/server/router.js +147 -0
- package/dist/sync/barrier.d.ts +69 -0
- package/dist/sync/barrier.js +240 -0
- package/dist/sync/hot-update.d.ts +16 -0
- package/dist/sync/hot-update.js +58 -0
- package/dist/sync/script-graph.d.ts +79 -0
- package/dist/sync/script-graph.js +214 -0
- package/dist/vite-config.d.ts +12 -0
- package/dist/vite-config.js +62 -0
- package/package.json +60 -6
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { HostFilesClient, type HostWriteResult } from './host-files.ts';
|
|
2
|
+
export { HostFilesClient } from './host-files.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Page side of the plugin (plan §B.2 variant B, §B.3 step 4): runs in the editor tab, imported by
|
|
5
|
+
* `virtual:pix3/editor-host`. It owns the `/__pix3/ws` connection, answers the plugin's `sync`
|
|
6
|
+
* and `flush` requests, and is the `EditorHost` the editor mounts against.
|
|
7
|
+
*
|
|
8
|
+
* Contract B (S1, finding 3): nothing in the editor's chain may make Vite inject `/@vite/client`
|
|
9
|
+
* — no `import.meta.hot`, no CSS imports, no non-literal `import()`. The one dynamic import this
|
|
10
|
+
* file needs (re-importing the script roots with a fresh `?t=`) goes through `Function`, which
|
|
11
|
+
* Vite's import analysis does not see.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScriptRoots {
|
|
14
|
+
readonly editorScripts: RootModule;
|
|
15
|
+
readonly botPolicies: RootModule;
|
|
16
|
+
}
|
|
17
|
+
export interface RootModule {
|
|
18
|
+
readonly __pix3Revision: number;
|
|
19
|
+
readonly modules: Record<string, Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
export interface FsFrame {
|
|
22
|
+
readonly type: 'pix3:fs';
|
|
23
|
+
readonly seq: number;
|
|
24
|
+
readonly revision: string;
|
|
25
|
+
readonly events: readonly {
|
|
26
|
+
readonly op: 'create' | 'modify' | 'delete' | 'rename';
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly kind: 'file' | 'dir';
|
|
29
|
+
readonly sha256?: string;
|
|
30
|
+
readonly from?: string;
|
|
31
|
+
readonly author: 'editor' | 'external';
|
|
32
|
+
}[];
|
|
33
|
+
readonly writerId?: string;
|
|
34
|
+
}
|
|
35
|
+
export type HookReply = {
|
|
36
|
+
readonly ok: boolean;
|
|
37
|
+
readonly reason?: string;
|
|
38
|
+
} & Record<string, unknown>;
|
|
39
|
+
export interface SyncInfo {
|
|
40
|
+
readonly rev: number;
|
|
41
|
+
/** `{path: sha256 | null}` — what the rescan found changed (null = deleted). */
|
|
42
|
+
readonly changed: Record<string, string | null>;
|
|
43
|
+
readonly roots: ScriptRoots;
|
|
44
|
+
/** Project modules only the bot policies import (the game never runs them). */
|
|
45
|
+
readonly policyOnly: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** What the editor plugs in. Every hook is optional: an absent one is a no-op that succeeds. */
|
|
48
|
+
export interface HostSyncHandlers {
|
|
49
|
+
/** Write dirty scenes now (plan §C.1); waits for pointerup up to `timeoutMs`. */
|
|
50
|
+
flush?(timeoutMs: number): Promise<HookReply>;
|
|
51
|
+
/**
|
|
52
|
+
* The roots were re-imported for a sync: register the new script classes, reload changed
|
|
53
|
+
* scenes. While play runs, answer `{ok:false, reason:'stale', playing, pending}` (plan §B.3).
|
|
54
|
+
*/
|
|
55
|
+
applySync?(info: SyncInfo): Promise<HookReply>;
|
|
56
|
+
}
|
|
57
|
+
export interface HostOptions {
|
|
58
|
+
/** Vite `base` (always ends with `/`). */
|
|
59
|
+
readonly base: string;
|
|
60
|
+
/** The roots as the page first imported them (static imports of `editor-host`). */
|
|
61
|
+
readonly roots: ScriptRoots;
|
|
62
|
+
}
|
|
63
|
+
export interface HostVersions {
|
|
64
|
+
readonly plugin: string;
|
|
65
|
+
readonly runtime: string | null;
|
|
66
|
+
readonly editorCore: string | null;
|
|
67
|
+
readonly vite: string;
|
|
68
|
+
}
|
|
69
|
+
/** The plugin's `welcome` frame (re-sent after every reconnect). */
|
|
70
|
+
export interface Hello {
|
|
71
|
+
readonly tabId: string;
|
|
72
|
+
readonly seq: number;
|
|
73
|
+
readonly revision: string;
|
|
74
|
+
readonly writerId: string | null;
|
|
75
|
+
readonly root: string;
|
|
76
|
+
readonly resRoot: string;
|
|
77
|
+
readonly projectName: string;
|
|
78
|
+
readonly versions: HostVersions;
|
|
79
|
+
}
|
|
80
|
+
export interface HostInfo {
|
|
81
|
+
readonly base: string;
|
|
82
|
+
readonly root: string;
|
|
83
|
+
readonly resRoot: string;
|
|
84
|
+
readonly projectName: string;
|
|
85
|
+
readonly tabId: string;
|
|
86
|
+
readonly seq: number;
|
|
87
|
+
readonly revision: string;
|
|
88
|
+
readonly versions: HostVersions;
|
|
89
|
+
}
|
|
90
|
+
export type HostHistoryAuthor = 'editor' | 'external' | 'restore' | 'rejected-draft';
|
|
91
|
+
export interface HostHistoryEntry {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly path: string;
|
|
94
|
+
readonly author: HostHistoryAuthor;
|
|
95
|
+
readonly at: number;
|
|
96
|
+
readonly sha256: string;
|
|
97
|
+
readonly size: number;
|
|
98
|
+
readonly note?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface HostHistory {
|
|
101
|
+
list(path: string): Promise<HostHistoryEntry[]>;
|
|
102
|
+
read(path: string, id: string): Promise<string | null>;
|
|
103
|
+
record(path: string, text: string, author: 'rejected-draft', note?: string): Promise<HostHistoryEntry>;
|
|
104
|
+
restore(path: string, id: string, options?: {
|
|
105
|
+
ifMatch?: string;
|
|
106
|
+
}): Promise<HostWriteResult>;
|
|
107
|
+
}
|
|
108
|
+
export type HostImageProvider = 'gemini' | 'openai';
|
|
109
|
+
export interface HostImageKeyStatus {
|
|
110
|
+
readonly set: boolean;
|
|
111
|
+
/** The key's last four characters (keys of 12+ characters only). */
|
|
112
|
+
readonly last4?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Image-generation keys and the proxy that uses them (plan §B.1); the page never sees a key. */
|
|
115
|
+
export interface HostImageGen {
|
|
116
|
+
keys(): Promise<Record<HostImageProvider, HostImageKeyStatus>>;
|
|
117
|
+
/** Store (`null` / `''` removes) a key on the dev server's machine; answers its status only. */
|
|
118
|
+
setKey(provider: HostImageProvider, key: string | null): Promise<HostImageKeyStatus & {
|
|
119
|
+
readonly where: 'home' | 'project';
|
|
120
|
+
}>;
|
|
121
|
+
/** `fetch` of `/__pix3/api/proxy/<provider>/<path>`; the plugin adds the key. */
|
|
122
|
+
fetch(provider: HostImageProvider, path: string, init?: RequestInit): Promise<Response>;
|
|
123
|
+
}
|
|
124
|
+
export interface HostClaim {
|
|
125
|
+
readonly writerId: string;
|
|
126
|
+
readonly seq: number;
|
|
127
|
+
readonly revision: string;
|
|
128
|
+
readonly hashes: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
type Listener<T> = (value: T) => void;
|
|
131
|
+
/**
|
|
132
|
+
* True when Vite's HMR client got onto this page — contract B is broken and a `full-reload` can
|
|
133
|
+
* reach the editor. The page's inline probe (`editor-page.ts`) keeps `__PIX3_VITE_CLIENT__`
|
|
134
|
+
* current for the tab's whole life; the timeline is read too, because the observer reports
|
|
135
|
+
* asynchronously. Checked at load and on every sync (the answer carries `viteClient: true`), and
|
|
136
|
+
* `pix3_status` shows it.
|
|
137
|
+
*/
|
|
138
|
+
export declare const viteClientLoaded: () => boolean;
|
|
139
|
+
/**
|
|
140
|
+
* The page's `EditorHost` (`@pix3/editor-core` defines the contract, `.plans/editor-core-port.md`
|
|
141
|
+
* §1): what `mountEditor(el, host)` talks to. Structural — the type lives in editor-core and this
|
|
142
|
+
* package only checks conformance in a spec, so no runtime edge exists between the two.
|
|
143
|
+
*/
|
|
144
|
+
export declare class EditorHostConnection {
|
|
145
|
+
#private;
|
|
146
|
+
readonly tabId: string;
|
|
147
|
+
readonly base: string;
|
|
148
|
+
readonly files: HostFilesClient;
|
|
149
|
+
readonly events: {
|
|
150
|
+
onFs(listener: Listener<FsFrame>): () => void;
|
|
151
|
+
onConnection(listener: Listener<'open' | 'closed'>): () => void;
|
|
152
|
+
};
|
|
153
|
+
readonly scripts: {
|
|
154
|
+
current(): ScriptRoots;
|
|
155
|
+
onChange(listener: Listener<ScriptRoots>): () => void;
|
|
156
|
+
};
|
|
157
|
+
readonly sync: {
|
|
158
|
+
setHandlers(handlers: HostSyncHandlers): void;
|
|
159
|
+
run(options?: {
|
|
160
|
+
expect?: Record<string, string>;
|
|
161
|
+
timeoutMs?: number;
|
|
162
|
+
}): Promise<HookReply>;
|
|
163
|
+
};
|
|
164
|
+
readonly writer: {
|
|
165
|
+
readonly id: string | null;
|
|
166
|
+
readonly isSelf: boolean;
|
|
167
|
+
claim(): Promise<HostClaim>;
|
|
168
|
+
onChange(listener: Listener<string | null>): () => void;
|
|
169
|
+
};
|
|
170
|
+
/** The version journal `.pix3/history/` (plan §C.4). */
|
|
171
|
+
readonly history: HostHistory;
|
|
172
|
+
readonly imageGen: HostImageGen;
|
|
173
|
+
private roots;
|
|
174
|
+
private handlers;
|
|
175
|
+
private socket;
|
|
176
|
+
private hello;
|
|
177
|
+
/** Tab the plugin says may write (from `welcome` and `pix3:writer`). */
|
|
178
|
+
private writerTab;
|
|
179
|
+
private readonly helloWaiters;
|
|
180
|
+
private readonly fsListeners;
|
|
181
|
+
private readonly connectionListeners;
|
|
182
|
+
private readonly scriptListeners;
|
|
183
|
+
private readonly writerListeners;
|
|
184
|
+
private retryMs;
|
|
185
|
+
/** The contract-B error is logged once per tab. */
|
|
186
|
+
private viteClientReported;
|
|
187
|
+
private closed;
|
|
188
|
+
constructor(options: HostOptions);
|
|
189
|
+
/** `/__pix3/api/<route>` for the editor-only routes: the session token on every method. */
|
|
190
|
+
private sessionCall;
|
|
191
|
+
private imageGenApi;
|
|
192
|
+
/** {@link api} with failures as {@link HostFileError} (`network` when nothing answered). */
|
|
193
|
+
private call;
|
|
194
|
+
private postJson;
|
|
195
|
+
/** Filled from the plugin's `welcome`; available once {@link ready} resolved. */
|
|
196
|
+
get info(): HostInfo;
|
|
197
|
+
get currentRoots(): ScriptRoots;
|
|
198
|
+
/** Resolves with the plugin's `welcome` (re-sent after every reconnect). */
|
|
199
|
+
ready(): Promise<Hello>;
|
|
200
|
+
connect(): void;
|
|
201
|
+
close(): void;
|
|
202
|
+
/** `fetch` against `/__pix3/api/<route>` with the headers mutations need. */
|
|
203
|
+
api(route: string, init?: RequestInit): Promise<Response>;
|
|
204
|
+
/** Become the writer (plan §C.3 step 2): the plugin answers with the disk to start from. */
|
|
205
|
+
private claim;
|
|
206
|
+
/**
|
|
207
|
+
* `pix3_sync` from this tab: flush first (this tab's own handler), then the plugin's barrier
|
|
208
|
+
* with this tab as the one that confirms.
|
|
209
|
+
*/
|
|
210
|
+
private runSync;
|
|
211
|
+
private emit;
|
|
212
|
+
private setWriter;
|
|
213
|
+
private reimportRoots;
|
|
214
|
+
private executed;
|
|
215
|
+
private reply;
|
|
216
|
+
private onMessage;
|
|
217
|
+
private onRequest;
|
|
218
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { failureOf, HostFileError, HostFilesClient, networkError, } from "./host-files.js";
|
|
2
|
+
export { HostFilesClient } from "./host-files.js";
|
|
3
|
+
/**
|
|
4
|
+
* The editor session token the page was served with (`<meta name="pix3-session">`), read once
|
|
5
|
+
* and removed from the DOM. Only a top-level navigation of the editor page carries one.
|
|
6
|
+
*/
|
|
7
|
+
const takeSessionToken = () => {
|
|
8
|
+
if (typeof document === 'undefined')
|
|
9
|
+
return null;
|
|
10
|
+
const meta = document.querySelector('meta[name="pix3-session"]');
|
|
11
|
+
const token = meta?.getAttribute('content') ?? null;
|
|
12
|
+
meta?.remove();
|
|
13
|
+
return token;
|
|
14
|
+
};
|
|
15
|
+
const subscribe = (set, listener) => {
|
|
16
|
+
set.add(listener);
|
|
17
|
+
return () => set.delete(listener);
|
|
18
|
+
};
|
|
19
|
+
const dynamicImport = new Function('url', 'return import(url)');
|
|
20
|
+
const rootUrl = (base, id) =>
|
|
21
|
+
// Vite strips only `t=\d{13}` from a module URL; anything else would not resolve (S1, finding 4).
|
|
22
|
+
`${base}@id/__x00__${id}?t=${String(Date.now()).padStart(13, '0')}`;
|
|
23
|
+
const newTabId = () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
24
|
+
? crypto.randomUUID()
|
|
25
|
+
: `tab-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
26
|
+
/**
|
|
27
|
+
* True when Vite's HMR client got onto this page — contract B is broken and a `full-reload` can
|
|
28
|
+
* reach the editor. The page's inline probe (`editor-page.ts`) keeps `__PIX3_VITE_CLIENT__`
|
|
29
|
+
* current for the tab's whole life; the timeline is read too, because the observer reports
|
|
30
|
+
* asynchronously. Checked at load and on every sync (the answer carries `viteClient: true`), and
|
|
31
|
+
* `pix3_status` shows it.
|
|
32
|
+
*/
|
|
33
|
+
export const viteClientLoaded = () => globalThis.__PIX3_VITE_CLIENT__ === true ||
|
|
34
|
+
(typeof performance !== 'undefined' &&
|
|
35
|
+
performance
|
|
36
|
+
.getEntriesByType('resource')
|
|
37
|
+
.some(entry => new URL(entry.name, location.href).pathname.endsWith('/@vite/client')));
|
|
38
|
+
const VITE_CLIENT_MESSAGE = '[pix3] /@vite/client is loaded on the editor page: a non-literal import(), import.meta.hot or a CSS import reached the editor chain (plan §B.2, contract B; `pix3 check` names the file).';
|
|
39
|
+
/**
|
|
40
|
+
* The page's `EditorHost` (`@pix3/editor-core` defines the contract, `.plans/editor-core-port.md`
|
|
41
|
+
* §1): what `mountEditor(el, host)` talks to. Structural — the type lives in editor-core and this
|
|
42
|
+
* package only checks conformance in a spec, so no runtime edge exists between the two.
|
|
43
|
+
*/
|
|
44
|
+
export class EditorHostConnection {
|
|
45
|
+
tabId = newTabId();
|
|
46
|
+
base;
|
|
47
|
+
files;
|
|
48
|
+
events;
|
|
49
|
+
scripts;
|
|
50
|
+
sync;
|
|
51
|
+
writer;
|
|
52
|
+
/** The version journal `.pix3/history/` (plan §C.4). */
|
|
53
|
+
history;
|
|
54
|
+
imageGen;
|
|
55
|
+
#session = takeSessionToken();
|
|
56
|
+
roots;
|
|
57
|
+
handlers = {};
|
|
58
|
+
socket = null;
|
|
59
|
+
hello = null;
|
|
60
|
+
/** Tab the plugin says may write (from `welcome` and `pix3:writer`). */
|
|
61
|
+
writerTab = null;
|
|
62
|
+
helloWaiters = [];
|
|
63
|
+
fsListeners = new Set();
|
|
64
|
+
connectionListeners = new Set();
|
|
65
|
+
scriptListeners = new Set();
|
|
66
|
+
writerListeners = new Set();
|
|
67
|
+
retryMs = 250;
|
|
68
|
+
/** The contract-B error is logged once per tab. */
|
|
69
|
+
viteClientReported = false;
|
|
70
|
+
closed = false;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.base = options.base;
|
|
73
|
+
this.roots = options.roots;
|
|
74
|
+
this.files = new HostFilesClient(this);
|
|
75
|
+
this.events = {
|
|
76
|
+
onFs: listener => subscribe(this.fsListeners, listener),
|
|
77
|
+
onConnection: listener => subscribe(this.connectionListeners, listener),
|
|
78
|
+
};
|
|
79
|
+
this.scripts = {
|
|
80
|
+
current: () => this.roots,
|
|
81
|
+
onChange: listener => subscribe(this.scriptListeners, listener),
|
|
82
|
+
};
|
|
83
|
+
this.sync = {
|
|
84
|
+
setHandlers: handlers => {
|
|
85
|
+
this.handlers = handlers;
|
|
86
|
+
},
|
|
87
|
+
run: options => this.runSync(options),
|
|
88
|
+
};
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
90
|
+
const connection = this;
|
|
91
|
+
this.writer = {
|
|
92
|
+
get id() {
|
|
93
|
+
return connection.writerTab;
|
|
94
|
+
},
|
|
95
|
+
get isSelf() {
|
|
96
|
+
return connection.writerTab === connection.tabId;
|
|
97
|
+
},
|
|
98
|
+
claim: () => this.claim(),
|
|
99
|
+
onChange: listener => subscribe(this.writerListeners, listener),
|
|
100
|
+
};
|
|
101
|
+
this.imageGen = this.imageGenApi();
|
|
102
|
+
this.history = {
|
|
103
|
+
list: async (path) => {
|
|
104
|
+
const response = await this.call(`history?path=${encodeURIComponent(path)}`);
|
|
105
|
+
if (!response.ok)
|
|
106
|
+
throw await failureOf(response);
|
|
107
|
+
return (await response.json()).entries;
|
|
108
|
+
},
|
|
109
|
+
read: async (path, id) => {
|
|
110
|
+
const response = await this.call(`history/version?path=${encodeURIComponent(path)}&id=${encodeURIComponent(id)}`);
|
|
111
|
+
if (response.status === 404)
|
|
112
|
+
return null;
|
|
113
|
+
if (!response.ok)
|
|
114
|
+
throw await failureOf(response);
|
|
115
|
+
return response.text();
|
|
116
|
+
},
|
|
117
|
+
record: (path, text, author, note) => this.postJson('history/record', {
|
|
118
|
+
path,
|
|
119
|
+
text,
|
|
120
|
+
author,
|
|
121
|
+
...(note ? { note } : {}),
|
|
122
|
+
}),
|
|
123
|
+
restore: (path, id, options = {}) => this.postJson('history/restore', { path, id }, options.ifMatch
|
|
124
|
+
? { 'If-Match': options.ifMatch === '*' ? '*' : `"${options.ifMatch}"` }
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** `/__pix3/api/<route>` for the editor-only routes: the session token on every method. */
|
|
129
|
+
sessionCall(route, init = {}) {
|
|
130
|
+
const headers = new Headers(init.headers);
|
|
131
|
+
headers.set('X-Pix3', '1');
|
|
132
|
+
if (this.#session)
|
|
133
|
+
headers.set('X-Pix3-Session', this.#session);
|
|
134
|
+
return fetch(`${this.base}__pix3/api/${route}`, { ...init, headers });
|
|
135
|
+
}
|
|
136
|
+
imageGenApi() {
|
|
137
|
+
const json = async (response) => {
|
|
138
|
+
if (!response.ok)
|
|
139
|
+
throw await failureOf(response);
|
|
140
|
+
return (await response.json());
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
keys: async () => (await json(await this.sessionCall('keys'))).keys,
|
|
144
|
+
setKey: async (provider, key) => json(await this.sessionCall('keys', {
|
|
145
|
+
method: 'PUT',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ provider, key }),
|
|
148
|
+
})),
|
|
149
|
+
fetch: (provider, path, init = {}) => this.sessionCall(`proxy/${provider}/${path.replace(/^\/+/, '')}`, {
|
|
150
|
+
...init,
|
|
151
|
+
method: init.method ?? 'POST',
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** {@link api} with failures as {@link HostFileError} (`network` when nothing answered). */
|
|
156
|
+
async call(route, init) {
|
|
157
|
+
try {
|
|
158
|
+
return await this.api(route, init);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw networkError(error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async postJson(route, body, headers = {}) {
|
|
165
|
+
const response = await this.call(route, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok)
|
|
171
|
+
throw await failureOf(response);
|
|
172
|
+
return (await response.json());
|
|
173
|
+
}
|
|
174
|
+
/** Filled from the plugin's `welcome`; available once {@link ready} resolved. */
|
|
175
|
+
get info() {
|
|
176
|
+
const hello = this.hello;
|
|
177
|
+
if (!hello)
|
|
178
|
+
throw new Error('The editor host is not connected yet (await host.ready()).');
|
|
179
|
+
return {
|
|
180
|
+
base: this.base,
|
|
181
|
+
root: hello.root,
|
|
182
|
+
resRoot: hello.resRoot,
|
|
183
|
+
projectName: hello.projectName,
|
|
184
|
+
tabId: this.tabId,
|
|
185
|
+
seq: hello.seq,
|
|
186
|
+
revision: hello.revision,
|
|
187
|
+
versions: hello.versions,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
get currentRoots() {
|
|
191
|
+
return this.roots;
|
|
192
|
+
}
|
|
193
|
+
/** Resolves with the plugin's `welcome` (re-sent after every reconnect). */
|
|
194
|
+
ready() {
|
|
195
|
+
if (this.hello)
|
|
196
|
+
return Promise.resolve(this.hello);
|
|
197
|
+
return new Promise(resolve => this.helloWaiters.push(resolve));
|
|
198
|
+
}
|
|
199
|
+
connect() {
|
|
200
|
+
if (this.closed)
|
|
201
|
+
return;
|
|
202
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
203
|
+
const socket = new WebSocket(`${protocol}//${location.host}${this.base}__pix3/ws`);
|
|
204
|
+
this.socket = socket;
|
|
205
|
+
socket.addEventListener('open', () => {
|
|
206
|
+
this.retryMs = 250;
|
|
207
|
+
socket.send(JSON.stringify({ type: 'hello', tabId: this.tabId }));
|
|
208
|
+
});
|
|
209
|
+
socket.addEventListener('message', event => {
|
|
210
|
+
void this.onMessage(String(event.data));
|
|
211
|
+
});
|
|
212
|
+
socket.addEventListener('close', () => {
|
|
213
|
+
if (this.socket !== socket)
|
|
214
|
+
return;
|
|
215
|
+
this.socket = null;
|
|
216
|
+
this.emit(this.connectionListeners, 'closed');
|
|
217
|
+
if (this.closed)
|
|
218
|
+
return;
|
|
219
|
+
// The dev server died or restarted: keep editing in memory and try again (plan §C.3).
|
|
220
|
+
setTimeout(() => this.connect(), this.retryMs);
|
|
221
|
+
this.retryMs = Math.min(this.retryMs * 2, 5_000);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
close() {
|
|
225
|
+
this.closed = true;
|
|
226
|
+
this.socket?.close();
|
|
227
|
+
}
|
|
228
|
+
/** `fetch` against `/__pix3/api/<route>` with the headers mutations need. */
|
|
229
|
+
async api(route, init = {}) {
|
|
230
|
+
const headers = new Headers(init.headers);
|
|
231
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
232
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
233
|
+
headers.set('X-Pix3', '1');
|
|
234
|
+
headers.set('X-Pix3-Writer', this.tabId);
|
|
235
|
+
}
|
|
236
|
+
return fetch(`${this.base}__pix3/api/${route}`, { ...init, headers });
|
|
237
|
+
}
|
|
238
|
+
/** Become the writer (plan §C.3 step 2): the plugin answers with the disk to start from. */
|
|
239
|
+
async claim() {
|
|
240
|
+
const response = await this.api('handover/claim', {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: { 'Content-Type': 'application/json' },
|
|
243
|
+
body: JSON.stringify({ writerId: this.tabId }),
|
|
244
|
+
});
|
|
245
|
+
const body = (await response.json());
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new HostFileError({
|
|
248
|
+
code: 'other',
|
|
249
|
+
status: response.status,
|
|
250
|
+
message: typeof body.message === 'string' ? body.message : 'Claim refused.',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
this.setWriter(this.tabId);
|
|
254
|
+
return body;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* `pix3_sync` from this tab: flush first (this tab's own handler), then the plugin's barrier
|
|
258
|
+
* with this tab as the one that confirms.
|
|
259
|
+
*/
|
|
260
|
+
async runSync(options = {}) {
|
|
261
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
262
|
+
const flushed = this.handlers.flush ? await this.handlers.flush(timeoutMs) : { ok: true };
|
|
263
|
+
if (!flushed.ok)
|
|
264
|
+
return { ...flushed, ok: false, step: 'flush' };
|
|
265
|
+
const response = await this.api('sync', {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ expect: options.expect, timeoutMs, tabId: this.tabId }),
|
|
269
|
+
});
|
|
270
|
+
return (await response.json());
|
|
271
|
+
}
|
|
272
|
+
emit(listeners, value) {
|
|
273
|
+
for (const listener of [...listeners]) {
|
|
274
|
+
try {
|
|
275
|
+
listener(value);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
console.error('[pix3] host listener failed', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
setWriter(writerId) {
|
|
283
|
+
if (writerId === this.writerTab)
|
|
284
|
+
return;
|
|
285
|
+
this.writerTab = writerId;
|
|
286
|
+
this.emit(this.writerListeners, writerId);
|
|
287
|
+
}
|
|
288
|
+
async reimportRoots() {
|
|
289
|
+
const [editorScripts, botPolicies] = await Promise.all([
|
|
290
|
+
dynamicImport(rootUrl(this.base, 'virtual:pix3/editor-scripts')),
|
|
291
|
+
dynamicImport(rootUrl(this.base, 'virtual:pix3/bot-policies')),
|
|
292
|
+
]);
|
|
293
|
+
this.roots = {
|
|
294
|
+
editorScripts: editorScripts,
|
|
295
|
+
botPolicies: botPolicies,
|
|
296
|
+
};
|
|
297
|
+
return this.roots;
|
|
298
|
+
}
|
|
299
|
+
executed() {
|
|
300
|
+
const stamps = globalThis.__pix3Executed;
|
|
301
|
+
return { ...(stamps ?? {}) };
|
|
302
|
+
}
|
|
303
|
+
reply(id, body) {
|
|
304
|
+
this.socket?.send(JSON.stringify({ type: 'reply', id, ...body }));
|
|
305
|
+
}
|
|
306
|
+
async onMessage(text) {
|
|
307
|
+
let message;
|
|
308
|
+
try {
|
|
309
|
+
message = JSON.parse(text);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
switch (message.type) {
|
|
315
|
+
case 'welcome': {
|
|
316
|
+
const hello = { ...message, tabId: this.tabId };
|
|
317
|
+
this.hello = hello;
|
|
318
|
+
this.setWriter(hello.writerId);
|
|
319
|
+
this.emit(this.connectionListeners, 'open');
|
|
320
|
+
for (const waiter of this.helloWaiters.splice(0))
|
|
321
|
+
waiter(hello);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case 'pix3:fs':
|
|
325
|
+
this.emit(this.fsListeners, message);
|
|
326
|
+
return;
|
|
327
|
+
case 'pix3:writer':
|
|
328
|
+
this.setWriter(typeof message.writerId === 'string' ? message.writerId : null);
|
|
329
|
+
return;
|
|
330
|
+
case 'pix3:scripts':
|
|
331
|
+
this.emit(this.scriptListeners, await this.reimportRoots());
|
|
332
|
+
return;
|
|
333
|
+
case 'request':
|
|
334
|
+
await this.onRequest(message);
|
|
335
|
+
return;
|
|
336
|
+
default:
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async onRequest(message) {
|
|
341
|
+
const id = String(message.id);
|
|
342
|
+
try {
|
|
343
|
+
if (message.kind === 'flush') {
|
|
344
|
+
const timeoutMs = typeof message.timeoutMs === 'number' ? message.timeoutMs : 10_000;
|
|
345
|
+
this.reply(id, this.handlers.flush ? await this.handlers.flush(timeoutMs) : { ok: true });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (message.kind === 'sync') {
|
|
349
|
+
const roots = await this.reimportRoots();
|
|
350
|
+
const changed = (message.changed ?? {});
|
|
351
|
+
const rev = roots.editorScripts.__pix3Revision;
|
|
352
|
+
const policyOnly = Array.isArray(message.policyOnly)
|
|
353
|
+
? message.policyOnly.filter((path) => typeof path === 'string')
|
|
354
|
+
: [];
|
|
355
|
+
const applied = this.handlers.applySync
|
|
356
|
+
? await this.handlers.applySync({ rev, changed, roots, policyOnly })
|
|
357
|
+
: { ok: true };
|
|
358
|
+
const viteClient = viteClientLoaded();
|
|
359
|
+
if (viteClient && !this.viteClientReported) {
|
|
360
|
+
this.viteClientReported = true;
|
|
361
|
+
console.error(VITE_CLIENT_MESSAGE);
|
|
362
|
+
}
|
|
363
|
+
this.reply(id, {
|
|
364
|
+
...applied,
|
|
365
|
+
rev,
|
|
366
|
+
executed: this.executed(),
|
|
367
|
+
...(viteClient ? { viteClient: true } : {}),
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.reply(id, { ok: false, reason: 'unknown_request' });
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
this.reply(id, {
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: 'page_error',
|
|
377
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.pix3/dev.json` (plan §B.1 «Обнаружение»): how `pix3 editor`, `pix3 check` and the agent kit
|
|
3
|
+
* find the running dev server. Written when the server listens, removed when it closes — and
|
|
4
|
+
* only by the process that wrote it, so a second dev server on the same project does not delete
|
|
5
|
+
* the first one's record on its way out.
|
|
6
|
+
*/
|
|
7
|
+
export interface Versions {
|
|
8
|
+
readonly plugin: string;
|
|
9
|
+
readonly runtime: string | null;
|
|
10
|
+
readonly editorCore: string | null;
|
|
11
|
+
readonly vite: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DevInfo {
|
|
14
|
+
readonly url: string;
|
|
15
|
+
readonly editorUrl: string;
|
|
16
|
+
readonly port: number;
|
|
17
|
+
readonly pid: number;
|
|
18
|
+
readonly versions: Versions;
|
|
19
|
+
readonly startedAt: string;
|
|
20
|
+
/**
|
|
21
|
+
* Where the browser reaches this server when that is not `url` (Remote SSH, plan §E.3: VS Code
|
|
22
|
+
* forwards 5173 to another local port). `PIX3_PUBLIC_URL` when set, else learnt from the
|
|
23
|
+
* `Origin` of an editor tab that connected through another address. Absent = `url`.
|
|
24
|
+
*/
|
|
25
|
+
readonly publicUrl?: string;
|
|
26
|
+
readonly publicEditorUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The public base URL for `dev.json`: `PIX3_PUBLIC_URL` (an origin or an origin with the base
|
|
30
|
+
* path) wins; otherwise a tab's `Origin` that is not this server's own loopback address. Null
|
|
31
|
+
* when neither says anything new. The result always ends with `base`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const publicUrlOf: (options: {
|
|
34
|
+
readonly env?: string | undefined;
|
|
35
|
+
readonly tabOrigin?: string | null;
|
|
36
|
+
readonly port: number;
|
|
37
|
+
readonly base: string;
|
|
38
|
+
}) => string | null;
|
|
39
|
+
export declare const devJsonPath: (root: string) => string;
|
|
40
|
+
export declare const writeDevInfo: (root: string, info: DevInfo) => void;
|
|
41
|
+
export declare const clearDevInfo: (root: string, pid?: number) => void;
|
|
42
|
+
/** The plugin's own version (its `package.json`, one level above `src/` and `dist/`). */
|
|
43
|
+
export declare const pluginVersion: () => string;
|
|
44
|
+
/**
|
|
45
|
+
* Directory of an installed package as the project sees it: `node_modules/<name>` in the root or
|
|
46
|
+
* any ancestor (hoisting). Not `require.resolve` — `@pix3/runtime` does not export its
|
|
47
|
+
* `package.json`.
|
|
48
|
+
*/
|
|
49
|
+
export declare const findPackageDir: (root: string, name: string) => string | null;
|
|
50
|
+
export declare const installedVersion: (root: string, name: string) => string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Plan §A.3 gate: the project's `@pix3/runtime` must equal `@pix3/editor-core` exactly for
|
|
53
|
+
* prereleases and share major.minor for releases. Returns the problem, or null.
|
|
54
|
+
*/
|
|
55
|
+
export declare const versionMismatch: (runtime: string | null, editorCore: string | null) => string | null;
|