#!/usr/bin/env bun import * as p from "@clack/prompts"; import { initRenderer, TargetExistsError } from '@gregorlohaus/tdir' import { z } from 'zod' import path from "node:path"; import { existsSync, readdirSync, statSync } from "node:fs"; p.intro(`create-glstack${process.env.GLSTACK_DEV === 'true' ? ' (dev)' : ''}`); const defaultName = "glstack-test" // Java and Kotlin keywords can't be Android package segments. const reservedWords = new Set(["abstract", "as", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "fun", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "is", "long", "native", "new", "null", "object", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typealias", "typeof", "val", "var", "void", "volatile", "when", "while"]) const isNonEmptyPath = (target: string) => existsSync(target) && (!statSync(target).isDirectory() || readdirSync(target).length > 0) const project = await p.group( { name: () => p.text({ message: "What would you like to name your project?", placeholder: defaultName, defaultValue: defaultName, validate: (value) => { const name = value || defaultName // Used as directory, npm scope, Go module path segment and database name. if (!/^[a-z][a-z0-9-]*$/.test(name)) { return "Use lowercase letters, digits and dashes, starting with a letter" } if (isNonEmptyPath(name)) return `./${name} already exists and is not empty` return undefined }, }), frontend: async () => await p.select({ message: 'Pick a frontend framework.', options: [ { value: "svelte-kit", label: "SvelteKit" }, { value: "solid-start", label: "SolidStart" }, { value: "nuxt", label: "Nuxt" }, { 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 nameSegment = String(results.name).replace(/[^a-z0-9]/g, "") const fallback = `com.${reservedWords.has(nameSegment) ? `${nameSegment}app` : nameSegment}.mobile` return await p.text({ message: "What bundle identifier should the mobile app use?", placeholder: fallback, defaultValue: fallback, validate: (value) => { if (!value) return undefined if (!/^[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" } const reserved = value.split(".").find((segment) => reservedWords.has(segment)) if (reserved) return `"${reserved}" is a Java/Kotlin keyword and can't be part of an Android package` 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) => { if (value && !/^[a-zA-Z0-9._~-]+(\/[a-zA-Z0-9._~-]+)*$/.test(value)) { return "Use a module path without scheme or trailing slash, e.g. github.com/you" } 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); try { // `results` in the bundleid prompt makes group() infer it as unknown. render(destDir, { project: { ...project, bundleid: project.bundleid as string } }, { reverseMap: true, overwrite: false, // npm never publishes .gitignore/.npmrc, so the template stores them as _gitignore/_npmrc. rename: { _gitignore: ".gitignore", _npmrc: ".npmrc" }, }) } catch (error) { if (!(error instanceof TargetExistsError)) throw error p.cancel(`./${project.name} already exists and is not empty.`) process.exit(1) } 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!");