srv-it 0.4.0 → 0.5.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 +25 -19
- package/license.md +20 -0
- package/package.json +1 -1
- package/src/cli.js +4 -3
- package/src/server.js +122 -34
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ srv-it
|
|
|
34
34
|
srv-it 3000
|
|
35
35
|
srv-it 8080 ./public
|
|
36
36
|
srv-it ./public --open
|
|
37
|
-
srv-it --style
|
|
37
|
+
srv-it --style paper --style-css ./srv-listing.css
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Config defaults
|
|
@@ -43,7 +43,7 @@ You can set defaults globally and per-project:
|
|
|
43
43
|
|
|
44
44
|
- global: `~/.srvrc.json`
|
|
45
45
|
- project: `./srv.config.json`
|
|
46
|
-
- override file: `srv --config ./my-srv.json`
|
|
46
|
+
- override file: `srv-it --config ./my-srv.json`
|
|
47
47
|
- template: `./srv.config.example.json`
|
|
48
48
|
|
|
49
49
|
Example `srv.config.json`:
|
|
@@ -74,23 +74,29 @@ Run:
|
|
|
74
74
|
srv-it --help
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- `-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- `--
|
|
83
|
-
- `--
|
|
84
|
-
- `--
|
|
85
|
-
- `--
|
|
86
|
-
- `--
|
|
87
|
-
- `--no-
|
|
88
|
-
- `--
|
|
89
|
-
- `--
|
|
90
|
-
-
|
|
91
|
-
- `--
|
|
92
|
-
- `--
|
|
93
|
-
-
|
|
77
|
+
### Supported Arguments
|
|
78
|
+
|
|
79
|
+
- `-h, --help`: show help output
|
|
80
|
+
- `-v, --version`: print the current version
|
|
81
|
+
- `-p, --port <number>`: set the server port
|
|
82
|
+
- `--host <host>`: set the bind host (default: `0.0.0.0`)
|
|
83
|
+
- `--open [path]`: open a browser to `/` or the provided path
|
|
84
|
+
- `--no-open`: disable automatic browser opening
|
|
85
|
+
- `--watch <path>` (repeat): add extra files/folders to watch for live reload
|
|
86
|
+
- `--ignore <glob>` (repeat): ignore matching watcher paths/globs
|
|
87
|
+
- `--no-css-inject`: use full page reload for CSS changes instead of hot CSS refresh
|
|
88
|
+
- `--cors`: enable CORS headers
|
|
89
|
+
- `--single`: serve `index.html` for unknown routes (SPA fallback)
|
|
90
|
+
- `--no-dir-listing`: disable generated directory listing pages
|
|
91
|
+
- `--style <midnight|paper>`: choose the directory listing style preset
|
|
92
|
+
- `--style-css <file>`: load custom CSS for directory listing pages
|
|
93
|
+
- `-c`: create `srv.config.json` in the served root if missing
|
|
94
|
+
- `--config <file>`: read additional config JSON file
|
|
95
|
+
- `--no-request-logging`: disable request logs
|
|
96
|
+
- `--log-level <0-3>`: set startup log verbosity
|
|
97
|
+
- `--ssl-cert <file>`: path to SSL certificate
|
|
98
|
+
- `--ssl-key <file>`: path to SSL private key
|
|
99
|
+
- `--ssl-pass <file>`: path to SSL passphrase file
|
|
94
100
|
|
|
95
101
|
## Notes
|
|
96
102
|
|
package/license.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elouan Grimm.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -46,7 +46,7 @@ function getHelpText() {
|
|
|
46
46
|
` ${chalk.yellow('--cors')} Enable CORS`,
|
|
47
47
|
` ${chalk.yellow('--single')} SPA fallback to /index.html`,
|
|
48
48
|
` ${chalk.yellow('--no-dir-listing')} Disable directory listing`,
|
|
49
|
-
` ${chalk.yellow('--style <
|
|
49
|
+
` ${chalk.yellow('--style <midnight|paper>')} Listing style preset`,
|
|
50
50
|
` ${chalk.yellow('--style-css <file>')} Custom CSS file for listing page`,
|
|
51
51
|
` ${chalk.yellow('-c')} Create srv.config.json in served root if missing`,
|
|
52
52
|
` ${chalk.yellow('--config <file>')} Read additional config JSON file`,
|
|
@@ -207,7 +207,7 @@ async function run() {
|
|
|
207
207
|
|
|
208
208
|
if (options.logLevel >= 1) {
|
|
209
209
|
const lines = [
|
|
210
|
-
`${chalk.
|
|
210
|
+
`${chalk.hex("#3b82f6").bold('srv-it is running')}`,
|
|
211
211
|
'',
|
|
212
212
|
`${chalk.bold('- Local:')} ${url}`,
|
|
213
213
|
`${chalk.bold('- Root:')} ${options.root}`,
|
|
@@ -230,7 +230,8 @@ async function run() {
|
|
|
230
230
|
boxen(content, {
|
|
231
231
|
padding: 1,
|
|
232
232
|
borderStyle: 'single',
|
|
233
|
-
borderColor: '
|
|
233
|
+
borderColor: '#3b82f6',
|
|
234
|
+
margin: 1,
|
|
234
235
|
}),
|
|
235
236
|
);
|
|
236
237
|
}
|
package/src/server.js
CHANGED
|
@@ -11,8 +11,19 @@ const os = require('node:os');
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
12
|
const { WebSocketServer } = require('ws');
|
|
13
13
|
|
|
14
|
+
const srvLogger = {
|
|
15
|
+
http: (...message) => console.info(chalk.bgBlue.bold(' HTTP '), ...message),
|
|
16
|
+
info: (...message) => console.info(chalk.bgMagenta.bold(' INFO '), ...message),
|
|
17
|
+
warn: (...message) => console.error(chalk.bgYellow.bold(' WARN '), ...message),
|
|
18
|
+
error: (...message) => console.error(chalk.bgRed.bold(' ERRR '), ...message),
|
|
19
|
+
log: console.log,
|
|
20
|
+
};
|
|
21
|
+
|
|
14
22
|
const BASE_WATCH_IGNORES = ['**/.git/**', '**/node_modules/**'];
|
|
15
23
|
|
|
24
|
+
const DIRECTORY_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#0c0a09" d="M3 5.5A2.5 2.5 0 0 1 5.5 3H10l2 2h6.5A2.5 2.5 0 0 1 21 7.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5z"/><path fill="#3b82f6" d="M3 8h18v8.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z"/></svg>';
|
|
25
|
+
const DIRECTORY_FAVICON_DATA_URL = `data:image/svg+xml,${encodeURIComponent(DIRECTORY_FAVICON_SVG)}`;
|
|
26
|
+
|
|
16
27
|
function toGlobPath(value) {
|
|
17
28
|
return value.replace(/\\/g, '/');
|
|
18
29
|
}
|
|
@@ -31,15 +42,29 @@ const LIVE_RELOAD_SNIPPET = `\n<script>\n(function(){\n if(!('WebSocket' in win
|
|
|
31
42
|
const STYLE_PRESETS = {
|
|
32
43
|
midnight: {
|
|
33
44
|
accent: '#3b82f6',
|
|
45
|
+
accentSoft: 'rgba(59, 130, 246, 0.16)',
|
|
46
|
+
bg: '#0c0a09',
|
|
47
|
+
bgRaised: '#1c1917',
|
|
48
|
+
bgSurface: '#292524',
|
|
49
|
+
border: '#292524',
|
|
34
50
|
borderStrong: '#78716c',
|
|
51
|
+
text: '#e7e5e4',
|
|
52
|
+
textMuted: '#a8a29e',
|
|
53
|
+
textFaint: '#78716c',
|
|
54
|
+
textBright: '#f5f5f4',
|
|
35
55
|
},
|
|
36
56
|
paper: {
|
|
37
|
-
accent: '#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
accent: '#d97706',
|
|
58
|
+
accentSoft: 'rgba(217, 119, 6, 0.16)',
|
|
59
|
+
bg: '#f5f5f4',
|
|
60
|
+
bgRaised: '#fafaf9',
|
|
61
|
+
bgSurface: '#e7e5e4',
|
|
62
|
+
border: '#d6d3d1',
|
|
42
63
|
borderStrong: '#a8a29e',
|
|
64
|
+
text: '#292524',
|
|
65
|
+
textMuted: '#57534e',
|
|
66
|
+
textFaint: '#78716c',
|
|
67
|
+
textBright: '#1c1917',
|
|
43
68
|
},
|
|
44
69
|
};
|
|
45
70
|
|
|
@@ -106,6 +131,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
106
131
|
<meta charset="utf-8" />
|
|
107
132
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
108
133
|
<title>Index of ${pathnameValue}</title>
|
|
134
|
+
<link rel="icon" href="${DIRECTORY_FAVICON_DATA_URL}" />
|
|
109
135
|
<style>
|
|
110
136
|
:root {
|
|
111
137
|
--stone-050: #fafaf9;
|
|
@@ -120,16 +146,17 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
120
146
|
--stone-900: #1c1917;
|
|
121
147
|
--stone-950: #0c0a09;
|
|
122
148
|
|
|
123
|
-
--bg:
|
|
124
|
-
--bg-raised:
|
|
125
|
-
--bg-surface:
|
|
126
|
-
--border:
|
|
149
|
+
--bg: ${palette.bg};
|
|
150
|
+
--bg-raised: ${palette.bgRaised};
|
|
151
|
+
--bg-surface: ${palette.bgSurface};
|
|
152
|
+
--border: ${palette.border};
|
|
127
153
|
--border-strong: ${palette.borderStrong};
|
|
128
|
-
--text:
|
|
129
|
-
--text-muted:
|
|
130
|
-
--text-faint:
|
|
131
|
-
--text-bright:
|
|
154
|
+
--text: ${palette.text};
|
|
155
|
+
--text-muted: ${palette.textMuted};
|
|
156
|
+
--text-faint: ${palette.textFaint};
|
|
157
|
+
--text-bright: ${palette.textBright};
|
|
132
158
|
--accent: ${palette.accent};
|
|
159
|
+
--accent-soft: ${palette.accentSoft};
|
|
133
160
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
134
161
|
--font-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Roboto Mono", "Cascadia Code", monospace;
|
|
135
162
|
--line-height: 1.6;
|
|
@@ -163,7 +190,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
163
190
|
|
|
164
191
|
h1 {
|
|
165
192
|
margin-bottom: 0.25rem;
|
|
166
|
-
color: var(--
|
|
193
|
+
color: var(--accent);
|
|
167
194
|
font-family: var(--font-mono);
|
|
168
195
|
font-size: 1.1rem;
|
|
169
196
|
line-height: var(--line-height-tight);
|
|
@@ -176,6 +203,8 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
176
203
|
color: var(--text-muted);
|
|
177
204
|
font-family: var(--font-mono);
|
|
178
205
|
font-size: 0.9rem;
|
|
206
|
+
border-left: 2px solid var(--accent);
|
|
207
|
+
padding-left: 0.6rem;
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
.up {
|
|
@@ -183,10 +212,10 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
183
212
|
margin-bottom: 0.75rem;
|
|
184
213
|
color: var(--accent);
|
|
185
214
|
text-decoration: none;
|
|
186
|
-
border: 1px solid var(--
|
|
215
|
+
border: 1px solid var(--accent);
|
|
187
216
|
padding: 0.3rem 0.55rem;
|
|
188
217
|
font-family: var(--font-mono);
|
|
189
|
-
transition: background-color 0.18s ease, color 0.18s ease;
|
|
218
|
+
transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
|
|
190
219
|
}
|
|
191
220
|
|
|
192
221
|
.up:hover {
|
|
@@ -209,7 +238,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
209
238
|
color: var(--text);
|
|
210
239
|
border-bottom: 1px solid var(--border);
|
|
211
240
|
font-family: var(--font-mono);
|
|
212
|
-
transition: background-color 0.18s ease, color 0.18s ease;
|
|
241
|
+
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
|
213
242
|
}
|
|
214
243
|
|
|
215
244
|
li:last-child a {
|
|
@@ -219,6 +248,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
219
248
|
li a:hover {
|
|
220
249
|
background-color: var(--bg-surface);
|
|
221
250
|
color: var(--text-bright);
|
|
251
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
.name {
|
|
@@ -229,10 +259,13 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
229
259
|
}
|
|
230
260
|
|
|
231
261
|
.type {
|
|
232
|
-
color: var(--
|
|
262
|
+
color: var(--accent);
|
|
233
263
|
text-transform: uppercase;
|
|
234
264
|
font-size: 0.72rem;
|
|
235
265
|
letter-spacing: 0.07em;
|
|
266
|
+
padding: 0.12rem 0.35rem;
|
|
267
|
+
border: 1px solid var(--accent-soft);
|
|
268
|
+
background-color: var(--accent-soft);
|
|
236
269
|
}
|
|
237
270
|
|
|
238
271
|
@media (max-width: 768px) {
|
|
@@ -278,6 +311,7 @@ async function createSrvServer(options) {
|
|
|
278
311
|
const root = path.resolve(options.root);
|
|
279
312
|
const compress = getCompressionMiddleware();
|
|
280
313
|
const clients = new Set();
|
|
314
|
+
const sockets = new Set();
|
|
281
315
|
|
|
282
316
|
let customCss = '';
|
|
283
317
|
if (options.styleCss) {
|
|
@@ -315,6 +349,12 @@ async function createSrvServer(options) {
|
|
|
315
349
|
try {
|
|
316
350
|
stat = await fsp.stat(filePath);
|
|
317
351
|
} catch (error) {
|
|
352
|
+
if (pathnameValue === '/sw.js' && error && error.code === 'ENOENT') {
|
|
353
|
+
// Browsers often probe /sw.js by default; treat missing file as a silent no-op.
|
|
354
|
+
res.statusCode = 204;
|
|
355
|
+
res.end();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
318
358
|
if (options.single) {
|
|
319
359
|
filePath = path.join(root, 'index.html');
|
|
320
360
|
stat = await fsp.stat(filePath);
|
|
@@ -385,18 +425,22 @@ async function createSrvServer(options) {
|
|
|
385
425
|
res.statusCode = 500;
|
|
386
426
|
res.end('Internal server error');
|
|
387
427
|
if (options.logLevel >= 1) {
|
|
388
|
-
|
|
428
|
+
srvLogger.error('[srv-it] request error:', error.message);
|
|
389
429
|
}
|
|
390
430
|
} finally {
|
|
391
431
|
if (!options.noRequestLogging) {
|
|
392
432
|
const statusCode = res.statusCode;
|
|
433
|
+
const suppressRequestLog = pathnameValue === '/sw.js' && statusCode === 204;
|
|
434
|
+
if (suppressRequestLog) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
393
437
|
const elapsed = Date.now() - start;
|
|
394
438
|
const sourceIp = (req.socket.remoteAddress || '-').replace('::ffff:', '');
|
|
395
439
|
const now = new Date();
|
|
396
440
|
const formattedTime = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
|
|
397
441
|
const methodColor = method === 'GET' ? 'cyan' : 'magenta';
|
|
398
442
|
|
|
399
|
-
|
|
443
|
+
srvLogger.http(
|
|
400
444
|
chalk.dim(formattedTime),
|
|
401
445
|
chalk.yellow(sourceIp),
|
|
402
446
|
chalk[methodColor](`${method} ${pathnameValue}`),
|
|
@@ -420,7 +464,7 @@ async function createSrvServer(options) {
|
|
|
420
464
|
res.statusCode = 500;
|
|
421
465
|
res.end('Internal server error');
|
|
422
466
|
if (options.logLevel >= 1) {
|
|
423
|
-
|
|
467
|
+
srvLogger.error('[srv-it] handler error:', error.message);
|
|
424
468
|
}
|
|
425
469
|
});
|
|
426
470
|
});
|
|
@@ -430,12 +474,17 @@ async function createSrvServer(options) {
|
|
|
430
474
|
res.statusCode = 500;
|
|
431
475
|
res.end('Internal server error');
|
|
432
476
|
if (options.logLevel >= 1) {
|
|
433
|
-
|
|
477
|
+
srvLogger.error('[srv-it] handler error:', error.message);
|
|
434
478
|
}
|
|
435
479
|
});
|
|
436
480
|
});
|
|
437
481
|
}
|
|
438
482
|
|
|
483
|
+
server.on('connection', (socket) => {
|
|
484
|
+
sockets.add(socket);
|
|
485
|
+
socket.on('close', () => sockets.delete(socket));
|
|
486
|
+
});
|
|
487
|
+
|
|
439
488
|
const wss = new WebSocketServer({ server, path: '/__srv_ws' });
|
|
440
489
|
wss.on('connection', (socket) => {
|
|
441
490
|
clients.add(socket);
|
|
@@ -474,7 +523,7 @@ async function createSrvServer(options) {
|
|
|
474
523
|
}
|
|
475
524
|
}
|
|
476
525
|
if (options.logLevel >= 2) {
|
|
477
|
-
|
|
526
|
+
srvLogger.info(`[srv-it] ${isCss ? 'css refresh' : 'reload'}: ${changePath}`);
|
|
478
527
|
}
|
|
479
528
|
};
|
|
480
529
|
|
|
@@ -487,27 +536,66 @@ async function createSrvServer(options) {
|
|
|
487
536
|
if (error && error.code === 'ENOSPC') {
|
|
488
537
|
liveReloadEnabled = false;
|
|
489
538
|
await closeWatcher();
|
|
490
|
-
|
|
491
|
-
|
|
539
|
+
srvLogger.error('[srv-it] live reload disabled: file watcher limit reached (ENOSPC).');
|
|
540
|
+
srvLogger.warn('[srv-it] use --ignore to exclude noisy paths, or raise inotify limits on Linux.');
|
|
492
541
|
return;
|
|
493
542
|
}
|
|
494
543
|
|
|
495
544
|
if (options.logLevel >= 1) {
|
|
496
|
-
|
|
545
|
+
srvLogger.error(`[srv-it] watcher error: ${error.message}`);
|
|
497
546
|
}
|
|
498
547
|
});
|
|
499
548
|
|
|
549
|
+
let closingPromise;
|
|
500
550
|
const closeAll = async () => {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
551
|
+
if (closingPromise) {
|
|
552
|
+
return closingPromise;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
closingPromise = (async () => {
|
|
556
|
+
await closeWatcher();
|
|
557
|
+
|
|
558
|
+
for (const client of clients) {
|
|
559
|
+
try {
|
|
560
|
+
client.terminate();
|
|
561
|
+
} catch (_error) {
|
|
562
|
+
// Ignore client termination errors during shutdown.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await new Promise((resolve) => wss.close(resolve));
|
|
567
|
+
|
|
568
|
+
for (const socket of sockets) {
|
|
569
|
+
socket.destroy();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
await new Promise((resolve) => server.close(resolve));
|
|
573
|
+
})();
|
|
574
|
+
|
|
575
|
+
return closingPromise;
|
|
504
576
|
};
|
|
505
577
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
578
|
+
let shuttingDown = false;
|
|
579
|
+
const onSigint = async () => {
|
|
580
|
+
if (shuttingDown) {
|
|
581
|
+
process.exit(130);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
shuttingDown = true;
|
|
586
|
+
process.stdout.write('\u001B[2K\r');
|
|
587
|
+
console.log(chalk.bgWhite.bold('\n[srv-it]') + ' shutting down...');
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await closeAll();
|
|
591
|
+
process.exit(0);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
srvLogger.error('[srv-it] shutdown error:', error.message);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
process.once('SIGINT', onSigint);
|
|
511
599
|
|
|
512
600
|
await new Promise((resolve, reject) => {
|
|
513
601
|
server.once('error', reject);
|