diff --git a/init/deno.json b/init/deno.json index 706c73858ed..15c95398ad9 100644 --- a/init/deno.json +++ b/init/deno.json @@ -1,6 +1,6 @@ { "name": "@marvinh-test/fresh-init", - "version": "0.0.1-prealpha.1", + "version": "0.0.1", "exports": { ".": "./mod.ts" }, @@ -15,6 +15,9 @@ "exclude": ["**/*_test.*", "*.todo"] }, "imports": { - "@marvinh-test/fresh": "jsr:@marvinh-test/fresh@^2.0.0-prealpha.0" + "@marvinh-test/fresh": "jsr:@marvinh-test/fresh@^2.0.0-prealpha.0", + "@std/cli": "jsr:@std/cli@^0.220.1", + "@std/fmt": "jsr:@std/fmt@^0.220.1", + "@std/path": "jsr:@std/path@^0.220.1" } } diff --git a/init/mod.ts b/init/mod.ts index e911cab90eb..b44c32546b0 100644 --- a/init/mod.ts +++ b/init/mod.ts @@ -1 +1,552 @@ -console.log("it works"); +import { parseArgs } from "@std/cli/parse_args"; +import * as colors from "@std/fmt/colors"; +import * as path from "@std/path"; + +const flags = parseArgs(Deno.args, { + boolean: ["force", "tailwind", "twind", "vscode", "docker", "help"], + default: { + force: null, + tailwind: null, + twind: null, + vscode: null, + docker: null, + }, + alias: { + help: "h", + }, +}); + +const help = `@fresh/init + +Initialize a new Fresh project. This will create all the necessary files for a +new project. + +To generate a project in the './foobar' subdirectory: + deno run -Ar jsr:@fresh/init ./foobar + +To generate a project in the current directory: + deno run -Ar jsr:@fresh/init . + +USAGE: + deno run -Ar jsr:@fresh/init [DIRECTORY] + +OPTIONS: + --force Overwrite existing files + --tailwind Use Tailwind for styling + --vscode Setup project for VS Code + --docker Setup Project to use Docker +`; + +if (flags.help) { + console.log(help); + Deno.exit(0); +} + +console.log(); +console.log( + colors.bgRgb8( + colors.rgb8(" 🍋 Fresh: The next-gen web framework. ", 0), + 121, + ), +); +console.log(); + +function error(message: string): never { + console.error(`%cerror%c: ${message}`, "color: red; font-weight: bold", ""); + Deno.exit(1); +} + +let unresolvedDirectory = Deno.args[0]; +if (flags._.length !== 1) { + const userInput = prompt("Project Name:", "fresh-project"); + if (!userInput) { + error(help); + } + + unresolvedDirectory = userInput; +} + +const CONFIRM_EMPTY_MESSAGE = + "The target directory is not empty (files could get overwritten). Do you want to continue anyway?"; + +const projectDir = path.resolve(unresolvedDirectory); + +try { + const dir = [...Deno.readDirSync(projectDir)]; + const isEmpty = dir.length === 0 || + dir.length === 1 && dir[0].name === ".git"; + if ( + !isEmpty && + !(flags.force === null ? confirm(CONFIRM_EMPTY_MESSAGE) : flags.force) + ) { + error("Directory is not empty."); + } +} catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } +} +const useDocker = flags.docker; +let useTailwind = flags.tailwind || false; +if (flags.tailwind == null) { + if (confirm(`Set up ${colors.cyan("Tailwind CSS")} for styling?`)) { + useTailwind = true; + } +} + +const USE_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`; +const useVSCode = flags.vscode === null + ? confirm(USE_VSCODE_MESSAGE) + : flags.vscode; + +async function writeProjectFile( + pathname: string, + content: + | string + | Uint8Array + | ReadableStream + | Record, +) { + const filePath = path.join( + projectDir, + ...pathname.split("/").filter(Boolean), + ); + try { + await Deno.mkdir( + path.dirname(filePath), + { recursive: true }, + ); + if (typeof content === "string") { + let formatted = content; + if (!content.endsWith("\n\n")) { + formatted += "\n"; + } + await Deno.writeTextFile(filePath, formatted); + } else if ( + content instanceof Uint8Array || content instanceof ReadableStream + ) { + await Deno.writeFile(filePath, content); + } else { + await Deno.writeTextFile( + filePath, + JSON.stringify(content, null, 2) + "\n", + ); + } + } catch (err) { + if (!(err instanceof Deno.errors.AlreadyExists)) { + throw err; + } + } +} + +const GITIGNORE = `# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# Fresh build directory +_fresh/ +# npm dependencies +node_modules/ +`; + +await writeProjectFile(".gitignore", GITIGNORE); + +if (useDocker) { + const DENO_VERSION = Deno.version.deno; + const DOCKERFILE_TEXT = ` +FROM denoland/deno:${DENO_VERSION} + +ARG GIT_REVISION +ENV DENO_DEPLOYMENT_ID=\${GIT_REVISION} + +WORKDIR /app + +COPY . . +RUN deno cache main.ts + +EXPOSE 8000 + +CMD ["run", "-A", "main.ts"] + +`; + await writeProjectFile("Dockerfile", DOCKERFILE_TEXT); +} + +const TAILWIND_CONFIG_TS = `import { type Config } from "tailwindcss"; + +export default { + content: [ + "{routes,islands,components}/**/*.{ts,tsx}", + ], +} satisfies Config; +`; +if (useTailwind) { + await writeProjectFile("tailwind.config.ts", TAILWIND_CONFIG_TS); +} + +const NO_TAILWIND_STYLES = ` +*, +*::before, +*::after { + box-sizing: border-box; +} +* { + margin: 0; +} +button { + color: inherit; +} +button, [role="button"] { + cursor: pointer; +} +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} +img, +svg { + display: block; +} +img, +video { + max-width: 100%; + height: auto; +} + +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} +.transition-colors { + transition-property: background-color, border-color, color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.my-6 { + margin-bottom: 1.5rem; + margin-top: 1.5rem; +} +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} +.my-4 { + margin-bottom: 1rem; + margin-top: 1rem; +} +.mx-auto { + margin-left: auto; + margin-right: auto; +} +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} +.py-8 { + padding-bottom: 2rem; + padding-top: 2rem; +} +.bg-\\[\\#86efac\\] { + background-color: #86efac; +} +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} +.py-6 { + padding-bottom: 1.5rem; + padding-top: 1.5rem; +} +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.py-1 { + padding-bottom: 0.25rem; + padding-top: 0.25rem; +} +.border-gray-500 { + border-color: #6b7280; +} +.bg-white { + background-color: #fff; +} +.flex { + display: flex; +} +.gap-8 { + grid-gap: 2rem; + gap: 2rem; +} +.font-bold { + font-weight: 700; +} +.max-w-screen-md { + max-width: 768px; +} +.flex-col { + flex-direction: column; +} +.items-center { + align-items: center; +} +.justify-center { + justify-content: center; +} +.border-2 { + border-width: 2px; +} +.rounded { + border-radius: 0.25rem; +} +.hover\\:bg-gray-200:hover { + background-color: #e5e7eb; +} +.tabular-nums { + font-variant-numeric: tabular-nums; +} +`; + +const TAILWIND_CSS = `@tailwind base; +@tailwind components; +@tailwind utilities;`; + +const cssStyles = useTailwind ? TAILWIND_CSS : NO_TAILWIND_STYLES; +await writeProjectFile("static/styles.css", cssStyles); + +const STATIC_LOGO = + ` + + + + +`; +await writeProjectFile("static/logo.svg", STATIC_LOGO); + +try { + const res = await fetch("https://fresh.deno.dev/favicon.ico"); + const buf = await res.arrayBuffer(); + await writeProjectFile("static/favicon.ico", new Uint8Array(buf)); +} catch { + // Skip this and be silent if there is a network issue. +} + +const MAIN_TS = + `import { FreshApp, freshStaticFiles, fsRoutes } from "@fresh/core"; + +export const app = new FreshApp(); + +app.use(freshStaticFiles()) + .get("/api/:joke", () => new Response("Hello World")) + .get("/", () => new Response("Hello World")); + +await fsRoutes(app, { + dir: Deno.cwd(), + loadIsland: (path) => import("./islands/" + path), + loadRoute: (path) => import("./routes/" + path), +}); + +if (import.meta.main) { + await app.listen(); +} +`; +await writeProjectFile("main.ts", MAIN_TS); + +const DEV_TS = `#!/usr/bin/env -S deno run -A --watch=static/,routes/ +import { tailwind } from "@fresh/plugin-tailwind"; +import { FreshDevApp } from "@fresh/core/dev"; +import { app } from "./main.ts"; + +const devApp = new FreshDevApp(); +${useTailwind ? "tailwind(devApp, {});\n" : "\n"} +devApp.mountApp("/", app); + +if (Deno.args.includes("build")) { + await devApp.build(); +} else { + await devApp.listen(); +} +`; +await writeProjectFile("dev.ts", DEV_TS); + +const config = { + lock: false, + tasks: { + check: + "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + start: "deno run -A --watch=static/,routes/ dev.ts", + build: "deno run -A dev.ts build", + preview: "deno run -A main.ts", + update: "deno run -A -r https://fresh.deno.dev/update .", + }, + lint: { + rules: { + tags: ["fresh", "recommended"], + }, + }, + exclude: ["**/_fresh/*"], + imports: {} as Record, + compilerOptions: { + jsx: "react-jsx", + jsxImportSource: "preact", + }, +}; + +await writeProjectFile("deno.json", config); + +const README_MD = `# Fresh project + +Your new Fresh project is ready to go. You can follow the Fresh "Getting +Started" guide here: https://fresh.deno.dev/docs/getting-started + +### Usage + +Make sure to install Deno: https://deno.land/manual/getting_started/installation + +Then start the project: + +\`\`\` +deno task start +\`\`\` + +This will watch the project directory and restart as necessary. +`; +await writeProjectFile("README.md", README_MD); + +if (useVSCode) { + const vscodeSettings = { + "deno.enable": true, + "deno.lint": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "[typescriptreact]": { + "editor.defaultFormatter": "denoland.vscode-deno", + }, + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno", + }, + "[javascriptreact]": { + "editor.defaultFormatter": "denoland.vscode-deno", + }, + "[javascript]": { + "editor.defaultFormatter": "denoland.vscode-deno", + }, + "css.customData": useTailwind ? [".vscode/tailwind.json"] : undefined, + }; + + await writeProjectFile(".vscode/settings.json", vscodeSettings); + + const recommendations = ["denoland.vscode-deno"]; + if (useTailwind) recommendations.push("bradlc.vscode-tailwindcss"); + await writeProjectFile(".vscode/extensions.json", { recommendations }); + + if (useTailwind) { + const tailwindCustomData = { + "version": 1.1, + "atDirectives": [ + { + "name": "@tailwind", + "description": + "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": + "https://tailwindcss.com/docs/functions-and-directives#tailwind", + }, + ], + }, + { + "name": "@apply", + "description": + "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", + "references": [ + { + "name": "Tailwind Documentation", + "url": + "https://tailwindcss.com/docs/functions-and-directives#apply", + }, + ], + }, + { + "name": "@responsive", + "description": + "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": + "https://tailwindcss.com/docs/functions-and-directives#responsive", + }, + ], + }, + { + "name": "@screen", + "description": + "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": + "https://tailwindcss.com/docs/functions-and-directives#screen", + }, + ], + }, + { + "name": "@variants", + "description": + "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": + "https://tailwindcss.com/docs/functions-and-directives#variants", + }, + ], + }, + ], + }; + + await writeProjectFile(".vscode/tailwind.json", tailwindCustomData); + } +} + +// Specifically print unresolvedDirectory, rather than resolvedDirectory in order to +// not leak personal info (e.g. `/Users/MyName`) +console.log("\n%cProject initialized!\n", "color: green; font-weight: bold"); + +if (unresolvedDirectory !== ".") { + console.log( + `Enter your project directory using %ccd ${unresolvedDirectory}%c.`, + "color: cyan", + "", + ); +} +console.log( + "Run %cdeno task start%c to start the project. %cCTRL-C%c to stop.", + "color: cyan", + "", + "color: cyan", + "", +); +console.log(); +console.log( + "Stuck? Join our Discord %chttps://discord.gg/deno", + "color: cyan", + "", +); +console.log(); +console.log( + "%cHappy hacking! 🦕", + "color: gray", +);