@webstir-io/webstir 0.1.1 → 0.1.3
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/README.md +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +103 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +215 -144
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +30 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +25 -14
- package/src/workspace-lock.ts +207 -0
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceWatchLockHandle {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
release(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface WorkspaceWatchLockOwner {
|
|
10
|
+
readonly kind: 'watch';
|
|
11
|
+
readonly pid: number;
|
|
12
|
+
readonly workspaceRoot: string;
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WorkspaceWatchLockState {
|
|
17
|
+
readonly exists: boolean;
|
|
18
|
+
readonly active: boolean;
|
|
19
|
+
readonly lockPath: string;
|
|
20
|
+
readonly owner?: WorkspaceWatchLockOwner;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const WEBSTIR_DIR = '.webstir';
|
|
24
|
+
const WATCH_LOCK_DIR = 'watch.lock';
|
|
25
|
+
const OWNER_FILE = 'owner.json';
|
|
26
|
+
const UNKNOWN_OWNER_GRACE_MS = 30_000;
|
|
27
|
+
|
|
28
|
+
export class WorkspaceWatchLockConflictError extends Error {
|
|
29
|
+
public constructor(
|
|
30
|
+
workspaceRoot: string,
|
|
31
|
+
command: 'build' | 'publish' | 'watch',
|
|
32
|
+
owner?: WorkspaceWatchLockOwner,
|
|
33
|
+
) {
|
|
34
|
+
const ownerDetails = owner ? ` (pid ${owner.pid})` : '';
|
|
35
|
+
const action = command === 'watch' ? 'start webstir watch' : `run webstir ${command}`;
|
|
36
|
+
super(
|
|
37
|
+
`Cannot ${action} because webstir watch is active for this workspace${ownerDetails}. Stop the watch process before running another build pipeline against ${workspaceRoot}.`,
|
|
38
|
+
);
|
|
39
|
+
this.name = 'WorkspaceWatchLockConflictError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function acquireWorkspaceWatchLock(
|
|
44
|
+
workspaceRoot: string,
|
|
45
|
+
): Promise<WorkspaceWatchLockHandle> {
|
|
46
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
47
|
+
const lockPath = getWorkspaceWatchLockPath(resolvedRoot);
|
|
48
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
49
|
+
|
|
50
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
51
|
+
try {
|
|
52
|
+
await mkdir(lockPath);
|
|
53
|
+
try {
|
|
54
|
+
await writeOwner(lockPath, resolvedRoot);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
await removeLockDirectory(lockPath);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return createLockHandle(lockPath);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!isErrno(error, 'EEXIST')) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const state = await readWorkspaceWatchLockState(resolvedRoot);
|
|
66
|
+
if (state.active) {
|
|
67
|
+
throw new WorkspaceWatchLockConflictError(resolvedRoot, 'watch', state.owner);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await removeLockDirectory(state.lockPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const state = await readWorkspaceWatchLockState(resolvedRoot);
|
|
75
|
+
throw new WorkspaceWatchLockConflictError(resolvedRoot, 'watch', state.owner);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function assertNoActiveWorkspaceWatch(
|
|
79
|
+
workspaceRoot: string,
|
|
80
|
+
command: 'build' | 'publish',
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
83
|
+
const state = await readWorkspaceWatchLockState(resolvedRoot);
|
|
84
|
+
if (!state.exists) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (state.active) {
|
|
89
|
+
throw new WorkspaceWatchLockConflictError(resolvedRoot, command, state.owner);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await removeLockDirectory(state.lockPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getWorkspaceWatchLockPath(workspaceRoot: string): string {
|
|
96
|
+
return path.join(workspaceRoot, WEBSTIR_DIR, WATCH_LOCK_DIR);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createLockHandle(lockPath: string): WorkspaceWatchLockHandle {
|
|
100
|
+
let released = false;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
path: lockPath,
|
|
104
|
+
async release() {
|
|
105
|
+
if (released) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
released = true;
|
|
110
|
+
const owner = await readOwner(lockPath);
|
|
111
|
+
if (owner?.pid !== process.pid) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await removeLockDirectory(lockPath);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readWorkspaceWatchLockState(
|
|
121
|
+
workspaceRoot: string,
|
|
122
|
+
): Promise<WorkspaceWatchLockState> {
|
|
123
|
+
const lockPath = getWorkspaceWatchLockPath(workspaceRoot);
|
|
124
|
+
let modifiedAtMs = 0;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const stats = await lstat(lockPath);
|
|
128
|
+
modifiedAtMs = stats.mtimeMs;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (isErrno(error, 'ENOENT')) {
|
|
131
|
+
return { exists: false, active: false, lockPath };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const owner = await readOwner(lockPath);
|
|
138
|
+
if (!owner) {
|
|
139
|
+
return {
|
|
140
|
+
exists: true,
|
|
141
|
+
active: Date.now() - modifiedAtMs < UNKNOWN_OWNER_GRACE_MS,
|
|
142
|
+
lockPath,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
exists: true,
|
|
148
|
+
active: isProcessActive(owner.pid),
|
|
149
|
+
lockPath,
|
|
150
|
+
owner,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function writeOwner(lockPath: string, workspaceRoot: string): Promise<void> {
|
|
155
|
+
const owner: WorkspaceWatchLockOwner = {
|
|
156
|
+
kind: 'watch',
|
|
157
|
+
pid: process.pid,
|
|
158
|
+
workspaceRoot,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
await writeFile(path.join(lockPath, OWNER_FILE), `${JSON.stringify(owner, null, 2)}\n`, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readOwner(lockPath: string): Promise<WorkspaceWatchLockOwner | undefined> {
|
|
165
|
+
try {
|
|
166
|
+
const raw = await readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
167
|
+
const parsed = JSON.parse(raw) as Partial<WorkspaceWatchLockOwner>;
|
|
168
|
+
if (
|
|
169
|
+
parsed.kind !== 'watch' ||
|
|
170
|
+
typeof parsed.pid !== 'number' ||
|
|
171
|
+
typeof parsed.workspaceRoot !== 'string' ||
|
|
172
|
+
typeof parsed.createdAt !== 'string'
|
|
173
|
+
) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
kind: 'watch',
|
|
179
|
+
pid: parsed.pid,
|
|
180
|
+
workspaceRoot: parsed.workspaceRoot,
|
|
181
|
+
createdAt: parsed.createdAt,
|
|
182
|
+
};
|
|
183
|
+
} catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isProcessActive(pid: number): boolean {
|
|
189
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
process.kill(pid, 0);
|
|
195
|
+
return true;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return !isErrno(error, 'ESRCH');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function removeLockDirectory(lockPath: string): Promise<void> {
|
|
202
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
206
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
|
|
207
|
+
}
|
package/src/workspace-watcher.ts
CHANGED
|
@@ -30,10 +30,7 @@ export class WorkspaceWatcher {
|
|
|
30
30
|
|
|
31
31
|
public constructor(options: WorkspaceWatcherOptions) {
|
|
32
32
|
this.workspaceRoot = path.resolve(options.workspaceRoot);
|
|
33
|
-
this.treeRoots = [
|
|
34
|
-
path.join(this.workspaceRoot, 'src'),
|
|
35
|
-
path.join(this.workspaceRoot, 'types'),
|
|
36
|
-
];
|
|
33
|
+
this.treeRoots = [path.join(this.workspaceRoot, 'src'), path.join(this.workspaceRoot, 'types')];
|
|
37
34
|
this.onEvent = options.onEvent;
|
|
38
35
|
this.debounceMs = options.debounceMs ?? 75;
|
|
39
36
|
}
|
|
@@ -112,7 +109,11 @@ export class WorkspaceWatcher {
|
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
for (const watchedDirectory of Array.from(this.treeWatchers.keys())) {
|
|
115
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
watchedDirectory !== root &&
|
|
114
|
+
watchedDirectory.startsWith(`${root}${path.sep}`) &&
|
|
115
|
+
!current.has(watchedDirectory)
|
|
116
|
+
) {
|
|
116
117
|
this.treeWatchers.get(watchedDirectory)?.close();
|
|
117
118
|
this.treeWatchers.delete(watchedDirectory);
|
|
118
119
|
}
|
|
@@ -234,7 +235,10 @@ async function collectDirectories(root: string): Promise<readonly string[]> {
|
|
|
234
235
|
const stack = [path.resolve(root)];
|
|
235
236
|
|
|
236
237
|
while (stack.length > 0) {
|
|
237
|
-
const current = stack.pop()
|
|
238
|
+
const current = stack.pop();
|
|
239
|
+
if (!current) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
238
242
|
directories.push(current);
|
|
239
243
|
|
|
240
244
|
const entries = await readdir(current, { withFileTypes: true });
|
package/src/workspace.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function parseWorkspaceMode(value: unknown): WorkspaceMode {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
throw new Error(
|
|
28
|
-
`Unsupported webstir.mode "${value}". Expected one of: ${SUPPORTED_WORKSPACE_MODES.join(', ')}
|
|
28
|
+
`Unsupported webstir.mode "${value}". Expected one of: ${SUPPORTED_WORKSPACE_MODES.join(', ')}.`,
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -44,7 +44,9 @@ export async function readWorkspaceDescriptor(workspacePath: string): Promise<Wo
|
|
|
44
44
|
try {
|
|
45
45
|
packageJson = JSON.parse(rawPackageJson) as WorkspacePackageJson;
|
|
46
46
|
} catch (error) {
|
|
47
|
-
throw new Error(`Workspace package.json at ${packageJsonPath} is not valid JSON.`, {
|
|
47
|
+
throw new Error(`Workspace package.json at ${packageJsonPath} is not valid JSON.`, {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
return {
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
|
-
import { createInterface, type Interface as ReadLineInterface } from 'node:readline';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
import { createWorkspaceRuntimeEnv, resolveRuntimeCommand } from './runtime.ts';
|
|
7
|
-
import { parseStructuredDiagnosticLine, type StructuredDiagnosticPayload } from './watch-events.ts';
|
|
8
|
-
import { ensureLocalPackageArtifacts } from './providers.ts';
|
|
9
|
-
|
|
10
|
-
type WatchDaemonCommand =
|
|
11
|
-
| { readonly type: 'start' }
|
|
12
|
-
| { readonly type: 'change'; readonly path: string }
|
|
13
|
-
| { readonly type: 'reload' }
|
|
14
|
-
| { readonly type: 'shutdown' };
|
|
15
|
-
|
|
16
|
-
export interface FrontendWatchDaemonClientOptions {
|
|
17
|
-
readonly workspaceRoot: string;
|
|
18
|
-
readonly verbose?: boolean;
|
|
19
|
-
readonly hmrVerbose?: boolean;
|
|
20
|
-
readonly env?: Record<string, string | undefined>;
|
|
21
|
-
readonly onLine?: (line: string) => void;
|
|
22
|
-
readonly onErrorLine?: (line: string) => void;
|
|
23
|
-
readonly onDiagnostic?: (payload: StructuredDiagnosticPayload) => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class FrontendWatchDaemonClient {
|
|
27
|
-
private readonly workspaceRoot: string;
|
|
28
|
-
private readonly verbose: boolean;
|
|
29
|
-
private readonly hmrVerbose: boolean;
|
|
30
|
-
private readonly env?: Record<string, string | undefined>;
|
|
31
|
-
private readonly onLine?: (line: string) => void;
|
|
32
|
-
private readonly onErrorLine?: (line: string) => void;
|
|
33
|
-
private readonly onDiagnostic?: (payload: StructuredDiagnosticPayload) => void;
|
|
34
|
-
private child?: ChildProcessWithoutNullStreams;
|
|
35
|
-
private stdoutReader?: ReadLineInterface;
|
|
36
|
-
private stderrReader?: ReadLineInterface;
|
|
37
|
-
private exitPromise?: Promise<number | null>;
|
|
38
|
-
private isStopping = false;
|
|
39
|
-
|
|
40
|
-
public constructor(options: FrontendWatchDaemonClientOptions) {
|
|
41
|
-
this.workspaceRoot = path.resolve(options.workspaceRoot);
|
|
42
|
-
this.verbose = options.verbose ?? false;
|
|
43
|
-
this.hmrVerbose = options.hmrVerbose ?? false;
|
|
44
|
-
this.env = options.env;
|
|
45
|
-
this.onLine = options.onLine;
|
|
46
|
-
this.onErrorLine = options.onErrorLine;
|
|
47
|
-
this.onDiagnostic = options.onDiagnostic;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public async start(): Promise<void> {
|
|
51
|
-
if (this.child) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await ensureLocalPackageArtifacts();
|
|
56
|
-
const frontendCliPath = fileURLToPath(import.meta.resolve('@webstir-io/webstir-frontend/cli'));
|
|
57
|
-
const args = [
|
|
58
|
-
frontendCliPath,
|
|
59
|
-
'watch-daemon',
|
|
60
|
-
'--workspace',
|
|
61
|
-
this.workspaceRoot,
|
|
62
|
-
'--no-auto-start',
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
if (this.verbose) {
|
|
66
|
-
args.push('--verbose');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (this.hmrVerbose) {
|
|
70
|
-
args.push('--hmr-verbose');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const child = spawn(resolveRuntimeCommand(), args, {
|
|
74
|
-
cwd: this.workspaceRoot,
|
|
75
|
-
env: createWorkspaceRuntimeEnv(this.workspaceRoot, 'build', this.env),
|
|
76
|
-
stdio: 'pipe',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
this.child = child;
|
|
80
|
-
this.exitPromise = new Promise<number | null>((resolve, reject) => {
|
|
81
|
-
child.once('error', reject);
|
|
82
|
-
child.once('close', (code) => resolve(code));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
this.stdoutReader = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
86
|
-
this.stdoutReader.on('line', (line) => {
|
|
87
|
-
const diagnostic = parseStructuredDiagnosticLine(line);
|
|
88
|
-
if (diagnostic) {
|
|
89
|
-
this.onDiagnostic?.(diagnostic);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this.onLine?.(line);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
this.stderrReader = createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
97
|
-
this.stderrReader.on('line', (line) => {
|
|
98
|
-
this.onErrorLine?.(line);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public async sendStart(): Promise<void> {
|
|
103
|
-
await this.send({ type: 'start' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public async sendChange(filePath: string): Promise<void> {
|
|
107
|
-
await this.send({ type: 'change', path: filePath });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public async sendReload(): Promise<void> {
|
|
111
|
-
await this.send({ type: 'reload' });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
public async stop(): Promise<number | null> {
|
|
115
|
-
if (!this.child || !this.exitPromise) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!this.isStopping) {
|
|
120
|
-
this.isStopping = true;
|
|
121
|
-
try {
|
|
122
|
-
await this.send({ type: 'shutdown' });
|
|
123
|
-
} catch {
|
|
124
|
-
// Fall through to best-effort teardown.
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.child.stdin.end();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const code = await this.exitPromise;
|
|
131
|
-
this.cleanup();
|
|
132
|
-
return code;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
public async waitForExit(): Promise<number | null> {
|
|
136
|
-
if (!this.exitPromise) {
|
|
137
|
-
throw new Error('Frontend watch daemon has not started.');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const code = await this.exitPromise;
|
|
141
|
-
this.cleanup();
|
|
142
|
-
return code;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private async send(command: WatchDaemonCommand): Promise<void> {
|
|
146
|
-
if (!this.child || !this.child.stdin.writable) {
|
|
147
|
-
throw new Error('Frontend watch daemon is not running.');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
await new Promise<void>((resolve, reject) => {
|
|
151
|
-
this.child!.stdin.write(`${JSON.stringify(command)}\n`, (error) => {
|
|
152
|
-
if (error) {
|
|
153
|
-
reject(error);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
resolve();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private cleanup(): void {
|
|
163
|
-
this.stdoutReader?.close();
|
|
164
|
-
this.stderrReader?.close();
|
|
165
|
-
this.stdoutReader = undefined;
|
|
166
|
-
this.stderrReader = undefined;
|
|
167
|
-
this.child = undefined;
|
|
168
|
-
this.exitPromise = undefined;
|
|
169
|
-
this.isStopping = false;
|
|
170
|
-
}
|
|
171
|
-
}
|