@loadbare/app 0.21.0 → 0.22.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 (57) hide show
  1. package/README.md +18 -0
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +8 -3
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/build.d.ts +62 -0
  6. package/dist/build/build.d.ts.map +1 -0
  7. package/dist/build/build.js +148 -0
  8. package/dist/build/build.js.map +1 -0
  9. package/dist/build/cli.d.ts +6 -1
  10. package/dist/build/cli.d.ts.map +1 -1
  11. package/dist/build/cli.js +25 -78
  12. package/dist/build/cli.js.map +1 -1
  13. package/dist/build/declared.d.ts.map +1 -1
  14. package/dist/build/declared.js +3 -0
  15. package/dist/build/declared.js.map +1 -1
  16. package/dist/build/skills-cli.d.ts +2 -1
  17. package/dist/build/skills-cli.d.ts.map +1 -1
  18. package/dist/build/skills-cli.js +8 -1
  19. package/dist/build/skills-cli.js.map +1 -1
  20. package/dist/build/skills.d.ts +2 -0
  21. package/dist/build/skills.d.ts.map +1 -1
  22. package/dist/build/skills.js +2 -0
  23. package/dist/build/skills.js.map +1 -1
  24. package/dist/core/lb-constants.d.ts +14 -0
  25. package/dist/core/lb-constants.d.ts.map +1 -1
  26. package/dist/core/lb-constants.js +25 -0
  27. package/dist/core/lb-constants.js.map +1 -1
  28. package/dist/dev/lb-dev.d.ts +128 -0
  29. package/dist/dev/lb-dev.d.ts.map +1 -0
  30. package/dist/dev/lb-dev.js +289 -0
  31. package/dist/dev/lb-dev.js.map +1 -0
  32. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  33. package/dist/hub/lb-hub.browser.js +119 -15
  34. package/dist/hub/lb-hub.browser.js.map +1 -1
  35. package/docs/comparison.md +4 -3
  36. package/docs/reference/builder.md +12 -4
  37. package/docs/reference/chrome.md +52 -1
  38. package/docs/reference/data-binding.md +15 -9
  39. package/docs/reference/dev-server.md +171 -0
  40. package/docs/reference/names.md +14 -1
  41. package/docs/reference/overview.md +1 -0
  42. package/docs/reference/server.md +8 -3
  43. package/docs/reference/widgets.md +10 -277
  44. package/docs/testing.md +2 -1
  45. package/docs/tutorials/000-getting-started.md +1 -1
  46. package/docs/tutorials/040-displaying-data.md +1 -1
  47. package/docs/tutorials/090-using-widget-libraries.md +1 -1
  48. package/package.json +4 -2
  49. package/skills/loadbare-app/SKILL.md +28 -10
  50. package/skills/loadbare-app/references/builder.md +12 -4
  51. package/skills/loadbare-app/references/chrome.md +52 -1
  52. package/skills/loadbare-app/references/data-binding.md +15 -9
  53. package/skills/loadbare-app/references/dev-server.md +171 -0
  54. package/skills/loadbare-app/references/names.md +14 -1
  55. package/skills/loadbare-app/references/overview.md +1 -0
  56. package/skills/loadbare-app/references/server.md +8 -3
  57. package/skills/loadbare-app/references/widgets.md +10 -277
