idcmd 0.0.4 → 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 +89 -24
- 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/commands/init.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { copyDir, isDirEmpty, replaceInFile } from "../fs";
|
|
1
|
+
import { copyDir, ensureDir, isDirEmpty, replaceInFile } from "../fs";
|
|
2
2
|
import {
|
|
3
3
|
normalizeOptionalString,
|
|
4
4
|
parsePort,
|
|
5
5
|
toPackageName,
|
|
6
6
|
} from "../normalize";
|
|
7
|
-
import { basename, joinPath } from "../path";
|
|
7
|
+
import { basename, dirname, joinPath } from "../path";
|
|
8
8
|
import { promptOptionalText, promptText } from "../prompt";
|
|
9
9
|
import { run } from "../run";
|
|
10
10
|
import { readPackageVersion } from "../version";
|
|
@@ -23,6 +23,60 @@ export interface InitFlags {
|
|
|
23
23
|
const resolveTemplateDir = (): string =>
|
|
24
24
|
joinPath(import.meta.dir, "..", "..", "..", "templates", "default");
|
|
25
25
|
|
|
26
|
+
const TEMPLATE_DOTPATHS = [".gitignore", ".github/workflows/ci.yml"];
|
|
27
|
+
const DEFAULT_OXLINT_CONFIG = `{
|
|
28
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
29
|
+
"extends": [
|
|
30
|
+
"./node_modules/ultracite/config/oxlint/core/.oxlintrc.json",
|
|
31
|
+
"./node_modules/ultracite/config/oxlint/react/.oxlintrc.json"
|
|
32
|
+
],
|
|
33
|
+
"rules": {
|
|
34
|
+
"jest/require-hook": "off"
|
|
35
|
+
},
|
|
36
|
+
"overrides": [
|
|
37
|
+
{
|
|
38
|
+
"files": [
|
|
39
|
+
"**/*.test.ts",
|
|
40
|
+
"**/*.test.tsx",
|
|
41
|
+
"**/*.test.js",
|
|
42
|
+
"**/*.test.jsx",
|
|
43
|
+
"**/*.spec.ts",
|
|
44
|
+
"**/*.spec.tsx",
|
|
45
|
+
"**/*.spec.js",
|
|
46
|
+
"**/*.spec.jsx"
|
|
47
|
+
],
|
|
48
|
+
"rules": {
|
|
49
|
+
"jest/require-hook": "error"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const DEFAULT_OXFMT_CONFIG = `// Ultracite oxfmt Configuration
|
|
56
|
+
// https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html
|
|
57
|
+
{
|
|
58
|
+
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
|
59
|
+
"printWidth": 80,
|
|
60
|
+
"tabWidth": 2,
|
|
61
|
+
"useTabs": false,
|
|
62
|
+
"semi": true,
|
|
63
|
+
"singleQuote": false,
|
|
64
|
+
"quoteProps": "as-needed",
|
|
65
|
+
"jsxSingleQuote": false,
|
|
66
|
+
"trailingComma": "es5",
|
|
67
|
+
"bracketSpacing": true,
|
|
68
|
+
"bracketSameLine": false,
|
|
69
|
+
"arrowParens": "always",
|
|
70
|
+
"endOfLine": "lf",
|
|
71
|
+
"experimentalSortPackageJson": true,
|
|
72
|
+
"experimentalSortImports": {
|
|
73
|
+
"ignoreCase": true,
|
|
74
|
+
"newlinesBetween": true,
|
|
75
|
+
"order": "asc",
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
26
80
|
const commentOutBaseUrl = (text: string): string =>
|
|
27
81
|
text.replace(
|
|
28
82
|
'"baseUrl": "__IDCMD_SITE_BASE_URL__",',
|
|
@@ -56,6 +110,11 @@ const fillPackageJson = (args: {
|
|
|
56
110
|
.replaceAll("__IDCMD_IDCMD_VERSION__", `^${args.idcmdVersion}`)
|
|
57
111
|
.replaceAll("__IDCMD_DEV_PORT__", String(args.port));
|
|
58
112
|
|
|
113
|
+
const fillReadme = (args: { siteName: string; text: string }): string =>
|
|
114
|
+
args.text
|
|
115
|
+
.replaceAll("__IDCMD_SITE_NAME__", args.siteName)
|
|
116
|
+
.replaceAll("IDCMD_SITE_NAME", args.siteName);
|
|
117
|
+
|
|
59
118
|
interface InitDefaults {
|
|
60
119
|
defaultPort: number;
|
|
61
120
|
defaultSiteName: string;
|
|
@@ -105,6 +164,31 @@ const readInitInputs = async (
|
|
|
105
164
|
const scaffoldFromTemplate = async (targetDir: string): Promise<void> => {
|
|
106
165
|
const templateDir = resolveTemplateDir();
|
|
107
166
|
await copyDir(templateDir, targetDir);
|
|
167
|
+
await copyTemplateDotpaths({ targetDir, templateDir });
|
|
168
|
+
await writeDefaultLintConfigs(targetDir);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const copyTemplateDotpaths = async (args: {
|
|
172
|
+
targetDir: string;
|
|
173
|
+
templateDir: string;
|
|
174
|
+
}): Promise<void> => {
|
|
175
|
+
for (const relativePath of TEMPLATE_DOTPATHS) {
|
|
176
|
+
const srcPath = joinPath(args.templateDir, relativePath);
|
|
177
|
+
if (!(await Bun.file(srcPath).exists())) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const dstPath = joinPath(args.targetDir, relativePath);
|
|
181
|
+
// Hidden paths are not copied by glob scan; explicitly create parent dirs.
|
|
182
|
+
// eslint-disable-next-line no-await-in-loop
|
|
183
|
+
await ensureDir(dirname(dstPath));
|
|
184
|
+
// eslint-disable-next-line no-await-in-loop
|
|
185
|
+
await Bun.write(dstPath, Bun.file(srcPath));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const writeDefaultLintConfigs = async (targetDir: string): Promise<void> => {
|
|
190
|
+
await Bun.write(joinPath(targetDir, ".oxlintrc.json"), DEFAULT_OXLINT_CONFIG);
|
|
191
|
+
await Bun.write(joinPath(targetDir, ".oxfmtrc.jsonc"), DEFAULT_OXFMT_CONFIG);
|
|
108
192
|
};
|
|
109
193
|
|
|
110
194
|
const applySubstitutions = async (args: {
|
|
@@ -133,6 +217,13 @@ const applySubstitutions = async (args: {
|
|
|
133
217
|
text,
|
|
134
218
|
})
|
|
135
219
|
);
|
|
220
|
+
|
|
221
|
+
await replaceInFile(joinPath(args.targetDir, "README.md"), (text) =>
|
|
222
|
+
fillReadme({
|
|
223
|
+
siteName: args.siteName,
|
|
224
|
+
text,
|
|
225
|
+
})
|
|
226
|
+
);
|
|
136
227
|
};
|
|
137
228
|
|
|
138
229
|
const maybeInitGit = async (
|
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
|