@phreshos/cli 0.1.45 → 0.1.47
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 +6 -5
- package/dist/cli.js +5 -2
- package/dist/control/shared.js +1 -0
- package/dist/init.js +12 -10
- package/dist/launch.js +25 -58
- package/dist/system/lifecycle.js +25 -8
- package/dist/system/paths.js +2 -0
- package/dist/template/package.json +6 -6
- package/dist/template/phresh.config.ts +1 -2
- package/dist/template/scripts/build.ts +1 -1
- package/dist/template/vite.client.ts +2 -3
- package/dist/template.json +2 -2
- package/package.json +3 -3
- package/dist/development-client.js +0 -197
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ phresh pack
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
`create` produces the official starter Program. The remaining commands operate
|
|
34
|
-
on the current Program project
|
|
35
|
-
|
|
34
|
+
on the current Program project through the Node Project contract and present
|
|
35
|
+
its build, development, installation, and runtime lifecycles.
|
|
36
36
|
|
|
37
37
|
## System
|
|
38
38
|
|
|
@@ -89,9 +89,10 @@ for the command model.
|
|
|
89
89
|
|
|
90
90
|
## Repository boundary
|
|
91
91
|
|
|
92
|
-
This repository owns terminal interaction,
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
This repository owns terminal interaction, command routing, System acquisition,
|
|
93
|
+
and host service integration. Node owns the external JavaScript interface and
|
|
94
|
+
Project workflows, Core owns shared contracts, and the System owns authoritative
|
|
95
|
+
state.
|
|
95
96
|
|
|
96
97
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository workflow and
|
|
97
98
|
[SECURITY.md](SECURITY.md) for private vulnerability reporting.
|
package/dist/cli.js
CHANGED
|
@@ -61,7 +61,7 @@ describe(program.command("init")
|
|
|
61
61
|
.option("--server-development-entry-file <path>", "development Server worker entry")
|
|
62
62
|
.option("--client", "include a Client endpoint")
|
|
63
63
|
.option("--client-location <path>", "production Client directory")
|
|
64
|
-
.option("--client-development-url <url>", "development Client URL")
|
|
64
|
+
.option("--client-development-url <url>", "fixed or external development Client URL")
|
|
65
65
|
.option("--client-development-start-command <command>", "development Client command")
|
|
66
66
|
.option("--force", "replace an existing phresh.config.ts")
|
|
67
67
|
.action(async function (options) {
|
|
@@ -74,7 +74,10 @@ describe(program.command("init")
|
|
|
74
74
|
serverEntryFile: options.serverEntryFile,
|
|
75
75
|
serverDevelopmentStartCommand: options.serverDevelopmentStartCommand,
|
|
76
76
|
serverDevelopmentEntryFile: options.serverDevelopmentEntryFile,
|
|
77
|
-
client: options.client === true
|
|
77
|
+
client: options.client === true
|
|
78
|
+
|| options.clientLocation !== undefined
|
|
79
|
+
|| options.clientDevelopmentUrl !== undefined
|
|
80
|
+
|| options.clientDevelopmentStartCommand !== undefined,
|
|
78
81
|
clientLocation: options.clientLocation,
|
|
79
82
|
clientDevelopmentUrl: options.clientDevelopmentUrl,
|
|
80
83
|
clientDevelopmentStartCommand: options.clientDevelopmentStartCommand,
|
package/dist/control/shared.js
CHANGED
package/dist/init.js
CHANGED
|
@@ -83,24 +83,28 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
83
83
|
if (interactive && developmentUrl === undefined && developmentStartCommand === undefined) {
|
|
84
84
|
const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
|
|
85
85
|
if (await yes("A development server can provide live updates instead of built Client files.", "Use a Client development server?", Boolean(suggested))) {
|
|
86
|
-
|
|
87
|
-
if (await yes("The CLI can own the development server and stop it when the session ends.", "Should phresh dev start the Client server?", Boolean(suggested))) {
|
|
86
|
+
if (await yes("The Project can own the development server and stop it when the session ends.", "Should phresh dev start the Client server?", Boolean(suggested))) {
|
|
88
87
|
developmentStartCommand = await ask("This command remains attached to the phresh dev session.", "What command starts the Client development server?", suggested || undefined);
|
|
89
88
|
}
|
|
89
|
+
else
|
|
90
|
+
developmentUrl = await ask("The System routes the Program asset path to this HTTP or HTTPS address during phresh dev.", "What URL serves the external development Client?", "http://localhost:5173/");
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
if (developmentStartCommand !== undefined && developmentUrl === undefined)
|
|
93
|
-
throw new Error("--client-development-url is required with --client-development-start-command");
|
|
94
93
|
if (developmentUrl !== undefined && developmentUrl.trim().length === 0)
|
|
95
94
|
throw new Error("A client development URL must not be empty");
|
|
96
95
|
if (developmentUrl !== undefined && !httpUrl(developmentUrl))
|
|
97
96
|
throw new Error("A client development URL must use HTTP or HTTPS");
|
|
98
97
|
if (developmentStartCommand !== undefined && developmentStartCommand.trim().length === 0)
|
|
99
98
|
throw new Error("A client development command must not be empty");
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
let development;
|
|
100
|
+
if (developmentStartCommand !== undefined)
|
|
101
|
+
development = {
|
|
102
|
+
startCommand: developmentStartCommand,
|
|
103
|
+
...developmentUrl !== undefined && { url: developmentUrl }
|
|
104
|
+
};
|
|
105
|
+
else if (developmentUrl !== undefined)
|
|
106
|
+
development = { url: developmentUrl };
|
|
107
|
+
client = { location, ...development && { development } };
|
|
104
108
|
}
|
|
105
109
|
const described = {
|
|
106
110
|
identity: manifest.name,
|
|
@@ -124,8 +128,6 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
124
128
|
if (config.client) {
|
|
125
129
|
interaction.message();
|
|
126
130
|
interaction.detail("base URL", "./", "required for production assets");
|
|
127
|
-
if (config.client.development)
|
|
128
|
-
interaction.detail("CORS", "enabled", "required on the development server");
|
|
129
131
|
}
|
|
130
132
|
interaction.finish(`${configFile} created`);
|
|
131
133
|
}
|
package/dist/launch.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Project } from "@phreshos/node";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
|
-
import { assertAvailable, commandFailure, DevelopmentClient, waitForDevelopmentClient } from "./development-client.js";
|
|
4
3
|
import { blank, dim, heading, line } from "./style.js";
|
|
5
4
|
/** Run the current project through one connected System. */
|
|
6
5
|
export default async function launch(mode, directory = process.cwd(), options = {}) {
|
|
@@ -19,14 +18,9 @@ export default async function launch(mode, directory = process.cwd(), options =
|
|
|
19
18
|
blank();
|
|
20
19
|
const system = await (await import("@phreshos/node")).System.connect();
|
|
21
20
|
const controller = new AbortController();
|
|
22
|
-
const development = mode === "development" && definition.client && (definition.client.start ?? true)
|
|
23
|
-
? project.config.client?.development
|
|
24
|
-
: undefined;
|
|
25
|
-
const command = development?.startCommand;
|
|
26
|
-
let client;
|
|
27
21
|
let interrupted = false;
|
|
28
22
|
let ended = null;
|
|
29
|
-
let
|
|
23
|
+
let attached = false;
|
|
30
24
|
const stop = () => {
|
|
31
25
|
if (interrupted)
|
|
32
26
|
return;
|
|
@@ -36,18 +30,11 @@ export default async function launch(mode, directory = process.cwd(), options =
|
|
|
36
30
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
37
31
|
process.on(signal, stop);
|
|
38
32
|
try {
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for await (const event of waitForDevelopmentClient(development, client, controller.signal))
|
|
45
|
-
presentDevelopment(event);
|
|
46
|
-
}
|
|
47
|
-
if (mode === "production")
|
|
48
|
-
await project.build();
|
|
49
|
-
program = await system.forceCreateProgram(definition);
|
|
50
|
-
ended = await consume(program.process.run({ options }, { signal: controller.signal }), client);
|
|
33
|
+
const lifecycle = mode === "development"
|
|
34
|
+
? await project.dev(system, { options, signal: controller.signal })
|
|
35
|
+
: await project.start(system, { options, signal: controller.signal });
|
|
36
|
+
attached = true;
|
|
37
|
+
ended = await consume(lifecycle);
|
|
51
38
|
}
|
|
52
39
|
catch (error) {
|
|
53
40
|
if (!interrupted)
|
|
@@ -55,16 +42,25 @@ export default async function launch(mode, directory = process.cwd(), options =
|
|
|
55
42
|
}
|
|
56
43
|
finally {
|
|
57
44
|
controller.abort(new Error("The local Program run ended"));
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
program ?
|
|
61
|
-
|
|
45
|
+
let cleanupFailure;
|
|
46
|
+
try {
|
|
47
|
+
const program = attached ? await system.program.find(definition.identity) : null;
|
|
48
|
+
if (program)
|
|
49
|
+
await forget(program);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
cleanupFailure = error;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await system.disconnect();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
cleanupFailure ??= error;
|
|
59
|
+
}
|
|
62
60
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
63
61
|
process.off(signal, stop);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (failure?.status === "rejected")
|
|
67
|
-
throw failure.reason;
|
|
62
|
+
if (cleanupFailure)
|
|
63
|
+
throw cleanupFailure;
|
|
68
64
|
}
|
|
69
65
|
if (interrupted)
|
|
70
66
|
process.exit(130);
|
|
@@ -75,34 +71,10 @@ export default async function launch(mode, directory = process.cwd(), options =
|
|
|
75
71
|
blank();
|
|
76
72
|
process.exit(ended.signal ? 128 : ended.code ?? 0);
|
|
77
73
|
}
|
|
78
|
-
async function consume(lifecycle
|
|
79
|
-
const iterator = lifecycle[Symbol.asyncIterator]();
|
|
80
|
-
let next = iterator.next();
|
|
81
|
-
let developmentExit = client?.exited();
|
|
82
|
-
let developmentOutput = client?.outputAvailable();
|
|
74
|
+
async function consume(lifecycle) {
|
|
83
75
|
let process = null;
|
|
84
76
|
let ending = null;
|
|
85
|
-
|
|
86
|
-
for (const event of client?.drain() ?? [])
|
|
87
|
-
presentDevelopment(event);
|
|
88
|
-
const outcome = await Promise.race([
|
|
89
|
-
next.then(result => ({ source: "system", result })),
|
|
90
|
-
...(developmentExit ? [developmentExit.then(result => ({ source: "client", result }))] : []),
|
|
91
|
-
...(developmentOutput ? [developmentOutput.then(() => ({ source: "output" }))] : [])
|
|
92
|
-
]);
|
|
93
|
-
if (outcome.source === "output") {
|
|
94
|
-
developmentOutput = client?.outputAvailable();
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (outcome.source === "client") {
|
|
98
|
-
developmentExit = undefined;
|
|
99
|
-
if (!client?.endingWasRequested())
|
|
100
|
-
throw commandFailure(outcome.result);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (outcome.result.done)
|
|
104
|
-
break;
|
|
105
|
-
const event = outcome.result.value;
|
|
77
|
+
for await (const event of lifecycle) {
|
|
106
78
|
if (event.event === "started") {
|
|
107
79
|
process = event.process.identity;
|
|
108
80
|
line("process", process);
|
|
@@ -111,16 +83,11 @@ async function consume(lifecycle, client) {
|
|
|
111
83
|
write(event.stream, event.text);
|
|
112
84
|
else
|
|
113
85
|
ending = { code: event.exit.code, signal: event.exit.signal };
|
|
114
|
-
next = iterator.next();
|
|
115
86
|
}
|
|
116
87
|
if (!process || !ending)
|
|
117
88
|
throw new Error("The System ended the Program run without a complete Process lifecycle");
|
|
118
89
|
return { process, ...ending };
|
|
119
90
|
}
|
|
120
|
-
function presentDevelopment(event) {
|
|
121
|
-
if (event.event === "output")
|
|
122
|
-
write(event.stream === "err" ? "stderr" : "stdout", event.text);
|
|
123
|
-
}
|
|
124
91
|
function write(stream, text) {
|
|
125
92
|
(stream === "stderr" ? process.stderr : process.stdout).write(text);
|
|
126
93
|
}
|
package/dist/system/lifecycle.js
CHANGED
|
@@ -7,7 +7,7 @@ import nodeExecutable from "./node.js";
|
|
|
7
7
|
import installProgram from "../install.js";
|
|
8
8
|
import phreshosHome from "../home.js";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
/** Coordinates acquisition, immutable files, and the native service as one transaction. */
|
|
@@ -115,9 +115,10 @@ export default class SystemLifecycle {
|
|
|
115
115
|
const { installation, service } = this.dependencies;
|
|
116
116
|
const [installed, state] = await Promise.all([installation.current(), service.inspect()]);
|
|
117
117
|
const ready = state.running && await this.dependencies.ready(installation.paths.gateway);
|
|
118
|
+
const desktop = await desktopOrigin(installation.paths.storage);
|
|
118
119
|
return {
|
|
119
120
|
...(installed ? { installed } : {}),
|
|
120
|
-
desktop
|
|
121
|
+
desktop,
|
|
121
122
|
...state,
|
|
122
123
|
ready,
|
|
123
124
|
root: installation.paths.root,
|
|
@@ -185,17 +186,20 @@ export default class SystemLifecycle {
|
|
|
185
186
|
}
|
|
186
187
|
async launchService() {
|
|
187
188
|
const { installation, service } = this.dependencies;
|
|
188
|
-
const { homeRequest, transientHome } = installation.paths;
|
|
189
|
-
await rm(homeRequest, { force: true });
|
|
190
|
-
if (transientHome) {
|
|
189
|
+
const { homeRequest, portRequest, transientHome, transientPorts } = installation.paths;
|
|
190
|
+
await Promise.all([rm(homeRequest, { force: true }), rm(portRequest, { force: true })]);
|
|
191
|
+
if (transientHome || transientPorts !== undefined) {
|
|
191
192
|
await mkdir(dirname(homeRequest), { recursive: true });
|
|
192
|
-
await
|
|
193
|
+
await Promise.all([
|
|
194
|
+
...transientHome ? [writeFile(homeRequest, transientHome, { mode: 0o600 })] : [],
|
|
195
|
+
...transientPorts === undefined ? [] : [writeFile(portRequest, transientPorts, { mode: 0o600 })]
|
|
196
|
+
]);
|
|
193
197
|
}
|
|
194
198
|
try {
|
|
195
199
|
await service.start();
|
|
196
200
|
}
|
|
197
201
|
catch (error) {
|
|
198
|
-
await rm(homeRequest, { force: true });
|
|
202
|
+
await Promise.all([rm(homeRequest, { force: true }), rm(portRequest, { force: true })]);
|
|
199
203
|
throw error;
|
|
200
204
|
}
|
|
201
205
|
}
|
|
@@ -211,9 +215,22 @@ function definition(installation, executable) {
|
|
|
211
215
|
"--default-home",
|
|
212
216
|
phreshosHome({}, homedir()),
|
|
213
217
|
"--home-request",
|
|
214
|
-
installation.paths.homeRequest
|
|
218
|
+
installation.paths.homeRequest,
|
|
219
|
+
"--port-request",
|
|
220
|
+
installation.paths.portRequest
|
|
215
221
|
],
|
|
216
222
|
directory: installation.paths.current,
|
|
217
223
|
output: installation.paths.log
|
|
218
224
|
};
|
|
219
225
|
}
|
|
226
|
+
async function desktopOrigin(storage) {
|
|
227
|
+
try {
|
|
228
|
+
const value = (await readFile(join(storage, "desktop"), "utf8")).trim();
|
|
229
|
+
return /^http:\/\/localhost:\d+$/.test(value) ? value : "http://localhost:4300";
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === "ENOENT")
|
|
233
|
+
return "http://localhost:4300";
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
package/dist/system/paths.js
CHANGED
|
@@ -19,7 +19,9 @@ export default function systemPaths(platform = process.platform, userHome = home
|
|
|
19
19
|
storage,
|
|
20
20
|
gateway: gatewayPath(storage, platform),
|
|
21
21
|
homeRequest: join(root, "next-home"),
|
|
22
|
+
portRequest: join(root, "next-port"),
|
|
22
23
|
...(variables.PHRESHOS_HOME === undefined ? {} : { transientHome: storage }),
|
|
24
|
+
...(variables.PHRESHOS_PORT === undefined ? {} : { transientPorts: variables.PHRESHOS_PORT }),
|
|
23
25
|
log: join(storage, "service.log")
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phresh",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.29",
|
|
5
5
|
"description": "The official PhreshOS starter Program.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"starter"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@phreshos/client": "^0.1.
|
|
18
|
-
"@phreshos/core": "^0.1.
|
|
19
|
-
"@phreshos/react": "^0.1.
|
|
20
|
-
"@phreshos/server": "^0.1.
|
|
17
|
+
"@phreshos/client": "^0.1.33",
|
|
18
|
+
"@phreshos/core": "^0.1.37",
|
|
19
|
+
"@phreshos/react": "^0.1.19",
|
|
20
|
+
"@phreshos/server": "^0.1.37",
|
|
21
21
|
"react": "^19.2.8",
|
|
22
22
|
"react-dom": "^19.2.8"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@phreshos/cli": "^0.1.
|
|
25
|
+
"@phreshos/cli": "^0.1.47",
|
|
26
26
|
"@types/node": "^26.4.0",
|
|
27
27
|
"@types/react": "^19.2.18",
|
|
28
28
|
"@types/react-dom": "^19.2.5",
|
|
@@ -4,7 +4,7 @@ export default defineConfig({
|
|
|
4
4
|
identity: "phresh",
|
|
5
5
|
name: "Phresh Program",
|
|
6
6
|
description: "A minimal counter with direct Client and Server endpoints.",
|
|
7
|
-
version: "0.1.
|
|
7
|
+
version: "0.1.29",
|
|
8
8
|
icon: "icon.png",
|
|
9
9
|
categories: ["Development"],
|
|
10
10
|
keywords: ["example", "counter", "client", "server"],
|
|
@@ -22,7 +22,6 @@ export default defineConfig({
|
|
|
22
22
|
title: "Phresh Program",
|
|
23
23
|
size: { width: 600, height: 500 },
|
|
24
24
|
development: {
|
|
25
|
-
url: "http://localhost:5200/",
|
|
26
25
|
startCommand: "vite --config vite.client.ts"
|
|
27
26
|
}
|
|
28
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import packageConfig from "../package.json" with { type: "json" }
|
|
1
2
|
import { externalDependencies } from "../vite.config"
|
|
2
3
|
import { rm, writeFile } from "node:fs/promises"
|
|
3
|
-
import packageConfig from "../package.json" with { type: "json" }
|
|
4
4
|
|
|
5
5
|
process.env.NODE_ENV = "production"
|
|
6
6
|
|
|
@@ -5,14 +5,13 @@ import { resolve } from "node:path"
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
root: "client",
|
|
7
7
|
plugins: [react()],
|
|
8
|
-
base: "./",
|
|
8
|
+
base: process.env.PHRESHOS_CLIENT_BASE ?? "./",
|
|
9
9
|
resolve: {
|
|
10
10
|
tsconfigPaths: true,
|
|
11
11
|
dedupe: ["react"]
|
|
12
12
|
},
|
|
13
13
|
server: {
|
|
14
|
-
|
|
15
|
-
port: 5200,
|
|
14
|
+
port: Number(process.env.PHRESHOS_CLIENT_PORT ?? "5200"),
|
|
16
15
|
strictPort: true
|
|
17
16
|
},
|
|
18
17
|
build: {
|
package/dist/template.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "PhreshOS/phresh-program",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"sha256": "
|
|
3
|
+
"version": "0.1.29",
|
|
4
|
+
"sha256": "af18a08633faeb6b824c8d5233403d7c9d412aeaac1c456ee2a9e30925852113",
|
|
5
5
|
"development": true
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
|
|
4
|
+
"version": "0.1.47",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.10"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"packageManager": "bun@1.3.14",
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@clack/prompts": "^1.7.0",
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
"@phreshos/core": "^0.1.37",
|
|
53
|
+
"@phreshos/node": "^0.1.13",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"commander": "^15.0.0",
|
|
56
56
|
"picocolors": "^1.1.1"
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { connect } from "node:net";
|
|
3
|
-
import { delimiter, join } from "node:path";
|
|
4
|
-
const readinessTimeout = 15_000;
|
|
5
|
-
const pollingInterval = 200;
|
|
6
|
-
const reportingInterval = 2_000;
|
|
7
|
-
const sandboxedClientOrigin = "null";
|
|
8
|
-
/** One Client development server presented and supervised by the CLI. */
|
|
9
|
-
export class DevelopmentClient {
|
|
10
|
-
child;
|
|
11
|
-
output = [];
|
|
12
|
-
stopped = false;
|
|
13
|
-
result = null;
|
|
14
|
-
outputWaiter = null;
|
|
15
|
-
completion;
|
|
16
|
-
constructor(command, directory) {
|
|
17
|
-
this.child = spawn(command, {
|
|
18
|
-
cwd: directory,
|
|
19
|
-
env: commandEnvironment(directory),
|
|
20
|
-
shell: true,
|
|
21
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
-
detached: true
|
|
23
|
-
});
|
|
24
|
-
this.child.stdout?.on("data", chunk => this.push(outputEvent("out", chunk)));
|
|
25
|
-
this.child.stderr?.on("data", chunk => this.push(outputEvent("err", chunk)));
|
|
26
|
-
this.completion = new Promise(resolve => {
|
|
27
|
-
const finish = (exit) => {
|
|
28
|
-
if (this.result)
|
|
29
|
-
return;
|
|
30
|
-
this.result = exit;
|
|
31
|
-
this.outputWaiter?.();
|
|
32
|
-
this.outputWaiter = null;
|
|
33
|
-
resolve(exit);
|
|
34
|
-
};
|
|
35
|
-
this.child.once("error", error => finish({ code: null, signal: null, error }));
|
|
36
|
-
this.child.once("exit", (code, signal) => finish({ code, signal, error: null }));
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
drain() { return this.output.splice(0); }
|
|
40
|
-
exited() { return this.completion; }
|
|
41
|
-
exitResult() { return this.result; }
|
|
42
|
-
outputAvailable() {
|
|
43
|
-
if (this.output.length || this.result)
|
|
44
|
-
return Promise.resolve();
|
|
45
|
-
return new Promise(resolve => { this.outputWaiter = resolve; });
|
|
46
|
-
}
|
|
47
|
-
async stop() {
|
|
48
|
-
if (this.stopped)
|
|
49
|
-
return;
|
|
50
|
-
this.stopped = true;
|
|
51
|
-
if (!running(this.child))
|
|
52
|
-
return;
|
|
53
|
-
terminate(this.child, "SIGTERM");
|
|
54
|
-
await waitUntilStopped(this.child, 1_000);
|
|
55
|
-
if (running(this.child))
|
|
56
|
-
terminate(this.child, "SIGKILL");
|
|
57
|
-
await waitUntilStopped(this.child, 1_000);
|
|
58
|
-
}
|
|
59
|
-
endingWasRequested() { return this.stopped; }
|
|
60
|
-
push(event) {
|
|
61
|
-
this.output.push(event);
|
|
62
|
-
this.outputWaiter?.();
|
|
63
|
-
this.outputWaiter = null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Refuse to claim a URL already served by an unrelated process. */
|
|
67
|
-
export async function assertAvailable(url) {
|
|
68
|
-
if (!await occupied(url))
|
|
69
|
-
return;
|
|
70
|
-
throw new Error(`Client development URL is already in use: ${url}`);
|
|
71
|
-
}
|
|
72
|
-
/** Wait until a sandboxed Program iframe can load the development Client. */
|
|
73
|
-
export async function* waitForDevelopmentClient(config, client, signal) {
|
|
74
|
-
const began = Date.now();
|
|
75
|
-
let nextReport = began + reportingInterval;
|
|
76
|
-
while (Date.now() - began < readinessTimeout) {
|
|
77
|
-
throwIfAborted(signal);
|
|
78
|
-
for (const event of client?.drain() ?? [])
|
|
79
|
-
yield event;
|
|
80
|
-
const exit = client?.exitResult();
|
|
81
|
-
if (exit && !client?.endingWasRequested())
|
|
82
|
-
throw commandFailure(exit);
|
|
83
|
-
const availability = await inspect(config.url, readinessTimeout - (Date.now() - began));
|
|
84
|
-
if (availability === "ready")
|
|
85
|
-
return;
|
|
86
|
-
if (availability === "cors-blocked") {
|
|
87
|
-
throw new Error([
|
|
88
|
-
`Client development URL responded, but does not allow the sandboxed Client origin: ${config.url}`,
|
|
89
|
-
"Enable CORS so the response includes Access-Control-Allow-Origin: *."
|
|
90
|
-
].join("\n"));
|
|
91
|
-
}
|
|
92
|
-
const now = Date.now();
|
|
93
|
-
if (now >= nextReport) {
|
|
94
|
-
yield { event: "waiting", subject: "client", url: config.url };
|
|
95
|
-
while (nextReport <= now)
|
|
96
|
-
nextReport += reportingInterval;
|
|
97
|
-
}
|
|
98
|
-
await pause(Math.min(pollingInterval, readinessTimeout - (now - began)), signal);
|
|
99
|
-
}
|
|
100
|
-
throw new Error(`Client development URL did not respond within 15 seconds: ${config.url}`);
|
|
101
|
-
}
|
|
102
|
-
export function commandFailure(exit) {
|
|
103
|
-
if (exit.error)
|
|
104
|
-
return new Error(`Client development command failed: ${exit.error.message}`);
|
|
105
|
-
if (exit.signal)
|
|
106
|
-
return new Error(`Client development command ended on ${exit.signal}`);
|
|
107
|
-
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
108
|
-
}
|
|
109
|
-
function outputEvent(stream, chunk) {
|
|
110
|
-
return { event: "output", stream, text: String(chunk) };
|
|
111
|
-
}
|
|
112
|
-
function commandEnvironment(directory) {
|
|
113
|
-
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
114
|
-
const inherited = process.env[key];
|
|
115
|
-
return { ...process.env, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
|
|
116
|
-
}
|
|
117
|
-
async function occupied(url) {
|
|
118
|
-
const location = new URL(url);
|
|
119
|
-
const port = Number(location.port || (location.protocol === "https:" ? 443 : 80));
|
|
120
|
-
return await new Promise(resolve => {
|
|
121
|
-
const socket = connect({ host: location.hostname, port });
|
|
122
|
-
let done = false;
|
|
123
|
-
const finish = (value) => {
|
|
124
|
-
if (done)
|
|
125
|
-
return;
|
|
126
|
-
done = true;
|
|
127
|
-
socket.destroy();
|
|
128
|
-
resolve(value);
|
|
129
|
-
};
|
|
130
|
-
socket.setTimeout(500);
|
|
131
|
-
socket.once("connect", () => finish(true));
|
|
132
|
-
socket.once("error", () => finish(false));
|
|
133
|
-
socket.once("timeout", () => finish(false));
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
async function inspect(url, remaining) {
|
|
137
|
-
try {
|
|
138
|
-
const response = await fetch(url, {
|
|
139
|
-
headers: { origin: sandboxedClientOrigin },
|
|
140
|
-
signal: AbortSignal.timeout(Math.max(1, Math.min(500, remaining)))
|
|
141
|
-
});
|
|
142
|
-
const allowedOrigin = response.headers.get("access-control-allow-origin")?.trim();
|
|
143
|
-
await response.body?.cancel();
|
|
144
|
-
return allowedOrigin === "*" || allowedOrigin === sandboxedClientOrigin ? "ready" : "cors-blocked";
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return "unavailable";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function terminate(child, signal) {
|
|
151
|
-
if (!child.pid)
|
|
152
|
-
return;
|
|
153
|
-
try {
|
|
154
|
-
process.kill(-child.pid, signal);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
child.kill(signal);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function running(child) {
|
|
161
|
-
if (!child.pid)
|
|
162
|
-
return false;
|
|
163
|
-
try {
|
|
164
|
-
process.kill(-child.pid, 0);
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return child.exitCode === null && child.signalCode === null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function waitUntilStopped(child, milliseconds) {
|
|
172
|
-
const deadline = Date.now() + milliseconds;
|
|
173
|
-
while (running(child) && Date.now() < deadline)
|
|
174
|
-
await new Promise(resolve => setTimeout(resolve, 20));
|
|
175
|
-
}
|
|
176
|
-
function pause(milliseconds, signal) {
|
|
177
|
-
return new Promise((resolve, reject) => {
|
|
178
|
-
const timer = setTimeout(finish, Math.max(0, milliseconds));
|
|
179
|
-
const cancel = () => {
|
|
180
|
-
cleanup();
|
|
181
|
-
reject(signal?.reason instanceof Error ? signal.reason : new Error("The operation was cancelled"));
|
|
182
|
-
};
|
|
183
|
-
const cleanup = () => {
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
signal?.removeEventListener("abort", cancel);
|
|
186
|
-
};
|
|
187
|
-
function finish() { cleanup(); resolve(); }
|
|
188
|
-
if (signal?.aborted)
|
|
189
|
-
cancel();
|
|
190
|
-
else
|
|
191
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function throwIfAborted(signal) {
|
|
195
|
-
if (signal?.aborted)
|
|
196
|
-
throw signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled");
|
|
197
|
-
}
|