@@ -0,0 +1,128 @@
1
+ /**
2
+ * A development server for a Loadbare application: after any change, the
3
+ * developer refreshes the browser, and never restarts anything.
4
+ *
5
+ * browser --> devProxy :port --> appServer :appPort (a child process)
6
+ * |
7
+ * +-- watchApp (in this process)
8
+ *
9
+ * `devServer` is the whole recipe. It is built from four parts, each
10
+ * exported, so an application that wants something else assembles its own:
11
+ *
12
+ * watchApp @loadbare/app/build: rebuilds `dist/`, and says when each
13
+ * build starts and ends
14
+ * appServer the application's own server as a child process, restarted
15
+ * on demand
16
+ * gate holds requests while a build runs or the server restarts
17
+ * devProxy the port the browser uses, passing requests through the gate
18
+ *
19
+ * Why only the server restarts: `app.html`, `client.js` and `app.css` are
20
+ * read from disk on each request, so a rebuild is all they need. The
21
+ * server's own modules are loaded once, and Node cannot unload a module, so
22
+ * a server-side change needs a new process. The URL holds the page's state,
23
+ * so a refresh puts the user back where they were.
24
+ *
25
+ * The application's server runs as it does in production, unchanged, apart
26
+ * from reading its port from the environment.
27
+ */
28
+ import http from "node:http";
29
+ export interface Gate {
30
+ /** Hold requests from now until this close is matched by an open. */
31
+ close(reason: string): void;
32
+ /** Match one close of this reason. Requests go on once none is left. */
33
+ open(reason: string): void;
34
+ /** Resolves once the gate is open. */
35
+ wait(): Promise<void>;
36
+ readonly isOpen: boolean;
37
+ }
38
+ /**
39
+ * Requests wait while any reason to hold them stands: a build running, the
40
+ * server starting. Each close is matched by its own open, so one ending
41
+ * does not open the gate on another still in progress, of the same reason
42
+ * or another.
43
+ */
44
+ export declare function gate(): Gate;
45
+ export interface AppServerOptions {
46
+ /** The server's entry file, such as `server.ts`. */
47
+ entry: string;
48
+ /** The port it listens on. */
49
+ port: number;
50
+ /** The environment variable it reads its port from, `PORT` when absent. */
51
+ portEnv?: string;
52
+ /** Node's arguments before the entry, `--env-file-if-exists=.env` when absent. */
53
+ node?: string[];
54
+ /** Where its progress goes. */
55
+ log?: (line: string) => void;
56
+ }
57
+ export type AppServerState = "starting" | "running" | "down" | "stopped";
58
+ export interface AppServer {
59
+ /**
60
+ * Stop the server if it runs, and start it again. Resolves once it
61
+ * accepts connections, or once it has exited instead.
62
+ */
63
+ restart(): Promise<void>;
64
+ /** Stop it, for good: a restart asked for after this does nothing. */
65
+ stop(): Promise<void>;
66
+ /** `down` is a server that exited on its own, as on a throw at import. */
67
+ readonly state: AppServerState;
68
+ }
69
+ /**
70
+ * The application's server as a child process. The child never outlives
71
+ * this process, however this process ends: it is killed on exit as well as
72
+ * on stop.
73
+ */
74
+ export declare function appServer(options: AppServerOptions): AppServer;
75
+ export interface DevProxyOptions {
76
+ /** The port the browser uses. */
77
+ port: number;
78
+ /** The host to listen on, `127.0.0.1` when absent. */
79
+ host?: string;
80
+ /** Where requests go on to: the application's server. */
81
+ target: number;
82
+ /** Requests wait at it while it is closed. */
83
+ gate: Gate;
84
+ /** True while the application's server is down, which answers 502. */
85
+ isDown: () => boolean;
86
+ }
87
+ /** Said to the browser while the application's server is down. */
88
+ export declare const DOWN_MESSAGE = "The app server is down; see the dev console.";
89
+ /**
90
+ * Listen on the browser's port, and pass every request through the gate to
91
+ * the application's server, method, path, headers and body as they came.
92
+ * Resolves once listening; rejects with a plain message when the port is in
93
+ * use.
94
+ */
95
+ export declare function devProxy(options: DevProxyOptions): Promise<http.Server>;
96
+ export interface DevServerOptions {
97
+ /** The application's server entry file, such as `server.ts`. */
98
+ server: string;
99
+ /** The port the browser uses: `PORT` from the environment, or 8787. */
100
+ port?: number;
101
+ /** The host the browser's port listens on, `127.0.0.1` when absent. */
102
+ host?: string;
103
+ /** The port the application's server gets, `port + 10000` when absent. */
104
+ appPort?: number;
105
+ /** The environment variable the application's server reads its port from, `PORT` when absent. */
106
+ portEnv?: string;
107
+ /** Node's arguments before the entry, `--env-file-if-exists=.env` when absent. */
108
+ node?: string[];
109
+ /** The builder's source tree, `src` when absent. */
110
+ src?: string;
111
+ /** The builder's output, `dist` when absent. */
112
+ out?: string;
113
+ /** Where progress goes, the console when absent. */
114
+ log?: (line: string) => void;
115
+ }
116
+ export interface DevServer {
117
+ /** The port the browser uses. */
118
+ port: number;
119
+ /** Stop everything: the watch, the application's server and the port. */
120
+ close(): Promise<void>;
121
+ }
122
+ /**
123
+ * The whole dev server: watch and rebuild, restart the application's
124
+ * server when a server-side file changed, and hold every request while
125
+ * either is under way. Stops everything on Ctrl-C or a kill.
126
+ */
127
+ export declare function devServer(options: DevServerOptions): Promise<DevServer>;
128
+ //# sourceMappingURL=lb-dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-dev.d.ts","sourceRoot":"","sources":["../../dev/lb-dev.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,IAAI,MAAM,WAAW,CAAC;AAO7B,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wEAAwE;IACxE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,sCAAsC;IACtC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,IAAI,IAAI,CAwB3B;AAID,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,+BAA+B;IAC/B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,sEAAsE;IACtE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CA0D9D;AAoBD,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,IAAI,EAAE,IAAI,CAAC;IACX,sEAAsE;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC;CACvB;AAED,kEAAkE;AAClE,eAAO,MAAM,YAAY,iDAAiD,CAAC;AAE3E;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAqCvE;AAID,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,SAAS;IACxB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAWD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CA4F7E"}
@@ -0,0 +1,289 @@
1
+ /**
2
+ * A development server for a Loadbare application: after any change, the
3
+ * developer refreshes the browser, and never restarts anything.
4
+ *
5
+ * browser --> devProxy :port --> appServer :appPort (a child process)
6
+ * |
7
+ * +-- watchApp (in this process)
8
+ *
9
+ * `devServer` is the whole recipe. It is built from four parts, each
10
+ * exported, so an application that wants something else assembles its own:
11
+ *
12
+ * watchApp @loadbare/app/build: rebuilds `dist/`, and says when each
13
+ * build starts and ends
14
+ * appServer the application's own server as a child process, restarted
15
+ * on demand
16
+ * gate holds requests while a build runs or the server restarts
17
+ * devProxy the port the browser uses, passing requests through the gate
18
+ *
19
+ * Why only the server restarts: `app.html`, `client.js` and `app.css` are
20
+ * read from disk on each request, so a rebuild is all they need. The
21
+ * server's own modules are loaded once, and Node cannot unload a module, so
22
+ * a server-side change needs a new process. The URL holds the page's state,
23
+ * so a refresh puts the user back where they were.
24
+ *
25
+ * The application's server runs as it does in production, unchanged, apart
26
+ * from reading its port from the environment.
27
+ */
28
+ import { spawn } from "node:child_process";
29
+ import { watch } from "node:fs";
30
+ import { readFile } from "node:fs/promises";
31
+ import http from "node:http";
32
+ import net from "node:net";
33
+ import path from "node:path";
34
+ import { watchApp } from "../build/build.js";
35
+ /**
36
+ * Requests wait while any reason to hold them stands: a build running, the
37
+ * server starting. Each close is matched by its own open, so one ending
38
+ * does not open the gate on another still in progress, of the same reason
39
+ * or another.
40
+ */
41
+ export function gate() {
42
+ const reasons = new Map();
43
+ let waiting = [];
44
+ return {
45
+ close(reason) {
46
+ reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
47
+ },
48
+ open(reason) {
49
+ const left = (reasons.get(reason) ?? 0) - 1;
50
+ if (left > 0)
51
+ reasons.set(reason, left);
52
+ else
53
+ reasons.delete(reason);
54
+ if (reasons.size > 0)
55
+ return;
56
+ for (const go of waiting)
57
+ go();
58
+ waiting = [];
59
+ },
60
+ wait() {
61
+ return reasons.size === 0
62
+ ? Promise.resolve()
63
+ : new Promise((go) => waiting.push(go));
64
+ },
65
+ get isOpen() {
66
+ return reasons.size === 0;
67
+ },
68
+ };
69
+ }
70
+ /**
71
+ * The application's server as a child process. The child never outlives
72
+ * this process, however this process ends: it is killed on exit as well as
73
+ * on stop.
74
+ */
75
+ export function appServer(options) {
76
+ const { entry, port, portEnv = "PORT", node = ["--env-file-if-exists=.env"], log = () => { }, } = options;
77
+ let child = null;
78
+ let state = "stopped";
79
+ let stopped = false;
80
+ const kill = () => child?.kill();
81
+ process.on("exit", kill);
82
+ /** Stop the child that runs now, and wait for it to go. */
83
+ const end = async () => {
84
+ const old = child;
85
+ // Forgotten first, so its exit is not taken for a crash.
86
+ child = null;
87
+ if (!old || old.exitCode !== null || old.signalCode !== null)
88
+ return;
89
+ await new Promise((done) => {
90
+ old.once("exit", done);
91
+ old.kill();
92
+ });
93
+ };
94
+ return {
95
+ async restart() {
96
+ if (stopped)
97
+ return;
98
+ const again = child !== null;
99
+ await end();
100
+ if (again)
101
+ log("restarting the app server");
102
+ state = "starting";
103
+ const started = spawn(process.execPath, [...node, entry], {
104
+ stdio: "inherit",
105
+ env: { ...process.env, [portEnv]: String(port) },
106
+ });
107
+ child = started;
108
+ started.once("exit", (code) => {
109
+ // An exit asked for is the restart's or the stop's business.
110
+ if (child !== started)
111
+ return;
112
+ child = null;
113
+ state = "down";
114
+ log(`the app server stopped (exit ${code}); save a fix to restart it`);
115
+ });
116
+ await listening(port, () => child !== started);
117
+ if (child === started && !stopped)
118
+ state = "running";
119
+ },
120
+ async stop() {
121
+ stopped = true;
122
+ state = "stopped";
123
+ await end();
124
+ process.off("exit", kill);
125
+ },
126
+ get state() {
127
+ return state;
128
+ },
129
+ };
130
+ }
131
+ /** Resolves once something accepts connections on the port, or `abandon` says to give up. */
132
+ async function listening(port, abandon) {
133
+ while (!abandon()) {
134
+ const up = await new Promise((done) => {
135
+ const socket = net.connect(port, "127.0.0.1");
136
+ socket.once("connect", () => {
137
+ socket.end();
138
+ done(true);
139
+ });
140
+ socket.once("error", () => done(false));
141
+ });
142
+ if (up)
143
+ return;
144
+ await new Promise((wait) => setTimeout(wait, 50));
145
+ }
146
+ }
147
+ /** Said to the browser while the application's server is down. */
148
+ export const DOWN_MESSAGE = "The app server is down; see the dev console.";
149
+ /**
150
+ * Listen on the browser's port, and pass every request through the gate to
151
+ * the application's server, method, path, headers and body as they came.
152
+ * Resolves once listening; rejects with a plain message when the port is in
153
+ * use.
154
+ */
155
+ export function devProxy(options) {
156
+ const { port, host = "127.0.0.1", target, gate, isDown } = options;
157
+ const server = http.createServer(async (req, res) => {
158
+ await gate.wait();
159
+ if (isDown()) {
160
+ res.writeHead(502, { "content-type": "text/plain" }).end(DOWN_MESSAGE);
161
+ return;
162
+ }
163
+ const ahead = http.request({
164
+ host: "127.0.0.1",
165
+ port: target,
166
+ method: req.method,
167
+ path: req.url,
168
+ headers: req.headers,
169
+ }, (answer) => {
170
+ res.writeHead(answer.statusCode ?? 502, answer.headers);
171
+ answer.pipe(res);
172
+ });
173
+ ahead.on("error", () => {
174
+ if (!res.headersSent)
175
+ res.writeHead(502, { "content-type": "text/plain" });
176
+ res.end(DOWN_MESSAGE);
177
+ });
178
+ req.pipe(ahead);
179
+ });
180
+ return new Promise((resolve, reject) => {
181
+ server.once("error", (error) => {
182
+ reject(error.code === "EADDRINUSE"
183
+ ? new Error(`port ${port} is in use; is another dev server running?`)
184
+ : error);
185
+ });
186
+ server.listen(port, host, () => resolve(server));
187
+ });
188
+ }
189
+ /**
190
+ * Whether a build's changes need the server restarted: a module the server
191
+ * loads. Markup, stylesheets and browser code are read from disk per
192
+ * request, or bundled into a file that is.
193
+ */
194
+ function serverSide(file) {
195
+ return file.endsWith(".ts") && !file.endsWith(".browser.ts");
196
+ }
197
+ /**
198
+ * The whole dev server: watch and rebuild, restart the application's
199
+ * server when a server-side file changed, and hold every request while
200
+ * either is under way. Stops everything on Ctrl-C or a kill.
201
+ */
202
+ export async function devServer(options) {
203
+ const port = options.port ?? Number(process.env.PORT || 8787);
204
+ const appPort = options.appPort ?? port + 10000;
205
+ const out = options.out ?? "dist";
206
+ const log = options.log ?? ((line) => console.log(`loadbare-dev: ${line}`));
207
+ const entry = path.resolve(options.server);
208
+ const pagesFile = path.resolve(out, "pages.ts");
209
+ const readPages = () => readFile(pagesFile, "utf-8").catch(() => null);
210
+ const held = gate();
211
+ held.close("build");
212
+ const app = appServer({
213
+ entry,
214
+ port: appPort,
215
+ portEnv: options.portEnv,
216
+ node: options.node,
217
+ log,
218
+ });
219
+ // One restart at a time, in the order asked for: a second asked for while
220
+ // one runs follows it, rather than killing the server it is starting.
221
+ let restarts = Promise.resolve();
222
+ const restart = () => {
223
+ held.close("server");
224
+ restarts = restarts
225
+ .then(() => app.restart())
226
+ .finally(() => held.open("server"));
227
+ return restarts;
228
+ };
229
+ // Started first, so a busy port is reported before anything else runs.
230
+ const proxy = await devProxy({
231
+ port,
232
+ host: options.host,
233
+ target: appPort,
234
+ gate: held,
235
+ isDown: () => app.state === "down",
236
+ });
237
+ let pages = null;
238
+ const watcher = watchApp({
239
+ src: options.src,
240
+ out,
241
+ onStart: () => held.close("build"),
242
+ onBuilt: (outcome) => {
243
+ if (outcome.ok)
244
+ log(outcome.built.summary);
245
+ else
246
+ log(`build failed, serving the last good build: ${outcome.error.message}`);
247
+ void (async () => {
248
+ // The registry the server imports names the page modules, so a page
249
+ // gained or lost changes it without any server-side file changing.
250
+ const now = await readPages();
251
+ const restartNeeded = app.state !== "running" ||
252
+ now !== pages ||
253
+ outcome.changed.some(serverSide);
254
+ pages = now;
255
+ // The first build's server is started once the watch is ready.
256
+ if (restartNeeded && outcome.changed.length > 0)
257
+ await restart();
258
+ held.open("build");
259
+ })();
260
+ },
261
+ });
262
+ // The entry's directory, not the file: an editor that saves by replacing
263
+ // the file would leave a watcher on the one it replaced.
264
+ const entryWatcher = watch(path.dirname(entry), (_event, file) => {
265
+ if (file === path.basename(entry))
266
+ void restart();
267
+ });
268
+ await watcher.ready;
269
+ pages = await readPages();
270
+ await restart();
271
+ held.open("build");
272
+ log(`http://${options.host ?? "127.0.0.1"}:${port}, app server on ${appPort}`);
273
+ const close = async () => {
274
+ watcher.close();
275
+ entryWatcher.close();
276
+ await app.stop();
277
+ // A browser holds its connection open between requests, and close()
278
+ // waits for every open connection to end.
279
+ proxy.closeAllConnections();
280
+ await new Promise((done) => proxy.close(done));
281
+ process.off("SIGINT", onSignal);
282
+ process.off("SIGTERM", onSignal);
283
+ };
284
+ const onSignal = () => void close().then(() => process.exit(0));
285
+ process.on("SIGINT", onSignal);
286
+ process.on("SIGTERM", onSignal);
287
+ return { port, close };
288
+ }
289
+ //# sourceMappingURL=lb-dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-dev.js","sourceRoot":"","sources":["../../dev/lb-dev.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A development server for a Loadbare application: after any change, the\n * developer refreshes the browser, and never restarts anything.\n *\n * browser --> devProxy :port --> appServer :appPort (a child process)\n * |\n * +-- watchApp (in this process)\n *\n * `devServer` is the whole recipe. It is built from four parts, each\n * exported, so an application that wants something else assembles its own:\n *\n * watchApp @loadbare/app/build: rebuilds `dist/`, and says when each\n * build starts and ends\n * appServer the application's own server as a child process, restarted\n * on demand\n * gate holds requests while a build runs or the server restarts\n * devProxy the port the browser uses, passing requests through the gate\n *\n * Why only the server restarts: `app.html`, `client.js` and `app.css` are\n * read from disk on each request, so a rebuild is all they need. The\n * server's own modules are loaded once, and Node cannot unload a module, so\n * a server-side change needs a new process. The URL holds the page's state,\n * so a refresh puts the user back where they were.\n *\n * The application's server runs as it does in production, unchanged, apart\n * from reading its port from the environment.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { watch } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport http from \"node:http\";\nimport net from \"node:net\";\nimport path from \"node:path\";\nimport { watchApp, type BuildOutcome } from \"../build/build.js\";\n\n// --- the gate ---------------------------------------------------------------\n\nexport interface Gate {\n /** Hold requests from now until this close is matched by an open. */\n close(reason: string): void;\n /** Match one close of this reason. Requests go on once none is left. */\n open(reason: string): void;\n /** Resolves once the gate is open. */\n wait(): Promise<void>;\n readonly isOpen: boolean;\n}\n\n/**\n * Requests wait while any reason to hold them stands: a build running, the\n * server starting. Each close is matched by its own open, so one ending\n * does not open the gate on another still in progress, of the same reason\n * or another.\n */\nexport function gate(): Gate {\n const reasons = new Map<string, number>();\n let waiting: (() => void)[] = [];\n return {\n close(reason) {\n reasons.set(reason, (reasons.get(reason) ?? 0) + 1);\n },\n open(reason) {\n const left = (reasons.get(reason) ?? 0) - 1;\n if (left > 0) reasons.set(reason, left);\n else reasons.delete(reason);\n if (reasons.size > 0) return;\n for (const go of waiting) go();\n waiting = [];\n },\n wait() {\n return reasons.size === 0\n ? Promise.resolve()\n : new Promise((go) => waiting.push(go));\n },\n get isOpen() {\n return reasons.size === 0;\n },\n };\n}\n\n// --- the application's server ----------------------------------------------\n\nexport interface AppServerOptions {\n /** The server's entry file, such as `server.ts`. */\n entry: string;\n /** The port it listens on. */\n port: number;\n /** The environment variable it reads its port from, `PORT` when absent. */\n portEnv?: string;\n /** Node's arguments before the entry, `--env-file-if-exists=.env` when absent. */\n node?: string[];\n /** Where its progress goes. */\n log?: (line: string) => void;\n}\n\nexport type AppServerState = \"starting\" | \"running\" | \"down\" | \"stopped\";\n\nexport interface AppServer {\n /**\n * Stop the server if it runs, and start it again. Resolves once it\n * accepts connections, or once it has exited instead.\n */\n restart(): Promise<void>;\n /** Stop it, for good: a restart asked for after this does nothing. */\n stop(): Promise<void>;\n /** `down` is a server that exited on its own, as on a throw at import. */\n readonly state: AppServerState;\n}\n\n/**\n * The application's server as a child process. The child never outlives\n * this process, however this process ends: it is killed on exit as well as\n * on stop.\n */\nexport function appServer(options: AppServerOptions): AppServer {\n const {\n entry,\n port,\n portEnv = \"PORT\",\n node = [\"--env-file-if-exists=.env\"],\n log = () => {},\n } = options;\n let child: ChildProcess | null = null;\n let state: AppServerState = \"stopped\";\n let stopped = false;\n const kill = () => child?.kill();\n process.on(\"exit\", kill);\n\n /** Stop the child that runs now, and wait for it to go. */\n const end = async () => {\n const old = child;\n // Forgotten first, so its exit is not taken for a crash.\n child = null;\n if (!old || old.exitCode !== null || old.signalCode !== null) return;\n await new Promise((done) => {\n old.once(\"exit\", done);\n old.kill();\n });\n };\n\n return {\n async restart() {\n if (stopped) return;\n const again = child !== null;\n await end();\n if (again) log(\"restarting the app server\");\n state = \"starting\";\n const started = spawn(process.execPath, [...node, entry], {\n stdio: \"inherit\",\n env: { ...process.env, [portEnv]: String(port) },\n });\n child = started;\n started.once(\"exit\", (code) => {\n // An exit asked for is the restart's or the stop's business.\n if (child !== started) return;\n child = null;\n state = \"down\";\n log(`the app server stopped (exit ${code}); save a fix to restart it`);\n });\n await listening(port, () => child !== started);\n if (child === started && !stopped) state = \"running\";\n },\n async stop() {\n stopped = true;\n state = \"stopped\";\n await end();\n process.off(\"exit\", kill);\n },\n get state() {\n return state;\n },\n };\n}\n\n/** Resolves once something accepts connections on the port, or `abandon` says to give up. */\nasync function listening(port: number, abandon: () => boolean): Promise<void> {\n while (!abandon()) {\n const up = await new Promise<boolean>((done) => {\n const socket = net.connect(port, \"127.0.0.1\");\n socket.once(\"connect\", () => {\n socket.end();\n done(true);\n });\n socket.once(\"error\", () => done(false));\n });\n if (up) return;\n await new Promise((wait) => setTimeout(wait, 50));\n }\n}\n\n// --- the port the browser uses ---------------------------------------------\n\nexport interface DevProxyOptions {\n /** The port the browser uses. */\n port: number;\n /** The host to listen on, `127.0.0.1` when absent. */\n host?: string;\n /** Where requests go on to: the application's server. */\n target: number;\n /** Requests wait at it while it is closed. */\n gate: Gate;\n /** True while the application's server is down, which answers 502. */\n isDown: () => boolean;\n}\n\n/** Said to the browser while the application's server is down. */\nexport const DOWN_MESSAGE = \"The app server is down; see the dev console.\";\n\n/**\n * Listen on the browser's port, and pass every request through the gate to\n * the application's server, method, path, headers and body as they came.\n * Resolves once listening; rejects with a plain message when the port is in\n * use.\n */\nexport function devProxy(options: DevProxyOptions): Promise<http.Server> {\n const { port, host = \"127.0.0.1\", target, gate, isDown } = options;\n const server = http.createServer(async (req, res) => {\n await gate.wait();\n if (isDown()) {\n res.writeHead(502, { \"content-type\": \"text/plain\" }).end(DOWN_MESSAGE);\n return;\n }\n const ahead = http.request(\n {\n host: \"127.0.0.1\",\n port: target,\n method: req.method,\n path: req.url,\n headers: req.headers,\n },\n (answer) => {\n res.writeHead(answer.statusCode ?? 502, answer.headers);\n answer.pipe(res);\n },\n );\n ahead.on(\"error\", () => {\n if (!res.headersSent) res.writeHead(502, { \"content-type\": \"text/plain\" });\n res.end(DOWN_MESSAGE);\n });\n req.pipe(ahead);\n });\n return new Promise((resolve, reject) => {\n server.once(\"error\", (error: NodeJS.ErrnoException) => {\n reject(\n error.code === \"EADDRINUSE\"\n ? new Error(`port ${port} is in use; is another dev server running?`)\n : error,\n );\n });\n server.listen(port, host, () => resolve(server));\n });\n}\n\n// --- the recipe ---------------------------------------------------------------\n\nexport interface DevServerOptions {\n /** The application's server entry file, such as `server.ts`. */\n server: string;\n /** The port the browser uses: `PORT` from the environment, or 8787. */\n port?: number;\n /** The host the browser's port listens on, `127.0.0.1` when absent. */\n host?: string;\n /** The port the application's server gets, `port + 10000` when absent. */\n appPort?: number;\n /** The environment variable the application's server reads its port from, `PORT` when absent. */\n portEnv?: string;\n /** Node's arguments before the entry, `--env-file-if-exists=.env` when absent. */\n node?: string[];\n /** The builder's source tree, `src` when absent. */\n src?: string;\n /** The builder's output, `dist` when absent. */\n out?: string;\n /** Where progress goes, the console when absent. */\n log?: (line: string) => void;\n}\n\nexport interface DevServer {\n /** The port the browser uses. */\n port: number;\n /** Stop everything: the watch, the application's server and the port. */\n close(): Promise<void>;\n}\n\n/**\n * Whether a build's changes need the server restarted: a module the server\n * loads. Markup, stylesheets and browser code are read from disk per\n * request, or bundled into a file that is.\n */\nfunction serverSide(file: string): boolean {\n return file.endsWith(\".ts\") && !file.endsWith(\".browser.ts\");\n}\n\n/**\n * The whole dev server: watch and rebuild, restart the application's\n * server when a server-side file changed, and hold every request while\n * either is under way. Stops everything on Ctrl-C or a kill.\n */\nexport async function devServer(options: DevServerOptions): Promise<DevServer> {\n const port = options.port ?? Number(process.env.PORT || 8787);\n const appPort = options.appPort ?? port + 10000;\n const out = options.out ?? \"dist\";\n const log =\n options.log ?? ((line: string) => console.log(`loadbare-dev: ${line}`));\n const entry = path.resolve(options.server);\n const pagesFile = path.resolve(out, \"pages.ts\");\n const readPages = () => readFile(pagesFile, \"utf-8\").catch(() => null);\n\n const held = gate();\n held.close(\"build\");\n\n const app = appServer({\n entry,\n port: appPort,\n portEnv: options.portEnv,\n node: options.node,\n log,\n });\n // One restart at a time, in the order asked for: a second asked for while\n // one runs follows it, rather than killing the server it is starting.\n let restarts = Promise.resolve();\n const restart = (): Promise<void> => {\n held.close(\"server\");\n restarts = restarts\n .then(() => app.restart())\n .finally(() => held.open(\"server\"));\n return restarts;\n };\n\n // Started first, so a busy port is reported before anything else runs.\n const proxy = await devProxy({\n port,\n host: options.host,\n target: appPort,\n gate: held,\n isDown: () => app.state === \"down\",\n });\n\n let pages: string | null = null;\n const watcher = watchApp({\n src: options.src,\n out,\n onStart: () => held.close(\"build\"),\n onBuilt: (outcome: BuildOutcome) => {\n if (outcome.ok) log(outcome.built.summary);\n else log(`build failed, serving the last good build: ${outcome.error.message}`);\n void (async () => {\n // The registry the server imports names the page modules, so a page\n // gained or lost changes it without any server-side file changing.\n const now = await readPages();\n const restartNeeded =\n app.state !== \"running\" ||\n now !== pages ||\n outcome.changed.some(serverSide);\n pages = now;\n // The first build's server is started once the watch is ready.\n if (restartNeeded && outcome.changed.length > 0) await restart();\n held.open(\"build\");\n })();\n },\n });\n\n // The entry's directory, not the file: an editor that saves by replacing\n // the file would leave a watcher on the one it replaced.\n const entryWatcher = watch(path.dirname(entry), (_event, file) => {\n if (file === path.basename(entry)) void restart();\n });\n\n await watcher.ready;\n pages = await readPages();\n await restart();\n held.open(\"build\");\n log(`http://${options.host ?? \"127.0.0.1\"}:${port}, app server on ${appPort}`);\n\n const close = async () => {\n watcher.close();\n entryWatcher.close();\n await app.stop();\n // A browser holds its connection open between requests, and close()\n // waits for every open connection to end.\n proxy.closeAllConnections();\n await new Promise((done) => proxy.close(done));\n process.off(\"SIGINT\", onSignal);\n process.off(\"SIGTERM\", onSignal);\n };\n const onSignal = () => void close().then(() => process.exit(0));\n process.on(\"SIGINT\", onSignal);\n process.on(\"SIGTERM\", onSignal);\n\n return { port, close };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA4EA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA0FA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}