117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import * as p from "@clack/prompts";
|
|
import { initRenderer } from '@gregorlohaus/tdir'
|
|
import { z } from 'zod'
|
|
import path from "node:path";
|
|
import { readdir, rename } from "node:fs/promises";
|
|
|
|
p.intro(`create-glstack ${process.env.GLSTACK_DEV && 'isDev'}`);
|
|
|
|
const project = await p.group(
|
|
{
|
|
name: () =>
|
|
p.text({
|
|
message: "What would you like to name your project?",
|
|
placeholder: "glstack-test",
|
|
defaultValue: "glstack-test",
|
|
validate: (value) => {
|
|
return undefined
|
|
},
|
|
}),
|
|
frontend: async () =>
|
|
await p.select({
|
|
message: 'Pick a frontend framework.',
|
|
options: [
|
|
{ value: "svelte-kit", label: "SvelteKit" },
|
|
{ value: "solid-start", label: "SolidStart" },
|
|
{ value: "none", label: "None" }
|
|
]
|
|
})
|
|
,
|
|
mobile: async () =>
|
|
await p.select({
|
|
message: 'Pick a mobile framework.',
|
|
options: [
|
|
{ value: "expo", label: "ReactNative + Expo" },
|
|
{ value: "none", label: "None" }
|
|
]
|
|
})
|
|
,
|
|
bundleid: async ({ results }) => {
|
|
if (results.mobile !== "expo") return ""
|
|
// Valid as both an iOS bundle identifier and an Android package name.
|
|
const fallback = `com.${String(results.name).toLowerCase().replace(/[^a-z0-9]/g, "").replace(/^[0-9]+/, "") || "app"}.mobile`
|
|
return await p.text({
|
|
message: "What bundle identifier should the mobile app use?",
|
|
placeholder: fallback,
|
|
defaultValue: fallback,
|
|
validate: (value) => {
|
|
if (value && !/^[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)+$/.test(value)) {
|
|
return "Use reverse-DNS segments of letters and digits, e.g. com.example.app"
|
|
}
|
|
return undefined
|
|
},
|
|
})
|
|
},
|
|
goprefix: () =>
|
|
p.text({
|
|
message: "What would you like to use as a go package prefix?",
|
|
placeholder: "github.com/glstack-test",
|
|
defaultValue: "github.com/glstack-test",
|
|
validate: (value) => {
|
|
return undefined
|
|
},
|
|
}),
|
|
installDeps: async () => {
|
|
return await p.confirm({message: "Install dependencies?", })
|
|
}
|
|
},
|
|
{
|
|
onCancel: () => {
|
|
p.cancel("Operation cancelled.");
|
|
process.exit(0);
|
|
},
|
|
}
|
|
);
|
|
|
|
const templateDir = (process.env.GLSTACK_DEV == 'true' ? './template' : path.join(import.meta.dir, '..', 'template'))
|
|
const createRenderer = initRenderer(templateDir)
|
|
const render = createRenderer(z.object({
|
|
project: z.object({
|
|
name: z.string(),
|
|
goprefix: z.string(),
|
|
frontend: z.string(),
|
|
mobile: z.string(),
|
|
bundleid: z.string()
|
|
})
|
|
}))
|
|
const destDir = path.join("./", project.name);
|
|
// `results` in the bundleid prompt makes group() infer it as unknown.
|
|
render(destDir, { project: { ...project, bundleid: project.bundleid as string } }, { reverseMap: true })
|
|
|
|
// npm never publishes .gitignore/.npmrc, so the template stores them as _gitignore/_npmrc.
|
|
const dotfiles: Record<string, string> = { _gitignore: ".gitignore", _npmrc: ".npmrc" }
|
|
for (const file of await readdir(destDir, { recursive: true })) {
|
|
const dotname = dotfiles[path.basename(file)]
|
|
if (dotname) await rename(path.join(destDir, file), path.join(destDir, path.dirname(file), dotname))
|
|
}
|
|
const mapFile = Bun.file(path.join(destDir, ".tdir-map.json"))
|
|
if (await mapFile.exists()) {
|
|
const map = await mapFile.json()
|
|
for (const entry of map.files) {
|
|
const dotname = dotfiles[path.basename(entry.outputPath)]
|
|
if (dotname) entry.outputPath = path.join(path.dirname(entry.outputPath), dotname)
|
|
}
|
|
await Bun.write(mapFile, JSON.stringify(map, null, 2))
|
|
}
|
|
|
|
const s = p.spinner();
|
|
if (project.installDeps) {
|
|
s.start("Installing dependencies");
|
|
await Bun.$`bun install`.cwd(path.join(destDir)).quiet();
|
|
s.stop("Dependencies installed.");
|
|
}
|
|
|
|
p.outro("You're all set!");
|