finalize initial starters

This commit is contained in:
dswbx
2025-02-14 13:55:46 +01:00
parent 7d3c76d7c1
commit 3b487ade2a
8 changed files with 180 additions and 64 deletions

View File

@@ -6,7 +6,7 @@ import { typewriter, wait } from "cli/utils/cli";
import { exec, getVersion } from "cli/utils/sys";
import { Option } from "commander";
import color from "picocolors";
import { updateBkndPackages } from "./npm";
import { overridePackageJson, updateBkndPackages } from "./npm";
import { type Template, templates } from "./templates";
const config = {
@@ -36,16 +36,21 @@ export const create: CliCommand = (program) => {
.action(action);
};
function errorOutro() {
$p.outro(color.red("Failed to create project."));
console.log(
color.yellow("Sorry that this happened. If you think this is a bug, please report it at: ") +
color.cyan("https://github.com/bknd-io/bknd/issues")
);
console.log("");
process.exit(1);
}
async function action(options: { template?: string; dir?: string; integration?: string }) {
const _config = {
defaultDir: process.env.LOCAL ? "./.template" : "./",
speed: {
typewriter: process.env.LOCAL ? 0 : 20,
wait: process.env.LOCAL ? 0 : 250
}
};
console.log("");
const downloadOpts = {
dir: options.dir || _config.defaultDir,
dir: options.dir || "./",
clean: false
};
@@ -56,12 +61,8 @@ async function action(options: { template?: string; dir?: string; integration?:
await $p.stream.message(
(async function* () {
yield* typewriter(
"Thanks for choosing to create a new project with bknd!",
_config.speed.typewriter,
color.dim
);
await wait(_config.speed.wait);
yield* typewriter("Thanks for choosing to create a new project with bknd!", color.dim);
await wait();
})()
);
@@ -90,6 +91,12 @@ async function action(options: { template?: string; dir?: string; integration?:
downloadOpts.clean = clean;
}
let name = downloadOpts.dir.includes("/")
? downloadOpts.dir.split("/").pop()
: downloadOpts.dir.replace(/[./]/g, "");
if (!name || name.length === 0) name = "bknd";
let template: Template | undefined;
if (options.template) {
template = templates.find((t) => t.key === options.template) as Template;
@@ -102,14 +109,10 @@ async function action(options: { template?: string; dir?: string; integration?:
if (!integration) {
await $p.stream.info(
(async function* () {
yield* typewriter("Ready? ", _config.speed.typewriter * 1.5, color.bold);
await wait(500);
yield* typewriter(
"Let's find the perfect template for you.",
_config.speed.typewriter,
color.dim
);
await wait(500);
yield* typewriter("Ready? ", color.bold, 1.5);
await wait(2);
yield* typewriter("Let's find the perfect template for you.", color.dim);
await wait(2);
})()
);
@@ -169,12 +172,13 @@ async function action(options: { template?: string; dir?: string; integration?:
process.exit(1);
}
const ctx = { template, dir: downloadOpts.dir };
const ctx = { template, dir: downloadOpts.dir, name };
{
// @todo: only while in PR
const ref = "feat/cli-starters";
//const ref = `v${version}`;
const ref = process.env.BKND_CLI_CREATE_REF ?? `v${version}`;
if (process.env.BKND_CLI_CREATE_REF) {
$p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(ref));
}
const prefix =
template.ref === true
@@ -187,11 +191,21 @@ async function action(options: { template?: string; dir?: string; integration?:
//console.log("url", url);
const s = $p.spinner();
s.start("Downloading template...");
const result = await downloadTemplate(url, {
dir: ctx.dir,
force: downloadOpts.clean ? "clean" : true
});
//console.log("result", result);
try {
await downloadTemplate(url, {
dir: ctx.dir,
force: downloadOpts.clean ? "clean" : true
});
} catch (e) {
if (e instanceof Error) {
s.stop("Failed to download template: " + color.red(e.message), 1);
} else {
console.error(e);
s.stop("Failed to download template. Check logs above.", 1);
}
errorOutro();
}
s.stop("Template downloaded.");
await updateBkndPackages(ctx.dir);
@@ -201,6 +215,16 @@ async function action(options: { template?: string; dir?: string; integration?:
}
}
// update package name
await overridePackageJson(
(pkg) => ({
...pkg,
name: ctx.name
}),
{ dir: ctx.dir }
);
$p.log.success(`Updated package name to ${color.cyan(ctx.name)}`);
{
const install = await $p.confirm({
message: "Install dependencies?"
@@ -209,9 +233,23 @@ async function action(options: { template?: string; dir?: string; integration?:
if ($p.isCancel(install)) {
process.exit(1);
} else if (install) {
const install_cmd = template.scripts?.install || "npm install";
const s = $p.spinner();
s.start("Installing dependencies...");
exec(`cd ${ctx.dir} && npm install`, { silent: true });
try {
exec(`cd ${ctx.dir} && ${install_cmd}`, { silent: true });
} catch (e) {
if (e instanceof Error) {
s.stop("Failed to install: " + color.red(e.message), 1);
} else {
console.error(e);
s.stop("Failed to install. Check logs above.", 1);
}
errorOutro();
}
s.stop("Dependencies installed.");
if (template!.postinstall) {
@@ -220,12 +258,12 @@ async function action(options: { template?: string; dir?: string; integration?:
} else {
await $p.stream.warn(
(async function* () {
yield* typewriter("Remember to run ", _config.speed.typewriter, color.dim);
await wait(_config.speed.typewriter);
yield* typewriter("npm install", _config.speed.typewriter, color.cyan);
await wait(_config.speed.typewriter);
yield* typewriter(" after setup", _config.speed.typewriter, color.dim);
await wait(_config.speed.wait / 2);
yield* typewriter(
color.dim("Remember to run ") +
color.cyan("npm install") +
color.dim(" after setup")
);
await wait();
})()
);
}
@@ -237,17 +275,16 @@ async function action(options: { template?: string; dir?: string; integration?:
await $p.stream.success(
(async function* () {
yield* typewriter("That's it! ", _config.speed.typewriter);
await wait(_config.speed.wait / 2);
yield* typewriter("That's it! ");
await wait(0.5);
yield "🎉";
await wait(_config.speed.wait);
await wait();
yield "\n\n";
yield* typewriter(
`Enter your project's directory using ${color.cyan("cd " + ctx.dir)}
If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`,
_config.speed.typewriter
If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`
);
await wait(_config.speed.wait * 2);
await wait(2);
})()
);

View File

@@ -1,5 +1,6 @@
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { getVersion as sysGetVersion } from "cli/utils/sys";
export type TPackageJson = Partial<{
name: string;
@@ -67,7 +68,7 @@ export async function replacePackageJsonVersions(
export async function updateBkndPackages(dir?: string, map?: Record<string, string>) {
const versions = {
bknd: "^" + (await getVersion("bknd")),
bknd: "^" + (await sysGetVersion()),
...(map ?? {})
};
await replacePackageJsonVersions(

View File

@@ -4,6 +4,8 @@ import { uuid } from "core/utils";
import color from "picocolors";
import type { Template, TemplateSetupCtx } from ".";
const WRANGLER_FILE = "wrangler.json";
export const cloudflare = {
key: "cloudflare",
title: "Cloudflare Basic",
@@ -12,11 +14,12 @@ export const cloudflare = {
path: "gh:bknd-io/bknd/examples/cloudflare-worker",
ref: true,
setup: async (ctx) => {
// overwrite assets directory
// overwrite assets directory & name
await overrideJson(
"wrangler.json",
WRANGLER_FILE,
(json) => ({
...json,
name,
assets: {
directory: "node_modules/bknd/dist/static"
}
@@ -70,7 +73,7 @@ async function createD1(ctx: TemplateSetupCtx) {
}
await overrideJson(
"wrangler.json",
WRANGLER_FILE,
(json) => ({
...json,
d1_databases: [
@@ -90,7 +93,7 @@ async function createD1(ctx: TemplateSetupCtx) {
async function createLibsql(ctx: TemplateSetupCtx) {
await overrideJson(
"wrangler.json",
WRANGLER_FILE,
(json) => ({
...json,
vars: {

View File

@@ -3,10 +3,12 @@ import { cloudflare } from "./cloudflare";
export type TemplateSetupCtx = {
template: Template;
dir: string;
name: string;
};
export type Integration = "node" | "bun" | "cloudflare" | "nextjs" | "remix" | "astro" | "custom";
type TemplateScripts = "install" | "dev" | "build" | "start";
export type Template = {
/**
* unique key for the template
@@ -23,12 +25,13 @@ export type Template = {
* adds a ref "#{ref}" to the path. If "true", adds the current version of bknd
*/
ref?: true | string;
scripts?: Partial<Record<TemplateScripts, string>>;
preinstall?: (ctx: TemplateSetupCtx) => Promise<void>;
postinstall?: (ctx: TemplateSetupCtx) => Promise<void>;
setup?: (ctx: TemplateSetupCtx) => Promise<void>;
};
export const templates = [
export const templates: Template[] = [
{
key: "node",
title: "Node.js Basic",
@@ -45,5 +48,33 @@ export const templates = [
path: "gh:bknd-io/bknd/examples/bun",
ref: true
},
cloudflare
] as const satisfies Template[];
cloudflare,
{
key: "remix",
title: "Remix Basic",
integration: "remix",
description: "A basic bknd Remix starter",
path: "gh:bknd-io/bknd/examples/remix",
ref: true
},
{
// @todo: add `concurrently`?
key: "nextjs",
title: "Next.js Basic",
integration: "nextjs",
description: "A basic bknd Next.js starter",
path: "gh:bknd-io/bknd/examples/nextjs",
scripts: {
install: "npm install --force"
},
ref: true
},
{
key: "astro",
title: "Astro Basic",
integration: "astro",
description: "A basic bknd Astro starter",
path: "gh:bknd-io/bknd/examples/astro",
ref: true
}
];

View File

@@ -1,15 +1,17 @@
#!/usr/bin/env node
import { Command } from "commander";
import color from "picocolors";
import * as commands from "./commands";
import { getVersion } from "./utils/sys";
const program = new Command();
export async function main() {
const version = await getVersion();
program
.name("bknd")
.description("bknd cli")
.version(await getVersion());
.description(color.yellowBright("⚡") + " bknd cli " + color.bold(color.cyan(`v${version}`)))
.version(version);
// register commands
for (const command of Object.values(commands)) {

View File

@@ -1,14 +1,56 @@
export async function wait(ms: number) {
const _SPEEDUP = process.env.LOCAL;
const DEFAULT_WAIT = _SPEEDUP ? 0 : 250;
export async function wait(factor: number = 1, strict?: boolean) {
const ms = strict === true ? factor : DEFAULT_WAIT * factor;
return new Promise((r) => setTimeout(r, ms));
}
// from https://github.com/chalk/ansi-regex/blob/main/index.js
export default function ansiRegex({ onlyFirst = false } = {}) {
// Valid string terminator sequences are BEL, ESC\, and 0x9c
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
const pattern = [
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
].join("|");
return new RegExp(pattern, onlyFirst ? undefined : "g");
}
const DEFAULT_WAIT_WRITER = _SPEEDUP ? 0 : 20;
export async function* typewriter(
text: string,
delay: number,
transform?: (char: string) => string
transform?: (char: string) => string,
_delay?: number
) {
for (const char of text) {
yield transform ? transform(char) : char;
await wait(delay);
const delay = DEFAULT_WAIT_WRITER * (_delay ?? 1);
const regex = ansiRegex();
const parts: string[] = [];
let match: RegExpExecArray | null;
let lastIndex = 0;
// Extract ANSI escape sequences as standalone units
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
while ((match = regex.exec(text)) !== null) {
if (lastIndex < match.index) {
parts.push(...text.slice(lastIndex, match.index).split(""));
}
parts.push(match[0]); // Add the ANSI escape sequence as a full chunk
lastIndex = regex.lastIndex;
}
// Add any remaining characters after the last ANSI sequence
if (lastIndex < text.length) {
parts.push(...text.slice(lastIndex).split(""));
}
// Yield characters or ANSI sequences in order
for (const chunk of parts) {
yield transform ? transform(chunk) : chunk;
// Delay only for normal characters, not ANSI codes
if (!regex.test(chunk)) {
await wait(delay, true);
}
}
}

View File

@@ -17,9 +17,9 @@ export function getRelativeDistPath() {
return path.relative(process.cwd(), getDistPath());
}
export async function getVersion() {
export async function getVersion(_path: string = "") {
try {
const resolved = path.resolve(getRootPath(), "package.json");
const resolved = path.resolve(getRootPath(), path.join(_path, "package.json"));
const pkg = await readFile(resolved, "utf-8");
if (pkg) {
return JSON.parse(pkg).version ?? "preview";

View File

@@ -19,7 +19,7 @@
"isbot": "^5.1.18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-utils": "^8.0.0"
"remix-utils": "^7.0.0"
},
"devDependencies": {
"@remix-run/dev": "^2.15.2",