@mintlify/cli 4.0.1605 → 4.0.1606
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/bin/cli.js +33 -0
- package/bin/og.js +145 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/cli.tsx +39 -0
- package/src/og.tsx +185 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1606",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@mintlify/common": "1.0.1224",
|
|
49
49
|
"@mintlify/link-rot": "3.0.1433",
|
|
50
50
|
"@mintlify/models": "0.0.369",
|
|
51
|
+
"@mintlify/og": "0.0.18",
|
|
51
52
|
"@mintlify/prebuild": "1.0.1383",
|
|
52
53
|
"@mintlify/previewing": "4.0.1456",
|
|
53
54
|
"@mintlify/validation": "0.1.901",
|
|
@@ -105,5 +106,5 @@
|
|
|
105
106
|
"vitest": "2.1.9",
|
|
106
107
|
"vitest-mock-process": "1.0.4"
|
|
107
108
|
},
|
|
108
|
-
"gitHead": "
|
|
109
|
+
"gitHead": "400e2092034a4d72ead202b8d8e1e2abbec1f5d8"
|
|
109
110
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
BrokenLinksLog,
|
|
13
13
|
WarningLog,
|
|
14
14
|
} from '@mintlify/previewing';
|
|
15
|
+
import { thumbnailOverrideSchema } from '@mintlify/validation';
|
|
15
16
|
import { render, Text } from 'ink';
|
|
16
17
|
import fs from 'node:fs/promises';
|
|
17
18
|
import path from 'path';
|
|
@@ -45,6 +46,7 @@ import { logout } from './logout.js';
|
|
|
45
46
|
import { mdxLinter } from './mdxLinter.js';
|
|
46
47
|
import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
|
|
47
48
|
import { mintTestHandler } from './mintTest.js';
|
|
49
|
+
import { ogCommand } from './og.js';
|
|
48
50
|
import {
|
|
49
51
|
buildOverlayRegistryForCheck,
|
|
50
52
|
checkOpenApiFile,
|
|
@@ -699,6 +701,43 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
699
701
|
await terminate(accessibilityCheckTerminateCode || mdxLinterTerminateCode);
|
|
700
702
|
}
|
|
701
703
|
)
|
|
704
|
+
.command(
|
|
705
|
+
'og [page]',
|
|
706
|
+
'Render the social preview (OG) image for a page',
|
|
707
|
+
(yargs) =>
|
|
708
|
+
yargs
|
|
709
|
+
.positional('page', {
|
|
710
|
+
type: 'string',
|
|
711
|
+
description: 'Page path, such as guides/webhooks. Defaults to index',
|
|
712
|
+
})
|
|
713
|
+
.option('preset', {
|
|
714
|
+
choices: thumbnailOverrideSchema.shape.preset.unwrap().options,
|
|
715
|
+
description: 'Try a preset without editing docs.json',
|
|
716
|
+
})
|
|
717
|
+
.option('surface', {
|
|
718
|
+
choices: thumbnailOverrideSchema.shape.surface.unwrap().options,
|
|
719
|
+
description: 'Try a surface without editing docs.json',
|
|
720
|
+
})
|
|
721
|
+
.option('appearance', {
|
|
722
|
+
choices: thumbnailOverrideSchema.shape.appearance.unwrap().options,
|
|
723
|
+
description: 'Try an appearance without editing docs.json',
|
|
724
|
+
})
|
|
725
|
+
.option('output', {
|
|
726
|
+
type: 'string',
|
|
727
|
+
description: 'Where to write the PNG. Defaults to a temporary file',
|
|
728
|
+
})
|
|
729
|
+
.option('open', {
|
|
730
|
+
type: 'boolean',
|
|
731
|
+
default: true,
|
|
732
|
+
description: 'Open the image after rendering. Use --no-open in CI',
|
|
733
|
+
})
|
|
734
|
+
.example('mint og guides/webhooks', 'Render one page and open it')
|
|
735
|
+
.example('mint og --preset headline --surface brand', 'Try settings on the home page')
|
|
736
|
+
.example('mint og --output og.png --no-open', 'Render to a file in CI'),
|
|
737
|
+
async (argv) => {
|
|
738
|
+
await terminate(await ogCommand(argv));
|
|
739
|
+
}
|
|
740
|
+
)
|
|
702
741
|
.command(
|
|
703
742
|
['version', 'v'],
|
|
704
743
|
'Display the current version of the CLI and client',
|
package/src/og.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { parseFrontmatter, slugToTitle } from '@mintlify/common';
|
|
2
|
+
import { renderOgCardPng } from '@mintlify/og';
|
|
3
|
+
import {
|
|
4
|
+
applyCardThemeOverride,
|
|
5
|
+
applyThumbnailOverride,
|
|
6
|
+
buildOgCardInput,
|
|
7
|
+
checkOgCard,
|
|
8
|
+
createLocalAssetLoaders,
|
|
9
|
+
deriveOgThemeParams,
|
|
10
|
+
getOgPageParams,
|
|
11
|
+
MAX_OG_ASSET_BYTES,
|
|
12
|
+
toOgFontAsset,
|
|
13
|
+
toOgImageAsset,
|
|
14
|
+
type OgAssetLoaders,
|
|
15
|
+
} from '@mintlify/og/config';
|
|
16
|
+
import { getConfigObj } from '@mintlify/prebuild';
|
|
17
|
+
import { addLog, ErrorLog, SuccessLog, WarningLog } from '@mintlify/previewing';
|
|
18
|
+
import { validateDocsConfig, type ThumbnailOverride } from '@mintlify/validation';
|
|
19
|
+
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import open from 'open';
|
|
23
|
+
|
|
24
|
+
import { CMD_EXEC_PATH } from './helpers.js';
|
|
25
|
+
|
|
26
|
+
export type OgCommandOptions = Pick<ThumbnailOverride, 'preset' | 'surface' | 'appearance'> & {
|
|
27
|
+
page?: string;
|
|
28
|
+
output?: string;
|
|
29
|
+
open: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const REMOTE_TIMEOUT_MS = 5_000;
|
|
33
|
+
const PAGE_EXTENSIONS = ['.mdx', '.md'];
|
|
34
|
+
const MAX_REPORTED_ISSUES = 5;
|
|
35
|
+
|
|
36
|
+
const readLimited = async (response: Response): Promise<Uint8Array | null> => {
|
|
37
|
+
const declared = Number(response.headers.get('content-length'));
|
|
38
|
+
if (declared > MAX_OG_ASSET_BYTES || !response.body) return null;
|
|
39
|
+
|
|
40
|
+
const reader = response.body.getReader();
|
|
41
|
+
const chunks: Uint8Array[] = [];
|
|
42
|
+
let size = 0;
|
|
43
|
+
for (let result = await reader.read(); !result.done; result = await reader.read()) {
|
|
44
|
+
size += result.value.byteLength;
|
|
45
|
+
if (size > MAX_OG_ASSET_BYTES) {
|
|
46
|
+
await reader.cancel();
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
chunks.push(result.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const bytes = new Uint8Array(size);
|
|
53
|
+
let offset = 0;
|
|
54
|
+
for (const chunk of chunks) {
|
|
55
|
+
bytes.set(chunk, offset);
|
|
56
|
+
offset += chunk.byteLength;
|
|
57
|
+
}
|
|
58
|
+
return bytes;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const fetchAsset = async (src: string) => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(src, { signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS) });
|
|
64
|
+
if (!response.ok) return null;
|
|
65
|
+
const bytes = await readLimited(response);
|
|
66
|
+
return bytes && { bytes, contentType: response.headers.get('content-type') ?? undefined };
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const remoteLoaders: OgAssetLoaders = {
|
|
73
|
+
image: async (src) => {
|
|
74
|
+
const asset = await fetchAsset(src);
|
|
75
|
+
return asset && toOgImageAsset(asset.bytes, asset.contentType);
|
|
76
|
+
},
|
|
77
|
+
font: async (src) => {
|
|
78
|
+
const asset = await fetchAsset(src);
|
|
79
|
+
return asset && toOgFontAsset(asset.bytes);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const normalizePagePath = (page: string | undefined) =>
|
|
84
|
+
path.posix
|
|
85
|
+
.normalize(`/${(page ?? '').replace(/\\/g, '/')}`)
|
|
86
|
+
.replace(/^\/+|\/+$/g, '')
|
|
87
|
+
.replace(/\.mdx?$/, '') || 'index';
|
|
88
|
+
|
|
89
|
+
const pageHref = (pagePath: string) => pagePath.replace(/(^|\/)index$/, '');
|
|
90
|
+
|
|
91
|
+
const pageCandidates = (pagePath: string) =>
|
|
92
|
+
[pagePath, `${pagePath}/index`].flatMap((base) =>
|
|
93
|
+
PAGE_EXTENSIONS.map((extension) => `${base}${extension}`)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const readPage = async (pagePath: string) => {
|
|
97
|
+
const root = path.resolve(CMD_EXEC_PATH);
|
|
98
|
+
for (const candidate of pageCandidates(pagePath)) {
|
|
99
|
+
const filePath = path.resolve(root, candidate);
|
|
100
|
+
if (!filePath.startsWith(`${root}${path.sep}`)) return null;
|
|
101
|
+
try {
|
|
102
|
+
return await readFile(filePath, 'utf-8');
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const stringAttribute = (attributes: Record<string, unknown>, key: string) => {
|
|
111
|
+
const value = attributes[key];
|
|
112
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const defaultOutputPath = async (pagePath: string) =>
|
|
116
|
+
path.join(
|
|
117
|
+
await mkdtemp(path.join(os.tmpdir(), 'mintlify-og-')),
|
|
118
|
+
`${pagePath.replace(/\//g, '-')}.png`
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export async function ogCommand(options: OgCommandOptions): Promise<0 | 1> {
|
|
122
|
+
const rawConfig = await getConfigObj(CMD_EXEC_PATH, 'docs');
|
|
123
|
+
if (!rawConfig) {
|
|
124
|
+
addLog(<ErrorLog message="No docs.json found. Run mint og from your docs directory." />);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validation = validateDocsConfig(rawConfig);
|
|
129
|
+
if (!validation.success) {
|
|
130
|
+
const issues = validation.error.issues
|
|
131
|
+
.slice(0, MAX_REPORTED_ISSUES)
|
|
132
|
+
.map((issue) => `${issue.path.join('.') || 'docs.json'}: ${issue.message}`);
|
|
133
|
+
addLog(<ErrorLog message={`docs.json is invalid:\n${issues.join('\n')}`} />);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const docsConfig = validation.data;
|
|
137
|
+
|
|
138
|
+
const pagePath = normalizePagePath(options.page);
|
|
139
|
+
const contents = await readPage(pagePath);
|
|
140
|
+
if (contents === null) {
|
|
141
|
+
addLog(
|
|
142
|
+
<ErrorLog message={`Couldn't find a page at ${pageCandidates(pagePath).join(', ')}.`} />
|
|
143
|
+
);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { attributes } = parseFrontmatter<Record<string, unknown>>(contents);
|
|
148
|
+
const pageParams = getOgPageParams(
|
|
149
|
+
{
|
|
150
|
+
path: pagePath,
|
|
151
|
+
title:
|
|
152
|
+
stringAttribute(attributes, 'title') ??
|
|
153
|
+
stringAttribute(attributes, 'sidebarTitle') ??
|
|
154
|
+
slugToTitle(pageHref(pagePath)),
|
|
155
|
+
description: stringAttribute(attributes, 'description'),
|
|
156
|
+
thumbnail: attributes.thumbnail,
|
|
157
|
+
},
|
|
158
|
+
docsConfig
|
|
159
|
+
);
|
|
160
|
+
const themeParams = applyThumbnailOverride(
|
|
161
|
+
applyCardThemeOverride(deriveOgThemeParams(docsConfig), pageParams.thumbnail),
|
|
162
|
+
options
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const input = await buildOgCardInput(
|
|
166
|
+
pageParams,
|
|
167
|
+
themeParams,
|
|
168
|
+
createLocalAssetLoaders(CMD_EXEC_PATH, remoteLoaders)
|
|
169
|
+
);
|
|
170
|
+
const png = await renderOgCardPng(input);
|
|
171
|
+
|
|
172
|
+
const outputPath = options.output
|
|
173
|
+
? path.resolve(options.output)
|
|
174
|
+
: await defaultOutputPath(pagePath);
|
|
175
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
176
|
+
await writeFile(outputPath, new Uint8Array(png));
|
|
177
|
+
|
|
178
|
+
for (const warning of checkOgCard(input, themeParams)) {
|
|
179
|
+
addLog(<WarningLog message={warning} />);
|
|
180
|
+
}
|
|
181
|
+
addLog(<SuccessLog message={`Wrote ${outputPath}`} />);
|
|
182
|
+
|
|
183
|
+
if (options.open && !process.env.CI) await open(outputPath);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|