serverless-ircd 0.7.0 → 0.9.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/.github/workflows/ci.yml +7 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +518 -29
- package/README.md +221 -161
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/container-server.ts +21 -1
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +95 -31
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/index.html +237 -16
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +46 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/tests/build-smoke.test.ts +43 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +578 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +20 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +130 -8
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +200 -0
- package/packages/irc-core/src/commands/chanserv.ts +1166 -0
- package/packages/irc-core/src/commands/hostserv.ts +522 -0
- package/packages/irc-core/src/commands/index.ts +13 -0
- package/packages/irc-core/src/commands/join.ts +171 -9
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +402 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +78 -12
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +60 -10
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +38 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
- package/packages/irc-core/tests/commands/join.test.ts +572 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +121 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +988 -15
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → static-HTML render pipeline for the `docs/` submodule.
|
|
3
|
+
*
|
|
4
|
+
* Renders every `docs/*.md` to a standalone HTML page under `dist/docs/` so
|
|
5
|
+
* the Worker `[assets]` binding serves them at `/docs/<name>.html` (and
|
|
6
|
+
* `/docs/` for the index — `Home.md` becomes `index.html`). Wired into the
|
|
7
|
+
* `apps/web` build by `scripts/build.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from the build script so the rendering pipeline is unit-testable
|
|
10
|
+
* without running the (heavy) Kiwi upstream SPA build.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { marked } from 'marked';
|
|
17
|
+
|
|
18
|
+
// GFM is on by default in marked v18+, but pin it explicitly so a future
|
|
19
|
+
// upstream default flip can't silently change the rendered output. `async:
|
|
20
|
+
// false` makes `.parse()` return a plain string (its return type is the
|
|
21
|
+
// union `string | Promise<string>` — narrowed with a type assertion at the
|
|
22
|
+
// call site).
|
|
23
|
+
marked.use({ gfm: true, async: false });
|
|
24
|
+
|
|
25
|
+
/** Basename of the docs landing page (rendered to `index.html`). */
|
|
26
|
+
const HOME_SLUG = 'Home';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render GitHub-flavored markdown to an HTML fragment (no surrounding
|
|
30
|
+
* `<html>`/`<body>` — {@link buildDocs} wraps the result in a full page).
|
|
31
|
+
*/
|
|
32
|
+
export function renderMarkdown(md: string): string {
|
|
33
|
+
return marked.parse(md, { async: false }) as string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite intra-docs links in already-rendered HTML so relative references
|
|
38
|
+
* resolve on the static site. Two source shapes are handled:
|
|
39
|
+
*
|
|
40
|
+
* 1. `[x](OtherDoc.md)` / `[x](./OtherDoc.md)` — Gitea-relative, emitted by
|
|
41
|
+
* marked as `<a href="./OtherDoc.md">`.
|
|
42
|
+
* 2. `[x](OtherDoc)` — Gitea-wiki "bare" link (no extension), used heavily
|
|
43
|
+
* in `Home.md` and `ADR-Index.md`.
|
|
44
|
+
*
|
|
45
|
+
* Only slugs present in `knownDocs` are rewritten, so external links, repo
|
|
46
|
+
* paths, and unknown targets are left untouched. Anchor fragments
|
|
47
|
+
* (`#section`) are preserved.
|
|
48
|
+
*/
|
|
49
|
+
export function rewriteDocLinks(html: string, knownDocs: Set<string>): string {
|
|
50
|
+
return html.replace(/href="([^"]*)"/g, (match, raw: string) => {
|
|
51
|
+
// Leave external URLs and pure-anchor links alone.
|
|
52
|
+
if (raw === '' || raw.startsWith('#') || /^(?:https?:|mailto:|tel:|data:)/i.test(raw)) {
|
|
53
|
+
return match;
|
|
54
|
+
}
|
|
55
|
+
const [pathPartRaw, ...fragParts] = raw.split('#');
|
|
56
|
+
const pathPart = pathPartRaw ?? '';
|
|
57
|
+
const fragment = fragParts.length > 0 ? `#${fragParts.join('#')}` : '';
|
|
58
|
+
const slug = pathPart.replace(/^\.\//, '').replace(/\.md$/i, '').trim();
|
|
59
|
+
if (slug === '' || !knownDocs.has(slug)) {
|
|
60
|
+
return match;
|
|
61
|
+
}
|
|
62
|
+
return `href="${slug}.html${fragment}"`;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BuildDocsOptions {
|
|
67
|
+
/** Source directory containing the `*.md` files (typically the `docs/` submodule). */
|
|
68
|
+
srcDir: string;
|
|
69
|
+
/** Output directory to write the rendered site (typically `dist/docs/`). */
|
|
70
|
+
outDir: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BuildDocsResult {
|
|
74
|
+
/** Relative names of the files written under `outDir`. */
|
|
75
|
+
emitted: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render every `*.md` under `srcDir` into a standalone HTML page under
|
|
80
|
+
* `outDir`. `Home.md` becomes `index.html`; every other file `<Slug>.md`
|
|
81
|
+
* becomes `<Slug>.html`. Throws a clear error if `srcDir` is missing or
|
|
82
|
+
* empty — the build script surfaces this so a stale `docs/` submodule
|
|
83
|
+
* checkout fails loudly (mirroring the Kiwi `upstream/` guard).
|
|
84
|
+
*/
|
|
85
|
+
export async function buildDocs({ srcDir, outDir }: BuildDocsOptions): Promise<BuildDocsResult> {
|
|
86
|
+
if (!existsSync(srcDir)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Docs source missing at ${srcDir}. Run \`git submodule update --init docs\` first.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const entries = await readdir(srcDir);
|
|
92
|
+
const mdFiles = entries.filter((f) => f.toLowerCase().endsWith('.md'));
|
|
93
|
+
if (mdFiles.length === 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Docs source directory is empty at ${srcDir}. Run \`git submodule update --init docs\` first.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const knownSlugs = new Set(mdFiles.map((f) => f.replace(/\.md$/i, '')));
|
|
100
|
+
await mkdir(outDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Shared stylesheet — single source of truth for every page, mirroring the
|
|
103
|
+
// landing page's dark theme (CSS variables kept inline so the rendered
|
|
104
|
+
// docs work as static files served from any path).
|
|
105
|
+
await writeFile(path.join(outDir, 'docs.css'), DOCS_CSS, 'utf8');
|
|
106
|
+
|
|
107
|
+
const emitted: string[] = [];
|
|
108
|
+
for (const file of mdFiles) {
|
|
109
|
+
const slug = file.replace(/\.md$/i, '');
|
|
110
|
+
const md = await readFile(path.join(srcDir, file), 'utf8');
|
|
111
|
+
const body = rewriteDocLinks(renderMarkdown(md), knownSlugs);
|
|
112
|
+
const title = extractTitle(md) ?? slug;
|
|
113
|
+
const outName = slug === HOME_SLUG ? 'index.html' : `${slug}.html`;
|
|
114
|
+
const html = wrapPage(title, body, slug === HOME_SLUG);
|
|
115
|
+
await writeFile(path.join(outDir, outName), html, 'utf8');
|
|
116
|
+
emitted.push(outName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { emitted };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract the first ATX `# Heading` as the page title; `undefined` if none. */
|
|
123
|
+
function extractTitle(md: string): string | undefined {
|
|
124
|
+
const match = md.match(/^#\s+(.+?)\s*$/m);
|
|
125
|
+
return match?.[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Escape the four significant HTML characters in a text string. */
|
|
129
|
+
function escapeHtml(s: string): string {
|
|
130
|
+
return s
|
|
131
|
+
.replace(/&/g, '&')
|
|
132
|
+
.replace(/</g, '<')
|
|
133
|
+
.replace(/>/g, '>')
|
|
134
|
+
.replace(/"/g, '"');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Wrap a rendered HTML fragment in a full page with the shared stylesheet. */
|
|
138
|
+
function wrapPage(title: string, body: string, isIndex: boolean): string {
|
|
139
|
+
const back = isIndex ? '' : '<p class="back"><a href="./index.html">Docs home</a></p>';
|
|
140
|
+
return `<!doctype html>
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="utf-8" />
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
145
|
+
<title>${escapeHtml(title)} — ServerlessIRCd Docs</title>
|
|
146
|
+
<link rel="stylesheet" href="./docs.css" />
|
|
147
|
+
</head>
|
|
148
|
+
<body>
|
|
149
|
+
<header>
|
|
150
|
+
<div class="container">
|
|
151
|
+
<h1><a href="./index.html">ServerlessIRCd Docs</a></h1>
|
|
152
|
+
</div>
|
|
153
|
+
</header>
|
|
154
|
+
<main class="container">
|
|
155
|
+
${back}
|
|
156
|
+
<article>
|
|
157
|
+
${body}
|
|
158
|
+
</article>
|
|
159
|
+
</main>
|
|
160
|
+
<footer>
|
|
161
|
+
<div class="container">
|
|
162
|
+
<p><a href="/">Back to landing</a></p>
|
|
163
|
+
</div>
|
|
164
|
+
</footer>
|
|
165
|
+
</body>
|
|
166
|
+
</html>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const DOCS_CSS = `:root {
|
|
171
|
+
--bg: #0d1117;
|
|
172
|
+
--surface: #161b22;
|
|
173
|
+
--border: #30363d;
|
|
174
|
+
--text: #e6edf3;
|
|
175
|
+
--muted: #8b949e;
|
|
176
|
+
--accent: #58a6ff;
|
|
177
|
+
--accent-hover: #79c0ff;
|
|
178
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
179
|
+
"Liberation Mono", monospace;
|
|
180
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
|
181
|
+
sans-serif;
|
|
182
|
+
}
|
|
183
|
+
* { box-sizing: border-box; }
|
|
184
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
185
|
+
body {
|
|
186
|
+
margin: 0;
|
|
187
|
+
font-family: var(--sans);
|
|
188
|
+
line-height: 1.6;
|
|
189
|
+
color: var(--text);
|
|
190
|
+
background-color: var(--bg);
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
min-height: 100vh;
|
|
194
|
+
}
|
|
195
|
+
.container {
|
|
196
|
+
width: 100%;
|
|
197
|
+
max-width: 820px;
|
|
198
|
+
margin: 0 auto;
|
|
199
|
+
padding: 0 20px;
|
|
200
|
+
}
|
|
201
|
+
header {
|
|
202
|
+
padding: 24px 0 14px;
|
|
203
|
+
border-bottom: 1px solid var(--border);
|
|
204
|
+
}
|
|
205
|
+
header h1 {
|
|
206
|
+
margin: 0;
|
|
207
|
+
font-size: 1.4rem;
|
|
208
|
+
letter-spacing: -0.02em;
|
|
209
|
+
}
|
|
210
|
+
header h1 a {
|
|
211
|
+
color: var(--accent);
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
}
|
|
214
|
+
header h1 a:hover { text-decoration: underline; }
|
|
215
|
+
main { flex: 1 0 auto; padding: 28px 0; }
|
|
216
|
+
article {
|
|
217
|
+
font-size: 1rem;
|
|
218
|
+
overflow-wrap: break-word;
|
|
219
|
+
}
|
|
220
|
+
article h1, article h2, article h3, article h4 {
|
|
221
|
+
letter-spacing: -0.01em;
|
|
222
|
+
line-height: 1.25;
|
|
223
|
+
margin-top: 1.6em;
|
|
224
|
+
margin-bottom: 0.6em;
|
|
225
|
+
}
|
|
226
|
+
article h1 { font-size: 1.7rem; margin-top: 0; }
|
|
227
|
+
article h2 { font-size: 1.35rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
|
228
|
+
article h3 { font-size: 1.12rem; }
|
|
229
|
+
article h4 { font-size: 1rem; color: var(--accent-hover); }
|
|
230
|
+
article p { margin: 0.8em 0; }
|
|
231
|
+
article a { color: var(--accent); text-decoration: none; }
|
|
232
|
+
article a:hover { text-decoration: underline; }
|
|
233
|
+
article ul, article ol { padding-left: 1.6em; }
|
|
234
|
+
article li { margin: 0.25em 0; }
|
|
235
|
+
article blockquote {
|
|
236
|
+
margin: 0.9em 0;
|
|
237
|
+
padding: 0.4em 1em;
|
|
238
|
+
border-left: 3px solid var(--border);
|
|
239
|
+
color: var(--muted);
|
|
240
|
+
}
|
|
241
|
+
article blockquote p { margin: 0.4em 0; }
|
|
242
|
+
article code {
|
|
243
|
+
font-family: var(--mono);
|
|
244
|
+
background-color: var(--surface);
|
|
245
|
+
padding: 2px 6px;
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
font-size: 0.9em;
|
|
248
|
+
}
|
|
249
|
+
article pre {
|
|
250
|
+
font-family: var(--mono);
|
|
251
|
+
background-color: var(--surface);
|
|
252
|
+
border: 1px solid var(--border);
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
padding: 14px 16px;
|
|
255
|
+
margin: 12px 0;
|
|
256
|
+
overflow-x: auto;
|
|
257
|
+
line-height: 1.5;
|
|
258
|
+
font-size: 0.88rem;
|
|
259
|
+
}
|
|
260
|
+
article pre code {
|
|
261
|
+
background: none; border: none; padding: 0; font-size: inherit;
|
|
262
|
+
}
|
|
263
|
+
article table {
|
|
264
|
+
border-collapse: collapse;
|
|
265
|
+
margin: 1em 0;
|
|
266
|
+
display: block;
|
|
267
|
+
overflow-x: auto;
|
|
268
|
+
}
|
|
269
|
+
article table th, article table td {
|
|
270
|
+
border: 1px solid var(--border);
|
|
271
|
+
padding: 6px 12px;
|
|
272
|
+
text-align: left;
|
|
273
|
+
}
|
|
274
|
+
article table th { background-color: var(--surface); }
|
|
275
|
+
article hr {
|
|
276
|
+
border: none;
|
|
277
|
+
border-top: 1px solid var(--border);
|
|
278
|
+
margin: 1.6em 0;
|
|
279
|
+
}
|
|
280
|
+
article img { max-width: 100%; }
|
|
281
|
+
.back { margin-top: 0; color: var(--muted); font-size: 0.92rem; }
|
|
282
|
+
.back a { color: var(--accent); }
|
|
283
|
+
footer {
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
padding: 18px 0;
|
|
286
|
+
border-top: 1px solid var(--border);
|
|
287
|
+
color: var(--muted);
|
|
288
|
+
font-size: 0.9rem;
|
|
289
|
+
}
|
|
290
|
+
footer a { color: var(--muted); text-decoration: none; }
|
|
291
|
+
footer a:hover { color: var(--accent); }
|
|
292
|
+
`;
|
|
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
5
5
|
import { parseKiwiConfig } from '../src/config-schema';
|
|
6
6
|
|
|
7
7
|
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
-
const appDist = path.join(pkgRoot, 'dist', '
|
|
8
|
+
const appDist = path.join(pkgRoot, 'dist', 'webclient');
|
|
9
9
|
const indexPath = path.join(appDist, 'index.html');
|
|
10
10
|
const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
|
|
11
11
|
const landingDist = path.join(pkgRoot, 'dist', 'index.html');
|
|
@@ -19,12 +19,12 @@ const built = existsSync(indexPath);
|
|
|
19
19
|
// artifact; gate the build-output assertion on its own presence.
|
|
20
20
|
const landingBuilt = existsSync(landingDist) && statSync(landingDist).size > 0;
|
|
21
21
|
|
|
22
|
-
describe.skipIf(!built)('Kiwi SPA build output (dist/
|
|
23
|
-
it('emits dist/
|
|
22
|
+
describe.skipIf(!built)('Kiwi SPA build output (dist/webclient/)', () => {
|
|
23
|
+
it('emits dist/webclient/index.html', () => {
|
|
24
24
|
expect(existsSync(indexPath)).toBe(true);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('layers a schema-valid static/config.json into dist/
|
|
27
|
+
it('layers a schema-valid static/config.json into dist/webclient/static/config.json', () => {
|
|
28
28
|
const configPath = path.join(appDist, 'static', 'config.json');
|
|
29
29
|
expect(existsSync(configPath)).toBe(true);
|
|
30
30
|
// The baked config must clear the Zod schema no matter which --env was
|
|
@@ -34,17 +34,17 @@ describe.skipIf(!built)('Kiwi SPA build output (dist/app/)', () => {
|
|
|
34
34
|
expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('prefixes every asset reference with /
|
|
37
|
+
it('prefixes every asset reference with /webclient/', () => {
|
|
38
38
|
const html = readFileSync(indexPath, 'utf8');
|
|
39
39
|
// No bare relative `static/...` references may remain.
|
|
40
40
|
const bareStaticRefs = html.match(/(?:src|href)=["']static\//g) ?? [];
|
|
41
41
|
expect(bareStaticRefs).toEqual([]);
|
|
42
|
-
// At least one asset must be /
|
|
43
|
-
const appPrefixed = html.match(/(?:src|href)=["']\/
|
|
42
|
+
// At least one asset must be /webclient/-prefixed (the main bundle).
|
|
43
|
+
const appPrefixed = html.match(/(?:src|href)=["']\/webclient\/static\//g) ?? [];
|
|
44
44
|
expect(appPrefixed.length).toBeGreaterThan(0);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it('ships a hashed JS bundle under dist/
|
|
47
|
+
it('ships a hashed JS bundle under dist/webclient/static/js/', () => {
|
|
48
48
|
const jsDir = path.join(appDist, 'static', 'js');
|
|
49
49
|
expect(existsSync(jsDir)).toBe(true);
|
|
50
50
|
const js = readdirSync(jsDir).filter((f) => f.endsWith('.js'));
|
|
@@ -65,19 +65,19 @@ describe('landing page source (landing/index.html)', () => {
|
|
|
65
65
|
expect(html).toContain('<title>');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('links to the IRC client at /
|
|
68
|
+
it('links to the IRC client at /webclient/', () => {
|
|
69
69
|
const html = readFileSync(landingSrc, 'utf8');
|
|
70
|
-
expect(html).toMatch(/href=["']\/
|
|
70
|
+
expect(html).toMatch(/href=["']\/webclient\/?["']/);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
it('links to the project docs', () => {
|
|
73
|
+
it('links to the rendered project docs at /docs/', () => {
|
|
74
74
|
const html = readFileSync(landingSrc, 'utf8');
|
|
75
|
-
expect(html).toMatch(/href=["']
|
|
75
|
+
expect(html).toMatch(/href=["']\/docs\/?["']/);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('links to the source repository', () => {
|
|
78
|
+
it('links to the source repository on Gitea', () => {
|
|
79
79
|
const html = readFileSync(landingSrc, 'utf8');
|
|
80
|
-
expect(html.toLowerCase()).toMatch(/
|
|
80
|
+
expect(html.toLowerCase()).toMatch(/gitea\.com/);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('is responsive (declares a viewport meta tag)', () => {
|
|
@@ -101,3 +101,32 @@ describe.skipIf(!landingBuilt)('landing page build output (dist/index.html)', ()
|
|
|
101
101
|
expect(dist).toBe(src);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
// Validates the docs/ build pass: `pnpm --filter web build` renders every
|
|
106
|
+
// `docs/*.md` to standalone HTML under `dist/docs/` so the Worker `[assets]`
|
|
107
|
+
// binding serves them at `/docs/<name>.html` (and `/docs/` for the index).
|
|
108
|
+
// Gated on the directory's presence — like the Kiwi SPA gate above — so CI
|
|
109
|
+
// can run this test file without paying the heavy SPA build cost; the
|
|
110
|
+
// pipeline runs in full via `apps/web/tests/render-docs.test.ts`.
|
|
111
|
+
const docsDist = path.join(pkgRoot, 'dist', 'docs');
|
|
112
|
+
const docsBuilt = existsSync(docsDist) && statSync(docsDist).size > 0;
|
|
113
|
+
|
|
114
|
+
describe.skipIf(!docsBuilt)('docs build output (dist/docs/)', () => {
|
|
115
|
+
it('emits dist/docs/index.html', () => {
|
|
116
|
+
expect(existsSync(path.join(docsDist, 'index.html'))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('emits Services.html with rendered HTML (an <h1>, not raw markdown)', () => {
|
|
120
|
+
const target = path.join(docsDist, 'Services.html');
|
|
121
|
+
expect(existsSync(target)).toBe(true);
|
|
122
|
+
const html = readFileSync(target, 'utf8');
|
|
123
|
+
expect(html).toMatch(/<h1[^>]*>/);
|
|
124
|
+
// The markdown H1 marker must not survive rendering.
|
|
125
|
+
expect(html).not.toMatch(/^# IRC Services Reference/m);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('shares a stylesheet (a .css file is emitted alongside the HTML)', () => {
|
|
129
|
+
const css = readdirSync(docsDist).filter((f) => f.endsWith('.css'));
|
|
130
|
+
expect(css.length).toBeGreaterThanOrEqual(1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -250,7 +250,7 @@ describe('KiwiConfigSchema — WS path (direct_path)', () => {
|
|
|
250
250
|
expect(() =>
|
|
251
251
|
parseKiwiConfig(
|
|
252
252
|
validConfig({
|
|
253
|
-
startupOptions: validStartupOptions({ direct_path: '/
|
|
253
|
+
startupOptions: validStartupOptions({ direct_path: '/webclient/' }),
|
|
254
254
|
}),
|
|
255
255
|
),
|
|
256
256
|
).toThrowError(/direct_path/u);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
|
|
8
|
+
|
|
9
|
+
// Pure-string assertions over landing/index.html. The file is copied verbatim
|
|
10
|
+
// to dist/index.html by scripts/build.mjs (verified separately by
|
|
11
|
+
// build-smoke.test.ts), so these checks run against the source in CI without
|
|
12
|
+
// paying the Kiwi SPA build cost.
|
|
13
|
+
const html = readFileSync(landingSrc, 'utf8');
|
|
14
|
+
|
|
15
|
+
describe('landing page "Connect" section', () => {
|
|
16
|
+
it('contains a "Connect" heading', () => {
|
|
17
|
+
// The project front door must tell visitors how to reach the IRC server.
|
|
18
|
+
expect(html).toMatch(/<h2[^>]*>\s*Connect\s*<\/h2>/i);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('documents the WeeChat client', () => {
|
|
22
|
+
expect(html).toMatch(/WeeChat/i);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('documents the HexChat client', () => {
|
|
26
|
+
expect(html).toMatch(/HexChat/i);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('documents the irssi client', () => {
|
|
30
|
+
expect(html).toMatch(/irssi/i);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('documents the IRCCloud client', () => {
|
|
34
|
+
expect(html).toMatch(/IRCCloud/i);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('documents TheLounge client', () => {
|
|
38
|
+
expect(html).toMatch(/TheLounge/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('mentions the wss (secure WebSocket) transport for the web client', () => {
|
|
42
|
+
expect(html).toMatch(/\bwss\b/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('mentions port 443 for the wss transport', () => {
|
|
46
|
+
expect(html).toMatch(/443\b/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('mentions the irc+tls :6697 direct-connect transport', () => {
|
|
50
|
+
expect(html).toMatch(/6697\b/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('hides the low-level WebSocket subprotocol framing detail', () => {
|
|
54
|
+
// The subprotocol negotiation + 510-byte frame budget is IRC-wire-level
|
|
55
|
+
// detail that doesn't belong on the project front door. It lives in the
|
|
56
|
+
// docs; the page should not surface `text.ircv3.net` or the 510 figure.
|
|
57
|
+
expect(html).not.toMatch(/text\.ircv3\.net/i);
|
|
58
|
+
expect(html).not.toMatch(/\b510\b/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('links to the Web Client Guide doc', () => {
|
|
62
|
+
// Cross-references to the docs/ subtree keep the page terse while still
|
|
63
|
+
// pointing at full instructions. The docs are out-of-tree (git submodule)
|
|
64
|
+
// so the link is a relative repo path, not a served route.
|
|
65
|
+
expect(html).toMatch(/WebClientGuide/i);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('references the tcp-ws-forwarder bridge for stock TCP clients', () => {
|
|
69
|
+
expect(html).toMatch(/tcp-ws-forwarder/i);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses a generic hostname placeholder (no per-env host baked in)', () => {
|
|
73
|
+
// The landing page is built once and served across staging + prod, so it
|
|
74
|
+
// must NOT pin a concrete worker hostname.
|
|
75
|
+
expect(html).toMatch(/irc\.example\.com|{{\s*hostname\s*}}|your deployed Worker host/i);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('includes a WeeChat /server add snippet', () => {
|
|
79
|
+
expect(html).toMatch(/\/server add/i);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders the connect instructions as a radio-backed client tab picker', () => {
|
|
83
|
+
// A horizontal list of client "buttons" selects which instructions show.
|
|
84
|
+
// Radio inputs share one name so the picker works without JavaScript
|
|
85
|
+
// (the build-smoke suite asserts the page ships no <script> tags).
|
|
86
|
+
const tabRadios = html.match(/type=["']radio["'][^>]*name=["']client-tab["']/gi) ?? [];
|
|
87
|
+
expect(tabRadios.length).toBeGreaterThanOrEqual(4);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders a tab label for each documented client', () => {
|
|
91
|
+
expect(html).toMatch(/<label[^>]*for=["']tab-web["']/i);
|
|
92
|
+
expect(html).toMatch(/<label[^>]*for=["']tab-weechat["']/i);
|
|
93
|
+
expect(html).toMatch(/<label[^>]*for=["']tab-hexchat["']/i);
|
|
94
|
+
expect(html).toMatch(/<label[^>]*for=["']tab-irccloud["']/i);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('hides client panels by default and reveals the selected one via :checked', () => {
|
|
98
|
+
// Pure-CSS tabs: panels are display:none unless their radio is checked,
|
|
99
|
+
// then `:checked ~ #panel-x` flips them to display:block.
|
|
100
|
+
expect(html).toMatch(/\.panel\s*\{[^}]*display:\s*none/i);
|
|
101
|
+
expect(html).toMatch(/:checked\s*~\s*#panel-/i);
|
|
102
|
+
});
|
|
103
|
+
});
|