jq79 0.3.26 → 0.3.28

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 CHANGED
@@ -10,7 +10,7 @@
10
10
  [![esm](https://jgermade.github.io/jq79/badges/esm-size.svg)](https://jgermade.github.io/jq79/)
11
11
  [![cjs](https://jgermade.github.io/jq79/badges/cjs-size.svg)](https://jgermade.github.io/jq79/)
12
12
 
13
- A mini reactive component library that ships as a single file. Svelte-style reactive scripts, fine-grained DOM updates via proxy-based dependency tracking
13
+ A independent reactive component library that ships as a single file. Svelte-style reactive scripts, fine-grained DOM updates via proxy-based dependency tracking
14
14
 
15
15
  > no compiler required, no virtual DOM, no dependencies.
16
16
 
@@ -114,6 +114,7 @@ When the fetch resolves, the assignments to `firstName`/`lastName` re-run the `$
114
114
  - [Reactive data](docs/reactive-data.md) — the standalone `$reactive` store: `$on`, `$onAny`, `$effect`.
115
115
  - [DOM helpers](docs/dom-helpers.md) — `$`, `$$` and `$create`.
116
116
  - [Vite plugin](docs/vite-plugin.md) — importing `.html` components as bundled modules, HMR, options.
117
+ - [Dev server](docs/dev-server.md) — `npx jq79 dev`: serve and hot-reload components with no build step.
117
118
  - [Development](docs/development.md) — running tests, building, publishing releases.
118
119
 
119
120
  ## License
package/dev/cli.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { devServer } from "./dev"
2
+
3
+ // the jq79 command. One subcommand so far:
4
+ //
5
+ // npx jq79 dev [dir] [--port 4179] [--host localhost]
6
+ //
7
+ // This is the entry point the no-bundle path deserves: someone who chose jq79
8
+ // to avoid a toolchain shouldn't have to write a script to get a dev server.
9
+
10
+ const USAGE = `jq79 - a mini reactive component library
11
+
12
+ usage:
13
+ jq79 dev [dir] serve dir (default: .) with hot reload
14
+
15
+ options:
16
+ -p, --port <port> port to listen on (default: 4179)
17
+ -H, --host <host> host to bind (default: localhost)
18
+ -h, --help show this message
19
+ `
20
+
21
+ const args = process.argv.slice(2)
22
+ const [command, ...rest] = args
23
+
24
+ if (command !== "dev" || rest.includes("-h") || rest.includes("--help")) {
25
+ const unknown = command && command !== "dev" && !command.startsWith("-")
26
+ if (unknown) console.error(`unknown command: ${command}\n`)
27
+ console.log(USAGE)
28
+ process.exit(unknown ? 1 : 0)
29
+ }
30
+
31
+ const options: { rootDir?: string; port?: number; host?: string } = {}
32
+
33
+ for (let i = 0; i < rest.length; i++) {
34
+ const arg = rest[i]
35
+ if (arg === "-p" || arg === "--port") options.port = Number(rest[++i])
36
+ else if (arg === "-H" || arg === "--host") options.host = rest[++i]
37
+ else if (!arg.startsWith("-")) options.rootDir ??= arg // the directory to serve
38
+ else {
39
+ console.error(`unknown option: ${arg}\n${USAGE}`)
40
+ process.exit(1)
41
+ }
42
+ }
43
+
44
+ if (options.port !== undefined && !Number.isInteger(options.port)) {
45
+ console.error("--port takes a number")
46
+ process.exit(1)
47
+ }
48
+
49
+ const server = await devServer(options)
50
+
51
+ console.log(`jq79 dev → ${server.url}`)
52
+
53
+ const stop = () => {
54
+ void server.close().then(() => process.exit(0))
55
+ }
56
+ process.on("SIGINT", stop)
57
+ process.on("SIGTERM", stop)
package/dev/dev.ts ADDED
@@ -0,0 +1,252 @@
1
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"
2
+ import { readFile, realpath, stat } from "node:fs/promises"
3
+ import { watch, type FSWatcher } from "node:fs"
4
+ import { basename, extname, join, relative, resolve, sep } from "node:path"
5
+
6
+ // A dev server for the no-bundle path: serve a directory of .html components
7
+ // over HTTP, watch it, and hot-reload the components that changed.
8
+ //
9
+ // npx jq79 dev // the CLI
10
+ // import { devServer } from "jq79/dev" // or from a script
11
+ // await devServer({ rootDir: "." })
12
+ //
13
+ // It is a static file server and nothing else - no transforms, no bundling, no
14
+ // module graph. Which is the point: the files it serves are the files you would
15
+ // deploy, so what you develop against and what a static host serves are the same
16
+ // bytes. The one thing it adds is the hot-reload channel, and it adds it to
17
+ // *documents* only (a component fetched by the runtime is served verbatim).
18
+ //
19
+ // The reload is fine-grained. On a change the server pushes the new source down
20
+ // an SSE channel and the runtime swaps it into every live instance of that file,
21
+ // keeping its data - see hotUpdate in jq79.ts. Anything the runtime can't place
22
+ // (a page, a stylesheet, a component nothing has mounted yet) falls back to a
23
+ // full page reload.
24
+
25
+ export interface DevServerOptions {
26
+ // the directory to serve, and to watch (default: the current directory)
27
+ rootDir?: string
28
+ // default: 4179, or the first free port after it
29
+ port?: number
30
+ // default: localhost
31
+ host?: string
32
+ }
33
+
34
+ export interface DevServer {
35
+ url: string
36
+ port: number
37
+ close: () => Promise<void>
38
+ }
39
+
40
+ const CONTENT_TYPES: Record<string, string> = {
41
+ ".html": "text/html; charset=utf-8",
42
+ ".js": "text/javascript; charset=utf-8",
43
+ ".mjs": "text/javascript; charset=utf-8",
44
+ ".json": "application/json; charset=utf-8",
45
+ ".css": "text/css; charset=utf-8",
46
+ ".svg": "image/svg+xml",
47
+ ".png": "image/png",
48
+ ".jpg": "image/jpeg",
49
+ ".jpeg": "image/jpeg",
50
+ ".gif": "image/gif",
51
+ ".webp": "image/webp",
52
+ ".avif": "image/avif",
53
+ ".ico": "image/x-icon",
54
+ ".woff": "font/woff",
55
+ ".woff2": "font/woff2",
56
+ ".map": "application/json; charset=utf-8",
57
+ }
58
+
59
+ const CLIENT_URL = "/__jq79/client.js"
60
+ const EVENTS_URL = "/__jq79/events"
61
+
62
+ // Served as a *classic* script, and injected into the <head>: classic scripts
63
+ // run at parse time and module scripts are deferred, so the flag is set before
64
+ // the page's `import ... from "jq79"` evaluates - which is what the runtime
65
+ // waits for before it starts tracking instances. The client can't import the
66
+ // runtime itself: the page's copy may come from a CDN or an import map, and a
67
+ // second copy would have a second, empty registry.
68
+ const CLIENT = `(() => {
69
+ window.__JQ79_HMR_ENABLED__ = true
70
+
71
+ const events = new EventSource(${JSON.stringify(EVENTS_URL)})
72
+
73
+ events.addEventListener("update", event => {
74
+ const { url, src } = JSON.parse(event.data)
75
+ const runtime = window.__JQ79_HMR__
76
+ // no runtime (the page doesn't use jq79), or no live instance from this
77
+ // file (it isn't mounted, or it *is* the page) - nothing to swap into
78
+ const patched = runtime ? runtime.update(url, src) : 0
79
+ if (patched) console.log("[jq79] hot-updated " + url + " (" + patched + (patched === 1 ? " instance)" : " instances)"))
80
+ else location.reload()
81
+ })
82
+
83
+ events.addEventListener("reload", () => location.reload())
84
+ })()`
85
+
86
+ const posix = (path: string) => path.split(sep).join("/")
87
+
88
+ const isDocument = (req: IncomingMessage) => req.headers["sec-fetch-dest"] === "document"
89
+
90
+ // the client goes in the <head> so it is the first thing the page runs. A file
91
+ // with neither <head> nor <body> is still a document a browser will render, so
92
+ // fall back to the top of it rather than skipping the injection
93
+ const injectClient = (html: string): string => {
94
+ const tag = `<script src="${CLIENT_URL}"></script>`
95
+ const open = /<head[^>]*>/i.exec(html) ?? /<body[^>]*>/i.exec(html)
96
+ if (!open) return tag + html
97
+ const at = open.index + open[0].length
98
+ return html.slice(0, at) + tag + html.slice(at)
99
+ }
100
+
101
+ export const devServer = async (options: DevServerOptions = {}): Promise<DevServer> => {
102
+ // the *real* path: the watcher reports what changed relative to the directory
103
+ // it actually opened, so a root reached through a symlink (/tmp and /var are
104
+ // symlinks on macOS) would hand back paths that don't line up with it
105
+ const root = await realpath(resolve(options.rootDir ?? "."))
106
+ const host = options.host ?? "localhost"
107
+
108
+ const clients = new Set<ServerResponse>()
109
+
110
+ const send = (event: string, data: unknown) => {
111
+ const frame = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
112
+ clients.forEach(client => client.write(frame))
113
+ }
114
+
115
+ // --- serving ---------------------------------------------------------------
116
+
117
+ const serveStatic = async (req: IncomingMessage, res: ServerResponse, pathname: string) => {
118
+ // a URL path is not a file path: decode it, then keep the result inside the
119
+ // root (".." in a request must not walk out of the served directory)
120
+ let file: string
121
+ try {
122
+ file = resolve(join(root, decodeURIComponent(pathname)))
123
+ } catch {
124
+ res.writeHead(400).end("bad request")
125
+ return
126
+ }
127
+ if (file !== root && !file.startsWith(root + sep)) {
128
+ res.writeHead(403).end("forbidden")
129
+ return
130
+ }
131
+
132
+ try {
133
+ if ((await stat(file)).isDirectory()) file = join(file, "index.html")
134
+ const body = await readFile(file)
135
+ const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream"
136
+
137
+ // only a navigation gets the hot-reload client. A component is fetched by
138
+ // the runtime (sec-fetch-dest: empty), and it must arrive as written -
139
+ // injecting a <script> into it would make the runtime parse and run it
140
+ const html = type.startsWith("text/html") && isDocument(req)
141
+ const payload = html ? Buffer.from(injectClient(body.toString("utf8"))) : body
142
+
143
+ res.writeHead(200, {
144
+ "content-type": type,
145
+ "content-length": payload.byteLength,
146
+ "cache-control": "no-store", // the file on disk is always the truth here
147
+ })
148
+ res.end(payload)
149
+ } catch {
150
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("not found")
151
+ }
152
+ }
153
+
154
+ const server: Server = createServer((req, res) => {
155
+ const pathname = (req.url ?? "/").split(/[?#]/)[0]
156
+
157
+ if (pathname === CLIENT_URL) {
158
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" })
159
+ res.end(CLIENT)
160
+ return
161
+ }
162
+
163
+ if (pathname === EVENTS_URL) {
164
+ res.writeHead(200, {
165
+ "content-type": "text/event-stream",
166
+ "cache-control": "no-store",
167
+ connection: "keep-alive",
168
+ })
169
+ res.write(": jq79\n\n") // opens the stream, so the browser fires onopen
170
+ clients.add(res)
171
+ req.on("close", () => clients.delete(res))
172
+ return
173
+ }
174
+
175
+ void serveStatic(req, res, pathname)
176
+ })
177
+
178
+ // --- watching --------------------------------------------------------------
179
+
180
+ const ignored = (rel: string) =>
181
+ rel.split(sep).some(part => part.startsWith(".") || part === "node_modules")
182
+
183
+ // one save can arrive as several events (a rename plus a change, an editor's
184
+ // atomic write); collapsing per file keeps that down to one push
185
+ const pending = new Map<string, NodeJS.Timeout>()
186
+
187
+ const changed = async (rel: string) => {
188
+ const file = join(root, rel)
189
+
190
+ let src: string | null = null
191
+ try {
192
+ // a directory changes whenever anything inside it does, and the event for
193
+ // the file itself is already on its way - acting on both would reload the
194
+ // page every time a component is saved
195
+ if ((await stat(file)).isDirectory()) return
196
+ if (rel.endsWith(".html")) src = await readFile(file, "utf8")
197
+ } catch {
198
+ // gone: deleted, or renamed away - and there is nothing to swap in, so the
199
+ // page has to reload. Unless it was never there: macOS reports a change to
200
+ // the watched directory *itself* under its own basename, which resolves to
201
+ // a path inside it that does not exist
202
+ if (rel === basename(root)) return
203
+ }
204
+
205
+ // the url is the one the component was served from, because that is what the
206
+ // runtime resolves its instances' filenames against
207
+ const url = "/" + posix(rel)
208
+ if (src === null) send("reload", { url })
209
+ else send("update", { url, src })
210
+ }
211
+
212
+ const watcher: FSWatcher = watch(root, { recursive: true }, (_event, filename) => {
213
+ if (!filename) return
214
+ const rel = relative(root, resolve(root, filename.toString()))
215
+ if (!rel || rel.startsWith("..") || ignored(rel)) return
216
+
217
+ clearTimeout(pending.get(rel))
218
+ pending.set(rel, setTimeout(() => {
219
+ pending.delete(rel)
220
+ void changed(rel)
221
+ }, 30))
222
+ })
223
+
224
+ // proxies and load balancers cut an idle stream; a comment every 30s is the
225
+ // conventional way to keep it open. unref'd, so it never holds the process up
226
+ const heartbeat = setInterval(() => clients.forEach(client => client.write(": ping\n\n")), 30_000)
227
+ heartbeat.unref()
228
+
229
+ // --- go --------------------------------------------------------------------
230
+
231
+ await new Promise<void>((done, fail) => {
232
+ server.once("error", fail)
233
+ server.listen(options.port ?? 4179, host, done)
234
+ })
235
+ const { port } = server.address() as { port: number }
236
+
237
+ return {
238
+ url: `http://${host}:${port}`,
239
+ port,
240
+ close: () =>
241
+ new Promise(done => {
242
+ clearInterval(heartbeat)
243
+ watcher.close()
244
+ pending.forEach(clearTimeout)
245
+ clients.forEach(client => client.end())
246
+ clients.clear()
247
+ server.close(() => done())
248
+ }),
249
+ }
250
+ }
251
+
252
+ export default devServer
@@ -118,12 +118,12 @@ const compileStyleBlocks = async (
118
118
  //
119
119
  // In dev, `hot.data` carries the exported instance across updates: importers
120
120
  // hold a reference to the *first* module evaluation's instance, so later
121
- // evaluations patch that same instance in place (the parsed parts are public
122
- // fields) instead of exporting a new one nobody sees. A live instance is
123
- // re-rendered where it stands, seeded with a snapshot of its current store;
124
- // an instance only used as a definition (nested component clones can't be
125
- // reached from here) falls back to a full reload. `mountRoot` is internal to
126
- // Component79, but plugin and runtime ship in lockstep from the same package.
121
+ // evaluations patch that same instance in place instead of exporting a new one
122
+ // nobody sees. The patching itself is the runtime's `hotReplace` - the same
123
+ // swap the jq79/dev server drives, from the one place that can reach a
124
+ // component's markers. An instance only used as a definition has nothing to
125
+ // re-render (nested clones can't be reached from this module), so it falls
126
+ // back to a full reload.
127
127
  const componentModule = (source: string, include: RegExp, filename: string): string => {
128
128
  const hoisted = hoistableImports(source, include)
129
129
  const imports = hoisted
@@ -147,18 +147,12 @@ let component
147
147
 
148
148
  if (import.meta.hot && import.meta.hot.data.component) {
149
149
  const prior = import.meta.hot.data.component
150
- const next = new Component79(src)
151
- prior.template = next.template
152
- prior.scripts = next.scripts
153
- prior.styles = next.styles
154
150
  prior.modules = modules
155
151
  prior.filename = filename
156
- const root = prior.mountRoot
157
- if (root) {
158
- prior.mount(root, { ...prior.data })
159
- } else if (!prior.data) {
160
- import.meta.hot.invalidate()
161
- }
152
+ // re-renders it where it stands, keeping its data. false means it was never
153
+ // rendered - a definition used only as a nested component - and a reload is
154
+ // the only way to reach the clones made from it
155
+ if (!prior.hotReplace(src) && !prior.data) import.meta.hot.invalidate()
162
156
  component = prior
163
157
  } else {
164
158
  component = new Component79(src, { modules, filename })
package/dist/cli.js ADDED
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ // dev/dev.ts
4
+ import { createServer } from "http";
5
+ import { readFile, realpath, stat } from "fs/promises";
6
+ import { watch } from "fs";
7
+ import { basename, extname, join, relative, resolve, sep } from "path";
8
+ var CONTENT_TYPES = {
9
+ ".html": "text/html; charset=utf-8",
10
+ ".js": "text/javascript; charset=utf-8",
11
+ ".mjs": "text/javascript; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".css": "text/css; charset=utf-8",
14
+ ".svg": "image/svg+xml",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".avif": "image/avif",
21
+ ".ico": "image/x-icon",
22
+ ".woff": "font/woff",
23
+ ".woff2": "font/woff2",
24
+ ".map": "application/json; charset=utf-8"
25
+ };
26
+ var CLIENT_URL = "/__jq79/client.js";
27
+ var EVENTS_URL = "/__jq79/events";
28
+ var CLIENT = `(() => {
29
+ window.__JQ79_HMR_ENABLED__ = true
30
+
31
+ const events = new EventSource(${JSON.stringify(EVENTS_URL)})
32
+
33
+ events.addEventListener("update", event => {
34
+ const { url, src } = JSON.parse(event.data)
35
+ const runtime = window.__JQ79_HMR__
36
+ // no runtime (the page doesn't use jq79), or no live instance from this
37
+ // file (it isn't mounted, or it *is* the page) - nothing to swap into
38
+ const patched = runtime ? runtime.update(url, src) : 0
39
+ if (patched) console.log("[jq79] hot-updated " + url + " (" + patched + (patched === 1 ? " instance)" : " instances)"))
40
+ else location.reload()
41
+ })
42
+
43
+ events.addEventListener("reload", () => location.reload())
44
+ })()`;
45
+ var posix = (path) => path.split(sep).join("/");
46
+ var isDocument = (req) => req.headers["sec-fetch-dest"] === "document";
47
+ var injectClient = (html) => {
48
+ const tag = `<script src="${CLIENT_URL}"></script>`;
49
+ const open = /<head[^>]*>/i.exec(html) ?? /<body[^>]*>/i.exec(html);
50
+ if (!open) return tag + html;
51
+ const at = open.index + open[0].length;
52
+ return html.slice(0, at) + tag + html.slice(at);
53
+ };
54
+ var devServer = async (options2 = {}) => {
55
+ const root = await realpath(resolve(options2.rootDir ?? "."));
56
+ const host = options2.host ?? "localhost";
57
+ const clients = /* @__PURE__ */ new Set();
58
+ const send = (event, data) => {
59
+ const frame = `event: ${event}
60
+ data: ${JSON.stringify(data)}
61
+
62
+ `;
63
+ clients.forEach((client) => client.write(frame));
64
+ };
65
+ const serveStatic = async (req, res, pathname) => {
66
+ let file;
67
+ try {
68
+ file = resolve(join(root, decodeURIComponent(pathname)));
69
+ } catch {
70
+ res.writeHead(400).end("bad request");
71
+ return;
72
+ }
73
+ if (file !== root && !file.startsWith(root + sep)) {
74
+ res.writeHead(403).end("forbidden");
75
+ return;
76
+ }
77
+ try {
78
+ if ((await stat(file)).isDirectory()) file = join(file, "index.html");
79
+ const body = await readFile(file);
80
+ const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
81
+ const html = type.startsWith("text/html") && isDocument(req);
82
+ const payload = html ? Buffer.from(injectClient(body.toString("utf8"))) : body;
83
+ res.writeHead(200, {
84
+ "content-type": type,
85
+ "content-length": payload.byteLength,
86
+ "cache-control": "no-store"
87
+ // the file on disk is always the truth here
88
+ });
89
+ res.end(payload);
90
+ } catch {
91
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("not found");
92
+ }
93
+ };
94
+ const server2 = createServer((req, res) => {
95
+ const pathname = (req.url ?? "/").split(/[?#]/)[0];
96
+ if (pathname === CLIENT_URL) {
97
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" });
98
+ res.end(CLIENT);
99
+ return;
100
+ }
101
+ if (pathname === EVENTS_URL) {
102
+ res.writeHead(200, {
103
+ "content-type": "text/event-stream",
104
+ "cache-control": "no-store",
105
+ connection: "keep-alive"
106
+ });
107
+ res.write(": jq79\n\n");
108
+ clients.add(res);
109
+ req.on("close", () => clients.delete(res));
110
+ return;
111
+ }
112
+ void serveStatic(req, res, pathname);
113
+ });
114
+ const ignored = (rel) => rel.split(sep).some((part) => part.startsWith(".") || part === "node_modules");
115
+ const pending = /* @__PURE__ */ new Map();
116
+ const changed = async (rel) => {
117
+ const file = join(root, rel);
118
+ let src = null;
119
+ try {
120
+ if ((await stat(file)).isDirectory()) return;
121
+ if (rel.endsWith(".html")) src = await readFile(file, "utf8");
122
+ } catch {
123
+ if (rel === basename(root)) return;
124
+ }
125
+ const url = "/" + posix(rel);
126
+ if (src === null) send("reload", { url });
127
+ else send("update", { url, src });
128
+ };
129
+ const watcher = watch(root, { recursive: true }, (_event, filename) => {
130
+ if (!filename) return;
131
+ const rel = relative(root, resolve(root, filename.toString()));
132
+ if (!rel || rel.startsWith("..") || ignored(rel)) return;
133
+ clearTimeout(pending.get(rel));
134
+ pending.set(rel, setTimeout(() => {
135
+ pending.delete(rel);
136
+ void changed(rel);
137
+ }, 30));
138
+ });
139
+ const heartbeat = setInterval(() => clients.forEach((client) => client.write(": ping\n\n")), 3e4);
140
+ heartbeat.unref();
141
+ await new Promise((done, fail) => {
142
+ server2.once("error", fail);
143
+ server2.listen(options2.port ?? 4179, host, done);
144
+ });
145
+ const { port } = server2.address();
146
+ return {
147
+ url: `http://${host}:${port}`,
148
+ port,
149
+ close: () => new Promise((done) => {
150
+ clearInterval(heartbeat);
151
+ watcher.close();
152
+ pending.forEach(clearTimeout);
153
+ clients.forEach((client) => client.end());
154
+ clients.clear();
155
+ server2.close(() => done());
156
+ })
157
+ };
158
+ };
159
+
160
+ // dev/cli.ts
161
+ var USAGE = `jq79 - a mini reactive component library
162
+
163
+ usage:
164
+ jq79 dev [dir] serve dir (default: .) with hot reload
165
+
166
+ options:
167
+ -p, --port <port> port to listen on (default: 4179)
168
+ -H, --host <host> host to bind (default: localhost)
169
+ -h, --help show this message
170
+ `;
171
+ var args = process.argv.slice(2);
172
+ var [command, ...rest] = args;
173
+ if (command !== "dev" || rest.includes("-h") || rest.includes("--help")) {
174
+ const unknown = command && command !== "dev" && !command.startsWith("-");
175
+ if (unknown) console.error(`unknown command: ${command}
176
+ `);
177
+ console.log(USAGE);
178
+ process.exit(unknown ? 1 : 0);
179
+ }
180
+ var options = {};
181
+ for (let i = 0; i < rest.length; i++) {
182
+ const arg = rest[i];
183
+ if (arg === "-p" || arg === "--port") options.port = Number(rest[++i]);
184
+ else if (arg === "-H" || arg === "--host") options.host = rest[++i];
185
+ else if (!arg.startsWith("-")) options.rootDir ??= arg;
186
+ else {
187
+ console.error(`unknown option: ${arg}
188
+ ${USAGE}`);
189
+ process.exit(1);
190
+ }
191
+ }
192
+ if (options.port !== void 0 && !Number.isInteger(options.port)) {
193
+ console.error("--port takes a number");
194
+ process.exit(1);
195
+ }
196
+ var server = await devServer(options);
197
+ console.log(`jq79 dev \u2192 ${server.url}`);
198
+ var stop = () => {
199
+ void server.close().then(() => process.exit(0));
200
+ };
201
+ process.on("SIGINT", stop);
202
+ process.on("SIGTERM", stop);