diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1f3a55b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Root: PNPM workspace (`pnpm-workspace.yaml`), TypeScript + Vitest. +- Packages: `packages/*` (e.g., `packages/create-effect-app`). Source in `src/`, build to `dist/`. +- Examples: `examples/*` (e.g., `examples/http-server`). Not part of the workspace; manage deps per example. +- Templates & Scripts: `templates/*`, `scripts/*`; CLI shim in `bin/`. + +## Build, Test, and Development Commands +- Env: Node >=18 (CI uses 24.5.0), PNPM 10.x. Optional: `nix develop` for a reproducible shell. +- Install: `pnpm install` (root workspace only). +- Type check: `pnpm check` (runs TS project references across packages). +- Lint: `pnpm lint` (ESLint + dprint rules). Fix: `pnpm lint-fix`. +- Build: `pnpm build` (builds all workspace packages in parallel). +- Test (workspace): `pnpm vitest`. +- Test one package: `pnpm vitest packages/create-effect-app` (workspace filter by path). +- Run an example: `cd examples/http-server && pnpm install && pnpm dev` (or `pnpm test`). + +## Coding Style & Naming Conventions +- Language: TypeScript (ESM). Indent 2 spaces; max line 120. +- Quotes: double; semicolons: ASI; trailing commas: never. +- Imports: prefer explicit type imports; avoid unused vars (prefix `_` if intentional). +- Lint/format is enforced via ESLint (`eslint.config.mjs`) with `@effect/dprint`. + +## Testing Guidelines +- Framework: Vitest. Unit tests live under `test/**/*.test.ts`. +- Workspace config: `vitest.workspace.ts`; shared defaults in `vitest.shared.ts`. +- Keep tests fast/deterministic; mock external IO. Aim for green in <10 minutes CI. + +## Commit & Pull Request Guidelines +- Small, focused PRs. Describe the change, rationale, and acceptance examples. +- Link related issues. Include screenshots/CLI output when relevant. +- For package changes, add a changeset: `pnpm changeset` (CI handles versioning/publish). +- Pre-push: `pnpm lint && pnpm check && pnpm vitest`. + +## Agent Workflow (XP) +- Work in thin slices: pair → TDD (red→green→refactor) → integrate. +- After each change: ensure tests pass, clarify code, remove duplication, and simplify. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 0b7e7df..a3e8a6b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -[![Nightly Build](https://github.com/Effect-TS/examples/workflows/Nightly%20Checks/badge.svg)](https://github.com/Effect-TS/examples/actions) +## Quick Start + +```sh +pnpm dlx create-effect-app@github:effect-native/examples +``` + +This is a fork of the https://github.com/Effect-TS/examples repo with extra examples for Effect Native. + +--- # Effect Examples diff --git a/bin/create-effect-app.mjs b/bin/create-effect-app.mjs old mode 100644 new mode 100755 index aba066c..f26a3eb --- a/bin/create-effect-app.mjs +++ b/bin/create-effect-app.mjs @@ -1,5 +1,56 @@ -#!/usr/bin/env -S node --enable-source-maps --import=tsx -// Tiny shim to run the TypeScript CLI without building -// Delegates to the workspace package's TS entrypoint +#!/usr/bin/env node +// Tiny shim to run the TypeScript CLI without building. +// Order: bun → tsx → pnpm dlx bun → npx bun -await import(new URL("../packages/create-effect-app/src/bin.ts", import.meta.url).href) +import { spawnSync } from "node:child_process" +import { fileURLToPath } from "node:url" +import path from "node:path" + +const args = process.argv.slice(2) +const here = path.dirname(fileURLToPath(import.meta.url)) +const tsEntry = path.resolve(here, "../packages/create-effect-app/src/bin.ts") + +function tryRun(cmd, cmdArgs, stdio = "inherit") { + try { + const result = spawnSync(cmd, cmdArgs, { stdio }) + if (typeof result.status === "number") return result.status + return 1 + } catch (err) { + if (err && err.code === "ENOENT") return 127 + throw err + } +} + +function has(cmd) { + return tryRun(cmd, ["--version"], "ignore") === 0 +} + +// 1) If bun is available locally, use it to run TS directly +if (has("bun")) { + const code = tryRun("bun", [tsEntry, ...args]) + process.exit(code) +} + +// 2) If tsx is available, use it under Node +if (has("tsx")) { + const code = tryRun("tsx", [tsEntry, ...args]) + process.exit(code) +} + +// 3) Try fetching Bun via pnpm dlx +let code = tryRun("pnpm", ["dlx", "bun", tsEntry, ...args]) + +if (code === 127 || code === 1) { + // 4) Try fetching Bun via npx + code = tryRun("npx", ["bun", tsEntry, ...args]) +} + +if (code !== 0) { + console.error( + "\ncreate-effect-app: could not execute with bun or tsx.\n" + + "Tried local bun, local tsx, pnpm dlx bun, then npx bun.\n" + + "Install Bun (recommended) or tsx, or ensure pnpm/npm can fetch bun.\n" + ) +} + +process.exit(code) diff --git a/examples/http-server/gitignore b/examples/http-server/gitignore new file mode 100644 index 0000000..26167fd --- /dev/null +++ b/examples/http-server/gitignore @@ -0,0 +1,8 @@ +.direnv/ +node_modules/ +dist/ +/data/* +!.gitkeep +.env +*.tsbuildinfo +.DS_Store diff --git a/flake.lock b/flake.lock index 16cb372..9a57450 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1726583932, - "narHash": "sha256-zACxiQx8knB3F8+Ze+1BpiYrI+CbhxyWpcSID9kVhkQ=", + "lastModified": 1757034884, + "narHash": "sha256-PgLSZDBEWUHpfTRfFyklmiiLBE1i1aGCtz4eRA3POao=", "owner": "nixos", "repo": "nixpkgs", - "rev": "658e7223191d2598641d50ee4e898126768fe847", + "rev": "ca77296380960cd497a765102eeb1356eb80fed0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e5d2a59..1087f27 100644 --- a/flake.nix +++ b/flake.nix @@ -14,12 +14,12 @@ { formatter = forAllSystems (pkgs: pkgs.alejandra); devShells = forAllSystems (pkgs: { - default = pkgs.mkShell { + default = pkgs.mkShellNoCC { packages = with pkgs; [ - corepack findutils jq nodejs + pnpm ]; }; }); diff --git a/package.json b/package.json index efed6d3..ea5b853 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,23 @@ "changeset-version": "changeset version && node ./scripts/version.mjs && node ./scripts/examples.mjs && node ./scripts/templates.mjs", "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish" }, + "files": [ + "bin/", + "packages/create-effect-app/src/", + "examples/", + "templates/", + "README.md", + ".gitignore" + ], "bin": { "create-effect-app": "bin/create-effect-app.mjs" }, "dependencies": { - "@effect/cli": "^0.48.21", - "@effect/platform": "^0.69.21", - "@effect/platform-node": "^0.64.23", - "@effect/printer-ansi": "^0.38.14", - "effect": "^3.10.14", + "@effect/cli": "^0.48.31", + "@effect/platform": "^0.69.31", + "@effect/platform-node": "^0.64.33", + "@effect/printer-ansi": "^0.38.19", + "effect": "^3.17.13", "tar": "^7.4.3", "tsx": "^4.19.2", "yaml": "^2.6.0" @@ -33,7 +41,8 @@ "@dprint/formatter": "^0.4.1", "@effect/eslint-plugin": "^0.3.2", "@effect/language-service": "latest", - "@effect/vitest": "^0.13.14", + "@effect/typeclass": "^0.29.19", + "@effect/vitest": "^0.13.19", "@eslint/compat": "^1.2.2", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.13.0", diff --git a/packages/create-effect-app/package.json b/packages/create-effect-app/package.json index e07a3c3..7d4d060 100644 --- a/packages/create-effect-app/package.json +++ b/packages/create-effect-app/package.json @@ -28,12 +28,12 @@ "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@effect/cli": "^0.48.21", - "@effect/platform": "^0.69.21", - "@effect/platform-node": "^0.64.23", - "@effect/printer": "^0.38.14", - "@effect/printer-ansi": "^0.38.14", - "effect": "^3.10.14", + "@effect/cli": "^0.48.31", + "@effect/platform": "^0.69.31", + "@effect/platform-node": "^0.64.33", + "@effect/printer": "^0.38.19", + "@effect/printer-ansi": "^0.38.19", + "effect": "^3.17.13", "tar": "^7.4.3", "tsup": "^8.3.5", "yaml": "^2.6.0" diff --git a/packages/create-effect-app/src/Cli.ts b/packages/create-effect-app/src/Cli.ts index 20a8065..a543036 100644 --- a/packages/create-effect-app/src/Cli.ts +++ b/packages/create-effect-app/src/Cli.ts @@ -16,7 +16,7 @@ import { ProjectType } from "./Domain.js" import { GitHub } from "./GitHub.js" import type { Example } from "./internal/examples.js" import { examples } from "./internal/examples.js" -import { type Template, templates } from "./internal/templates.js" +import { type Template, templateChoices, templates } from "./internal/templates.js" import * as InternalVersion from "./internal/version.js" import { validateProjectName } from "./Utils.js" @@ -45,6 +45,19 @@ const templateType = Options.choice("template", templates).pipe( ) ) +// Optional alternative sources to built-in templates +const templateFolder = Options.text("template-folder").pipe( + Options.withDescription( + "Path to a local template folder (overrides --template if provided)" + ) +) + +const templateRepo = Options.text("template-repo").pipe( + Options.withDescription( + "GitHub repo to use as template (e.g. owner/repo, gh:owner/repo, or https://github.com/owner/repo). Optional path via /sub/dir and ref via @ref" + ) +) + const withChangesets = Options.boolean("changesets").pipe( Options.withDescription("Initialize project with Changesets") ) @@ -58,13 +71,60 @@ const withESLint = Options.boolean("eslint").pipe( ) const withWorkflows = Options.boolean("workflows").pipe( - Options.withDescription("Initialize project with Effect's recommended GitHub actions") + Options.withDescription( + "Initialize project with Effect's recommended GitHub actions" + ) ) +// We support multiple ways to specify a template: +// - built-in catalog via --template +// - local folder via --template-folder +// - GitHub repo via --template-repo const projectType: Options.Options> = Options.all({ example: exampleType }).pipe( Options.map(ProjectType.Example), + Options.orElse( + Options.all({ + templateFolder, + withChangesets, + withNixFlake, + withESLint, + withWorkflows + }).pipe( + Options.map((o) => + ProjectType.Template({ + // Use the folder basename as a friendly template label when possible + template: o.templateFolder ? o.templateFolder.split(/[\\/]/).slice(-1)[0]! : "folder", + templateFolder: o.templateFolder, + withChangesets: o.withChangesets, + withNixFlake: o.withNixFlake, + withESLint: o.withESLint, + withWorkflows: o.withWorkflows + }) + ) + ) + ), + Options.orElse( + Options.all({ + templateRepo, + withChangesets, + withNixFlake, + withESLint, + withWorkflows + }).pipe( + Options.map((o) => + ProjectType.Template({ + template: o.templateRepo, + templateRepo: o.templateRepo, + withChangesets: o.withChangesets, + withNixFlake: o.withNixFlake, + withESLint: o.withESLint, + withWorkflows: o.withWorkflows + }) + ) + ) + ), Options.orElse( Options.all({ template: templateType, @@ -101,7 +161,9 @@ const options = { } const command = Command.make("create-effect-app", options).pipe( - Command.withDescription("Create an Effect application from an example or a template repository"), + Command.withDescription( + "Create an Effect application from an example or a template repository" + ), Command.withHandler(handleCommand) ) @@ -134,7 +196,9 @@ function resolveProjectName(config: RawConfig) { Prompt.text({ message: "What is your project named?", default: "effect-app" - }).pipe(Effect.flatMap((name) => Path.Path.pipe(Effect.map((path) => path.resolve(name))))) + }).pipe( + Effect.flatMap((name) => Path.Path.pipe(Effect.map((path) => path.resolve(name)))) + ) }) } @@ -145,6 +209,48 @@ function resolveProjectType(config: RawConfig) { }) } +// After files are copied into the project directory, rename any +// top-level or nested `gitignore` files to `.gitignore` so that +// ignore rules are effective in the initialized repository, even +// when dotfiles were stripped in packaging. +function finalizeGitignoreFiles(projectDir: string) { + return Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + const tryRename = (from: string, to: string) => fs.rename(from, to).pipe(Effect.ignore) + + const visit: (dir: string) => Effect.Effect = Effect.fn( + function*(dir: string) { + // Rename at this level if present + const gi = path.join(dir, "gitignore") + const dot = path.join(dir, ".gitignore") + const hasGi = yield* fs.exists(gi) + if (hasGi) { + // If a .gitignore already exists, prefer the dotfile and remove duplicate + const hasDot = yield* fs.exists(dot) + if (!hasDot) { + yield* tryRename(gi, dot) + } + } + + // Recurse into subdirectories + const entries = yield* fs.readDirectory(dir) + for (const name of entries) { + const full = path.join(dir, name) + const stat = yield* fs.stat(full) + if (stat.type === "Directory") { + yield* visit(full) + } + } + }, + (effect) => effect.pipe(Effect.ignore) + ) as (dir: string) => Effect.Effect + + yield* visit(projectDir) + }).pipe(Effect.ignore) +} + /** * Examples are simply cloned as is from GitHub */ @@ -152,27 +258,39 @@ function createExample(config: ExampleConfig) { return Effect.gen(function*() { const fs = yield* FileSystem.FileSystem - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Creating a new Effect application in: "), - AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.magenta)) - ])) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Creating a new Effect application in: "), + AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.magenta)) + ]) + ) // Create the project path yield* fs.makeDirectory(config.projectName, { recursive: true }) - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Initializing example project:"), - AnsiDoc.text(config.projectType.example).pipe(AnsiDoc.annotate(Ansi.magenta)) - ])) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Initializing example project:"), + AnsiDoc.text(config.projectType.example).pipe( + AnsiDoc.annotate(Ansi.magenta) + ) + ]) + ) // Download the example project from GitHub yield* GitHub.downloadExample(config) - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Success!").pipe(AnsiDoc.annotate(Ansi.green)), - AnsiDoc.text("Effect example application was initialized in: "), - AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.cyan)) - ])) + // (gitignore normalization happens at the end of all mutations) + + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Success!").pipe(AnsiDoc.annotate(Ansi.green)), + AnsiDoc.text("Effect example application was initialized in: "), + AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.cyan)) + ]) + ) + + // No external post-create hooks; any post processing is handled inline }) } @@ -185,36 +303,64 @@ function createTemplate(config: TemplateConfig) { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Creating a new Effect project in"), - AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.green)) - ])) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Creating a new Effect project in"), + AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.green)) + ]) + ) // Create the project directory yield* fs.makeDirectory(config.projectName, { recursive: true }) - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Initializing project with template:"), - AnsiDoc.text(config.projectType.template).pipe(AnsiDoc.annotate(Ansi.magenta)) - ])) - - // Download the template project from GitHub - yield* GitHub.downloadTemplate(config) + const templateLabel = config.projectType.templateRepo + ?? config.projectType.templateFolder + ?? config.projectType.template - const packageJson = yield* fs.readFileString(path.join(config.projectName, "package.json")).pipe( - Effect.map((json) => JSON.parse(json)) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Initializing project with template:"), + AnsiDoc.text(String(templateLabel)).pipe(AnsiDoc.annotate(Ansi.magenta)) + ]) ) + // Materialize the template into the project directory + if (config.projectType.templateRepo) { + // Download from an arbitrary GitHub repo spec + yield* GitHub.downloadFromRepo( + config.projectType.templateRepo, + config.projectName + ) + } else if (config.projectType.templateFolder) { + // Copy from a local folder (copy contents, not the folder itself) + const entries = yield* fs.readDirectory(config.projectType.templateFolder) + yield* fs.makeDirectory(config.projectName, { recursive: true }) + for (const name of entries) { + const from = path.join(config.projectType.templateFolder, name) + const to = path.join(config.projectName, name) + yield* fs.copy(from, to) + } + } else { + // Built-in catalog template from this repo or remote fall-back + yield* GitHub.downloadTemplate(config) + } + + const packageJson = yield* fs + .readFileString(path.join(config.projectName, "package.json")) + .pipe(Effect.map((json) => JSON.parse(json))) + // Handle user preferences for changesets if (!config.projectType.withChangesets) { // Remove the .changesets directory - yield* fs.remove(path.join(config.projectName, ".changeset"), { - recursive: true - }).pipe(Effect.ignore) + yield* fs + .remove(path.join(config.projectName, ".changeset"), { + recursive: true + }) + .pipe(Effect.ignore) // Remove patches for changesets - const patches = yield* fs.readDirectory(path.join(config.projectName, "patches")).pipe( - Effect.map(Array.filter((file) => file.includes("changeset"))) - ) + const patches = yield* fs + .readDirectory(path.join(config.projectName, "patches")) + .pipe(Effect.map(Array.filter((file) => file.includes("changeset")))) yield* Effect.forEach(patches, (patch) => fs.remove(path.join(config.projectName, "patches", patch))).pipe( Effect.ignore ) @@ -244,22 +390,27 @@ function createTemplate(config: TemplateConfig) { } // If git workflows are enabled, remove changesets related workflows if (config.projectType.withWorkflows) { - yield* fs.remove(path.join(config.projectName, ".github", "workflows", "release.yml")).pipe(Effect.ignore) + yield* fs + .remove( + path.join(config.projectName, ".github", "workflows", "release.yml") + ) + .pipe(Effect.ignore) } } // Handle user preferences for Nix flakes if (!config.projectType.withNixFlake) { - yield* Effect.forEach( - [".envrc", "flake.nix"], - (file) => fs.remove(path.join(config.projectName, file)) - ).pipe(Effect.ignore) + yield* Effect.forEach([".envrc", "flake.nix"], (file) => fs.remove(path.join(config.projectName, file))).pipe( + Effect.ignore + ) } // Handle user preferences for ESLint if (!config.projectType.withESLint) { // Remove eslint.config.mjs - yield* fs.remove(path.join(config.projectName, "eslint.config.mjs")).pipe(Effect.ignore) + yield* fs + .remove(path.join(config.projectName, "eslint.config.mjs")) + .pipe(Effect.ignore) // Remove eslint dependencies const eslintDeps = Array.filter( Object.keys(packageJson["devDependencies"]), @@ -278,20 +429,30 @@ function createTemplate(config: TemplateConfig) { } // If git workflows are enabled, remove lint workflows if (config.projectType.withWorkflows) { - const checkWorkflowPath = path.join(config.projectName, ".github", "workflows", "check.yml") + const checkWorkflowPath = path.join( + config.projectName, + ".github", + "workflows", + "check.yml" + ) const checkWorkflow = yield* fs.readFileString(checkWorkflowPath) const checkYaml = Yaml.parse(checkWorkflow) delete checkYaml["jobs"]["lint"] - yield* fs.writeFileString(checkWorkflowPath, Yaml.stringify(checkYaml, undefined, 2)) + yield* fs.writeFileString( + checkWorkflowPath, + Yaml.stringify(checkYaml, undefined, 2) + ) } } // Handle user preferences for GitHub workflows if (!config.projectType.withWorkflows) { // Remove the .github directory - yield* fs.remove(path.join(config.projectName, ".github"), { - recursive: true - }).pipe(Effect.ignore) + yield* fs + .remove(path.join(config.projectName, ".github"), { + recursive: true + }) + .pipe(Effect.ignore) } // Write out the updated package.json @@ -300,47 +461,77 @@ function createTemplate(config: TemplateConfig) { JSON.stringify(packageJson, undefined, 2) ) - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Success!").pipe(AnsiDoc.annotate(Ansi.green)), - AnsiDoc.text(`Effect template project was initialized in:`), - AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.cyan)) - ])) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Success!").pipe(AnsiDoc.annotate(Ansi.green)), + AnsiDoc.text(`Effect template project was initialized in:`), + AnsiDoc.text(config.projectName).pipe(AnsiDoc.annotate(Ansi.cyan)) + ]) + ) - yield* Effect.logInfo(AnsiDoc.hsep([ - AnsiDoc.text("Take a look at the template's"), - AnsiDoc.text("README.md").pipe(AnsiDoc.annotate(Ansi.cyan)), - AnsiDoc.text("for more information") - ])) + yield* Effect.logInfo( + AnsiDoc.hsep([ + AnsiDoc.text("Take a look at the template's"), + AnsiDoc.text("README.md").pipe(AnsiDoc.annotate(Ansi.cyan)), + AnsiDoc.text("for more information") + ]) + ) const filesToCheck = [] if (config.projectType.withChangesets) { - filesToCheck.push(path.join(config.projectName, ".changeset", "config.json")) + filesToCheck.push( + path.join(config.projectName, ".changeset", "config.json") + ) } - if (config.projectType.template === "monorepo") { - filesToCheck.push(path.join(config.projectName, "packages", "cli", "package.json")) - filesToCheck.push(path.join(config.projectName, "packages", "domain", "package.json")) - filesToCheck.push(path.join(config.projectName, "packages", "server", "package.json")) - filesToCheck.push(path.join(config.projectName, "packages", "cli", "LICENSE")) - filesToCheck.push(path.join(config.projectName, "packages", "domain", "LICENSE")) - filesToCheck.push(path.join(config.projectName, "packages", "server", "LICENSE")) + // Heuristic: treat as monorepo when a top-level "packages" directory exists + const isMonorepo = yield* fs.exists(path.join(config.projectName, "packages")) + if (isMonorepo) { + filesToCheck.push( + path.join(config.projectName, "packages", "cli", "package.json") + ) + filesToCheck.push( + path.join(config.projectName, "packages", "domain", "package.json") + ) + filesToCheck.push( + path.join(config.projectName, "packages", "server", "package.json") + ) + filesToCheck.push( + path.join(config.projectName, "packages", "cli", "LICENSE") + ) + filesToCheck.push( + path.join(config.projectName, "packages", "domain", "LICENSE") + ) + filesToCheck.push( + path.join(config.projectName, "packages", "server", "LICENSE") + ) } else { filesToCheck.push(path.join(config.projectName, "package.json")) filesToCheck.push(path.join(config.projectName, "LICENSE")) } - yield* Effect.logInfo(AnsiDoc.cats([ - AnsiDoc.hsep([ - AnsiDoc.text("Make sure to replace any"), - AnsiDoc.text("").pipe(AnsiDoc.annotate(Ansi.cyan)), - AnsiDoc.text("entries in the following files:") - ]), - pipe( - filesToCheck, - Array.map((file) => AnsiDoc.catWithSpace(AnsiDoc.char("-"), AnsiDoc.text(file))), - AnsiDoc.vsep, - AnsiDoc.indent(2) - ) - ])) + yield* Effect.logInfo( + AnsiDoc.cats([ + AnsiDoc.hsep([ + AnsiDoc.text("Make sure to replace any"), + AnsiDoc.text("").pipe(AnsiDoc.annotate(Ansi.cyan)), + AnsiDoc.text("entries in the following files:") + ]), + pipe( + filesToCheck, + Array.map((file) => AnsiDoc.catWithSpace(AnsiDoc.char("-"), AnsiDoc.text(file))), + AnsiDoc.vsep, + AnsiDoc.indent(2) + ) + ]) + ) + + // Inline, template-specific post processing (no external scripts) + // Run Expo configuration if an Expo app is detected (by presence of app.json) + const hasAppJson = yield* fs.exists(path.join(config.projectName, "app.json")) + if (hasAppJson) yield* configureExpoApp(config.projectName) + + // Ensure any packaged `gitignore` files become `.gitignore` so Git recognizes them + yield* finalizeGitignoreFiles(config.projectName) }) } @@ -358,61 +549,303 @@ const getUserInput = Prompt.select<"example" | "template">({ description: "An example project demonstrating usage of Effect" } ] -}).pipe(Prompt.flatMap((type): Prompt.Prompt => { - switch (type) { - case "example": { - return Prompt.all({ - example: Prompt.select({ - message: "What project example should be used?", +}).pipe( + Prompt.flatMap((type): Prompt.Prompt => { + switch (type) { + case "example": { + return Prompt.all({ + example: Prompt.select({ + message: "What project example should be used?", + choices: [ + { + title: "HTTP Server", + value: "http-server", + description: "An HTTP server application with authentication / authorization" + } + ] + }) + }).pipe(Prompt.map(ProjectType.Example)) + } + case "template": { + // Ask for template source first + const sourcePrompt = Prompt.select<"built-in" | "folder" | "repo">({ + message: "Where should the template come from?", choices: [ - { - title: "HTTP Server", - value: "http-server", - description: "An HTTP server application with authentication / authorization" - } + { title: "Built-in (catalog)", value: "built-in" }, + { title: "Local folder", value: "folder" }, + { title: "GitHub repo", value: "repo" } ] }) - }).pipe(Prompt.map(ProjectType.Example)) + + return Prompt.flatMap(sourcePrompt, (source) => { + switch (source) { + case "built-in": + return Prompt.all({ + template: Prompt.select