@somewhatintelligent/og 0.0.2-beta.1783732751.3b77dd8
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/package.json +47 -0
- package/src/cli.ts +70 -0
- package/src/config.ts +15 -0
- package/src/define.ts +12 -0
- package/src/discover.ts +58 -0
- package/src/fonts.ts +34 -0
- package/src/index.ts +5 -0
- package/src/render.ts +19 -0
- package/src/version.gen.ts +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@somewhatintelligent/og",
|
|
3
|
+
"version": "0.0.2-beta.1783732751.3b77dd8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/apostolos-geyer/platform.git",
|
|
12
|
+
"directory": "packages/og"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"platform-og": "./src/cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "vitest run"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
29
|
+
"satori": "^0.26.0",
|
|
30
|
+
"tinyglobby": "^0.2.16"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.5.2",
|
|
34
|
+
"@types/react": "^19.2.14",
|
|
35
|
+
"react": "19.2.4",
|
|
36
|
+
"typescript": "5.9.2",
|
|
37
|
+
"vitest": "^4.1.10"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "19.2.4"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"react": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { discoverOgDefinitions, loadOgConfig } from "./discover";
|
|
6
|
+
import { loadFonts } from "./fonts";
|
|
7
|
+
import { renderOg } from "./render";
|
|
8
|
+
|
|
9
|
+
async function build(args: string[]): Promise<void> {
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args,
|
|
12
|
+
options: {
|
|
13
|
+
cwd: { type: "string" },
|
|
14
|
+
out: { type: "string" },
|
|
15
|
+
glob: { type: "string" },
|
|
16
|
+
config: { type: "string" },
|
|
17
|
+
},
|
|
18
|
+
strict: true,
|
|
19
|
+
allowPositionals: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const cwd = resolve(values.cwd ?? process.cwd());
|
|
23
|
+
const outDir = resolve(cwd, values.out ?? "public/og");
|
|
24
|
+
const pattern = values.glob ?? "og/**/*.og.{tsx,ts}";
|
|
25
|
+
|
|
26
|
+
const entries = await discoverOgDefinitions(cwd, pattern);
|
|
27
|
+
if (entries.length === 0) {
|
|
28
|
+
console.error(`platform-og: no files matched ${pattern} in ${cwd}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = await loadOgConfig(cwd, values.config);
|
|
33
|
+
const fonts = await loadFonts(config.fonts ?? [], cwd);
|
|
34
|
+
|
|
35
|
+
await mkdir(outDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const t0 = performance.now();
|
|
38
|
+
for (const { file, definition } of entries) {
|
|
39
|
+
const element = await definition.render();
|
|
40
|
+
try {
|
|
41
|
+
const png = await renderOg(element, { size: definition.size, fonts });
|
|
42
|
+
const dest = resolve(outDir, `${definition.name}.png`);
|
|
43
|
+
await writeFile(dest, png);
|
|
44
|
+
console.log(` ✓ ${definition.name}.png ${definition.size.width}×${definition.size.height}`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(` ✗ ${file}`);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ms = Math.round(performance.now() - t0);
|
|
51
|
+
console.log(`platform-og: rendered ${entries.length} image(s) into ${outDir} in ${ms}ms`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main(): Promise<void> {
|
|
55
|
+
const [, , command, ...rest] = process.argv;
|
|
56
|
+
if (command === "build" || command === undefined) {
|
|
57
|
+
await build(rest);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.error(`platform-og: unknown command "${command}"`);
|
|
61
|
+
console.error(
|
|
62
|
+
"usage: platform-og build [--cwd <dir>] [--out <dir>] [--glob <pattern>] [--config <path>]",
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((err) => {
|
|
68
|
+
console.error(err);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FontInput } from "./fonts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The consumer-facing knob surface for `platform-og build`. There is no
|
|
5
|
+
* shipped default — a consumer whose `OgDefinition`s render no text can
|
|
6
|
+
* omit `fonts` entirely (feature absence is a branch, not a throw).
|
|
7
|
+
*/
|
|
8
|
+
export interface OgConfig {
|
|
9
|
+
/** Fonts made available to every `OgDefinition` the CLI discovers. */
|
|
10
|
+
fonts?: FontInput[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function defineOgConfig(config: OgConfig): OgConfig {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type OgDefinition = {
|
|
4
|
+
name: string;
|
|
5
|
+
size: { width: number; height: number };
|
|
6
|
+
contentType?: string;
|
|
7
|
+
render: () => ReactNode | Promise<ReactNode>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function defineOg(def: OgDefinition): OgDefinition {
|
|
11
|
+
return def;
|
|
12
|
+
}
|
package/src/discover.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
5
|
+
import type { OgDefinition } from "./define";
|
|
6
|
+
import type { OgConfig } from "./config";
|
|
7
|
+
|
|
8
|
+
export type DiscoveredOg = {
|
|
9
|
+
file: string;
|
|
10
|
+
definition: OgDefinition;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function discoverOgDefinitions(
|
|
14
|
+
cwd: string,
|
|
15
|
+
pattern = "og/**/*.og.{tsx,ts}",
|
|
16
|
+
): Promise<DiscoveredOg[]> {
|
|
17
|
+
const files = await glob(pattern, { cwd, absolute: true });
|
|
18
|
+
files.sort();
|
|
19
|
+
|
|
20
|
+
const discovered: DiscoveredOg[] = [];
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const mod = (await import(pathToFileURL(file).href)) as { default?: OgDefinition };
|
|
23
|
+
if (!mod.default) {
|
|
24
|
+
throw new Error(`${file} has no default export — use \`export default defineOg({...})\``);
|
|
25
|
+
}
|
|
26
|
+
assertOgDefinition(file, mod.default);
|
|
27
|
+
discovered.push({ file, definition: mod.default });
|
|
28
|
+
}
|
|
29
|
+
return discovered;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Loads the consumer's `defineOgConfig({...})` module, if any. Absence is
|
|
34
|
+
* not an error — `{ fonts: undefined }` is the supported "no text in any
|
|
35
|
+
* OgDefinition" state.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadOgConfig(cwd: string, configPath = "og.config.ts"): Promise<OgConfig> {
|
|
38
|
+
const resolved = resolve(cwd, configPath);
|
|
39
|
+
if (!existsSync(resolved)) return {};
|
|
40
|
+
const mod = (await import(pathToFileURL(resolved).href)) as { default?: OgConfig };
|
|
41
|
+
return mod.default ?? {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertOgDefinition(file: string, value: unknown): asserts value is OgDefinition {
|
|
45
|
+
if (!value || typeof value !== "object") {
|
|
46
|
+
throw new Error(`${file}: default export is not an OgDefinition object`);
|
|
47
|
+
}
|
|
48
|
+
const v = value as Partial<OgDefinition>;
|
|
49
|
+
if (typeof v.name !== "string" || !v.name) {
|
|
50
|
+
throw new Error(`${file}: OgDefinition.name must be a non-empty string`);
|
|
51
|
+
}
|
|
52
|
+
if (!v.size || typeof v.size.width !== "number" || typeof v.size.height !== "number") {
|
|
53
|
+
throw new Error(`${file}: OgDefinition.size must be { width, height }`);
|
|
54
|
+
}
|
|
55
|
+
if (typeof v.render !== "function") {
|
|
56
|
+
throw new Error(`${file}: OgDefinition.render must be a function`);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/fonts.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
|
|
5
|
+
export type FontStyle = "normal" | "italic";
|
|
6
|
+
|
|
7
|
+
export type SatoriFont = {
|
|
8
|
+
name: string;
|
|
9
|
+
weight: FontWeight;
|
|
10
|
+
style: FontStyle;
|
|
11
|
+
data: Buffer;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A consumer-declared font: either the bytes directly, or a filesystem path
|
|
16
|
+
* (resolved against `cwd` — see `loadFonts`). There is no shipped font set;
|
|
17
|
+
* a consumer whose `OgDefinition`s render no text needs to declare none.
|
|
18
|
+
*/
|
|
19
|
+
export type FontInput = { name: string; weight: FontWeight; style: FontStyle } & (
|
|
20
|
+
| { data: Buffer; path?: never }
|
|
21
|
+
| { path: string; data?: never }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** Resolves consumer-declared `FontInput[]` into satori-ready `SatoriFont[]`. */
|
|
25
|
+
export async function loadFonts(fonts: FontInput[], cwd: string = process.cwd()): Promise<SatoriFont[]> {
|
|
26
|
+
return Promise.all(
|
|
27
|
+
fonts.map(async (font) => ({
|
|
28
|
+
name: font.name,
|
|
29
|
+
weight: font.weight,
|
|
30
|
+
style: font.style,
|
|
31
|
+
data: font.data ?? (await readFile(resolve(cwd, font.path as string))),
|
|
32
|
+
})),
|
|
33
|
+
);
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { renderOg, type RenderOptions } from "./render";
|
|
2
|
+
export { loadFonts, type SatoriFont, type FontWeight, type FontStyle, type FontInput } from "./fonts";
|
|
3
|
+
export { defineOg, type OgDefinition } from "./define";
|
|
4
|
+
export { discoverOgDefinitions, loadOgConfig, type DiscoveredOg } from "./discover";
|
|
5
|
+
export { defineOgConfig, type OgConfig } from "./config";
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import satori from "satori";
|
|
3
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
4
|
+
import type { SatoriFont } from "./fonts";
|
|
5
|
+
|
|
6
|
+
export type RenderOptions = {
|
|
7
|
+
size: { width: number; height: number };
|
|
8
|
+
/** Omit for a definition with no text nodes — satori needs no font in that case. */
|
|
9
|
+
fonts?: SatoriFont[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function renderOg(element: ReactNode, options: RenderOptions): Promise<Uint8Array> {
|
|
13
|
+
const svg = await satori(element, {
|
|
14
|
+
width: options.size.width,
|
|
15
|
+
height: options.size.height,
|
|
16
|
+
fonts: options.fonts ?? [],
|
|
17
|
+
});
|
|
18
|
+
return new Resvg(svg).render().asPng();
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// AUTO-STAMPED by scripts/stamp-versions.ts at release — the checked-in
|
|
2
|
+
// values are dev placeholders, identical to the runtime fallbacks.
|
|
3
|
+
export const PKG = {
|
|
4
|
+
name: "@somewhatintelligent/og",
|
|
5
|
+
version: "0.0.2-beta.1783732751.3b77dd8",
|
|
6
|
+
commit: "3b77dd8189861ea843645f9e389cd5f874557345",
|
|
7
|
+
} as const;
|