idcmd 0.0.11 → 0.0.13

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.
@@ -1,18 +1,15 @@
1
- import type { LayoutProps } from "idcmd/client";
2
- /* eslint-disable react/no-danger */
3
- import type { JSX } from "preact";
1
+ /* eslint-disable react/jsx-key */
4
2
 
5
- import { render } from "preact-render-to-string";
3
+ import type { LayoutProps } from "idcmd/client";
6
4
 
7
5
  import { RightRail } from "./right-rail";
8
6
 
9
7
  type NavItem = LayoutProps["navigation"][number]["items"][number];
10
8
 
9
+ const escapeText = (value: string): string => Bun.escapeHTML(value);
10
+
11
11
  const Icon = ({ svg }: { svg: string }): JSX.Element => (
12
- <span
13
- class="inline-flex h-[18px] w-[18px]"
14
- dangerouslySetInnerHTML={{ __html: svg }}
15
- />
12
+ <span class="inline-flex h-[18px] w-[18px]">{svg}</span>
16
13
  );
17
14
 
18
15
  const isActiveLink = (item: NavItem, currentPath: string): boolean =>
@@ -36,19 +33,18 @@ const Sidebar = ({
36
33
  data-prefetch="hover"
37
34
  >
38
35
  <span class="text-muted-foreground">~/</span>
39
- {siteName}
36
+ {escapeText(siteName)}
40
37
  </a>
41
38
  </div>
42
39
  <div class="sidebar-content">
43
40
  {navigation.map((group) => (
44
- <div key={group.id} class="py-2">
41
+ <div class="py-2">
45
42
  <p class="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
46
- {group.label}
43
+ {escapeText(group.label)}
47
44
  </p>
48
45
  <nav class="space-y-1">
49
46
  {group.items.map((item) => (
50
47
  <a
51
- key={item.href}
52
48
  href={item.href}
53
49
  data-prefetch="hover"
54
50
  class={`flex items-center gap-3 px-3 py-1.5 text-sm transition-colors hover:text-sidebar-foreground ${
@@ -58,7 +54,7 @@ const Sidebar = ({
58
54
  }`}
59
55
  >
60
56
  <Icon svg={item.iconSvg} />
61
- <span>{item.title}</span>
57
+ <span>{escapeText(item.title)}</span>
62
58
  </a>
63
59
  ))}
64
60
  </nav>
@@ -68,56 +64,6 @@ const Sidebar = ({
68
64
  </aside>
69
65
  );
70
66
 
71
- const SearchForm = ({ query }: { query?: string }): JSX.Element => (
72
- <form
73
- method="get"
74
- action="/search/"
75
- class="flex w-full items-center"
76
- role="search"
77
- noValidate
78
- >
79
- <label htmlFor="site-search" class="sr-only">
80
- Search pages
81
- </label>
82
- <input
83
- id="site-search"
84
- name="q"
85
- type="search"
86
- autoComplete="off"
87
- spellcheck={false}
88
- placeholder="Search..."
89
- defaultValue={query ?? ""}
90
- class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
91
- />
92
- </form>
93
- );
94
-
95
- const TopNavbar = ({
96
- query,
97
- siteName,
98
- }: {
99
- query?: LayoutProps["searchQuery"];
100
- siteName: LayoutProps["siteName"];
101
- }): JSX.Element => (
102
- <header class="sticky top-0 z-30 border-b border-border bg-background/80 backdrop-blur-sm">
103
- <div class="mx-auto max-w-6xl px-8 py-3">
104
- <div class="flex items-center gap-4">
105
- <a
106
- href="/"
107
- class="text-sm font-mono font-medium tracking-tight lg:hidden"
108
- data-prefetch="hover"
109
- >
110
- <span class="text-muted-foreground">~/</span>
111
- {siteName}
112
- </a>
113
- <div class="not-prose ml-auto w-full max-w-xs">
114
- <SearchForm query={query} />
115
- </div>
116
- </div>
117
- </div>
118
- </header>
119
- );
120
-
121
67
  const buildHtmlClass = (
122
68
  smoothScroll: LayoutProps["rightRail"]["smoothScroll"]
123
69
  ): string => (smoothScroll ? "dark smooth-scroll" : "dark");
@@ -151,7 +97,6 @@ const Layout = ({
151
97
  currentPath,
152
98
  navigation,
153
99
  scriptPaths = [],
154
- searchQuery,
155
100
  showRightRail = true,
156
101
  rightRail,
157
102
  tocItems,
@@ -170,14 +115,16 @@ const Layout = ({
170
115
  <head>
171
116
  <meta charset="utf-8" />
172
117
  <meta name="viewport" content="width=device-width, initial-scale=1" />
173
- <title>{title}</title>
174
- {description ? <meta name="description" content={description} /> : null}
118
+ <title>{escapeText(title)}</title>
119
+ {description ? (
120
+ <meta name="description" content={escapeText(description)} />
121
+ ) : null}
175
122
  {canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
176
123
  <link rel="preconnect" href="https://fonts.googleapis.com" />
177
124
  <link
178
125
  rel="preconnect"
179
126
  href="https://fonts.gstatic.com"
180
- crossOrigin="anonymous"
127
+ crossorigin="anonymous"
181
128
  />
182
129
  <link
183
130
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
@@ -201,15 +148,16 @@ const Layout = ({
201
148
  currentPath={currentPath}
202
149
  />
203
150
  <div class="main-wrapper">
204
- <TopNavbar query={searchQuery} siteName={siteName} />
205
151
  <main class="main-content">
206
152
  <div class="mx-auto flex w-full max-w-6xl items-start gap-10">
207
153
  <article
208
154
  class={`prose min-w-0 flex-1${
209
155
  currentPath === "/" ? " prose-home" : ""
210
156
  }`}
211
- dangerouslySetInnerHTML={{ __html: content }}
212
- />
157
+ >
158
+ {/* content is pre-rendered markdown HTML */}
159
+ {content}
160
+ </article>
213
161
  {shouldShowRightRail ? (
214
162
  <RightRail
215
163
  canonicalUrl={canonicalUrl}
@@ -221,12 +169,12 @@ const Layout = ({
221
169
  </div>
222
170
  </main>
223
171
  <footer class="site-footer">
224
- Built with Preact SSR + Tailwind &nbsp;|&nbsp; Zero JavaScript on
172
+ Built with idcmd SSR + Tailwind &nbsp;|&nbsp; Zero JavaScript on
225
173
  content pages
226
174
  </footer>
227
175
  </div>
228
176
  {scriptPaths.map((scriptPath) => (
229
- <script key={scriptPath} defer src={scriptPath} />
177
+ <script defer src={scriptPath} />
230
178
  ))}
231
179
  </body>
232
180
  </html>
@@ -234,4 +182,4 @@ const Layout = ({
234
182
  };
235
183
 
236
184
  export const renderLayout = (props: LayoutProps): string =>
237
- `<!DOCTYPE html>${render(<Layout {...props} />)}`;
185
+ `<!DOCTYPE html>${<Layout {...props} />}`;
@@ -1,5 +1,8 @@
1
+ /* eslint-disable react/jsx-key */
2
+
1
3
  import type { RightRailProps } from "idcmd/client";
2
- import type { JSX } from "preact";
4
+
5
+ const escapeText = (value: string): string => Bun.escapeHTML(value);
3
6
 
4
7
  const CaretDownIcon = (): JSX.Element => (
5
8
  <svg
@@ -173,13 +176,13 @@ const OnThisPage = ({
173
176
  <div class="toc-scroll min-h-0 flex-1" data-toc-scroll-container="1">
174
177
  <ul class="space-y-2 text-sm text-muted-foreground">
175
178
  {items.map((item) => (
176
- <li key={item.id} class={item.level >= 3 ? "pl-3" : ""}>
179
+ <li class={item.level >= 3 ? "pl-3" : ""}>
177
180
  <a
178
181
  href={`#${encodeURIComponent(item.id)}`}
179
182
  class="hover:text-foreground"
180
183
  data-toc-link="1"
181
184
  >
182
- {item.text}
185
+ {escapeText(item.text)}
183
186
  </a>
184
187
  </li>
185
188
  ))}
@@ -1,7 +1,8 @@
1
+ /* eslint-disable react/jsx-key */
2
+
1
3
  import type { SearchPageProps } from "idcmd/client";
2
- import type { JSX } from "preact";
3
4
 
4
- import { render as renderToString } from "preact-render-to-string";
5
+ const escapeText = (value: string): string => Bun.escapeHTML(value);
5
6
 
6
7
  const ResultItem = ({
7
8
  result,
@@ -13,9 +14,11 @@ const ResultItem = ({
13
14
  href={result.slug}
14
15
  class="font-medium underline decoration-border underline-offset-4"
15
16
  >
16
- {result.title}
17
+ {escapeText(result.title)}
17
18
  </a>
18
- <p class="mt-1 text-sm text-muted-foreground">{result.description}</p>
19
+ <p class="mt-1 text-sm text-muted-foreground">
20
+ {escapeText(result.description)}
21
+ </p>
19
22
  </li>
20
23
  );
21
24
 
@@ -33,12 +36,12 @@ const EmptyState = ({
33
36
  <p class="mt-4 font-medium text-foreground">Popular pages</p>
34
37
  <ul class="mt-2 space-y-1">
35
38
  {topPages.map((page) => (
36
- <li key={page.href}>
39
+ <li>
37
40
  <a
38
41
  href={page.href}
39
42
  class="underline decoration-border underline-offset-4"
40
43
  >
41
- {page.title}
44
+ {escapeText(page.title)}
42
45
  </a>
43
46
  </li>
44
47
  ))}
@@ -63,8 +66,8 @@ const SearchPage = ({
63
66
  {showResults ? (
64
67
  <p class="mt-2 text-sm text-muted-foreground">
65
68
  {results.length === 0
66
- ? `No matches for "${trimmed}".`
67
- : `Found ${results.length} result(s) for "${trimmed}".`}
69
+ ? `No matches for "${escapeText(trimmed)}".`
70
+ : `Found ${results.length} result(s) for "${escapeText(trimmed)}".`}
68
71
  </p>
69
72
  ) : (
70
73
  <div class="mt-2">
@@ -75,7 +78,7 @@ const SearchPage = ({
75
78
  {showResults ? (
76
79
  <ul class="mt-4 space-y-2">
77
80
  {results.map((result) => (
78
- <ResultItem key={result.slug} result={result} />
81
+ <ResultItem result={result} />
79
82
  ))}
80
83
  </ul>
81
84
  ) : null}
@@ -84,4 +87,4 @@ const SearchPage = ({
84
87
  };
85
88
 
86
89
  export const renderSearchPageContent = (props: SearchPageProps): string =>
87
- renderToString(<SearchPage {...props} />);
90
+ `${<SearchPage {...props} />}`;
@@ -122,7 +122,7 @@
122
122
  }
123
123
  html,
124
124
  body {
125
- /* Prevent rubber-band overscroll from pulling the sticky top navbar down. */
125
+ /* Keep vertical overscroll behavior predictable across browsers. */
126
126
  overscroll-behavior-y: none;
127
127
  }
128
128
  body {
@@ -134,15 +134,6 @@ html.smooth-scroll {
134
134
  scroll-behavior: smooth;
135
135
  }
136
136
 
137
- /*
138
- Sticky + backdrop-filter can jitter on fast scroll in some browsers.
139
- Forcing the header into its own composited layer tends to reduce jumping.
140
- */
141
- header {
142
- transform: translateZ(0);
143
- will-change: transform;
144
- }
145
-
146
137
  /* ============================================
147
138
  LAYOUT - Sidebar and main content structure
148
139
  ============================================ */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "jsx": "react-jsx",
4
- "jsxImportSource": "preact",
4
+ "jsxImportSource": "@kitajs/html",
5
5
  "lib": ["ESNext", "DOM", "DOM.Iterable"],
6
6
  "target": "ESNext",
7
7
  "module": "Preserve",
@@ -1,223 +0,0 @@
1
- interface CommandResult {
2
- code: number;
3
- stderr: string;
4
- stdout: string;
5
- }
6
-
7
- const BASE_URL = process.env.IDCMD_SMOKE_BASE_URL ?? "http://127.0.0.1:4000";
8
- const CURL_MAX_TIME_SECONDS = "5";
9
- const READY_TIMEOUT_MS = 60_000;
10
- const READY_INTERVAL_MS = 500;
11
- const SHUTDOWN_TIMEOUT_MS = 5000;
12
-
13
- const delay = (ms: number): Promise<void> => Bun.sleep(ms);
14
-
15
- const runCommand = async (command: string[]): Promise<CommandResult> => {
16
- const proc = Bun.spawn(command, {
17
- cwd: process.cwd(),
18
- stderr: "pipe",
19
- stdout: "pipe",
20
- });
21
- const [stdout, stderr, code] = await Promise.all([
22
- new Response(proc.stdout).text(),
23
- new Response(proc.stderr).text(),
24
- proc.exited,
25
- ]);
26
- return { code, stderr, stdout };
27
- };
28
-
29
- const runCurl = (path: string): Promise<CommandResult> =>
30
- runCommand([
31
- "curl",
32
- "-fsS",
33
- "--max-time",
34
- CURL_MAX_TIME_SECONDS,
35
- `${BASE_URL}${path}`,
36
- ]);
37
-
38
- const assertCommandOk = (label: string, result: CommandResult): void => {
39
- if (result.code === 0) {
40
- return;
41
- }
42
- throw new Error(
43
- [
44
- `${label} failed with exit code ${String(result.code)}.`,
45
- "stdout:",
46
- result.stdout.trim() || "(empty)",
47
- "stderr:",
48
- result.stderr.trim() || "(empty)",
49
- ].join("\n")
50
- );
51
- };
52
-
53
- const expectIncludes = (args: {
54
- haystack: string;
55
- label: string;
56
- needle: string;
57
- }): void => {
58
- if (args.haystack.includes(args.needle)) {
59
- return;
60
- }
61
- throw new Error(`Expected ${args.label} to include ${args.needle}.`);
62
- };
63
-
64
- const waitForReady = async (): Promise<void> => {
65
- const startedAt = Date.now();
66
- let lastFailure = "(no attempts yet)";
67
-
68
- while (Date.now() - startedAt < READY_TIMEOUT_MS) {
69
- const ready = await runCurl("/");
70
- if (ready.code === 0) {
71
- return;
72
- }
73
- lastFailure = ready.stderr.trim() || ready.stdout.trim() || "curl failed";
74
- await delay(READY_INTERVAL_MS);
75
- }
76
-
77
- throw new Error(
78
- `dev server did not become ready within ${String(
79
- READY_TIMEOUT_MS
80
- )}ms. Last curl failure: ${lastFailure}`
81
- );
82
- };
83
-
84
- const shutdownDev = async (
85
- proc: ReturnType<typeof Bun.spawn>
86
- ): Promise<void> => {
87
- try {
88
- proc.kill("SIGTERM");
89
- } catch {
90
- return;
91
- }
92
-
93
- const didExit = await Promise.race([
94
- proc.exited.then(() => true),
95
- delay(SHUTDOWN_TIMEOUT_MS).then(() => false),
96
- ]);
97
- if (!didExit) {
98
- try {
99
- proc.kill("SIGKILL");
100
- } catch {
101
- // ignore
102
- }
103
- await proc.exited;
104
- }
105
- };
106
-
107
- const assertHomeResponse = async (): Promise<void> => {
108
- const home = await runCurl("/");
109
- assertCommandOk("curl /", home);
110
- expectIncludes({ haystack: home.stdout, label: "/", needle: "<html" });
111
- };
112
-
113
- const assertAboutResponse = async (): Promise<void> => {
114
- const about = await runCurl("/about/");
115
- assertCommandOk("curl /about/", about);
116
- if (!about.stdout.includes("# About") && !about.stdout.includes(">About<")) {
117
- throw new Error("Expected /about/ response to include About heading.");
118
- }
119
- };
120
-
121
- const assertLlmsResponse = async (): Promise<void> => {
122
- const llms = await runCurl("/llms.txt");
123
- assertCommandOk("curl /llms.txt", llms);
124
- if (llms.stdout.trim().length === 0 || !llms.stdout.includes("about.md")) {
125
- throw new Error("Expected /llms.txt to be non-empty and include about.md.");
126
- }
127
- };
128
-
129
- const assertApiResponse = async (): Promise<void> => {
130
- const api = await runCurl("/api/hello");
131
- assertCommandOk("curl /api/hello", api);
132
- const payload = JSON.parse(api.stdout) as { message?: string; ok?: boolean };
133
- if (payload.ok !== true || payload.message !== "Hello from idcmd route!") {
134
- throw new Error("Expected /api/hello payload to match template route.");
135
- }
136
- };
137
-
138
- const runSmokeChecks = async (): Promise<void> => {
139
- await assertHomeResponse();
140
- await assertAboutResponse();
141
- await assertLlmsResponse();
142
- await assertApiResponse();
143
- };
144
-
145
- const runProjectCheck = async (): Promise<void> => {
146
- const check = await runCommand([process.execPath, "run", "check"]);
147
- assertCommandOk("bun run check", check);
148
- };
149
-
150
- const startDev = (): {
151
- devProc: ReturnType<typeof Bun.spawn>;
152
- devStderr: Promise<string>;
153
- devStdout: Promise<string>;
154
- } => {
155
- const devProc = Bun.spawn([process.execPath, "run", "dev"], {
156
- cwd: process.cwd(),
157
- stderr: "pipe",
158
- stdout: "pipe",
159
- });
160
- return {
161
- devProc,
162
- devStderr: new Response(devProc.stderr).text(),
163
- devStdout: new Response(devProc.stdout).text(),
164
- };
165
- };
166
-
167
- const logDevFailure = async (args: {
168
- error: unknown;
169
- stderr: Promise<string>;
170
- stdout: Promise<string>;
171
- }): Promise<void> => {
172
- const [stdout, stderr] = await Promise.all([args.stdout, args.stderr]);
173
- const message =
174
- args.error instanceof Error ? args.error.message : String(args.error);
175
- console.error(message);
176
- console.error("dev stdout:");
177
- console.error(stdout.trim() || "(empty)");
178
- console.error("dev stderr:");
179
- console.error(stderr.trim() || "(empty)");
180
- };
181
-
182
- const toErrorMessage = (error: unknown): string => {
183
- if (error instanceof Error) {
184
- return error.message;
185
- }
186
- return String(error);
187
- };
188
-
189
- const runDevSmokeFlow = async (): Promise<number> => {
190
- const { devProc, devStderr, devStdout } = startDev();
191
-
192
- try {
193
- await waitForReady();
194
- await runSmokeChecks();
195
- } catch (error) {
196
- await logDevFailure({ error, stderr: devStderr, stdout: devStdout });
197
- return 1;
198
- } finally {
199
- await shutdownDev(devProc);
200
- }
201
- return 0;
202
- };
203
-
204
- const runPostDevCheck = async (): Promise<number> => {
205
- try {
206
- await runProjectCheck();
207
- return 0;
208
- } catch (error) {
209
- console.error(toErrorMessage(error));
210
- return 1;
211
- }
212
- };
213
-
214
- const main = async (): Promise<number> => {
215
- const smokeCode = await runDevSmokeFlow();
216
- if (smokeCode !== 0) {
217
- return smokeCode;
218
- }
219
- return runPostDevCheck();
220
- };
221
-
222
- const code = await main();
223
- process.exit(code);