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.
- package/README.md +12 -4
- package/package.json +2 -3
- package/src/cli/commands/build.ts +2 -1
- package/src/cli/commands/dev.ts +2 -1
- package/src/cli/commands/init.ts +12 -3
- package/src/cli/commands/preview.ts +36 -3
- package/src/cli/main.ts +244 -14
- package/src/render/layout.tsx +22 -81
- package/src/render/right-rail.tsx +5 -3
- package/src/search/index.ts +19 -24
- package/src/search/page.tsx +13 -11
- package/src/server/user-routes.ts +1 -1
- package/templates/default/.github/workflows/ci.yml +0 -3
- package/templates/default/README.md +1 -2
- package/templates/default/package.json +0 -1
- package/templates/default/src/server.ts +1 -1
- package/templates/default/src/ui/layout.tsx +21 -73
- package/templates/default/src/ui/right-rail.tsx +6 -3
- package/templates/default/src/ui/search-page.tsx +13 -10
- package/templates/default/styles/tailwind.css +1 -10
- package/templates/default/tsconfig.json +1 -1
- package/templates/default/scripts/smoke.ts +0 -223
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
/* eslint-disable react/no-danger */
|
|
3
|
-
import type { JSX } from "preact";
|
|
1
|
+
/* eslint-disable react/jsx-key */
|
|
4
2
|
|
|
5
|
-
import {
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
172
|
+
Built with idcmd SSR + Tailwind | Zero JavaScript on
|
|
225
173
|
content pages
|
|
226
174
|
</footer>
|
|
227
175
|
</div>
|
|
228
176
|
{scriptPaths.map((scriptPath) => (
|
|
229
|
-
<script
|
|
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>${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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">
|
|
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
|
|
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
|
|
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
|
-
|
|
90
|
+
`${<SearchPage {...props} />}`;
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
}
|
|
123
123
|
html,
|
|
124
124
|
body {
|
|
125
|
-
/*
|
|
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,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);
|