idcmd 0.0.5 → 0.0.6
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 +23 -4
- package/package.json +2 -2
- package/src/build.ts +4 -3
- package/src/cli/commands/build.ts +7 -0
- package/src/cli/commands/client.ts +317 -0
- package/src/cli/commands/dev.ts +87 -23
- package/src/cli/commands/init.ts +93 -2
- package/src/cli/main.ts +12 -0
- package/src/cli/runtime-assets.ts +92 -0
- package/src/client/index.ts +7 -1
- package/src/render/layout-loader.ts +6 -3
- package/src/render/layout.tsx +10 -2
- package/src/render/page-renderer.ts +12 -2
- package/src/render/right-rail-loader.ts +49 -0
- package/src/render/right-rail.tsx +10 -6
- package/src/search/page.tsx +4 -2
- package/src/search/search-page-loader.ts +51 -0
- package/src/search/server-page.ts +52 -18
- package/templates/default/.github/workflows/ci.yml +24 -0
- package/templates/default/README.md +23 -0
- package/templates/default/package.json +2 -1
- package/templates/default/scripts/check-internal.ts +56 -0
- package/templates/default/scripts/check.ts +318 -0
- package/templates/default/scripts/smoke.ts +193 -0
- package/templates/default/site/client/layout.tsx +237 -2
- package/templates/default/site/client/right-rail.tsx +246 -1
- package/templates/default/site/{public/_idcmd/llm-menu.js → client/runtime/llm-menu.ts} +27 -18
- package/templates/default/site/{public/_idcmd/nav-prefetch.js → client/runtime/nav-prefetch.ts} +3 -3
- package/templates/default/site/{public/_idcmd/right-rail-scrollspy.js → client/runtime/right-rail-scrollspy.ts} +73 -32
- package/templates/default/site/client/search-page.tsx +87 -1
- package/templates/default/tsconfig.json +1 -1
- /package/templates/default/site/{public/_idcmd/live-reload.js → client/runtime/live-reload.ts} +0 -0
package/src/cli/main.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ParsedArgs } from "./args";
|
|
|
4
4
|
|
|
5
5
|
import { parseArgs } from "./args";
|
|
6
6
|
import { buildCommand } from "./commands/build";
|
|
7
|
+
import { clientCommand } from "./commands/client";
|
|
7
8
|
import { deployCommand } from "./commands/deploy";
|
|
8
9
|
import { devCommand } from "./commands/dev";
|
|
9
10
|
import { initCommand } from "./commands/init";
|
|
@@ -23,12 +24,16 @@ const usage = (): string =>
|
|
|
23
24
|
" idcmd build",
|
|
24
25
|
" idcmd preview [--port <port>]",
|
|
25
26
|
" idcmd deploy",
|
|
27
|
+
" idcmd client <add|update> <layout|right-rail|search-page|runtime|all> [--dry-run] [--yes]",
|
|
26
28
|
"",
|
|
27
29
|
].join("\n");
|
|
28
30
|
|
|
29
31
|
const asStringFlag = (value: unknown): string | undefined =>
|
|
30
32
|
typeof value === "string" ? value : undefined;
|
|
31
33
|
|
|
34
|
+
const asBooleanFlag = (value: unknown): boolean =>
|
|
35
|
+
value === true || value === "true";
|
|
36
|
+
|
|
32
37
|
const handleInit = (parsed: ParsedArgs): Promise<number> => {
|
|
33
38
|
const [dir] = parsed.positionals;
|
|
34
39
|
return initCommand(dir, parsed.flags);
|
|
@@ -46,8 +51,15 @@ const handlePreview = (parsed: ParsedArgs): Promise<number> => {
|
|
|
46
51
|
|
|
47
52
|
const handleDeploy = (): Promise<number> => deployCommand();
|
|
48
53
|
|
|
54
|
+
const handleClient = (parsed: ParsedArgs): Promise<number> =>
|
|
55
|
+
clientCommand(parsed.positionals, {
|
|
56
|
+
dryRun: asBooleanFlag(parsed.flags["dry-run"]),
|
|
57
|
+
yes: asBooleanFlag(parsed.flags.yes),
|
|
58
|
+
});
|
|
59
|
+
|
|
49
60
|
const handlers: Record<string, (parsed: ParsedArgs) => Promise<number>> = {
|
|
50
61
|
build: () => handleBuild(),
|
|
62
|
+
client: (parsed) => handleClient(parsed),
|
|
51
63
|
deploy: () => handleDeploy(),
|
|
52
64
|
dev: (parsed) => handleDev(parsed),
|
|
53
65
|
init: (parsed) => handleInit(parsed),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { joinPath } from "./path";
|
|
2
|
+
|
|
3
|
+
const RUNTIME_SOURCE_DIR = joinPath("site", "client", "runtime");
|
|
4
|
+
const RUNTIME_OUTPUT_DIR = joinPath("site", "public", "_idcmd");
|
|
5
|
+
|
|
6
|
+
const RUNTIME_ENTRY_FILES = [
|
|
7
|
+
"live-reload.ts",
|
|
8
|
+
"llm-menu.ts",
|
|
9
|
+
"nav-prefetch.ts",
|
|
10
|
+
"right-rail-scrollspy.ts",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
const runtimeEntryPaths = (): string[] =>
|
|
14
|
+
RUNTIME_ENTRY_FILES.map((fileName) => joinPath(RUNTIME_SOURCE_DIR, fileName));
|
|
15
|
+
|
|
16
|
+
const scanRuntimeEntrypoints = async (
|
|
17
|
+
entries: readonly string[]
|
|
18
|
+
): Promise<{
|
|
19
|
+
existing: string[];
|
|
20
|
+
missing: string[];
|
|
21
|
+
}> => {
|
|
22
|
+
const existing: string[] = [];
|
|
23
|
+
const missing: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
// eslint-disable-next-line no-await-in-loop
|
|
27
|
+
if (await Bun.file(entry).exists()) {
|
|
28
|
+
existing.push(entry);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
missing.push(entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { existing, missing };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getRuntimeEntrypoints = async (): Promise<string[]> => {
|
|
38
|
+
const entries = runtimeEntryPaths();
|
|
39
|
+
const { existing, missing } = await scanRuntimeEntrypoints(entries);
|
|
40
|
+
|
|
41
|
+
if (existing.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Incomplete runtime scripts in ${RUNTIME_SOURCE_DIR}. Missing: ${missing.join(", ")}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return existing;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const buildRuntimeArgs = (entrypoints: string[], watch: boolean): string[] => [
|
|
55
|
+
"bun",
|
|
56
|
+
"build",
|
|
57
|
+
...entrypoints,
|
|
58
|
+
"--format",
|
|
59
|
+
"iife",
|
|
60
|
+
"--outdir",
|
|
61
|
+
RUNTIME_OUTPUT_DIR,
|
|
62
|
+
"--target",
|
|
63
|
+
"browser",
|
|
64
|
+
...(watch ? ["--watch"] : []),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const compileRuntimeAssetsOnce = async (): Promise<number> => {
|
|
68
|
+
const entrypoints = await getRuntimeEntrypoints();
|
|
69
|
+
if (entrypoints.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const proc = Bun.spawn(buildRuntimeArgs(entrypoints, false), {
|
|
74
|
+
stderr: "inherit",
|
|
75
|
+
stdout: "inherit",
|
|
76
|
+
});
|
|
77
|
+
return proc.exited;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const watchRuntimeAssets = async (): Promise<ReturnType<
|
|
81
|
+
typeof Bun.spawn
|
|
82
|
+
> | null> => {
|
|
83
|
+
const entrypoints = await getRuntimeEntrypoints();
|
|
84
|
+
if (entrypoints.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Bun.spawn(buildRuntimeArgs(entrypoints, true), {
|
|
89
|
+
stderr: "inherit",
|
|
90
|
+
stdout: "inherit",
|
|
91
|
+
});
|
|
92
|
+
};
|
package/src/client/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
export type { LayoutProps } from "../render/layout";
|
|
1
|
+
export type { LayoutProps, RenderLayout } from "../render/layout";
|
|
2
2
|
export { renderLayout } from "../render/layout";
|
|
3
3
|
|
|
4
4
|
export type { TocItem } from "../render/toc";
|
|
5
|
+
export type { RightRailComponent, RightRailProps } from "../render/right-rail";
|
|
5
6
|
export { RightRail } from "../render/right-rail";
|
|
6
7
|
|
|
8
|
+
export type {
|
|
9
|
+
RenderSearchPageContent,
|
|
10
|
+
SearchPageProps,
|
|
11
|
+
TopPageLink,
|
|
12
|
+
} from "../search/page";
|
|
7
13
|
export { renderSearchPageContent } from "../search/page";
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RenderLayout } from "./layout";
|
|
2
2
|
|
|
3
3
|
import { renderLayout as defaultRenderLayout } from "./layout";
|
|
4
4
|
|
|
5
|
-
export type RenderLayout = (props: LayoutProps) => string;
|
|
6
|
-
|
|
7
5
|
const USER_LAYOUT_PATH = "site/client/layout.tsx";
|
|
8
6
|
|
|
9
7
|
const loadUserLayout = async (
|
|
@@ -44,3 +42,8 @@ export const getRenderLayout = async (): Promise<RenderLayout> => {
|
|
|
44
42
|
cached = await tryLoadUserRenderLayout();
|
|
45
43
|
return cached ?? defaultRenderLayout;
|
|
46
44
|
};
|
|
45
|
+
|
|
46
|
+
export const resetLayoutLoaderForTests = (): void => {
|
|
47
|
+
cached = null;
|
|
48
|
+
attempted = false;
|
|
49
|
+
};
|
package/src/render/layout.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { render } from "preact-render-to-string";
|
|
|
5
5
|
|
|
6
6
|
import type { NavGroup, NavItem } from "../content/navigation";
|
|
7
7
|
import type { ResolvedRightRailConfig } from "../site/config";
|
|
8
|
+
import type { RightRailComponent } from "./right-rail";
|
|
8
9
|
import type { TocItem } from "./toc";
|
|
9
10
|
|
|
10
11
|
import { RightRail } from "./right-rail";
|
|
@@ -22,10 +23,13 @@ export interface LayoutProps {
|
|
|
22
23
|
scriptPaths?: string[];
|
|
23
24
|
searchQuery?: string;
|
|
24
25
|
showRightRail?: boolean;
|
|
26
|
+
rightRailComponent?: RightRailComponent;
|
|
25
27
|
rightRail: ResolvedRightRailConfig;
|
|
26
28
|
tocItems: TocItem[];
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export type RenderLayout = (props: LayoutProps) => string;
|
|
32
|
+
|
|
29
33
|
const Icon = ({ svg }: { svg: string }): JSX.Element => (
|
|
30
34
|
<span
|
|
31
35
|
class="inline-flex w-[18px] h-[18px]"
|
|
@@ -231,6 +235,7 @@ interface DocumentBodyProps {
|
|
|
231
235
|
shouldShowRightRail: boolean;
|
|
232
236
|
siteName: string;
|
|
233
237
|
tocItems: TocItem[];
|
|
238
|
+
rightRailComponent: RightRailComponent;
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
const DocumentBody = ({
|
|
@@ -245,6 +250,7 @@ const DocumentBody = ({
|
|
|
245
250
|
shouldShowRightRail,
|
|
246
251
|
siteName,
|
|
247
252
|
tocItems,
|
|
253
|
+
rightRailComponent: RightRailComponent,
|
|
248
254
|
}: DocumentBodyProps): JSX.Element => (
|
|
249
255
|
<body
|
|
250
256
|
class="bg-background text-foreground font-sans"
|
|
@@ -266,7 +272,7 @@ const DocumentBody = ({
|
|
|
266
272
|
dangerouslySetInnerHTML={{ __html: content }}
|
|
267
273
|
/>
|
|
268
274
|
{shouldShowRightRail ? (
|
|
269
|
-
<
|
|
275
|
+
<RightRailComponent
|
|
270
276
|
canonicalUrl={canonicalUrl}
|
|
271
277
|
currentPath={currentPath}
|
|
272
278
|
tocItems={tocItems}
|
|
@@ -299,6 +305,7 @@ const Layout = ({
|
|
|
299
305
|
scriptPaths = [],
|
|
300
306
|
searchQuery,
|
|
301
307
|
showRightRail = true,
|
|
308
|
+
rightRailComponent = RightRail,
|
|
302
309
|
rightRail,
|
|
303
310
|
tocItems,
|
|
304
311
|
}: LayoutProps): JSX.Element => {
|
|
@@ -330,10 +337,11 @@ const Layout = ({
|
|
|
330
337
|
shouldShowRightRail={shouldShowRightRail}
|
|
331
338
|
siteName={siteName}
|
|
332
339
|
tocItems={tocItems}
|
|
340
|
+
rightRailComponent={rightRailComponent}
|
|
333
341
|
/>
|
|
334
342
|
</html>
|
|
335
343
|
);
|
|
336
344
|
};
|
|
337
345
|
|
|
338
|
-
export const renderLayout = (props
|
|
346
|
+
export const renderLayout: RenderLayout = (props) =>
|
|
339
347
|
`<!DOCTYPE html>${render(<Layout {...props} />)}`;
|
|
@@ -13,6 +13,7 @@ import { loadSiteConfig, resolveRightRailConfig } from "../site/config";
|
|
|
13
13
|
import { resolveCanonicalUrl } from "../site/urls";
|
|
14
14
|
import { getRenderLayout } from "./layout-loader";
|
|
15
15
|
import { renderMarkdownToHtml } from "./markdown";
|
|
16
|
+
import { getRightRail } from "./right-rail-loader";
|
|
16
17
|
import { extractTocFromHtml } from "./toc";
|
|
17
18
|
|
|
18
19
|
const ASSET_PREFIX = "/_idcmd";
|
|
@@ -118,7 +119,14 @@ export const renderDocument = async (options: {
|
|
|
118
119
|
title: string;
|
|
119
120
|
tocItems: ReturnType<typeof extractTocFromHtml>;
|
|
120
121
|
}): Promise<string> => {
|
|
121
|
-
const renderLayout = await
|
|
122
|
+
const [renderLayout, rightRailComponent] = await Promise.all([
|
|
123
|
+
getRenderLayout(),
|
|
124
|
+
getRightRail(),
|
|
125
|
+
]);
|
|
126
|
+
const scriptPaths = options.scriptPaths ?? [
|
|
127
|
+
`${ASSET_PREFIX}/nav-prefetch.js`,
|
|
128
|
+
];
|
|
129
|
+
|
|
122
130
|
return renderLayout({
|
|
123
131
|
canonicalUrl: options.canonicalUrl,
|
|
124
132
|
content: options.contentHtml,
|
|
@@ -128,7 +136,8 @@ export const renderDocument = async (options: {
|
|
|
128
136
|
inlineCss: options.inlineCss,
|
|
129
137
|
navigation: options.navigation,
|
|
130
138
|
rightRail: options.rightRail,
|
|
131
|
-
|
|
139
|
+
rightRailComponent,
|
|
140
|
+
scriptPaths,
|
|
132
141
|
searchQuery: options.searchQuery,
|
|
133
142
|
showRightRail: options.showRightRail,
|
|
134
143
|
siteName: options.siteName,
|
|
@@ -244,6 +253,7 @@ const computeScriptPaths = (options: {
|
|
|
244
253
|
shouldShowRightRail: boolean;
|
|
245
254
|
tocItems: readonly unknown[];
|
|
246
255
|
}): string[] => [
|
|
256
|
+
`${ASSET_PREFIX}/nav-prefetch.js`,
|
|
247
257
|
...(options.isDev ? [`${ASSET_PREFIX}/live-reload.js`] : []),
|
|
248
258
|
...(options.shouldShowRightRail ? [`${ASSET_PREFIX}/llm-menu.js`] : []),
|
|
249
259
|
...(options.shouldShowRightRail &&
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RightRailComponent } from "./right-rail";
|
|
2
|
+
|
|
3
|
+
import { RightRail as defaultRightRail } from "./right-rail";
|
|
4
|
+
|
|
5
|
+
const USER_RIGHT_RAIL_PATH = "site/client/right-rail.tsx";
|
|
6
|
+
|
|
7
|
+
const loadUserRightRail = async (
|
|
8
|
+
filePath: string
|
|
9
|
+
): Promise<RightRailComponent | null> => {
|
|
10
|
+
try {
|
|
11
|
+
const url = Bun.pathToFileURL(filePath);
|
|
12
|
+
const mod = (await import(url.toString())) as unknown as {
|
|
13
|
+
RightRail?: RightRailComponent;
|
|
14
|
+
};
|
|
15
|
+
return mod.RightRail ?? null;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
console.warn(
|
|
19
|
+
`[right-rail] Failed to load user right rail from ${filePath}: ${message}`
|
|
20
|
+
);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const tryLoadUserRightRail = async (): Promise<RightRailComponent | null> => {
|
|
26
|
+
const exists = await Bun.file(USER_RIGHT_RAIL_PATH).exists();
|
|
27
|
+
if (!exists) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return loadUserRightRail(USER_RIGHT_RAIL_PATH);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let cached: RightRailComponent | null = null;
|
|
34
|
+
let attempted = false;
|
|
35
|
+
|
|
36
|
+
export const getRightRail = async (): Promise<RightRailComponent> => {
|
|
37
|
+
if (attempted) {
|
|
38
|
+
return cached ?? defaultRightRail;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attempted = true;
|
|
42
|
+
cached = await tryLoadUserRightRail();
|
|
43
|
+
return cached ?? defaultRightRail;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const resetRightRailLoaderForTests = (): void => {
|
|
47
|
+
cached = null;
|
|
48
|
+
attempted = false;
|
|
49
|
+
};
|
|
@@ -216,17 +216,21 @@ const getPanelClass = (
|
|
|
216
216
|
? "fixed top-24 bottom-0 right-8 z-20 w-64 flex flex-col gap-6 min-h-0"
|
|
217
217
|
: "sticky top-24 h-[calc(100vh-6rem)] flex flex-col gap-6 min-h-0";
|
|
218
218
|
|
|
219
|
+
export interface RightRailProps {
|
|
220
|
+
canonicalUrl?: string;
|
|
221
|
+
currentPath: string;
|
|
222
|
+
tocItems: TocItem[];
|
|
223
|
+
rightRailConfig: ResolvedRightRailConfig;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type RightRailComponent = (props: RightRailProps) => JSX.Element | null;
|
|
227
|
+
|
|
219
228
|
export const RightRail = ({
|
|
220
229
|
canonicalUrl,
|
|
221
230
|
currentPath,
|
|
222
231
|
tocItems,
|
|
223
232
|
rightRailConfig,
|
|
224
|
-
}: {
|
|
225
|
-
canonicalUrl?: string;
|
|
226
|
-
currentPath: string;
|
|
227
|
-
tocItems: TocItem[];
|
|
228
|
-
rightRailConfig: ResolvedRightRailConfig;
|
|
229
|
-
}): JSX.Element => {
|
|
233
|
+
}: RightRailProps): JSX.Element => {
|
|
230
234
|
const { chatgptUrl, claudeUrl, markdownPath } = buildAskUrls({
|
|
231
235
|
canonicalUrl,
|
|
232
236
|
currentPath,
|
package/src/search/page.tsx
CHANGED
|
@@ -9,13 +9,15 @@ export interface TopPageLink {
|
|
|
9
9
|
title: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
interface SearchPageProps {
|
|
12
|
+
export interface SearchPageProps {
|
|
13
13
|
query: string;
|
|
14
14
|
minQueryLength: number;
|
|
15
15
|
results: SearchResult[];
|
|
16
16
|
topPages: TopPageLink[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type RenderSearchPageContent = (props: SearchPageProps) => string;
|
|
20
|
+
|
|
19
21
|
const ResultItem = ({ result }: { result: SearchResult }): JSX.Element => (
|
|
20
22
|
<li class="rounded-md border border-border p-3">
|
|
21
23
|
<a
|
|
@@ -92,5 +94,5 @@ const SearchPage = ({
|
|
|
92
94
|
);
|
|
93
95
|
};
|
|
94
96
|
|
|
95
|
-
export const renderSearchPageContent = (props
|
|
97
|
+
export const renderSearchPageContent: RenderSearchPageContent = (props) =>
|
|
96
98
|
renderToString(<SearchPage {...props} />);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RenderSearchPageContent } from "./page";
|
|
2
|
+
|
|
3
|
+
import { renderSearchPageContent as defaultRenderSearchPageContent } from "./page";
|
|
4
|
+
|
|
5
|
+
const USER_SEARCH_PAGE_PATH = "site/client/search-page.tsx";
|
|
6
|
+
|
|
7
|
+
const loadUserSearchPage = async (
|
|
8
|
+
filePath: string
|
|
9
|
+
): Promise<RenderSearchPageContent | null> => {
|
|
10
|
+
try {
|
|
11
|
+
const url = Bun.pathToFileURL(filePath);
|
|
12
|
+
const mod = (await import(url.toString())) as unknown as {
|
|
13
|
+
renderSearchPageContent?: RenderSearchPageContent;
|
|
14
|
+
};
|
|
15
|
+
return mod.renderSearchPageContent ?? null;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
console.warn(
|
|
19
|
+
`[search-page] Failed to load user search page from ${filePath}: ${message}`
|
|
20
|
+
);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const tryLoadUserSearchPage =
|
|
26
|
+
async (): Promise<RenderSearchPageContent | null> => {
|
|
27
|
+
const exists = await Bun.file(USER_SEARCH_PAGE_PATH).exists();
|
|
28
|
+
if (!exists) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return loadUserSearchPage(USER_SEARCH_PAGE_PATH);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let cached: RenderSearchPageContent | null = null;
|
|
35
|
+
let attempted = false;
|
|
36
|
+
|
|
37
|
+
export const getRenderSearchPageContent =
|
|
38
|
+
async (): Promise<RenderSearchPageContent> => {
|
|
39
|
+
if (attempted) {
|
|
40
|
+
return cached ?? defaultRenderSearchPageContent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
attempted = true;
|
|
44
|
+
cached = await tryLoadUserSearchPage();
|
|
45
|
+
return cached ?? defaultRenderSearchPageContent;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const resetSearchPageLoaderForTests = (): void => {
|
|
49
|
+
cached = null;
|
|
50
|
+
attempted = false;
|
|
51
|
+
};
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "../site/config";
|
|
11
11
|
import { resolveCanonicalUrl } from "../site/urls";
|
|
12
12
|
import { loadSearchIndex, search as runSearch } from "./index";
|
|
13
|
-
import {
|
|
13
|
+
import { getRenderSearchPageContent } from "./search-page-loader";
|
|
14
14
|
|
|
15
15
|
export interface SearchPageHandlerEnv {
|
|
16
16
|
cacheHeaders: HeadersInit;
|
|
@@ -37,40 +37,74 @@ const getResults = (
|
|
|
37
37
|
? runSearch(index, query, scope).slice(0, env.maxResults)
|
|
38
38
|
: [];
|
|
39
39
|
|
|
40
|
+
const getSearchQuery = (url: URL): string =>
|
|
41
|
+
url.searchParams.get("q")?.trim() ?? "";
|
|
42
|
+
|
|
43
|
+
const loadSearchPageDependencies = async (options: {
|
|
44
|
+
isDev: boolean;
|
|
45
|
+
siteConfig: Awaited<ReturnType<typeof loadSiteConfig>>;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
navigation: Awaited<ReturnType<typeof getNavigation>>;
|
|
48
|
+
index: Awaited<ReturnType<typeof loadSearchIndex>>;
|
|
49
|
+
renderSearchPageContent: Awaited<
|
|
50
|
+
ReturnType<typeof getRenderSearchPageContent>
|
|
51
|
+
>;
|
|
52
|
+
}> => {
|
|
53
|
+
const [navigation, index, renderSearchPageContent] = await Promise.all([
|
|
54
|
+
getNavigation(options.isDev),
|
|
55
|
+
loadSearchIndex({
|
|
56
|
+
forceRefresh: options.isDev,
|
|
57
|
+
siteConfig: options.siteConfig,
|
|
58
|
+
}),
|
|
59
|
+
getRenderSearchPageContent(),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
return { index, navigation, renderSearchPageContent };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getCanonicalSearchPageUrl = (options: {
|
|
66
|
+
baseUrl?: string;
|
|
67
|
+
env: SearchPageHandlerEnv;
|
|
68
|
+
url: URL;
|
|
69
|
+
}): string | undefined =>
|
|
70
|
+
resolveCanonicalUrl(
|
|
71
|
+
{
|
|
72
|
+
configuredBaseUrl: options.baseUrl,
|
|
73
|
+
isDev: options.env.isDev,
|
|
74
|
+
requestOrigin: options.url.origin,
|
|
75
|
+
},
|
|
76
|
+
"/search/"
|
|
77
|
+
);
|
|
78
|
+
|
|
40
79
|
const buildSearchPageHtml = async (
|
|
41
80
|
url: URL,
|
|
42
81
|
env: SearchPageHandlerEnv
|
|
43
82
|
): Promise<string> => {
|
|
44
83
|
const siteConfig = await loadSiteConfig();
|
|
45
84
|
const scope = getSearchScope(siteConfig);
|
|
46
|
-
const query = url
|
|
85
|
+
const query = getSearchQuery(url);
|
|
47
86
|
const rightRail = resolveRightRailConfig(siteConfig.rightRail);
|
|
48
87
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
const { index, navigation, renderSearchPageContent } =
|
|
89
|
+
await loadSearchPageDependencies({
|
|
90
|
+
isDev: env.isDev,
|
|
91
|
+
siteConfig,
|
|
92
|
+
});
|
|
53
93
|
|
|
54
94
|
const results = getResults(index, query, scope, env);
|
|
55
|
-
const topPages = getTopPages(navigation);
|
|
56
95
|
const content = renderSearchPageContent({
|
|
57
96
|
minQueryLength: env.minQueryLength,
|
|
58
97
|
query,
|
|
59
98
|
results,
|
|
60
|
-
topPages,
|
|
99
|
+
topPages: getTopPages(navigation),
|
|
61
100
|
});
|
|
62
101
|
|
|
63
|
-
const canonicalUrl = resolveCanonicalUrl(
|
|
64
|
-
{
|
|
65
|
-
configuredBaseUrl: siteConfig.baseUrl,
|
|
66
|
-
isDev: env.isDev,
|
|
67
|
-
requestOrigin: url.origin,
|
|
68
|
-
},
|
|
69
|
-
"/search/"
|
|
70
|
-
);
|
|
71
|
-
|
|
72
102
|
return renderDocument({
|
|
73
|
-
canonicalUrl
|
|
103
|
+
canonicalUrl: getCanonicalSearchPageUrl({
|
|
104
|
+
baseUrl: siteConfig.baseUrl,
|
|
105
|
+
env,
|
|
106
|
+
url,
|
|
107
|
+
}),
|
|
74
108
|
contentHtml: content,
|
|
75
109
|
currentPath: "/search/",
|
|
76
110
|
description: siteConfig.description,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ci:
|
|
10
|
+
runs-on: ubuntu-22.04
|
|
11
|
+
env:
|
|
12
|
+
CI: true
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
- name: Setup Bun
|
|
17
|
+
uses: oven-sh/setup-bun@v2
|
|
18
|
+
- name: Install dependencies
|
|
19
|
+
run: bun install
|
|
20
|
+
- name: Run checks
|
|
21
|
+
run: bun run check
|
|
22
|
+
- name: Run smoke
|
|
23
|
+
timeout-minutes: 15
|
|
24
|
+
run: bun run smoke
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Everything you edit lives in `site/`.
|
|
4
4
|
|
|
5
|
+
This starter is intentionally opinionated for AI-friendly markdown sites.
|
|
6
|
+
|
|
5
7
|
## Quickstart
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -9,14 +11,35 @@ bun install
|
|
|
9
11
|
bun run dev
|
|
10
12
|
```
|
|
11
13
|
|
|
14
|
+
## CI Smoke
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun run check
|
|
18
|
+
bun run smoke
|
|
19
|
+
```
|
|
20
|
+
|
|
12
21
|
## Layout
|
|
13
22
|
|
|
14
23
|
- `site/content/` markdown pages (`index.md` -> `/`, `about.md` -> `/about/`)
|
|
24
|
+
- `site/client/` local UI implementation (`layout.tsx`, `right-rail.tsx`, `search-page.tsx`)
|
|
25
|
+
- `site/client/runtime/` local browser runtime TS (`*_idcmd` scripts compile from here)
|
|
15
26
|
- `site/styles/tailwind.css` Tailwind entrypoint (compiled to `site/public/styles.css`)
|
|
16
27
|
- `site/public/` static assets
|
|
17
28
|
- `site/server/routes/` file-based server routes (dev/server-host only)
|
|
18
29
|
- `site/site.jsonc` site configuration
|
|
19
30
|
|
|
31
|
+
## Sync Local Client Files
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
idcmd client add all
|
|
35
|
+
idcmd client update all --dry-run
|
|
36
|
+
idcmd client update layout --yes
|
|
37
|
+
idcmd client update runtime --yes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
These commands copy the latest baseline implementations from `idcmd` into `site/client/`.
|
|
41
|
+
Runtime files in `site/client/runtime/` are compiled automatically by `idcmd dev` and `idcmd build`.
|
|
42
|
+
|
|
20
43
|
## Deploy (Vercel static)
|
|
21
44
|
|
|
22
45
|
```bash
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"build": "idcmd build",
|
|
8
8
|
"preview": "idcmd preview",
|
|
9
9
|
"deploy": "idcmd deploy",
|
|
10
|
-
"check": "
|
|
10
|
+
"check": "bun run scripts/check.ts",
|
|
11
|
+
"smoke": "bun run scripts/smoke.ts",
|
|
11
12
|
"test": "bun test",
|
|
12
13
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
13
14
|
"fix": "ultracite fix"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
interface InternalCheck {
|
|
2
|
+
description: string;
|
|
3
|
+
run: () => Promise<boolean>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const fileExists = (path: string): Promise<boolean> => Bun.file(path).exists();
|
|
7
|
+
|
|
8
|
+
const checks: InternalCheck[] = [
|
|
9
|
+
{
|
|
10
|
+
description: "package.json must exist at the project root",
|
|
11
|
+
run: () => fileExists("package.json"),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
description: "site config must exist (site/site.jsonc or site.jsonc)",
|
|
15
|
+
run: async () =>
|
|
16
|
+
(await fileExists("site/site.jsonc")) || (await fileExists("site.jsonc")),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
description:
|
|
20
|
+
"tailwind input must exist (site/styles/tailwind.css or content/styles.css)",
|
|
21
|
+
run: async () =>
|
|
22
|
+
(await fileExists("site/styles/tailwind.css")) ||
|
|
23
|
+
(await fileExists("content/styles.css")),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const runInternalChecks = async (): Promise<string[]> => {
|
|
28
|
+
const failures: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const check of checks) {
|
|
31
|
+
// eslint-disable-next-line no-await-in-loop
|
|
32
|
+
const ok = await check.run();
|
|
33
|
+
if (!ok) {
|
|
34
|
+
failures.push(check.description);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return failures;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const main = async (): Promise<number> => {
|
|
42
|
+
const failures = await runInternalChecks();
|
|
43
|
+
if (failures.length === 0) {
|
|
44
|
+
console.log("Internal checks passed.");
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error("Internal checks failed:");
|
|
49
|
+
for (const failure of failures) {
|
|
50
|
+
console.error(`- ${failure}`);
|
|
51
|
+
}
|
|
52
|
+
return 1;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const code = await main();
|
|
56
|
+
process.exit(code);
|