mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-18 05:16:04 +00:00
Merge pull request #79 from bknd-io/feat/cli-starters
added cli-starters
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ packages/media/.env
|
|||||||
.history
|
.history
|
||||||
**/*/.db/*
|
**/*/.db/*
|
||||||
**/*/.configs/*
|
**/*/.configs/*
|
||||||
|
**/*/.template/*
|
||||||
**/*/*.db
|
**/*/*.db
|
||||||
**/*/*.db-shm
|
**/*/*.db-shm
|
||||||
**/*/*.db-wal
|
**/*/*.db-wal
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
"@aws-sdk/client-s3": "^3.613.0",
|
||||||
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/typebox-validator": "^0.2.6",
|
"@hono/typebox-validator": "^0.2.6",
|
||||||
"@hono/vite-dev-server": "^0.17.0",
|
"@hono/vite-dev-server": "^0.17.0",
|
||||||
|
|||||||
292
app/src/cli/commands/create/create.ts
Normal file
292
app/src/cli/commands/create/create.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { downloadTemplate } from "@bluwy/giget-core";
|
||||||
|
import * as $p from "@clack/prompts";
|
||||||
|
import type { CliCommand } from "cli/types";
|
||||||
|
import { typewriter, wait } from "cli/utils/cli";
|
||||||
|
import { exec, getVersion } from "cli/utils/sys";
|
||||||
|
import { Option } from "commander";
|
||||||
|
import color from "picocolors";
|
||||||
|
import { overridePackageJson, updateBkndPackages } from "./npm";
|
||||||
|
import { type Template, templates } from "./templates";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
types: {
|
||||||
|
runtime: "Runtime",
|
||||||
|
framework: "Framework"
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
node: "Node.js",
|
||||||
|
bun: "Bun",
|
||||||
|
cloudflare: "Cloudflare"
|
||||||
|
},
|
||||||
|
framework: {
|
||||||
|
nextjs: "Next.js",
|
||||||
|
remix: "Remix",
|
||||||
|
astro: "Astro"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const create: CliCommand = (program) => {
|
||||||
|
program
|
||||||
|
.command("create")
|
||||||
|
.addOption(new Option("-i, --integration <integration>", "integration to use"))
|
||||||
|
.addOption(new Option("-t, --template <template>", "template to use"))
|
||||||
|
.addOption(new Option("-d --dir <directory>", "directory to create in"))
|
||||||
|
.description("create a new project")
|
||||||
|
.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 }) {
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
const downloadOpts = {
|
||||||
|
dir: options.dir || "./",
|
||||||
|
clean: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const version = await getVersion();
|
||||||
|
$p.intro(
|
||||||
|
`👋 Welcome to the ${color.bold(color.cyan("bknd"))} create wizard ${color.bold(`v${version}`)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await $p.stream.message(
|
||||||
|
(async function* () {
|
||||||
|
yield* typewriter("Thanks for choosing to create a new project with bknd!", color.dim);
|
||||||
|
await wait();
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.dir) {
|
||||||
|
const dir = await $p.text({
|
||||||
|
message: "Where to create your project?",
|
||||||
|
placeholder: downloadOpts.dir,
|
||||||
|
initialValue: downloadOpts.dir
|
||||||
|
});
|
||||||
|
if ($p.isCancel(dir)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadOpts.dir = dir || "./";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(downloadOpts.dir)) {
|
||||||
|
const clean = await $p.confirm({
|
||||||
|
message: `Directory ${color.cyan(downloadOpts.dir)} exists. Clean it?`,
|
||||||
|
initialValue: false
|
||||||
|
});
|
||||||
|
if ($p.isCancel(clean)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (!template) {
|
||||||
|
$p.log.error(`Template ${color.cyan(options.template)} not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let integration: string | undefined = options.integration;
|
||||||
|
if (!integration) {
|
||||||
|
await $p.stream.info(
|
||||||
|
(async function* () {
|
||||||
|
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);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
const type = await $p.select({
|
||||||
|
message: "Pick an integration type",
|
||||||
|
options: Object.entries(config.types).map(([value, name]) => ({
|
||||||
|
value,
|
||||||
|
label: name,
|
||||||
|
hint: Object.values(config[value]).join(", ")
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($p.isCancel(type)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _integration = await $p.select({
|
||||||
|
message: `Which ${color.cyan(config.types[type])} do you want to continue with?`,
|
||||||
|
options: Object.entries(config[type]).map(([value, name]) => ({
|
||||||
|
value,
|
||||||
|
label: name
|
||||||
|
})) as any
|
||||||
|
});
|
||||||
|
if ($p.isCancel(_integration)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
integration = String(_integration);
|
||||||
|
}
|
||||||
|
if (!integration) {
|
||||||
|
$p.log.error("No integration selected");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("integration", { type, integration });
|
||||||
|
|
||||||
|
const choices = templates.filter((t) => t.integration === integration);
|
||||||
|
if (choices.length === 0) {
|
||||||
|
$p.log.error(`No templates found for "${color.cyan(String(integration))}"`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (choices.length > 1) {
|
||||||
|
const selected_template = await $p.select({
|
||||||
|
message: "Pick a template",
|
||||||
|
options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description }))
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($p.isCancel(selected_template)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
template = choices.find((t) => t.key === selected_template) as Template;
|
||||||
|
} else {
|
||||||
|
template = choices[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!template) {
|
||||||
|
$p.log.error("No template selected");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = { template, dir: downloadOpts.dir, name };
|
||||||
|
|
||||||
|
{
|
||||||
|
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
|
||||||
|
? `#${ref}`
|
||||||
|
: typeof template.ref === "string"
|
||||||
|
? `#${template.ref}`
|
||||||
|
: "";
|
||||||
|
const url = `${template.path}${prefix}`;
|
||||||
|
|
||||||
|
//console.log("url", url);
|
||||||
|
const s = $p.spinner();
|
||||||
|
s.start("Downloading template...");
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (template.preinstall) {
|
||||||
|
await template.preinstall(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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?"
|
||||||
|
});
|
||||||
|
|
||||||
|
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...");
|
||||||
|
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) {
|
||||||
|
await template.postinstall(ctx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await $p.stream.warn(
|
||||||
|
(async function* () {
|
||||||
|
yield* typewriter(
|
||||||
|
color.dim("Remember to run ") +
|
||||||
|
color.cyan("npm install") +
|
||||||
|
color.dim(" after setup")
|
||||||
|
);
|
||||||
|
await wait();
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.setup) {
|
||||||
|
await template.setup(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
await $p.stream.success(
|
||||||
|
(async function* () {
|
||||||
|
yield* typewriter("That's it! ");
|
||||||
|
await wait(0.5);
|
||||||
|
yield "🎉";
|
||||||
|
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!`
|
||||||
|
);
|
||||||
|
await wait(2);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
$p.outro(color.green("Setup complete."));
|
||||||
|
}
|
||||||
1
app/src/cli/commands/create/index.ts
Normal file
1
app/src/cli/commands/create/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create";
|
||||||
83
app/src/cli/commands/create/npm.ts
Normal file
83
app/src/cli/commands/create/npm.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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;
|
||||||
|
main: string;
|
||||||
|
version: string;
|
||||||
|
scripts: Record<string, string>;
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
devDependencies: Record<string, string>;
|
||||||
|
optionalDependencies: Record<string, string>;
|
||||||
|
[key: string]: any;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export async function overrideJson<File extends object = object>(
|
||||||
|
file: string,
|
||||||
|
fn: (pkg: File) => Promise<File> | File,
|
||||||
|
opts?: { dir?: string; indent?: number }
|
||||||
|
) {
|
||||||
|
const pkgPath = path.resolve(opts?.dir ?? process.cwd(), file);
|
||||||
|
const pkg = await readFile(pkgPath, "utf-8");
|
||||||
|
const newPkg = await fn(JSON.parse(pkg));
|
||||||
|
await writeFile(pkgPath, JSON.stringify(newPkg, null, opts?.indent || 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function overridePackageJson(
|
||||||
|
fn: (pkg: TPackageJson) => Promise<TPackageJson> | TPackageJson,
|
||||||
|
opts?: { dir?: string }
|
||||||
|
) {
|
||||||
|
return await overrideJson("package.json", fn, { dir: opts?.dir });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPackageInfo(pkg: string, version?: string): Promise<TPackageJson> {
|
||||||
|
const res = await fetch(`https://registry.npmjs.org/${pkg}${version ? `/${version}` : ""}`);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVersion(pkg: string, version: string = "latest") {
|
||||||
|
const info = await getPackageInfo(pkg, version);
|
||||||
|
return info.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _deps = ["dependencies", "devDependencies", "optionalDependencies"] as const;
|
||||||
|
export async function replacePackageJsonVersions(
|
||||||
|
fn: (pkg: string, version: string) => Promise<string | undefined> | string | undefined,
|
||||||
|
opts?: { include?: (keyof typeof _deps)[]; dir?: string }
|
||||||
|
) {
|
||||||
|
const deps = (opts?.include ?? _deps) as string[];
|
||||||
|
await overridePackageJson(
|
||||||
|
async (json) => {
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (dep in json) {
|
||||||
|
for (const [pkg, version] of Object.entries(json[dep])) {
|
||||||
|
const newVersion = await fn(pkg, version as string);
|
||||||
|
if (newVersion) {
|
||||||
|
json[dep][pkg] = newVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
},
|
||||||
|
{ dir: opts?.dir }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBkndPackages(dir?: string, map?: Record<string, string>) {
|
||||||
|
const versions = {
|
||||||
|
bknd: "^" + (await sysGetVersion()),
|
||||||
|
...(map ?? {})
|
||||||
|
};
|
||||||
|
await replacePackageJsonVersions(
|
||||||
|
async (pkg) => {
|
||||||
|
if (pkg in versions) {
|
||||||
|
return versions[pkg];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
{ dir }
|
||||||
|
);
|
||||||
|
}
|
||||||
121
app/src/cli/commands/create/templates/cloudflare.ts
Normal file
121
app/src/cli/commands/create/templates/cloudflare.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import * as $p from "@clack/prompts";
|
||||||
|
import { overrideJson, overridePackageJson } from "cli/commands/create/npm";
|
||||||
|
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",
|
||||||
|
integration: "cloudflare",
|
||||||
|
description: "A basic bknd Cloudflare worker",
|
||||||
|
path: "gh:bknd-io/bknd/examples/cloudflare-worker",
|
||||||
|
ref: true,
|
||||||
|
setup: async (ctx) => {
|
||||||
|
// overwrite assets directory & name
|
||||||
|
await overrideJson(
|
||||||
|
WRANGLER_FILE,
|
||||||
|
(json) => ({
|
||||||
|
...json,
|
||||||
|
name,
|
||||||
|
assets: {
|
||||||
|
directory: "node_modules/bknd/dist/static"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ dir: ctx.dir }
|
||||||
|
);
|
||||||
|
|
||||||
|
const db = await $p.select({
|
||||||
|
message: "What database do you want to use?",
|
||||||
|
options: [
|
||||||
|
{ label: "Cloudflare D1", value: "d1" },
|
||||||
|
{ label: "LibSQL", value: "libsql" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if ($p.isCancel(db)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (db) {
|
||||||
|
case "d1":
|
||||||
|
await createD1(ctx);
|
||||||
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await createLibsql(ctx);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid database");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const message = (e as any).message || "An error occurred";
|
||||||
|
$p.log.warn(
|
||||||
|
"Couldn't add database. You can add it manually later. Error: " + color.red(message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const satisfies Template;
|
||||||
|
|
||||||
|
async function createD1(ctx: TemplateSetupCtx) {
|
||||||
|
const name = await $p.text({
|
||||||
|
message: "Enter database name",
|
||||||
|
validate: (v) => {
|
||||||
|
if (!v) {
|
||||||
|
return "Invalid name";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ($p.isCancel(name)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await overrideJson(
|
||||||
|
WRANGLER_FILE,
|
||||||
|
(json) => ({
|
||||||
|
...json,
|
||||||
|
d1_databases: [
|
||||||
|
{
|
||||||
|
binding: "DB",
|
||||||
|
database_name: name,
|
||||||
|
database_id: uuid()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
{ dir: ctx.dir }
|
||||||
|
);
|
||||||
|
$p.log.info(
|
||||||
|
"Database created. Note that if you deploy, you have to create a real database using `npx wrangler d1 create <name>` and update your wrangler configuration."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createLibsql(ctx: TemplateSetupCtx) {
|
||||||
|
await overrideJson(
|
||||||
|
WRANGLER_FILE,
|
||||||
|
(json) => ({
|
||||||
|
...json,
|
||||||
|
vars: {
|
||||||
|
DB_URL: "http://127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ dir: ctx.dir }
|
||||||
|
);
|
||||||
|
|
||||||
|
await overridePackageJson((pkg) => ({
|
||||||
|
...pkg,
|
||||||
|
scripts: {
|
||||||
|
...pkg.scripts,
|
||||||
|
db: "turso db",
|
||||||
|
dev: "npm run db && wrangler dev"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
$p.log.info(
|
||||||
|
"Database set to LibSQL. You can now run `npm run db` to start the database and `npm run dev` to start the worker."
|
||||||
|
);
|
||||||
|
$p.log.info(
|
||||||
|
`Make sure you have Turso's CLI installed. Check their docs on how to install at ${color.cyan("https://docs.turso.tech/cli/introduction")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
80
app/src/cli/commands/create/templates/index.ts
Normal file
80
app/src/cli/commands/create/templates/index.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* the integration this template is for
|
||||||
|
*/
|
||||||
|
integration: Integration;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* 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: Template[] = [
|
||||||
|
{
|
||||||
|
key: "node",
|
||||||
|
title: "Node.js Basic",
|
||||||
|
integration: "node",
|
||||||
|
description: "A basic bknd Node.js server",
|
||||||
|
path: "gh:bknd-io/bknd/examples/node",
|
||||||
|
ref: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bun",
|
||||||
|
title: "Bun Basic",
|
||||||
|
integration: "bun",
|
||||||
|
description: "A basic bknd Bun server",
|
||||||
|
path: "gh:bknd-io/bknd/examples/bun",
|
||||||
|
ref: true
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -3,3 +3,4 @@ export { schema } from "./schema";
|
|||||||
export { run } from "./run";
|
export { run } from "./run";
|
||||||
export { debug } from "./debug";
|
export { debug } from "./debug";
|
||||||
export { user } from "./user";
|
export { user } from "./user";
|
||||||
|
export { create } from "./create";
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
import color from "picocolors";
|
||||||
import * as commands from "./commands";
|
import * as commands from "./commands";
|
||||||
import { getVersion } from "./utils/sys";
|
import { getVersion } from "./utils/sys";
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
|
const version = await getVersion();
|
||||||
program
|
program
|
||||||
.name("bknd")
|
.name("bknd")
|
||||||
.description("bknd cli")
|
.description(color.yellowBright("⚡") + " bknd cli " + color.bold(color.cyan(`v${version}`)))
|
||||||
.version(await getVersion());
|
.version(version);
|
||||||
|
|
||||||
// register commands
|
// register commands
|
||||||
for (const command of Object.values(commands)) {
|
for (const command of Object.values(commands)) {
|
||||||
|
|||||||
56
app/src/cli/utils/cli.ts
Normal file
56
app/src/cli/utils/cli.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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,
|
||||||
|
transform?: (char: string) => string,
|
||||||
|
_delay?: number
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { execSync } from "node:child_process";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
@@ -16,9 +17,9 @@ export function getRelativeDistPath() {
|
|||||||
return path.relative(process.cwd(), getDistPath());
|
return path.relative(process.cwd(), getDistPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVersion() {
|
export async function getVersion(_path: string = "") {
|
||||||
try {
|
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");
|
const pkg = await readFile(resolved, "utf-8");
|
||||||
if (pkg) {
|
if (pkg) {
|
||||||
return JSON.parse(pkg).version ?? "preview";
|
return JSON.parse(pkg).version ?? "preview";
|
||||||
@@ -38,3 +39,15 @@ export async function fileExists(filePath: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exec(command: string, opts?: { silent?: boolean; env?: Record<string, string> }) {
|
||||||
|
const stdio = opts?.silent ? "pipe" : "inherit";
|
||||||
|
const output = execSync(command, {
|
||||||
|
stdio: ["inherit", stdio, stdio],
|
||||||
|
env: { ...process.env, ...opts?.env }
|
||||||
|
});
|
||||||
|
if (!opts?.silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"db": "turso dev --db-file test.db",
|
"db": "turso dev",
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"start": "wrangler dev",
|
"start": "wrangler dev",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
|||||||
28
examples/cloudflare-worker/wrangler.json
Normal file
28
examples/cloudflare-worker/wrangler.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "bknd-cf-worker-example",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2025-02-04",
|
||||||
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
|
"workers_dev": true,
|
||||||
|
"minify": true,
|
||||||
|
"assets": {
|
||||||
|
"directory": "../../app/dist/static"
|
||||||
|
},
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "DB",
|
||||||
|
"database_name": "bknd-cf-example",
|
||||||
|
"database_id": "7ad67953-2bbf-47fc-8696-f4517dbfe674"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "BUCKET",
|
||||||
|
"bucket_name": "bknd-cf-example"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"isbot": "^5.1.18",
|
"isbot": "^5.1.18",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"remix-utils": "^8.0.0"
|
"remix-utils": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@remix-run/dev": "^2.15.2",
|
"@remix-run/dev": "^2.15.2",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.10.0",
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@tsconfig/strictest": "^2.0.5",
|
"@tsconfig/strictest": "^2.0.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"tsup": "^8.1.0",
|
"tsup": "^8.1.0",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"verdaccio": "^5.32.1",
|
"verdaccio": "^5.32.1",
|
||||||
"wrangler": "^3.71.0"
|
"wrangler": "^3.108.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user