#!/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 = { _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!");