@loadbare/app 0.21.0 → 0.23.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/README.md +18 -0
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +8 -3
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/build.d.ts +78 -0
- package/dist/build/build.d.ts.map +1 -0
- package/dist/build/build.js +220 -0
- package/dist/build/build.js.map +1 -0
- package/dist/build/cli.d.ts +6 -1
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +25 -78
- package/dist/build/cli.js.map +1 -1
- package/dist/build/declared.d.ts.map +1 -1
- package/dist/build/declared.js +3 -0
- package/dist/build/declared.js.map +1 -1
- package/dist/build/skills-cli.d.ts +2 -1
- package/dist/build/skills-cli.d.ts.map +1 -1
- package/dist/build/skills-cli.js +8 -1
- package/dist/build/skills-cli.js.map +1 -1
- package/dist/build/skills.d.ts +2 -0
- package/dist/build/skills.d.ts.map +1 -1
- package/dist/build/skills.js +2 -0
- package/dist/build/skills.js.map +1 -1
- package/dist/core/lb-constants.d.ts +14 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +25 -0
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/dev/lb-dev.d.ts +128 -0
- package/dist/dev/lb-dev.d.ts.map +1 -0
- package/dist/dev/lb-dev.js +289 -0
- package/dist/dev/lb-dev.js.map +1 -0
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +12 -6
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +119 -15
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/docs/comparison.md +4 -3
- package/docs/miswarts.md +3 -2
- package/docs/reference/builder.md +12 -4
- package/docs/reference/chrome.md +52 -1
- package/docs/reference/data-binding.md +28 -18
- package/docs/reference/dev-server.md +178 -0
- package/docs/reference/names.md +14 -1
- package/docs/reference/overview.md +1 -0
- package/docs/reference/server.md +8 -3
- package/docs/reference/widgets.md +10 -277
- package/docs/temp-proposals.md +251 -0
- package/docs/testing.md +2 -1
- package/docs/tutorials/000-getting-started.md +1 -1
- package/docs/tutorials/040-displaying-data.md +1 -1
- package/docs/tutorials/090-using-widget-libraries.md +1 -1
- package/package.json +4 -2
- package/skills/loadbare-app/SKILL.md +30 -11
- package/skills/loadbare-app/references/builder.md +12 -4
- package/skills/loadbare-app/references/chrome.md +52 -1
- package/skills/loadbare-app/references/data-binding.md +28 -18
- package/skills/loadbare-app/references/dev-server.md +178 -0
- package/skills/loadbare-app/references/miswarts.md +3 -2
- package/skills/loadbare-app/references/names.md +14 -1
- package/skills/loadbare-app/references/overview.md +1 -0
- package/skills/loadbare-app/references/server.md +8 -3
- 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-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"AAoFA,OAAO,EASL,KAAK,WAAW,EAKhB,KAAK,YAAY,EACjB,KAAK,GAAG,EACR,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AA0B7B,mEAAmE;AACnE,MAAM,WAAW,OAAQ,SAAQ,OAAO;IACtC,KAAK,EAAE,MAAM,CAAC;CACf;AA4BD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAE5C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAGrD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,EAAE,IAAI,OAAO,CAIpD;AAUD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,OAAO,GACV,IAAI,CAMN;
|
|
1
|
+
{"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"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"}
|
package/dist/hub/lb-apply.js
CHANGED
|
@@ -141,9 +141,11 @@ function land(el, value) {
|
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
if (isCheckbox(el)) {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
144
|
+
// Off for false, and for what an unchecked checkbox gathers or an absent
|
|
145
|
+
// query parm lands as, so a checkbox lands what it gathered even from a
|
|
146
|
+
// query whose values are strings, such as lb-url.
|
|
147
|
+
el.checked =
|
|
148
|
+
value !== false && value !== "false" && value !== "";
|
|
147
149
|
}
|
|
148
150
|
else if (isRadio(el)) {
|
|
149
151
|
el.checked =
|
|
@@ -1014,7 +1016,8 @@ function hide(el, hidden) {
|
|
|
1014
1016
|
}
|
|
1015
1017
|
/**
|
|
1016
1018
|
* Finish a group and everything under it, from the rows it shows: each
|
|
1017
|
-
* heading filled from the first, as it was when last placed by
|
|
1019
|
+
* heading filled and linked from the first, as it was when last placed by
|
|
1020
|
+
* the order,
|
|
1018
1021
|
* each aggregate set from all of them, and a group hidden when a filter hides
|
|
1019
1022
|
* every row in it. The rows it shows are returned.
|
|
1020
1023
|
*/
|
|
@@ -1032,9 +1035,12 @@ function finish(group, types) {
|
|
|
1032
1035
|
}
|
|
1033
1036
|
}
|
|
1034
1037
|
}
|
|
1035
|
-
if (shown[0])
|
|
1036
|
-
for (const head of group.heads)
|
|
1038
|
+
if (shown[0]) {
|
|
1039
|
+
for (const head of group.heads) {
|
|
1037
1040
|
fill(head, shown[0].placed);
|
|
1041
|
+
link(head, shown[0].placed);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1038
1044
|
aggregate(group.aggregates, rows, types);
|
|
1039
1045
|
return shown;
|
|
1040
1046
|
}
|