CLI runs on Node 20+: npx storebound (bunx keeps working)
- cli/lib/rt.ts: the runtime layer on node: modules (spawn / spawnSync with Bun's defaults, exit codes and "Executable not found", which with PATHEXT, sleep, serve, Glob, hasher, gunzip, stdin, SQLite, packageRoot, config imports with a type-stripping fallback for Node 20); every Bun.* use in cli/ goes through it - tools/gen-embedded.ts -> cli/lib/embedded.ts: templates/, cli/shots/*.html and Play's Data safety form embedded (replaces Bun text imports; the bundle and the compiled sidecar carry them), checked fresh by bun run ci - package.json: bin dist/storebound.mjs (Node ESM bundle, tools/build-node.ts, prepack), engines node >=20 - bun run ci: node bundle + node smoke (real node: --version, help, check same as Bun, scan, release plan --help, init, MCP handshake + tool call); Forgejo CI sets up Node 20; tests/node-compat.test.ts guards cli/ - install writes npx MCP commands when run with Node; bin/storebound falls back to Node - docs: npx storebound init (Node 20+) or bunx storebound init
This commit is contained in:
parent
614c32bcba
commit
36a8bf9625
73 changed files with 1195 additions and 251 deletions
|
|
@ -13,7 +13,8 @@
|
|||
// paths) are served from where they are at /fonts/local/... (local-fonts.ts); a file changed since `shots fonts` fails.
|
||||
// Style: shotsStyle() = shots.style, else app for the app / subscription-app profiles (template-app.html,
|
||||
// feature-app.html: calm gradient, device frame, headline + subline), else game (template.html, feature.html).
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { SHOTS_PAGES } from '../lib/embedded';
|
||||
import { fontCodepoints } from './cmap';
|
||||
import { readManifest } from './fonts';
|
||||
import { type FontManifest, fontUsage, measureFonts, roleTexts, uncoveredChars } from './glyphs';
|
||||
|
|
@ -27,6 +28,7 @@ import { shotsStyle } from '../lib/config';
|
|||
import { type CopyFile, SIZES, copyText, defaultSizes, frameDims, mergeCopy, palette, renderDir, scriptFont, sheetLayout, validateCopy, variantScenes } from './lib';
|
||||
import { faceFilePath, fontsRoute, resolveShotFonts, staleLocalFonts } from './local-fonts';
|
||||
import { type RenderReport, mergeReport, readReport, resolvedTablets } from './report';
|
||||
import * as rt from '../lib/rt';
|
||||
|
||||
const TYPES: Record<string, string> = { html: 'text/html; charset=utf-8', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||
webp: 'image/webp', woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf', css: 'text/css', svg: 'image/svg+xml' };
|
||||
|
|
@ -113,9 +115,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
for (const lang of langs) for (const f of manifest.langs[lang]?.fallbacks ?? [])
|
||||
(manifest.faces[f.face]?.chosen ? log : warn)('shots', `${lang} ${f.role}: ${[...f.chars].join(' ')} from ${f.family}${manifest.faces[f.face]?.chosen ? '' : ' (automatic fallback: shots check will fail)'}`);
|
||||
}
|
||||
const tpl = import.meta.dir;
|
||||
|
||||
const server = Bun.serve({
|
||||
const server = await rt.serve({
|
||||
port: a.num('port', 8792), hostname: '127.0.0.1',
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
|
@ -138,11 +138,9 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
else if (p.startsWith('/frames/')) file = join(framesDir, p.slice(8));
|
||||
else if (p.startsWith('/fonts/')) file = fontsRoute(p.slice(7), fontsDir, root, manifest) ?? '';
|
||||
else if (p.startsWith('/out/')) file = join(outDir, p.slice(5));
|
||||
else if (/^\/(template|feature)(-app)?\.html$/.test(p)) file = join(tpl, p.slice(1));
|
||||
else if (/^\/(template|feature)(-app)?\.html$/.test(p)) return new Response(SHOTS_PAGES[p.slice(1)]!, { headers: { 'content-type': TYPES.html! } });
|
||||
if (!file || file.includes('..')) return new Response('no', { status: 404 });
|
||||
const f = Bun.file(file);
|
||||
if (!(await f.exists())) return new Response('missing ' + p, { status: 404 });
|
||||
return new Response(f, { headers: { 'content-type': TYPES[file.split('.').pop()!.toLowerCase()] ?? 'application/octet-stream' } });
|
||||
return rt.fileResponse(file, { 'content-type': TYPES[file.split('.').pop()!.toLowerCase()] ?? 'application/octet-stream' }) ?? new Response('missing ' + p, { status: 404 });
|
||||
},
|
||||
});
|
||||
const base = `http://127.0.0.1:${server.port}`;
|
||||
|
|
@ -182,7 +180,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
if (overflow.length) warn('shots', `${relative(root, out)}: ${overflow.join('; ')}`);
|
||||
await page.screenshot({ path: tmp, clip: { x: 0, y: 0, width: w, height: h } });
|
||||
const ff = Bun.spawn(['ffmpeg', '-v', 'error', '-y', '-i', tmp, '-pix_fmt', 'rgb24', out], { stderr: 'pipe' });
|
||||
const ff = rt.spawn(['ffmpeg', '-v', 'error', '-y', '-i', tmp, '-pix_fmt', 'rgb24', out], { stderr: 'pipe' });
|
||||
const code = await ff.exited;
|
||||
rmSync(tmp, { force: true });
|
||||
if (code !== 0) throw new Error('ffmpeg: ' + await new Response(ff.stderr).text());
|
||||
|
|
@ -232,7 +230,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const lay = sheetLayout(rows);
|
||||
const imgs = lay.tiles.map(t => `<img src="/out/${relative(outDir, t.file)}" style="position:absolute;left:${t.x}px;top:${t.y}px;width:${t.w}px;height:${t.h}px;border-radius:14px">`).join('');
|
||||
const sheet = join(dir, '.sheet.html');
|
||||
await Bun.write(sheet, `<!doctype html><body style="margin:0;background:#141418;width:${lay.w}px;height:${lay.h}px;position:relative">${imgs}
|
||||
writeFileSync(sheet, `<!doctype html><body style="margin:0;background:#141418;width:${lay.w}px;height:${lay.h}px;position:relative">${imgs}
|
||||
<script>Promise.all([...document.images].map(i=>i.decode())).then(()=>document.body.dataset.ready='1')</script></body>`);
|
||||
await withPage(p => shoot(p, `${base}/out/${relative(outDir, sheet)}`, lay.w, lay.h, join(dir, 'sheet.png')));
|
||||
rmSync(sheet, { force: true });
|
||||
|
|
@ -241,7 +239,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
for (const p of pages) await p?.page.context().close().catch(() => {});
|
||||
await closeBrowser(browser);
|
||||
server.stop(true);
|
||||
if (Object.keys(report).length) await Bun.write(join(outDir, 'report.json'), JSON.stringify(mergeReport(readReport(outDir), report), null, 1) + '\n');
|
||||
if (Object.keys(report).length) writeFileSync(join(outDir, 'report.json'), JSON.stringify(mergeReport(readReport(outDir), report), null, 1) + '\n');
|
||||
}
|
||||
const flagged = Object.values(report).filter(r => r.overflow.length).length;
|
||||
if (flagged) warn('shots', `${flagged} images with text overflow flags: storebound shots check / shots gallery`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue