├── .changeset ├── config.json └── mean-garlics-speak.md ├── .envrc ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── check.yml │ ├── nightly.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── eslint.config.mjs ├── examples └── http-server │ ├── .env.example │ ├── .envrc │ ├── .gitignore │ ├── .vscode │ └── settings.json │ ├── README.md │ ├── data │ └── .gitkeep │ ├── eslint.config.mjs │ ├── flake.lock │ ├── flake.nix │ ├── package.json │ ├── src │ ├── Accounts.ts │ ├── Accounts │ │ ├── AccountsRepo.ts │ │ ├── Api.ts │ │ ├── Http.ts │ │ ├── Policy.ts │ │ └── UsersRepo.ts │ ├── Api.ts │ ├── Domain │ │ ├── AccessToken.ts │ │ ├── Account.ts │ │ ├── Email.ts │ │ ├── Group.ts │ │ ├── Person.ts │ │ ├── Policy.ts │ │ └── User.ts │ ├── Groups.ts │ ├── Groups │ │ ├── Api.ts │ │ ├── Http.ts │ │ ├── Policy.ts │ │ └── Repo.ts │ ├── Http.ts │ ├── People.ts │ ├── People │ │ ├── Api.ts │ │ ├── Http.ts │ │ ├── Policy.ts │ │ └── Repo.ts │ ├── Sql.ts │ ├── Tracing.ts │ ├── Uuid.ts │ ├── client.ts │ ├── lib │ │ └── Layer.ts │ ├── main.ts │ └── migrations │ │ ├── 00001_create users.ts │ │ ├── 00002_create groups.ts │ │ └── 00003_create_people.ts │ ├── test │ └── Accounts.test.ts │ ├── tsconfig.base.json │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── flake.lock ├── flake.nix ├── package.json ├── packages └── create-effect-app │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ └── copy-package-json.ts │ ├── src │ ├── Cli.ts │ ├── Domain.ts │ ├── GitHub.ts │ ├── Logger.ts │ ├── Utils.ts │ ├── bin.ts │ └── internal │ │ ├── examples.ts │ │ ├── templates.ts │ │ └── version.ts │ ├── test │ └── dummy.test.ts │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── patches ├── @changesets__assemble-release-plan@6.0.3.patch └── @changesets__get-github-info@0.6.0.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── examples.mjs ├── examples.template.txt ├── templates.mjs ├── templates.template.txt ├── version.mjs └── version.template.txt ├── setupTests.ts ├── templates ├── basic │ ├── .changeset │ │ └── config.json │ ├── .envrc │ ├── .github │ │ ├── actions │ │ │ └── setup │ │ │ │ └── action.yml │ │ └── workflows │ │ │ ├── check.yml │ │ │ ├── release.yml │ │ │ └── snapshot.yml │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── flake.nix │ ├── package.json │ ├── patches │ │ ├── @changesets__get-github-info@0.6.0.patch │ │ └── babel-plugin-annotate-pure-calls@0.4.0.patch │ ├── scratchpad │ │ └── tsconfig.json │ ├── setupTests.ts │ ├── src │ │ └── Program.ts │ ├── test │ │ └── Dummy.test.ts │ ├── tsconfig.base.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── cli │ ├── .changeset │ │ └── config.json │ ├── .envrc │ ├── .github │ │ ├── actions │ │ │ └── setup │ │ │ │ └── action.yml │ │ └── workflows │ │ │ ├── check.yml │ │ │ ├── release.yml │ │ │ └── snapshot.yml │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── flake.nix │ ├── package.json │ ├── patches │ │ └── @changesets__get-github-info@0.6.0.patch │ ├── scripts │ │ └── copy-package-json.ts │ ├── src │ │ ├── Cli.ts │ │ └── bin.ts │ ├── test │ │ └── Dummy.test.ts │ ├── tsconfig.base.json │ ├── tsconfig.json │ ├── tsconfig.scripts.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ ├── tsup.config.ts │ └── vitest.config.ts └── monorepo │ ├── .changeset │ └── config.json │ ├── .envrc │ ├── .github │ ├── actions │ │ └── setup │ │ │ └── action.yml │ └── workflows │ │ ├── check.yml │ │ ├── release.yml │ │ └── snapshot.yml │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── flake.lock │ ├── flake.nix │ ├── package.json │ ├── packages │ ├── cli │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ │ ├── Cli.ts │ │ │ ├── TodosClient.ts │ │ │ └── bin.ts │ │ ├── test │ │ │ └── Dummy.test.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── tsconfig.src.json │ │ ├── tsconfig.test.json │ │ └── vitest.config.ts │ ├── domain │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ │ └── TodosApi.ts │ │ ├── test │ │ │ └── Dummy.test.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── tsconfig.src.json │ │ ├── tsconfig.test.json │ │ └── vitest.config.ts │ └── server │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ ├── Api.ts │ │ ├── TodosRepository.ts │ │ └── server.ts │ │ ├── test │ │ └── Dummy.test.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── tsconfig.src.json │ │ ├── tsconfig.test.json │ │ └── vitest.config.ts │ ├── patches │ ├── @changesets__assemble-release-plan@6.0.5.patch │ ├── @changesets__get-github-info@0.6.0.patch │ └── babel-plugin-annotate-pure-calls@0.4.0.patch │ ├── pnpm-workspace.yaml │ ├── scratchpad │ └── tsconfig.json │ ├── scripts │ └── clean.mjs │ ├── setupTests.ts │ ├── tsconfig.base.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vitest.shared.ts │ └── vitest.workspace.ts ├── tsconfig.base.json ├── tsconfig.json ├── vitest.shared.ts └── vitest.workspace.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "Effect-TS/examples" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/mean-garlics-speak.md: -------------------------------------------------------------------------------- 1 | --- 2 | "create-effect-app": patch 3 | --- 4 | 5 | Fixes a typo in the description of the `template` option of ⁠`create-effect-app`. 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Perform standard setup and install dependencies using pnpm. 3 | inputs: 4 | node-version: 5 | description: The version of Node.js to install 6 | required: true 7 | default: 20.14.0 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v3 14 | - name: Install node 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: pnpm 18 | node-version: ${{ inputs.node-version }} 19 | - name: Install dependencies 20 | shell: bash 21 | run: pnpm install -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | types: 18 | name: Types 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install dependencies 24 | uses: ./.github/actions/setup 25 | - run: pnpm check 26 | 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 10 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install dependencies 34 | uses: ./.github/actions/setup 35 | - run: pnpm lint 36 | 37 | test: 38 | name: Test 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | strategy: 42 | fail-fast: false 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Install dependencies 46 | uses: ./.github/actions/setup 47 | - run: pnpm vitest 48 | env: 49 | NODE_OPTIONS: --max_old_space_size=8192 50 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Checks 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | schedule: 10 | # Run checks nightly at midnight 11 | - cron: '0 0 * * *' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | permissions: {} 18 | 19 | jobs: 20 | prepare: 21 | name: Prepare Targets 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | outputs: 25 | examples: ${{ steps.targets.outputs.examples }} 26 | templates: ${{ steps.targets.outputs.templates }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Find all build targets 30 | id: targets 31 | run: | 32 | examples=$( 33 | find templates -mindepth 1 -maxdepth 1 -type d | 34 | jq -cnR '[inputs | select(length > 0)]' 35 | ) 36 | templates=$( 37 | find templates -mindepth 1 -maxdepth 1 -type d | 38 | jq -cnR '[inputs | select(length > 0)]' 39 | ) 40 | echo "examples=${examples}" >> $GITHUB_OUTPUT 41 | echo "templates=${templates}" >> $GITHUB_OUTPUT 42 | 43 | examples: 44 | name: Check Examples 45 | runs-on: ubuntu-latest 46 | timeout-minutes: 10 47 | needs: [prepare] 48 | strategy: 49 | matrix: 50 | example: ${{ fromJSON(needs.prepare.outputs.examples) }} 51 | fail-fast: true 52 | steps: 53 | - uses: actions/checkout@v4 54 | with: 55 | sparse-checkout: ${{ matrix.example }} 56 | sparse-checkout-cone-mode: false 57 | - name: Move files to root 58 | run: | 59 | ls -lah 60 | shopt -s dotglob 61 | mv ${{ matrix.example }}/* . 62 | rm -rf examples 63 | ls -lah 64 | - name: Install pnpm 65 | uses: pnpm/action-setup@v3 66 | - name: Install node 67 | uses: actions/setup-node@v4 68 | with: 69 | node-version: 20.14.0 70 | - name: Install dependencies 71 | run: pnpm install 72 | - run: pnpm lint 73 | - run: pnpm check 74 | - run: pnpm test 75 | 76 | templates: 77 | name: Check Templates 78 | runs-on: ubuntu-latest 79 | timeout-minutes: 10 80 | needs: [prepare] 81 | strategy: 82 | matrix: 83 | template: ${{ fromJSON(needs.prepare.outputs.templates) }} 84 | fail-fast: true 85 | steps: 86 | - uses: actions/checkout@v4 87 | with: 88 | sparse-checkout: ${{ matrix.template }} 89 | sparse-checkout-cone-mode: false 90 | - name: Move files to root 91 | run: | 92 | ls -lah 93 | shopt -s dotglob 94 | mv ${{ matrix.template }}/* . 95 | rm -rf templates 96 | ls -lah 97 | - name: Install pnpm 98 | uses: pnpm/action-setup@v3 99 | - name: Install node 100 | uses: actions/setup-node@v4 101 | with: 102 | node-version: 20.14.0 103 | - name: Install dependencies 104 | run: pnpm install 105 | - run: pnpm lint 106 | - run: pnpm check 107 | - run: pnpm build 108 | - run: pnpm test 109 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | release: 14 | if: github.repository_owner == 'Effect-Ts' 15 | name: Release 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | permissions: 19 | contents: write 20 | id-token: write 21 | pull-requests: write 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install dependencies 25 | uses: ./.github/actions/setup 26 | - name: Create Release Pull Request or Publish 27 | uses: changesets/action@v1 28 | with: 29 | version: pnpm changeset-version 30 | publish: pnpm changeset-publish 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | snapshot: 11 | name: Snapshot 12 | if: github.repository_owner == 'Effect-Ts' 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install dependencies 18 | uses: ./.github/actions/setup 19 | - name: Build package 20 | run: pnpm build 21 | - name: Create snapshot 22 | id: snapshot 23 | run: pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off ./packages/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .direnv 3 | build/ 4 | dist/ 5 | node_modules/ 6 | scratchpad/* 7 | !scratchpad/tsconfig.json 8 | examples/*/pnpm-lock.yaml 9 | *.tsbuildinfo -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "editor.formatOnSave": true, 5 | "eslint.format.enable": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Nightly Build](https://github.com/Effect-TS/examples/workflows/Nightly%20Checks/badge.svg)](https://github.com/Effect-TS/examples/actions) 2 | 3 | # Effect Examples 4 | 5 | ## Create Effect App 6 | 7 | The easiest way to get started with Effect is by using `create-effect-app`. 8 | 9 | This CLI tool enables you to quickly bootstrap a project with Effect, with everything pre-configured for you. 10 | 11 | You can create a new project using one of our [project templates](./templates) or by using one of the [official Effect examples](./examples). 12 | 13 | See [the documentation](./packages/create-effect-app/README.md) for more information. 14 | 15 | ## Examples 16 | 17 | This repository contains examples which can be used to understand how to use Effect. You can also clone an example to your local machine via the `create-effect-app` CLI tool. 18 | 19 | The available examples include: 20 | 21 | |Name|Description| 22 | |----|----| 23 | |`http-server`| An HTTP server built with Effect complete with authentication and authorization. | 24 | 25 | ## Templates 26 | 27 | This repository contains templates which can be used to quickly bootstrap a new project with Effect via the `create-effect-app` CLI tool. 28 | 29 | These templates were developed to mirror the project configuration recommneded by the Effect core team and are thus somewhat opinionated. 30 | 31 | ### Basic 32 | 33 | The `basic` template is meant to serve as the foundation for building a single package or library with Effect. 34 | 35 | The template features: 36 | 37 | - Pre-configured build pipeline which supports both ESM and CJS 38 | - Pre-configured test pipeline via `vitest` 39 | - Pre-configured TypeScript configuration 40 | - ESLint & Dprint for linting and formatting, respectively (optional) 41 | - Nix to provide a consistent development shell (optional) 42 | - Changesets for version management and publication (optional) 43 | - The Effect team's recommended GitHub Actions (optional) 44 | 45 | For more information, see the template [README](./templates/basic/README.md). 46 | 47 | ### Monorepo 48 | 49 | The `monorepo` template is meant to serve as the foundation for building multiple packages or applications with Effect. 50 | 51 | The template features everything included with the `basic` template in addition to: 52 | 53 | - Pre-configured TypeScript path aliases and project references to support package interdependencies 54 | 55 | For more information, see the template [README](./templates/monorepo/README.md). 56 | 57 | ### CLI 58 | 59 | The `cli` template is meant to serve as the foundation for building a command-line application with [Effect CLI](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md). 60 | 61 | The template features everything included with the `basic` template, except with a different build pipeline: 62 | 63 | - Pre-configured build pipeline is via [`tsup`](https://github.com/egoist/tsup) to support bundling to a single file 64 | 65 | For more information, see the template [README](./templates/cli/README.md). 66 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": ["error", { 65 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 66 | message: "Do not use spread arguments in Array.push" 67 | }], 68 | 69 | "no-unused-vars": "off", 70 | "prefer-rest-params": "off", 71 | "prefer-spread": "off", 72 | "import/first": "error", 73 | "import/newline-after-import": "error", 74 | "import/no-duplicates": "error", 75 | "import/no-unresolved": "off", 76 | "import/order": "off", 77 | "simple-import-sort/imports": "off", 78 | "sort-destructure-keys/sort-destructure-keys": "error", 79 | "deprecation/deprecation": "off", 80 | 81 | "@typescript-eslint/array-type": ["warn", { 82 | default: "generic", 83 | readonly: "generic" 84 | }], 85 | 86 | "@typescript-eslint/member-delimiter-style": 0, 87 | "@typescript-eslint/no-non-null-assertion": "off", 88 | "@typescript-eslint/ban-types": "off", 89 | "@typescript-eslint/no-explicit-any": "off", 90 | "@typescript-eslint/no-empty-interface": "off", 91 | "@typescript-eslint/consistent-type-imports": "warn", 92 | 93 | "@typescript-eslint/no-unused-vars": ["error", { 94 | argsIgnorePattern: "^_", 95 | varsIgnorePattern: "^_" 96 | }], 97 | 98 | "@typescript-eslint/ban-ts-comment": "off", 99 | "@typescript-eslint/camelcase": "off", 100 | "@typescript-eslint/explicit-function-return-type": "off", 101 | "@typescript-eslint/explicit-module-boundary-types": "off", 102 | "@typescript-eslint/interface-name-prefix": "off", 103 | "@typescript-eslint/no-array-constructor": "off", 104 | "@typescript-eslint/no-use-before-define": "off", 105 | "@typescript-eslint/no-namespace": "off", 106 | 107 | "@effect/dprint": ["error", { 108 | config: { 109 | indentWidth: 2, 110 | lineWidth: 120, 111 | semiColons: "asi", 112 | quoteStyle: "alwaysDouble", 113 | trailingCommas: "never", 114 | operatorPosition: "maintain", 115 | "arrowFunction.useParentheses": "force" 116 | } 117 | }] 118 | } 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /examples/http-server/.env.example: -------------------------------------------------------------------------------- 1 | # HONEYCOMB_API_KEY= 2 | # HONEYCOMB_DATASET= 3 | # OTEL_EXPORTER_OTLP_ENDPOINT= 4 | -------------------------------------------------------------------------------- /examples/http-server/.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /examples/http-server/.gitignore: -------------------------------------------------------------------------------- 1 | .direnv/ 2 | node_modules/ 3 | dist/ 4 | /data/* 5 | !.gitkeep 6 | .env 7 | *.tsbuildinfo 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /examples/http-server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /examples/http-server/README.md: -------------------------------------------------------------------------------- 1 | # Effect HTTP Server Example 2 | 3 | ## Getting Started 4 | 5 | Make sure to create a `.env` file in the root of the repository based on the `.env.example` file. 6 | -------------------------------------------------------------------------------- /examples/http-server/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Effect-TS/examples/d88d8030518b9283bba9940b3e7b77c92475a0e3/examples/http-server/data/.gitkeep -------------------------------------------------------------------------------- /examples/http-server/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": ["error", { 65 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 66 | message: "Do not use spread arguments in Array.push" 67 | }], 68 | 69 | "no-unused-vars": "off", 70 | "prefer-rest-params": "off", 71 | "prefer-spread": "off", 72 | "import/first": "error", 73 | "import/newline-after-import": "error", 74 | "import/no-duplicates": "error", 75 | "import/no-unresolved": "off", 76 | "import/order": "off", 77 | "simple-import-sort/imports": "off", 78 | "sort-destructure-keys/sort-destructure-keys": "error", 79 | "deprecation/deprecation": "off", 80 | 81 | "@typescript-eslint/array-type": ["warn", { 82 | default: "generic", 83 | readonly: "generic" 84 | }], 85 | 86 | "@typescript-eslint/member-delimiter-style": 0, 87 | "@typescript-eslint/no-non-null-assertion": "off", 88 | "@typescript-eslint/ban-types": "off", 89 | "@typescript-eslint/no-explicit-any": "off", 90 | "@typescript-eslint/no-empty-interface": "off", 91 | "@typescript-eslint/consistent-type-imports": "warn", 92 | 93 | "@typescript-eslint/no-unused-vars": ["error", { 94 | argsIgnorePattern: "^_", 95 | varsIgnorePattern: "^_" 96 | }], 97 | 98 | "@typescript-eslint/ban-ts-comment": "off", 99 | "@typescript-eslint/camelcase": "off", 100 | "@typescript-eslint/explicit-function-return-type": "off", 101 | "@typescript-eslint/explicit-module-boundary-types": "off", 102 | "@typescript-eslint/interface-name-prefix": "off", 103 | "@typescript-eslint/no-array-constructor": "off", 104 | "@typescript-eslint/no-use-before-define": "off", 105 | "@typescript-eslint/no-namespace": "off", 106 | 107 | "@effect/dprint": ["error", { 108 | config: { 109 | indentWidth: 2, 110 | lineWidth: 120, 111 | semiColons: "asi", 112 | quoteStyle: "alwaysDouble", 113 | trailingCommas: "never", 114 | operatorPosition: "maintain", 115 | "arrowFunction.useParentheses": "force" 116 | } 117 | }] 118 | } 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /examples/http-server/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1727826117, 9 | "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1730272153, 24 | "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1727825735, 40 | "narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=", 41 | "type": "tarball", 42 | "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz" 43 | }, 44 | "original": { 45 | "type": "tarball", 46 | "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz" 47 | } 48 | }, 49 | "process-compose-flake": { 50 | "locked": { 51 | "lastModified": 1729544733, 52 | "narHash": "sha256-ir7WTVpG999N07wkOCs1kwZsQKitOv3CNDqNalCMK3c=", 53 | "owner": "Platonic-Systems", 54 | "repo": "process-compose-flake", 55 | "rev": "7241e6b247b65cfa579d6f838a8efc18e5401924", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "Platonic-Systems", 60 | "repo": "process-compose-flake", 61 | "type": "github" 62 | } 63 | }, 64 | "root": { 65 | "inputs": { 66 | "flake-parts": "flake-parts", 67 | "nixpkgs": "nixpkgs", 68 | "process-compose-flake": "process-compose-flake", 69 | "services-flake": "services-flake" 70 | } 71 | }, 72 | "services-flake": { 73 | "locked": { 74 | "lastModified": 1729988886, 75 | "narHash": "sha256-e3iGQvR89XYmEyGQ4s3TPBa9xdeKziGkxgbZqIauGT0=", 76 | "owner": "juspay", 77 | "repo": "services-flake", 78 | "rev": "deaa2b81d07d2e7ec2ee74e91e17020d31bf3cb7", 79 | "type": "github" 80 | }, 81 | "original": { 82 | "owner": "juspay", 83 | "repo": "services-flake", 84 | "type": "github" 85 | } 86 | } 87 | }, 88 | "root": "root", 89 | "version": 7 90 | } 91 | -------------------------------------------------------------------------------- /examples/http-server/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; 6 | services-flake.url = "github:juspay/services-flake"; 7 | }; 8 | outputs = inputs @ {flake-parts, ...}: 9 | flake-parts.lib.mkFlake {inherit inputs;} { 10 | systems = inputs.nixpkgs.lib.systems.flakeExposed; 11 | imports = [ 12 | inputs.process-compose-flake.flakeModule 13 | ]; 14 | perSystem = {pkgs, ...}: { 15 | devShells.default = pkgs.mkShell { 16 | nativeBuildInputs = with pkgs; [ 17 | corepack 18 | nodejs_22 19 | ]; 20 | }; 21 | 22 | process-compose."default" = {config, ...}: { 23 | imports = [ 24 | inputs.services-flake.processComposeModules.default 25 | ]; 26 | 27 | services.tempo.tempo.enable = true; 28 | services.grafana.grafana = { 29 | enable = true; 30 | http_port = 4000; 31 | extraConf = { 32 | "auth.anonymous" = { 33 | enabled = true; 34 | org_role = "Editor"; 35 | }; 36 | }; 37 | datasources = with config.services.tempo.tempo; [ 38 | { 39 | name = "Tempo"; 40 | type = "tempo"; 41 | access = "proxy"; 42 | url = "http://${httpAddress}:${builtins.toString httpPort}"; 43 | } 44 | ]; 45 | }; 46 | services.redis.redis.enable = true; 47 | settings.processes.tsx = { 48 | command = "pnpm dev"; 49 | }; 50 | }; 51 | }; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /examples/http-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-server", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "", 6 | "scripts": { 7 | "check": "tsc -b tsconfig.json", 8 | "dev": "tsx --env-file=.env --watch src/main.ts", 9 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 10 | "lint-fix": "pnpm lint --fix", 11 | "test": "vitest" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "packageManager": "pnpm@9.10.0", 16 | "devDependencies": { 17 | "@effect/language-service": "^0.2.0", 18 | "@effect/vitest": "^0.13.7", 19 | "@types/node": "^22.8.5", 20 | "@types/uuid": "^10.0.0", 21 | "prettier": "^3.3.3", 22 | "tsup": "^8.3.5", 23 | "tsx": "^4.19.2", 24 | "typescript": "^5.6.3", 25 | "vitest": "^2.1.4" 26 | }, 27 | "dependencies": { 28 | "@effect/experimental": "^0.30.13", 29 | "@effect/opentelemetry": "^0.39.7", 30 | "@effect/platform": "^0.69.12", 31 | "@effect/platform-node": "^0.64.13", 32 | "@effect/sql": "^0.18.13", 33 | "@effect/sql-sqlite-node": "^0.19.2", 34 | "@eslint/compat": "1.1.1", 35 | "@eslint/eslintrc": "3.1.0", 36 | "@eslint/js": "9.10.0", 37 | "@opentelemetry/exporter-trace-otlp-http": "^0.54.0", 38 | "@opentelemetry/sdk-trace-base": "^1.27.0", 39 | "@opentelemetry/sdk-trace-node": "^1.27.0", 40 | "@typescript-eslint/eslint-plugin": "^8.4.0", 41 | "@typescript-eslint/parser": "^8.4.0", 42 | "effect": "^3.10.7", 43 | "eslint": "^9.10.0", 44 | "eslint-import-resolver-typescript": "^3.6.3", 45 | "eslint-plugin-codegen": "^0.28.0", 46 | "eslint-plugin-import": "^2.30.0", 47 | "eslint-plugin-simple-import-sort": "^12.1.1", 48 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 49 | "uuid": "^11.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect, Layer, Option, pipe } from "effect" 3 | import { AccountsRepo } from "./Accounts/AccountsRepo.js" 4 | import { UsersRepo } from "./Accounts/UsersRepo.js" 5 | import type { AccessToken } from "./Domain/AccessToken.js" 6 | import { accessTokenFromString } from "./Domain/AccessToken.js" 7 | import { Account } from "./Domain/Account.js" 8 | import { policyRequire } from "./Domain/Policy.js" 9 | import type { UserId } from "./Domain/User.js" 10 | import { User, UserNotFound, UserWithSensitive } from "./Domain/User.js" 11 | import { SqlLive, SqlTest } from "./Sql.js" 12 | import { Uuid } from "./Uuid.js" 13 | 14 | export class Accounts extends Effect.Service()("Accounts", { 15 | effect: Effect.gen(function*() { 16 | const sql = yield* SqlClient.SqlClient 17 | const accountRepo = yield* AccountsRepo 18 | const userRepo = yield* UsersRepo 19 | const uuid = yield* Uuid 20 | 21 | const createUser = (user: typeof User.jsonCreate.Type) => 22 | accountRepo.insert(Account.insert.make({})).pipe( 23 | Effect.tap((account) => Effect.annotateCurrentSpan("account", account)), 24 | Effect.bindTo("account"), 25 | Effect.bind("accessToken", () => uuid.generate.pipe(Effect.map(accessTokenFromString))), 26 | Effect.bind("user", ({ accessToken, account }) => 27 | userRepo.insert( 28 | User.insert.make({ 29 | ...user, 30 | accountId: account.id, 31 | accessToken 32 | }) 33 | )), 34 | Effect.map( 35 | ({ account, user }) => 36 | new UserWithSensitive({ 37 | ...user, 38 | account 39 | }) 40 | ), 41 | sql.withTransaction, 42 | Effect.orDie, 43 | Effect.withSpan("Accounts.createUser", { attributes: { user } }), 44 | policyRequire("User", "create") 45 | ) 46 | 47 | const updateUser = ( 48 | id: UserId, 49 | user: Partial 50 | ) => 51 | userRepo.findById(id).pipe( 52 | Effect.flatMap( 53 | Option.match({ 54 | onNone: () => new UserNotFound({ id }), 55 | onSome: Effect.succeed 56 | }) 57 | ), 58 | Effect.andThen((previous) => 59 | userRepo.update({ 60 | ...previous, 61 | ...user, 62 | id, 63 | updatedAt: undefined 64 | }) 65 | ), 66 | sql.withTransaction, 67 | Effect.catchTag("SqlError", (err) => Effect.die(err)), 68 | Effect.withSpan("Accounts.updateUser", { attributes: { id, user } }), 69 | policyRequire("User", "update") 70 | ) 71 | 72 | const findUserByAccessToken = (apiKey: AccessToken) => 73 | pipe( 74 | userRepo.findByAccessToken(apiKey), 75 | Effect.withSpan("Accounts.findUserByAccessToken"), 76 | policyRequire("User", "read") 77 | ) 78 | 79 | const findUserById = (id: UserId) => 80 | pipe( 81 | userRepo.findById(id), 82 | Effect.withSpan("Accounts.findUserById", { 83 | attributes: { id } 84 | }), 85 | policyRequire("User", "read") 86 | ) 87 | 88 | const embellishUser = (user: User) => 89 | pipe( 90 | accountRepo.findById(user.accountId), 91 | Effect.flatten, 92 | Effect.map((account) => new UserWithSensitive({ ...user, account })), 93 | Effect.orDie, 94 | Effect.withSpan("Accounts.embellishUser", { 95 | attributes: { id: user.id } 96 | }), 97 | policyRequire("User", "readSensitive") 98 | ) 99 | 100 | return { 101 | createUser, 102 | updateUser, 103 | findUserByAccessToken, 104 | findUserById, 105 | embellishUser 106 | } as const 107 | }), 108 | dependencies: [ 109 | SqlLive, 110 | AccountsRepo.Default, 111 | UsersRepo.Default, 112 | Uuid.Default 113 | ] 114 | }) { 115 | static Test = this.DefaultWithoutDependencies.pipe( 116 | Layer.provideMerge(SqlTest), 117 | Layer.provideMerge(Uuid.Test) 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts/AccountsRepo.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@effect/sql" 2 | import { Effect } from "effect" 3 | import { Account } from "../Domain/Account.js" 4 | import { makeTestLayer } from "../lib/Layer.js" 5 | import { SqlLive } from "../Sql.js" 6 | 7 | export const make = Model.makeRepository(Account, { 8 | tableName: "accounts", 9 | spanPrefix: "AccountsRepo", 10 | idColumn: "id" 11 | }) 12 | 13 | export class AccountsRepo extends Effect.Service()( 14 | "Accounts/AccountsRepo", 15 | { 16 | effect: Model.makeRepository(Account, { 17 | tableName: "accounts", 18 | spanPrefix: "AccountsRepo", 19 | idColumn: "id" 20 | }), 21 | dependencies: [SqlLive] 22 | } 23 | ) { 24 | static Test = makeTestLayer(AccountsRepo)({}) 25 | } 26 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts/Api.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity, OpenApi } from "@effect/platform" 2 | import { Schema } from "effect" 3 | import { Unauthorized } from "../Domain/Policy.js" 4 | import { CurrentUser, User, UserIdFromString, UserNotFound, UserWithSensitive } from "../Domain/User.js" 5 | 6 | export class Authentication extends HttpApiMiddleware.Tag()( 7 | "Accounts/Api/Authentication", 8 | { 9 | provides: CurrentUser, 10 | failure: Unauthorized, 11 | security: { 12 | cookie: HttpApiSecurity.apiKey({ 13 | in: "cookie", 14 | key: "token" 15 | }) 16 | } 17 | } 18 | ) {} 19 | 20 | export class AccountsApi extends HttpApiGroup.make("accounts") 21 | .add( 22 | HttpApiEndpoint.patch("updateUser", "/users/:id") 23 | .setPath(Schema.Struct({ id: UserIdFromString })) 24 | .addSuccess(User.json) 25 | .addError(UserNotFound) 26 | .setPayload(Schema.partialWith(User.jsonUpdate, { exact: true })) 27 | ) 28 | .add( 29 | HttpApiEndpoint.get("getUserMe", "/users/me").addSuccess( 30 | UserWithSensitive.json 31 | ) 32 | ) 33 | .add( 34 | HttpApiEndpoint.get("getUser", "/users/:id") 35 | .setPath(Schema.Struct({ id: UserIdFromString })) 36 | .addSuccess(User.json) 37 | .addError(UserNotFound) 38 | ) 39 | .middlewareEndpoints(Authentication) 40 | // unauthenticated 41 | .add( 42 | HttpApiEndpoint.post("createUser", "/users") 43 | .addSuccess(UserWithSensitive.json) 44 | .setPayload(User.jsonCreate) 45 | ) 46 | .annotate(OpenApi.Title, "Accounts") 47 | .annotate(OpenApi.Description, "Manage user accounts") 48 | {} 49 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts/Http.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder } from "@effect/platform" 2 | import { Effect, Layer, Option, pipe } from "effect" 3 | import { Accounts } from "../Accounts.js" 4 | import { Api } from "../Api.js" 5 | import { accessTokenFromRedacted } from "../Domain/AccessToken.js" 6 | import { policyUse, Unauthorized, withSystemActor } from "../Domain/Policy.js" 7 | import { CurrentUser, UserId, UserNotFound } from "../Domain/User.js" 8 | import { Authentication } from "./Api.js" 9 | import { AccountsPolicy } from "./Policy.js" 10 | import { UsersRepo } from "./UsersRepo.js" 11 | 12 | export const AuthenticationLive = Layer.effect( 13 | Authentication, 14 | Effect.gen(function*() { 15 | const userRepo = yield* UsersRepo 16 | 17 | return Authentication.of({ 18 | cookie: (token) => 19 | userRepo.findByAccessToken(accessTokenFromRedacted(token)).pipe( 20 | Effect.flatMap( 21 | Option.match({ 22 | onNone: () => 23 | new Unauthorized({ 24 | actorId: UserId.make(-1), 25 | entity: "User", 26 | action: "read" 27 | }), 28 | onSome: Effect.succeed 29 | }) 30 | ), 31 | Effect.withSpan("Authentication.cookie") 32 | ) 33 | }) 34 | }) 35 | ).pipe(Layer.provide(UsersRepo.Default)) 36 | 37 | export const HttpAccountsLive = HttpApiBuilder.group( 38 | Api, 39 | "accounts", 40 | (handlers) => 41 | Effect.gen(function*() { 42 | const accounts = yield* Accounts 43 | const policy = yield* AccountsPolicy 44 | 45 | return handlers 46 | .handle("updateUser", ({ path, payload }) => 47 | pipe( 48 | accounts.updateUser(path.id, payload), 49 | policyUse(policy.canUpdate(path.id)) 50 | )) 51 | .handle("getUserMe", () => 52 | CurrentUser.pipe( 53 | Effect.flatMap(accounts.embellishUser), 54 | withSystemActor 55 | )) 56 | .handle("getUser", ({ path }) => 57 | pipe( 58 | accounts.findUserById(path.id), 59 | Effect.flatMap( 60 | Option.match({ 61 | onNone: () => new UserNotFound({ id: path.id }), 62 | onSome: Effect.succeed 63 | }) 64 | ), 65 | policyUse(policy.canRead(path.id)) 66 | )) 67 | .handle("createUser", ({ payload }) => 68 | accounts.createUser(payload).pipe( 69 | withSystemActor, 70 | Effect.tap((user) => 71 | HttpApiBuilder.securitySetCookie( 72 | Authentication.security.cookie, 73 | user.accessToken 74 | ) 75 | ) 76 | )) 77 | }) 78 | ).pipe( 79 | Layer.provide([Accounts.Default, AccountsPolicy.Default, AuthenticationLive]) 80 | ) 81 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts/Policy.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "effect" 2 | import { policy } from "../Domain/Policy.js" 3 | import type { UserId } from "../Domain/User.js" 4 | 5 | export class AccountsPolicy extends Effect.Service()( 6 | "Accounts/Policy", 7 | { 8 | // eslint-disable-next-line require-yield 9 | effect: Effect.gen(function*() { 10 | const canUpdate = (toUpdate: UserId) => policy("User", "update", (actor) => Effect.succeed(actor.id === toUpdate)) 11 | 12 | const canRead = (toRead: UserId) => policy("User", "read", (actor) => Effect.succeed(actor.id === toRead)) 13 | 14 | const canReadSensitive = (toRead: UserId) => 15 | policy("User", "readSensitive", (actor) => Effect.succeed(actor.id === toRead)) 16 | 17 | return { canUpdate, canRead, canReadSensitive } as const 18 | }) 19 | } 20 | ) {} 21 | -------------------------------------------------------------------------------- /examples/http-server/src/Accounts/UsersRepo.ts: -------------------------------------------------------------------------------- 1 | import { Model, SqlClient, SqlSchema } from "@effect/sql" 2 | import { Effect, pipe } from "effect" 3 | import { AccessToken } from "../Domain/AccessToken.js" 4 | import { User } from "../Domain/User.js" 5 | import { makeTestLayer } from "../lib/Layer.js" 6 | import { SqlLive } from "../Sql.js" 7 | 8 | export class UsersRepo extends Effect.Service()( 9 | "Accounts/UsersRepo", 10 | { 11 | effect: Effect.gen(function*() { 12 | const sql = yield* SqlClient.SqlClient 13 | const repo = yield* Model.makeRepository(User, { 14 | tableName: "users", 15 | spanPrefix: "UsersRepo", 16 | idColumn: "id" 17 | }) 18 | 19 | const findByAccessTokenSchema = SqlSchema.findOne({ 20 | Request: AccessToken, 21 | Result: User, 22 | execute: (key) => sql`select * from users where accessToken = ${key}` 23 | }) 24 | const findByAccessToken = (apiKey: AccessToken) => 25 | pipe( 26 | findByAccessTokenSchema(apiKey), 27 | Effect.orDie, 28 | Effect.withSpan("UsersRepo.findByAccessToken") 29 | ) 30 | 31 | return { ...repo, findByAccessToken } as const 32 | }), 33 | dependencies: [SqlLive] 34 | } 35 | ) { 36 | static Test = makeTestLayer(UsersRepo)({}) 37 | } 38 | -------------------------------------------------------------------------------- /examples/http-server/src/Api.ts: -------------------------------------------------------------------------------- 1 | import { HttpApi, OpenApi } from "@effect/platform" 2 | import { AccountsApi } from "./Accounts/Api.js" 3 | import { GroupsApi } from "./Groups/Api.js" 4 | import { PeopleApi } from "./People/Api.js" 5 | 6 | export class Api extends HttpApi.empty 7 | .add(AccountsApi) 8 | .add(GroupsApi) 9 | .add(PeopleApi) 10 | .annotate(OpenApi.Title, "Groups API") 11 | {} 12 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/AccessToken.ts: -------------------------------------------------------------------------------- 1 | import { Redacted, Schema } from "effect" 2 | 3 | export const AccessTokenString = Schema.String.pipe(Schema.brand("AccessToken")) 4 | export const AccessToken = Schema.Redacted(AccessTokenString) 5 | export type AccessToken = typeof AccessToken.Type 6 | 7 | export const accessTokenFromString = (token: string): AccessToken => Redacted.make(AccessTokenString.make(token)) 8 | 9 | export const accessTokenFromRedacted = ( 10 | token: Redacted.Redacted 11 | ): AccessToken => token as AccessToken 12 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/Account.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@effect/sql" 2 | import { Schema } from "effect" 3 | 4 | export const AccountId = Schema.Number.pipe(Schema.brand("AccountId")) 5 | export type AccountId = typeof AccountId.Type 6 | 7 | export class Account extends Model.Class("Account")({ 8 | id: Model.Generated(AccountId), 9 | createdAt: Model.DateTimeInsert, 10 | updatedAt: Model.DateTimeUpdate 11 | }) {} 12 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/Email.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "effect" 2 | 3 | export const Email = Schema.String.pipe( 4 | Schema.pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/), 5 | Schema.annotations({ 6 | title: "Email", 7 | description: "An email address" 8 | }), 9 | Schema.brand("Email"), 10 | Schema.annotations({ title: "Email" }) 11 | ) 12 | 13 | export type Email = typeof Email.Type 14 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/Group.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiSchema } from "@effect/platform" 2 | import { Model } from "@effect/sql" 3 | import { Schema } from "effect" 4 | import { AccountId } from "./Account.js" 5 | 6 | export const GroupId = Schema.Number.pipe(Schema.brand("GroupId")) 7 | export type GroupId = typeof GroupId.Type 8 | 9 | export const GroupIdFromString = Schema.NumberFromString.pipe( 10 | Schema.compose(GroupId) 11 | ) 12 | 13 | export class Group extends Model.Class("Group")({ 14 | id: Model.Generated(GroupId), 15 | ownerId: Model.GeneratedByApp(AccountId), 16 | name: Schema.NonEmptyTrimmedString, 17 | createdAt: Model.DateTimeInsert, 18 | updatedAt: Model.DateTimeUpdate 19 | }) {} 20 | 21 | export class GroupNotFound extends Schema.TaggedError()( 22 | "GroupNotFound", 23 | { id: GroupId }, 24 | HttpApiSchema.annotations({ status: 404 }) 25 | ) {} 26 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/Person.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@effect/sql" 2 | import { Schema } from "effect" 3 | import { GroupId } from "./Group.js" 4 | 5 | export const PersonId = Schema.Number.pipe(Schema.brand("PersonId")) 6 | export type PersonId = typeof PersonId.Type 7 | 8 | export const PersonIdFromString = Schema.NumberFromString.pipe( 9 | Schema.compose(PersonId) 10 | ) 11 | 12 | export class Person extends Model.Class("Person")({ 13 | id: Model.Generated(PersonId), 14 | groupId: Model.GeneratedByApp(GroupId), 15 | firstName: Schema.NonEmptyTrimmedString, 16 | lastName: Schema.NonEmptyTrimmedString, 17 | dateOfBirth: Model.FieldOption(Model.Date), 18 | createdAt: Model.DateTimeInsert, 19 | updatedAt: Model.DateTimeUpdate 20 | }) {} 21 | 22 | export class PersonNotFound extends Schema.TaggedError()( 23 | "PersonNotFound", 24 | { 25 | id: PersonId 26 | } 27 | ) {} 28 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/Policy.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiSchema } from "@effect/platform" 2 | import { Effect, Predicate, Schema } from "effect" 3 | import type { User } from "../Domain/User.js" 4 | import { CurrentUser, UserId } from "../Domain/User.js" 5 | 6 | export class Unauthorized extends Schema.TaggedError()( 7 | "Unauthorized", 8 | { 9 | actorId: UserId, 10 | entity: Schema.String, 11 | action: Schema.String 12 | }, 13 | HttpApiSchema.annotations({ status: 403 }) 14 | ) { 15 | get message() { 16 | return `Actor (${this.actorId}) is not authorized to perform action "${this.action}" on entity "${this.entity}"` 17 | } 18 | 19 | static is(u: unknown): u is Unauthorized { 20 | return Predicate.isTagged(u, "Unauthorized") 21 | } 22 | 23 | static refail(entity: string, action: string) { 24 | return ( 25 | effect: Effect.Effect 26 | ): Effect.Effect => 27 | Effect.catchIf( 28 | effect, 29 | (e) => !Unauthorized.is(e), 30 | () => 31 | Effect.flatMap( 32 | CurrentUser, 33 | (actor) => 34 | new Unauthorized({ 35 | actorId: actor.id, 36 | entity, 37 | action 38 | }) 39 | ) 40 | ) as any 41 | } 42 | } 43 | 44 | export const TypeId: unique symbol = Symbol.for("Domain/Policy/AuthorizedActor") 45 | export type TypeId = typeof TypeId 46 | 47 | export interface AuthorizedActor extends User { 48 | readonly [TypeId]: { 49 | readonly _Entity: Entity 50 | readonly _Action: Action 51 | } 52 | } 53 | 54 | export const authorizedActor = (user: User): AuthorizedActor => user as any 55 | 56 | export const policy = ( 57 | entity: Entity, 58 | action: Action, 59 | f: (actor: User) => Effect.Effect 60 | ): Effect.Effect< 61 | AuthorizedActor, 62 | E | Unauthorized, 63 | R | CurrentUser 64 | > => 65 | Effect.flatMap(CurrentUser, (actor) => 66 | Effect.flatMap(f(actor), (can) => 67 | can 68 | ? Effect.succeed(authorizedActor(actor)) 69 | : Effect.fail( 70 | new Unauthorized({ 71 | actorId: actor.id, 72 | entity, 73 | action 74 | }) 75 | ))) 76 | 77 | export const policyCompose = , E, R>( 78 | that: Effect.Effect 79 | ) => 80 | , E2, R2>( 81 | self: Effect.Effect 82 | ): Effect.Effect => Effect.zipRight(self, that) as any 83 | 84 | export const policyUse = , E, R>( 85 | policy: Effect.Effect 86 | ) => 87 | ( 88 | effect: Effect.Effect 89 | ): Effect.Effect | R> => policy.pipe(Effect.zipRight(effect)) as any 90 | 91 | export const policyRequire = ( 92 | _entity: Entity, 93 | _action: Action 94 | ) => 95 | ( 96 | effect: Effect.Effect 97 | ): Effect.Effect> => effect 98 | 99 | export const withSystemActor = ( 100 | effect: Effect.Effect 101 | ): Effect.Effect>> => effect as any 102 | -------------------------------------------------------------------------------- /examples/http-server/src/Domain/User.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiSchema } from "@effect/platform" 2 | import { Model } from "@effect/sql" 3 | import { Context, Schema } from "effect" 4 | import { AccessToken } from "./AccessToken.js" 5 | import { Account, AccountId } from "./Account.js" 6 | import { Email } from "./Email.js" 7 | 8 | export const UserId = Schema.Number.pipe(Schema.brand("UserId")) 9 | export type UserId = typeof UserId.Type 10 | 11 | export const UserIdFromString = Schema.NumberFromString.pipe( 12 | Schema.compose(UserId) 13 | ) 14 | 15 | export class User extends Model.Class("User")({ 16 | id: Model.Generated(UserId), 17 | accountId: Model.GeneratedByApp(AccountId), 18 | email: Email, 19 | accessToken: Model.Sensitive(AccessToken), 20 | createdAt: Model.DateTimeInsert, 21 | updatedAt: Model.DateTimeUpdate 22 | }) {} 23 | 24 | export class UserWithSensitive extends Model.Class( 25 | "UserWithSensitive" 26 | )({ 27 | ...Model.fields(User), 28 | accessToken: AccessToken, 29 | account: Account 30 | }) {} 31 | 32 | export class CurrentUser extends Context.Tag("Domain/User/CurrentUser")< 33 | CurrentUser, 34 | User 35 | >() {} 36 | 37 | export class UserNotFound extends Schema.TaggedError()( 38 | "UserNotFound", 39 | { id: UserId }, 40 | HttpApiSchema.annotations({ status: 404 }) 41 | ) {} 42 | -------------------------------------------------------------------------------- /examples/http-server/src/Groups.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect, Option, pipe } from "effect" 3 | import type { AccountId } from "./Domain/Account.js" 4 | import type { GroupId } from "./Domain/Group.js" 5 | import { Group, GroupNotFound } from "./Domain/Group.js" 6 | import { policyRequire } from "./Domain/Policy.js" 7 | import { GroupsRepo } from "./Groups/Repo.js" 8 | import { SqlLive } from "./Sql.js" 9 | 10 | export class Groups extends Effect.Service()("Groups", { 11 | effect: Effect.gen(function*() { 12 | const repo = yield* GroupsRepo 13 | const sql = yield* SqlClient.SqlClient 14 | 15 | const create = (ownerId: AccountId, group: typeof Group.jsonCreate.Type) => 16 | pipe( 17 | repo.insert( 18 | Group.insert.make({ 19 | ...group, 20 | ownerId 21 | }) 22 | ), 23 | Effect.withSpan("Groups.create", { attributes: { group } }), 24 | policyRequire("Group", "create") 25 | ) 26 | 27 | const update = ( 28 | group: Group, 29 | update: Partial 30 | ) => 31 | pipe( 32 | repo.update({ 33 | ...group, 34 | ...update, 35 | updatedAt: undefined 36 | }), 37 | Effect.withSpan("Groups.update", { 38 | attributes: { id: group.id, update } 39 | }), 40 | policyRequire("Group", "update") 41 | ) 42 | 43 | const findById = (id: GroupId) => 44 | pipe( 45 | repo.findById(id), 46 | Effect.withSpan("Groups.findById", { attributes: { id } }), 47 | policyRequire("Group", "read") 48 | ) 49 | 50 | const with_ = ( 51 | id: GroupId, 52 | f: (group: Group) => Effect.Effect 53 | ): Effect.Effect => 54 | pipe( 55 | repo.findById(id), 56 | Effect.flatMap( 57 | Option.match({ 58 | onNone: () => new GroupNotFound({ id }), 59 | onSome: Effect.succeed 60 | }) 61 | ), 62 | Effect.flatMap(f), 63 | sql.withTransaction, 64 | Effect.catchTag("SqlError", (err) => Effect.die(err)), 65 | Effect.withSpan("Groups.with", { attributes: { id } }) 66 | ) 67 | 68 | return { create, update, findById, with: with_ } as const 69 | }), 70 | dependencies: [SqlLive, GroupsRepo.Default] 71 | }) {} 72 | -------------------------------------------------------------------------------- /examples/http-server/src/Groups/Api.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" 2 | import { Schema } from "effect" 3 | import { Authentication } from "../Accounts/Api.js" 4 | import { Group, GroupIdFromString, GroupNotFound } from "../Domain/Group.js" 5 | 6 | export class GroupsApi extends HttpApiGroup.make("groups") 7 | .add( 8 | HttpApiEndpoint.post("create", "/") 9 | .addSuccess(Group.json) 10 | .setPayload(Group.jsonCreate) 11 | ) 12 | .add( 13 | HttpApiEndpoint.patch("update", "/:id") 14 | .setPath(Schema.Struct({ id: GroupIdFromString })) 15 | .addSuccess(Group.json) 16 | .setPayload(Group.jsonUpdate) 17 | .addError(GroupNotFound) 18 | ) 19 | .middleware(Authentication) 20 | .prefix("/groups") 21 | .annotate(OpenApi.Title, "Groups") 22 | .annotate(OpenApi.Description, "Manage groups") 23 | {} 24 | -------------------------------------------------------------------------------- /examples/http-server/src/Groups/Http.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder } from "@effect/platform" 2 | import { Effect, Layer, pipe } from "effect" 3 | import { AuthenticationLive } from "../Accounts/Http.js" 4 | import { Api } from "../Api.js" 5 | import { policyUse } from "../Domain/Policy.js" 6 | import { CurrentUser } from "../Domain/User.js" 7 | import { Groups } from "../Groups.js" 8 | import { GroupsPolicy } from "./Policy.js" 9 | 10 | export const HttpGroupsLive = HttpApiBuilder.group(Api, "groups", (handlers) => 11 | Effect.gen(function*() { 12 | const groups = yield* Groups 13 | const policy = yield* GroupsPolicy 14 | 15 | return handlers 16 | .handle("create", ({ payload }) => 17 | CurrentUser.pipe( 18 | Effect.flatMap((user) => groups.create(user.accountId, payload)), 19 | policyUse(policy.canCreate(payload)) 20 | )) 21 | .handle("update", ({ path, payload }) => 22 | groups.with(path.id, (group) => 23 | pipe( 24 | groups.update(group, payload), 25 | policyUse(policy.canUpdate(group)) 26 | ))) 27 | })).pipe( 28 | Layer.provide([AuthenticationLive, Groups.Default, GroupsPolicy.Default]) 29 | ) 30 | -------------------------------------------------------------------------------- /examples/http-server/src/Groups/Policy.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "effect" 2 | import type { Group } from "../Domain/Group.js" 3 | import { policy } from "../Domain/Policy.js" 4 | 5 | export class GroupsPolicy extends Effect.Service()( 6 | "Groups/Policy", 7 | { 8 | // eslint-disable-next-line require-yield 9 | effect: Effect.gen(function*() { 10 | const canCreate = (_group: typeof Group.jsonCreate.Type) => 11 | policy("Group", "create", (_actor) => Effect.succeed(true)) 12 | 13 | const canUpdate = (group: Group) => 14 | policy("Group", "update", (actor) => Effect.succeed(group.ownerId === actor.accountId)) 15 | 16 | return { canCreate, canUpdate } as const 17 | }) 18 | } 19 | ) {} 20 | -------------------------------------------------------------------------------- /examples/http-server/src/Groups/Repo.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@effect/sql" 2 | import { Effect } from "effect" 3 | import { Group } from "../Domain/Group.js" 4 | import { SqlLive } from "../Sql.js" 5 | 6 | export class GroupsRepo extends Effect.Service()("Groups/Repo", { 7 | effect: Model.makeRepository(Group, { 8 | tableName: "groups", 9 | spanPrefix: "GroupsRepo", 10 | idColumn: "id" 11 | }), 12 | dependencies: [SqlLive] 13 | }) {} 14 | -------------------------------------------------------------------------------- /examples/http-server/src/Http.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder, HttpApiSwagger, HttpMiddleware, HttpServer } from "@effect/platform" 2 | import { NodeHttpServer } from "@effect/platform-node" 3 | import { Layer } from "effect" 4 | import { createServer } from "http" 5 | import { HttpAccountsLive } from "./Accounts/Http.js" 6 | import { Api } from "./Api.js" 7 | import { HttpGroupsLive } from "./Groups/Http.js" 8 | import { HttpPeopleLive } from "./People/Http.js" 9 | 10 | const ApiLive = Layer.provide(HttpApiBuilder.api(Api), [ 11 | HttpAccountsLive, 12 | HttpGroupsLive, 13 | HttpPeopleLive 14 | ]) 15 | 16 | export const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( 17 | Layer.provide(HttpApiSwagger.layer()), 18 | Layer.provide(HttpApiBuilder.middlewareOpenApi()), 19 | Layer.provide(HttpApiBuilder.middlewareCors()), 20 | Layer.provide(ApiLive), 21 | HttpServer.withLogAddress, 22 | Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) 23 | ) 24 | -------------------------------------------------------------------------------- /examples/http-server/src/People.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect, Option, pipe } from "effect" 3 | import type { GroupId } from "./Domain/Group.js" 4 | import type { PersonId } from "./Domain/Person.js" 5 | import { Person, PersonNotFound } from "./Domain/Person.js" 6 | import { policyRequire } from "./Domain/Policy.js" 7 | import { PeopleRepo } from "./People/Repo.js" 8 | import { SqlLive } from "./Sql.js" 9 | 10 | export class People extends Effect.Service()("People", { 11 | effect: Effect.gen(function*() { 12 | const repo = yield* PeopleRepo 13 | const sql = yield* SqlClient.SqlClient 14 | 15 | const create = (groupId: GroupId, person: typeof Person.jsonCreate.Type) => 16 | pipe( 17 | repo.insert( 18 | Person.insert.make({ 19 | ...person, 20 | groupId 21 | }) 22 | ), 23 | Effect.withSpan("People.create", { attributes: { person, groupId } }), 24 | policyRequire("Person", "create") 25 | ) 26 | 27 | const findById = (id: PersonId) => 28 | pipe( 29 | repo.findById(id), 30 | Effect.withSpan("People.findById", { attributes: { id } }), 31 | policyRequire("Person", "read") 32 | ) 33 | 34 | const with_ = ( 35 | id: PersonId, 36 | f: (person: Person) => Effect.Effect 37 | ): Effect.Effect => 38 | pipe( 39 | repo.findById(id), 40 | Effect.flatMap( 41 | Option.match({ 42 | onNone: () => Effect.fail(new PersonNotFound({ id })), 43 | onSome: Effect.succeed 44 | }) 45 | ), 46 | Effect.flatMap(f), 47 | sql.withTransaction, 48 | Effect.catchTag("SqlError", (e) => Effect.die(e)), 49 | Effect.withSpan("People.with", { attributes: { id } }) 50 | ) 51 | 52 | return { create, findById, with: with_ } as const 53 | }), 54 | dependencies: [SqlLive, PeopleRepo.Default] 55 | }) {} 56 | -------------------------------------------------------------------------------- /examples/http-server/src/People/Api.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "@effect/platform" 2 | import { Schema } from "effect" 3 | import { Authentication } from "../Accounts/Api.js" 4 | import { GroupIdFromString, GroupNotFound } from "../Domain/Group.js" 5 | import { Person, PersonIdFromString, PersonNotFound } from "../Domain/Person.js" 6 | 7 | export class PeopleApi extends HttpApiGroup.make("people") 8 | .prefix("/people") 9 | .add( 10 | HttpApiEndpoint.post("create", "/groups/:groupId/people") 11 | .setPath(Schema.Struct({ groupId: GroupIdFromString })) 12 | .addSuccess(Person.json) 13 | .setPayload(Person.jsonCreate) 14 | .addError(GroupNotFound) 15 | ) 16 | .add( 17 | HttpApiEndpoint.get("findById", "/people/:id") 18 | .setPath(Schema.Struct({ id: PersonIdFromString })) 19 | .addSuccess(Person.json) 20 | .addError(PersonNotFound) 21 | ) 22 | .middleware(Authentication) 23 | .annotate(OpenApi.Title, "People") 24 | .annotate(OpenApi.Description, "Manage people") 25 | {} 26 | -------------------------------------------------------------------------------- /examples/http-server/src/People/Http.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder } from "@effect/platform" 2 | import { Effect, Layer, pipe } from "effect" 3 | import { AuthenticationLive } from "../Accounts/Http.js" 4 | import { Api } from "../Api.js" 5 | import { PersonNotFound } from "../Domain/Person.js" 6 | import { policyUse } from "../Domain/Policy.js" 7 | import { Groups } from "../Groups.js" 8 | import { People } from "../People.js" 9 | import { PeoplePolicy } from "./Policy.js" 10 | 11 | export const HttpPeopleLive = HttpApiBuilder.group(Api, "people", (handlers) => 12 | Effect.gen(function*() { 13 | const groups = yield* Groups 14 | const people = yield* People 15 | const policy = yield* PeoplePolicy 16 | 17 | return handlers 18 | .handle("create", ({ path, payload }) => 19 | groups.with(path.groupId, (group) => 20 | pipe( 21 | people.create(group.id, payload), 22 | policyUse(policy.canCreate(group.id, payload)) 23 | ))) 24 | .handle("findById", ({ path }) => 25 | pipe( 26 | people.findById(path.id), 27 | Effect.flatten, 28 | Effect.mapError(() => new PersonNotFound({ id: path.id })), 29 | policyUse(policy.canRead(path.id)) 30 | )) 31 | })).pipe( 32 | Layer.provide([ 33 | Groups.Default, 34 | People.Default, 35 | PeoplePolicy.Default, 36 | AuthenticationLive 37 | ]) 38 | ) 39 | -------------------------------------------------------------------------------- /examples/http-server/src/People/Policy.ts: -------------------------------------------------------------------------------- 1 | import { Effect, pipe } from "effect" 2 | import type { GroupId } from "../Domain/Group.js" 3 | import type { Person, PersonId } from "../Domain/Person.js" 4 | import type { policy, policyCompose, Unauthorized } from "../Domain/Policy.js" 5 | import { Groups } from "../Groups.js" 6 | import { GroupsPolicy } from "../Groups/Policy.js" 7 | import { People } from "../People.js" 8 | 9 | export class PeoplePolicy extends Effect.Service()( 10 | "People/Policy", 11 | { 12 | effect: Effect.gen(function*() { 13 | const groupsPolicy = yield* GroupsPolicy 14 | const groups = yield* Groups 15 | const people = yield* People 16 | 17 | const canCreate = ( 18 | groupId: GroupId, 19 | _person: typeof Person.jsonCreate.Type 20 | ) => 21 | Unauthorized.refail( 22 | "Person", 23 | "create" 24 | )( 25 | groups.with(groupId, (group) => 26 | pipe( 27 | groupsPolicy.canUpdate(group), 28 | policyCompose( 29 | policy("Person", "create", (_actor) => Effect.succeed(true)) 30 | ) 31 | )) 32 | ) 33 | 34 | const canRead = (id: PersonId) => 35 | Unauthorized.refail( 36 | "Person", 37 | "read" 38 | )( 39 | people.with(id, (person) => 40 | groups.with(person.groupId, (group) => 41 | pipe( 42 | groupsPolicy.canUpdate(group), 43 | policyCompose( 44 | policy("Person", "read", (_actor) => Effect.succeed(true)) 45 | ) 46 | ))) 47 | ) 48 | 49 | return { canCreate, canRead } as const 50 | }), 51 | dependencies: [GroupsPolicy.Default, Groups.Default, People.Default] 52 | } 53 | ) {} 54 | -------------------------------------------------------------------------------- /examples/http-server/src/People/Repo.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@effect/sql" 2 | import { Effect } from "effect" 3 | import { Person } from "../Domain/Person.js" 4 | import { SqlLive } from "../Sql.js" 5 | 6 | export class PeopleRepo extends Effect.Service()("People/Repo", { 7 | effect: Model.makeRepository(Person, { 8 | tableName: "people", 9 | spanPrefix: "PeopleRepo", 10 | idColumn: "id" 11 | }), 12 | dependencies: [SqlLive] 13 | }) {} 14 | -------------------------------------------------------------------------------- /examples/http-server/src/Sql.ts: -------------------------------------------------------------------------------- 1 | import { NodeContext } from "@effect/platform-node" 2 | import { SqlClient } from "@effect/sql" 3 | import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node" 4 | import { identity, Layer } from "effect" 5 | import { fileURLToPath } from "url" 6 | import { makeTestLayer } from "./lib/Layer.js" 7 | 8 | const ClientLive = SqliteClient.layer({ 9 | filename: "data/db.sqlite" 10 | }) 11 | 12 | const MigratorLive = SqliteMigrator.layer({ 13 | loader: SqliteMigrator.fromFileSystem( 14 | fileURLToPath(new URL("./migrations", import.meta.url)) 15 | ) 16 | }).pipe(Layer.provide(NodeContext.layer)) 17 | 18 | export const SqlLive = MigratorLive.pipe(Layer.provideMerge(ClientLive)) 19 | 20 | export const SqlTest = makeTestLayer(SqlClient.SqlClient)({ 21 | withTransaction: identity 22 | }) 23 | -------------------------------------------------------------------------------- /examples/http-server/src/Tracing.ts: -------------------------------------------------------------------------------- 1 | import * as NodeSdk from "@effect/opentelemetry/NodeSdk" 2 | import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" 3 | import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" 4 | import { Config, Effect, Layer, Redacted } from "effect" 5 | 6 | export const TracingLive = Layer.unwrapEffect( 7 | Effect.gen(function*() { 8 | const apiKey = yield* Config.option(Config.redacted("HONEYCOMB_API_KEY")) 9 | const dataset = yield* Config.withDefault( 10 | Config.string("HONEYCOMB_DATASET"), 11 | "effect-http-play" 12 | ) 13 | if (apiKey._tag === "None") { 14 | const endpoint = yield* Config.option( 15 | Config.string("OTEL_EXPORTER_OTLP_ENDPOINT") 16 | ) 17 | if (endpoint._tag === "None") { 18 | return Layer.empty 19 | } 20 | return NodeSdk.layer(() => ({ 21 | resource: { 22 | serviceName: dataset 23 | }, 24 | spanProcessor: new BatchSpanProcessor( 25 | new OTLPTraceExporter({ url: `${endpoint.value}/v1/traces` }) 26 | ) 27 | })) 28 | } 29 | 30 | const headers = { 31 | "X-Honeycomb-Team": Redacted.value(apiKey.value), 32 | "X-Honeycomb-Dataset": dataset 33 | } 34 | 35 | return NodeSdk.layer(() => ({ 36 | resource: { 37 | serviceName: dataset 38 | }, 39 | spanProcessor: new BatchSpanProcessor( 40 | new OTLPTraceExporter({ 41 | url: "https://api.honeycomb.io/v1/traces", 42 | headers 43 | }) 44 | ) 45 | })) 46 | }) 47 | ) 48 | -------------------------------------------------------------------------------- /examples/http-server/src/Uuid.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Layer } from "effect" 2 | import * as Api from "uuid" 3 | 4 | export class Uuid extends Effect.Service()("Uuid", { 5 | succeed: { 6 | generate: Effect.sync(() => Api.v7()) 7 | } 8 | }) { 9 | static Test = Layer.succeed( 10 | Uuid, 11 | new Uuid({ 12 | generate: Effect.succeed("test-uuid") 13 | }) 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/http-server/src/client.ts: -------------------------------------------------------------------------------- 1 | import { Cookies, HttpApiClient, HttpClient } from "@effect/platform" 2 | import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" 3 | import { Effect, Ref } from "effect" 4 | import { Api } from "./Api.js" 5 | import { Email } from "./Domain/Email.js" 6 | 7 | Effect.gen(function*() { 8 | const cookies = yield* Ref.make(Cookies.empty) 9 | const client = yield* HttpApiClient.make(Api, { 10 | baseUrl: "http://localhost:3000", 11 | transformClient: HttpClient.withCookiesRef(cookies) 12 | }) 13 | const user = yield* client.accounts.createUser({ 14 | payload: { 15 | email: Email.make("joe2.bloggs@example.com") 16 | } 17 | }) 18 | console.log(user) 19 | const me = yield* client.accounts.getUserMe() 20 | console.log(me) 21 | }).pipe(Effect.provide(NodeHttpClient.layerUndici), NodeRuntime.runMain) 22 | -------------------------------------------------------------------------------- /examples/http-server/src/lib/Layer.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "effect" 2 | import { Effect, Layer } from "effect" 3 | 4 | const makeUnimplemented = (id: string, prop: PropertyKey) => { 5 | const dead = Effect.die(`${id}: Unimplemented method "${prop.toString()}"`) 6 | function unimplemented() { 7 | return dead 8 | } 9 | Object.assign(unimplemented, dead) 10 | Object.setPrototypeOf(unimplemented, Object.getPrototypeOf(dead)) 11 | return unimplemented 12 | } 13 | 14 | const makeUnimplementedProxy = ( 15 | service: string, 16 | impl: Partial 17 | ): A => 18 | new Proxy({ ...impl } as A, { 19 | get(target, prop, _receiver) { 20 | if (prop in target) { 21 | return target[prop as keyof A] 22 | } 23 | return ((target as any)[prop] = makeUnimplemented(service, prop)) 24 | }, 25 | has: () => true 26 | }) 27 | 28 | export const makeTestLayer = (tag: Context.Tag) => (service: Partial): Layer.Layer => 29 | Layer.succeed(tag, makeUnimplementedProxy(tag.key, service)) 30 | -------------------------------------------------------------------------------- /examples/http-server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Layer } from "effect" 3 | import { HttpLive } from "./Http.js" 4 | import { TracingLive } from "./Tracing.js" 5 | 6 | HttpLive.pipe(Layer.provide(TracingLive), Layer.launch, NodeRuntime.runMain) 7 | -------------------------------------------------------------------------------- /examples/http-server/src/migrations/00001_create users.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect } from "effect" 3 | 4 | export default Effect.gen(function*() { 5 | const sql = yield* SqlClient.SqlClient 6 | yield* sql.onDialectOrElse({ 7 | pg: () => 8 | sql` 9 | CREATE TABLE accounts ( 10 | id SERIAL PRIMARY KEY, 11 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 12 | updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL 13 | ) 14 | `, 15 | orElse: () => 16 | sql` 17 | CREATE TABLE accounts ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT, 19 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 20 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL 21 | ) 22 | ` 23 | }) 24 | yield* sql.onDialectOrElse({ 25 | pg: () => 26 | sql` 27 | CREATE TABLE users ( 28 | id SERIAL PRIMARY KEY, 29 | accountId INTEGER NOT NULL, 30 | email TEXT UNIQUE NOT NULL, 31 | accessToken VARCHAR(255) UNIQUE NOT NULL, 32 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 33 | updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 34 | FOREIGN KEY (accountId) REFERENCES accounts(id) 35 | ) 36 | `, 37 | orElse: () => 38 | sql` 39 | CREATE TABLE users ( 40 | id INTEGER PRIMARY KEY AUTOINCREMENT, 41 | accountId INTEGER NOT NULL, 42 | email TEXT UNIQUE NOT NULL, 43 | accessToken VARCHAR(255) UNIQUE NOT NULL, 44 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 45 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 46 | FOREIGN KEY (accountId) REFERENCES accounts(id) 47 | ) 48 | ` 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /examples/http-server/src/migrations/00002_create groups.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect } from "effect" 3 | 4 | export default Effect.gen(function*() { 5 | const sql = yield* SqlClient.SqlClient 6 | yield* sql.onDialectOrElse({ 7 | pg: () => 8 | sql` 9 | CREATE TABLE groups ( 10 | id SERIAL PRIMARY KEY, 11 | ownerId INTEGER NOT NULL, 12 | name VARCHAR(255) NOT NULL, 13 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 14 | updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 15 | FOREIGN KEY (ownerId) REFERENCES accounts(id) 16 | ) 17 | `, 18 | orElse: () => 19 | sql` 20 | CREATE TABLE groups ( 21 | id INTEGER PRIMARY KEY AUTOINCREMENT, 22 | ownerId INTEGER NOT NULL, 23 | name TEXT NOT NULL, 24 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 25 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 26 | FOREIGN KEY (ownerId) REFERENCES accounts(id) 27 | ) 28 | ` 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /examples/http-server/src/migrations/00003_create_people.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient } from "@effect/sql" 2 | import { Effect } from "effect" 3 | 4 | export default Effect.gen(function*() { 5 | const sql = yield* SqlClient.SqlClient 6 | yield* sql.onDialectOrElse({ 7 | pg: () => 8 | sql` 9 | CREATE TABLE people ( 10 | id SERIAL PRIMARY KEY, 11 | groupId INTEGER NOT NULL, 12 | firstName VARCHAR(255) NOT NULL, 13 | lastName VARCHAR(255) NOT NULL, 14 | dateOfBirth DATE, 15 | createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 16 | updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 17 | FOREIGN KEY (ownerId) REFERENCES groups(id) 18 | ) 19 | `, 20 | orElse: () => 21 | sql` 22 | CREATE TABLE people ( 23 | id INTEGER PRIMARY KEY AUTOINCREMENT, 24 | groupId INTEGER NOT NULL, 25 | firstName TEXT NOT NULL, 26 | lastName TEXT NOT NULL, 27 | dateOfBirth DATE, 28 | createdAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 29 | updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 30 | FOREIGN KEY (groupId) REFERENCES groups(id) 31 | ) 32 | ` 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /examples/http-server/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "NodeNext", 15 | "lib": [ 16 | "ES2022", 17 | "DOM", 18 | "DOM.Iterable" 19 | ], 20 | "types": [], 21 | "isolatedModules": true, 22 | "sourceMap": true, 23 | "declarationMap": true, 24 | "noImplicitReturns": false, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": false, 27 | "noFallthroughCasesInSwitch": true, 28 | "noEmitOnError": false, 29 | "noErrorTruncation": false, 30 | "allowJs": false, 31 | "checkJs": false, 32 | "forceConsistentCasingInFileNames": true, 33 | "noImplicitAny": true, 34 | "noImplicitThis": true, 35 | "noUncheckedIndexedAccess": false, 36 | "strictNullChecks": true, 37 | "baseUrl": ".", 38 | "target": "ES2022", 39 | "module": "NodeNext", 40 | "incremental": true, 41 | "removeComments": false, 42 | "plugins": [ 43 | { 44 | "name": "@effect/language-service" 45 | } 46 | ], 47 | "paths": { 48 | "app/*": [ 49 | "src/*.js" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/http-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { 6 | "path": "tsconfig.src.json" 7 | }, 8 | { 9 | "path": "tsconfig.test.json" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/http-server/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "src" 5 | ], 6 | "compilerOptions": { 7 | "types": [ 8 | "node" 9 | ], 10 | "outDir": "dist", 11 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 12 | "rootDir": "src" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/http-server/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "test" 5 | ], 6 | "references": [ 7 | { 8 | "path": "tsconfig.src.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "types": [ 13 | "node" 14 | ], 15 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 16 | "rootDir": "test", 17 | "noEmit": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/http-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import * as Path from "node:path" 2 | import { defineConfig } from "vitest/config" 3 | 4 | export default defineConfig({ 5 | test: { 6 | alias: { 7 | app: Path.join(__dirname, "src") 8 | } 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1726583932, 6 | "narHash": "sha256-zACxiQx8knB3F8+Ze+1BpiYrI+CbhxyWpcSID9kVhkQ=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "658e7223191d2598641d50ee4e898126768fe847", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | outputs = 6 | { nixpkgs, ... }: 7 | let 8 | forAllSystems = 9 | function: 10 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 11 | system: function nixpkgs.legacyPackages.${system} 12 | ); 13 | in 14 | { 15 | formatter = forAllSystems (pkgs: pkgs.alejandra); 16 | devShells = forAllSystems (pkgs: { 17 | default = pkgs.mkShell { 18 | packages = with pkgs; [ 19 | corepack 20 | findutils 21 | jq 22 | nodejs 23 | ]; 24 | }; 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "packageManager": "pnpm@9.9.0", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "build": "pnpm --recursive --parallel run build", 10 | "check": "pnpm --recursive --parallel run check", 11 | "lint": "eslint \"**/{src,test,examples,scripts}/**/*.{ts,mjs}\"", 12 | "lint-fix": "pnpm lint --fix", 13 | "changeset-version": "changeset version && node ./scripts/version.mjs && node ./scripts/examples.mjs && node ./scripts/templates.mjs", 14 | "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish" 15 | }, 16 | "devDependencies": { 17 | "@changesets/changelog-github": "^0.5.0", 18 | "@changesets/cli": "^2.27.7", 19 | "@dprint/formatter": "^0.4.1", 20 | "@effect/eslint-plugin": "^0.2.0", 21 | "@effect/language-service": "^0.2.0", 22 | "@effect/vitest": "^0.13.14", 23 | "@eslint/compat": "^1.2.2", 24 | "@eslint/eslintrc": "^3.1.0", 25 | "@eslint/js": "^9.13.0", 26 | "@types/node": "^22.8.5", 27 | "@typescript-eslint/eslint-plugin": "^8.12.2", 28 | "@typescript-eslint/parser": "^8.12.2", 29 | "effect": "^3.10.14", 30 | "eslint": "^9.13.0", 31 | "eslint-import-resolver-typescript": "^3.6.3", 32 | "eslint-plugin-codegen": "^0.29.0", 33 | "eslint-plugin-import": "^2.31.0", 34 | "eslint-plugin-simple-import-sort": "^12.1.1", 35 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 36 | "tsx": "^4.19.2", 37 | "typescript": "^5.6.3", 38 | "vite": "^5.4.11", 39 | "vitest": "^2.0.5" 40 | }, 41 | "pnpm": { 42 | "overrides": { 43 | "vitest": "^2.0.5" 44 | }, 45 | "patchedDependencies": { 46 | "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch", 47 | "@changesets/assemble-release-plan@6.0.3": "patches/@changesets__assemble-release-plan@6.0.3.patch" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/create-effect-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-effect-app 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - [#56](https://github.com/Effect-TS/examples/pull/56) [`543b51c`](https://github.com/Effect-TS/examples/commit/543b51c262337925eaa0cb66785ca8614b09f59a) Thanks @IMax153! - update to latest effect 8 | 9 | ## 0.0.5 10 | 11 | ### Patch Changes 12 | 13 | - [#52](https://github.com/Effect-TS/examples/pull/52) [`5f92083`](https://github.com/Effect-TS/examples/commit/5f92083aba3fb78b4a88cc763b782dbd2ef22701) Thanks @tatchi! - Removes potential leftover files 14 | 15 | ## 0.0.4 16 | 17 | ### Patch Changes 18 | 19 | - [#47](https://github.com/Effect-TS/examples/pull/47) [`ce33c87`](https://github.com/Effect-TS/examples/commit/ce33c87c4534eeb6825d479bc113c917f8af6a3f) Thanks @IMax153! - Fix errors with package.json scripts in templates and examples 20 | 21 | ## 0.0.3 22 | 23 | ### Patch Changes 24 | 25 | - [#40](https://github.com/Effect-TS/examples/pull/40) [`2250f2c`](https://github.com/Effect-TS/examples/commit/2250f2cd44691ec94c2da8bcb2adb37711bc1a1d) Thanks @IMax153! - improve logging during project initialization 26 | 27 | ## 0.0.2 28 | 29 | ### Patch Changes 30 | 31 | - [#38](https://github.com/Effect-TS/examples/pull/38) [`9f8160a`](https://github.com/Effect-TS/examples/commit/9f8160ac3a6b4714634d1a167881c47edc1ea2b9) Thanks @IMax153! - bump version 32 | 33 | ## 0.0.1 34 | 35 | ### Patch Changes 36 | 37 | - [#28](https://github.com/Effect-TS/examples/pull/28) [`08b60f0`](https://github.com/Effect-TS/examples/commit/08b60f058fe15fc8d17d2820bbcf02abe331cbdc) Thanks @IMax153! - Initial release 38 | 39 | - [#31](https://github.com/Effect-TS/examples/pull/31) [`935213f`](https://github.com/Effect-TS/examples/commit/935213f903ccc38c91ad8a7df078a9fe4a69b73b) Thanks @IMax153! - add additional cli options for controlling features and configs 40 | -------------------------------------------------------------------------------- /packages/create-effect-app/README.md: -------------------------------------------------------------------------------- 1 | # Create Effect App 2 | 3 | The `create-effect-app` command-line application is a tool which allows you to quickly bootstrap new Effect applications from either a template or from an official Effect example application. 4 | 5 | ## Getting Started 6 | 7 | There are two main ways to use `create-effect-app`: 8 | 9 | ### Interactive 10 | 11 | The easiest way to use `create-effect-app` is interactively via your preferred package manager: 12 | 13 | *`npm`* 14 | 15 | ```sh 16 | npx create-effect-app [project-name] 17 | ``` 18 | 19 | *`pnpm`* 20 | 21 | ```sh 22 | pnpm create effect-app [project-name] 23 | ``` 24 | 25 | *`yarn`* 26 | 27 | ```sh 28 | yarn create effect-app [project-name] 29 | ``` 30 | 31 | *`bun`* 32 | 33 | ```sh 34 | bunx create-effect-app [project-name] 35 | ``` 36 | 37 | You will then be prompted to select the type of project you want to create and customize your project with additional options (see the full [usage](#usage) documentation below). 38 | 39 | ### Non-Interactive 40 | 41 | You can also invoke the `create-effect-app` CLI non-interactively: 42 | 43 | #### Usage 44 | 45 | ```sh 46 | Create Effect App 47 | 48 | USAGE 49 | 50 | $ create-effect-app [(-t, --template basic | cli | monorepo) [--changesets] [--flake] [--eslint] [--workflows]] [] 51 | 52 | $ create-effect-app [(-e, --example http-server)] [] 53 | 54 | DESCRIPTION 55 | 56 | Create an Effect application from an example or a template repository 57 | 58 | ARGUMENTS 59 | 60 | 61 | 62 | A directory that must not exist. 63 | 64 | The folder to output the Effect application code into 65 | 66 | This setting is optional. 67 | 68 | OPTIONS 69 | 70 | (-e, --example http-server) 71 | 72 | One of the following: http-server 73 | 74 | The name of an official Effect example to use to bootstrap the application 75 | 76 | (-t, --template basic | cli | monorepo) 77 | 78 | One of the following: basic, cli, monorepo 79 | 80 | The name of an official Effect example to use to bootstrap the application 81 | 82 | --changesets 83 | 84 | A true or false value. 85 | 86 | Initialize project with Changesets 87 | 88 | This setting is optional. 89 | 90 | --flake 91 | 92 | A true or false value. 93 | 94 | Initialize project with a Nix flake 95 | 96 | This setting is optional. 97 | 98 | --eslint 99 | 100 | A true or false value. 101 | 102 | Initialize project with ESLint 103 | 104 | This setting is optional. 105 | 106 | --workflows 107 | 108 | A true or false value. 109 | 110 | Initialize project with Effect's recommended GitHub actions 111 | 112 | This setting is optional. 113 | ``` 114 | -------------------------------------------------------------------------------- /packages/create-effect-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-effect-app", 3 | "version": "0.0.6", 4 | "type": "module", 5 | "license": "MIT", 6 | "description": "Create Effect-powered applications with a single command", 7 | "bin": { 8 | "create-effect-app": "./dist/bin.cjs" 9 | }, 10 | "engines": { 11 | "node": ">=18.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Effect-TS/examples.git", 16 | "directory": "packages/create-effect-app" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Effect-TS/examples/issues" 20 | }, 21 | "publishConfig": { 22 | "access": "public", 23 | "directory": "dist", 24 | "provenance": true 25 | }, 26 | "scripts": { 27 | "build": "tsup && tsx scripts/copy-package-json.ts", 28 | "check": "tsc -b tsconfig.json" 29 | }, 30 | "devDependencies": { 31 | "@effect/cli": "^0.48.21", 32 | "@effect/platform": "^0.69.21", 33 | "@effect/platform-node": "^0.64.23", 34 | "@effect/printer": "^0.38.14", 35 | "@effect/printer-ansi": "^0.38.14", 36 | "effect": "^3.10.14", 37 | "tar": "^7.4.3", 38 | "tsup": "^8.3.5", 39 | "yaml": "^2.6.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/create-effect-app/scripts/copy-package-json.ts: -------------------------------------------------------------------------------- 1 | import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" 2 | import * as FileSystem from "@effect/platform/FileSystem" 3 | import { Effect, pipe } from "effect" 4 | import * as path from "node:path" 5 | 6 | const read = pipe( 7 | FileSystem.FileSystem, 8 | Effect.flatMap((fileSystem) => fileSystem.readFileString("package.json")), 9 | Effect.map((_) => JSON.parse(_)), 10 | Effect.map((json) => ({ 11 | name: json.name, 12 | version: json.version, 13 | description: json.description, 14 | bin: { 15 | "create-effect-app": "bin.cjs" 16 | }, 17 | engines: json.engines, 18 | repository: json.repository, 19 | author: json.author, 20 | license: json.license, 21 | bugs: json.bugs, 22 | homepage: json.homepage, 23 | tags: json.tags, 24 | keywords: json.keywords, 25 | dependencies: json.dependencies 26 | })) 27 | ) 28 | 29 | const pathTo = path.join("dist", "package.json") 30 | 31 | const write = (pkg: object) => 32 | FileSystem.FileSystem.pipe( 33 | Effect.flatMap((fileSystem) => fileSystem.writeFileString(pathTo, JSON.stringify(pkg, null, 2))) 34 | ) 35 | 36 | const program = pipe( 37 | Effect.log(`Copying package.json to ${pathTo}...`), 38 | Effect.zipRight(read), 39 | Effect.flatMap(write), 40 | Effect.provide(NodeFileSystem.layer) 41 | ) 42 | 43 | Effect.runPromise(program) 44 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/Domain.ts: -------------------------------------------------------------------------------- 1 | import * as Data from "effect/Data" 2 | import type { Example } from "./internal/examples.js" 3 | import type { Template } from "./internal/templates.js" 4 | 5 | export type ProjectType = Data.TaggedEnum<{ 6 | readonly Example: { 7 | readonly example: Example 8 | } 9 | readonly Template: { 10 | readonly template: Template 11 | readonly withChangesets: boolean 12 | readonly withNixFlake: boolean 13 | readonly withESLint: boolean 14 | readonly withWorkflows: boolean 15 | } 16 | }> 17 | 18 | export const ProjectType = Data.taggedEnum() 19 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/GitHub.ts: -------------------------------------------------------------------------------- 1 | import * as HelpDoc from "@effect/cli/HelpDoc" 2 | import * as ValidationError from "@effect/cli/ValidationError" 3 | import * as NodeSink from "@effect/platform-node/NodeSink" 4 | import * as HttpClient from "@effect/platform/HttpClient" 5 | import * as HttpClientRequest from "@effect/platform/HttpClientRequest" 6 | import * as HttpClientResponse from "@effect/platform/HttpClientResponse" 7 | import * as Effect from "effect/Effect" 8 | import * as Stream from "effect/Stream" 9 | import * as Tar from "tar" 10 | import type { ExampleConfig, TemplateConfig } from "./Cli.js" 11 | 12 | export class GitHub extends Effect.Service()("app/GitHub", { 13 | accessors: true, 14 | effect: Effect.gen(function*() { 15 | const client = yield* HttpClient.HttpClient 16 | 17 | const codeloadBaseUrl = "https://codeload.github.com" 18 | 19 | const codeloadClient = client.pipe( 20 | HttpClient.filterStatusOk, 21 | HttpClient.mapRequest(HttpClientRequest.prependUrl(codeloadBaseUrl)) 22 | ) 23 | 24 | const downloadExample = (config: ExampleConfig) => 25 | codeloadClient.get("/Effect-TS/examples/tar.gz/main").pipe( 26 | HttpClientResponse.stream, 27 | Stream.run(NodeSink.fromWritable( 28 | () => 29 | Tar.extract({ 30 | cwd: config.projectName, 31 | strip: 2 + config.projectType.example.split("/").length, 32 | filter: (path) => path.includes(`examples-main/examples/${config.projectType.example}`) 33 | }), 34 | () => ValidationError.invalidValue(HelpDoc.p(`Failed to download example ${config.projectType.example}`)) 35 | )) 36 | ) 37 | 38 | const downloadTemplate = (config: TemplateConfig) => 39 | codeloadClient.get("/Effect-TS/examples/tar.gz/main").pipe( 40 | HttpClientResponse.stream, 41 | Stream.run(NodeSink.fromWritable( 42 | () => 43 | Tar.extract({ 44 | cwd: config.projectName, 45 | strip: 2 + config.projectType.template.split("/").length, 46 | filter: (path) => path.includes(`examples-main/templates/${config.projectType.template}`) 47 | }), 48 | () => ValidationError.invalidValue(HelpDoc.p(`Failed to download template ${config.projectType.template}`)) 49 | )) 50 | ) 51 | 52 | return { 53 | downloadExample, 54 | downloadTemplate 55 | } as const 56 | }) 57 | }) {} 58 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as Ansi from "@effect/printer-ansi/Ansi" 2 | import * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" 3 | import * as Array from "effect/Array" 4 | import * as Logger from "effect/Logger" 5 | 6 | export const AnsiDocLogger = Logger.make(({ message }) => { 7 | const messageArr = Array.ensure(message) 8 | for (let i = 0; i < messageArr.length; i++) { 9 | const currentMessage = messageArr[i] 10 | if (AnsiDoc.isDoc(currentMessage)) { 11 | const prefix = AnsiDoc.text("create-effect-app").pipe( 12 | AnsiDoc.annotate(Ansi.cyan), 13 | AnsiDoc.squareBracketed, 14 | AnsiDoc.cat(AnsiDoc.colon) 15 | ) 16 | const document = AnsiDoc.catWithSpace(prefix, currentMessage as AnsiDoc.AnsiDoc) 17 | globalThis.console.log(AnsiDoc.render(document, { style: "pretty" })) 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/Utils.ts: -------------------------------------------------------------------------------- 1 | import * as HelpDoc from "@effect/cli/HelpDoc" 2 | import * as Effect from "effect/Effect" 3 | 4 | const SCOPED_PACKAGE_REGEX = /^(?:@([^/]+?)[/])?([^/]+?)$/ 5 | 6 | const blockList = [ 7 | "node_modules", 8 | "favicon.ico" 9 | ] 10 | 11 | // Generated with node -e 'console.log(require("module").builtinModules)' 12 | const nodeBuiltins = [ 13 | "_http_agent", 14 | "_http_client", 15 | "_http_common", 16 | "_http_incoming", 17 | "_http_outgoing", 18 | "_http_server", 19 | "_stream_duplex", 20 | "_stream_passthrough", 21 | "_stream_readable", 22 | "_stream_transform", 23 | "_stream_wrap", 24 | "_stream_writable", 25 | "_tls_common", 26 | "_tls_wrap", 27 | "assert", 28 | "assert/strict", 29 | "async_hooks", 30 | "buffer", 31 | "child_process", 32 | "cluster", 33 | "console", 34 | "constants", 35 | "crypto", 36 | "dgram", 37 | "diagnostics_channel", 38 | "dns", 39 | "dns/promises", 40 | "domain", 41 | "events", 42 | "fs", 43 | "fs/promises", 44 | "http", 45 | "http2", 46 | "https", 47 | "inspector", 48 | "inspector/promises", 49 | "module", 50 | "net", 51 | "os", 52 | "path", 53 | "path/posix", 54 | "path/win32", 55 | "perf_hooks", 56 | "process", 57 | "punycode", 58 | "querystring", 59 | "readline", 60 | "readline/promises", 61 | "repl", 62 | "stream", 63 | "stream/consumers", 64 | "stream/promises", 65 | "stream/web", 66 | "string_decoder", 67 | "sys", 68 | "timers", 69 | "timers/promises", 70 | "tls", 71 | "trace_events", 72 | "tty", 73 | "url", 74 | "util", 75 | "util/types", 76 | "v8", 77 | "vm", 78 | "wasi", 79 | "worker_threads", 80 | "zlib" 81 | ] 82 | 83 | const invalid = (message: string) => Effect.fail(HelpDoc.p(message)) 84 | 85 | export function validateProjectName(name: string): Effect.Effect { 86 | if (name.length === 0) { 87 | return invalid("Project name must be a non-empty string") 88 | } 89 | if (name.length > 214) { 90 | return invalid("Project name must not contain more than 214 characters") 91 | } 92 | if (name.toLowerCase() !== name) { 93 | return invalid("Project name must not contain capital letters") 94 | } 95 | if (name.trim() !== name) { 96 | return invalid("Project name must not contain leading or trailing whitespace") 97 | } 98 | if (name.match(/^\./)) { 99 | return invalid("Project name must not start with a period") 100 | } 101 | if (name.match(/^_/)) { 102 | return invalid("Project name must not start with an underscore") 103 | } 104 | if (/[~'!()*]/.test(name.split("/").slice(-1)[0])) { 105 | return invalid("Project name must not contain the special scharacters ~'!()*") 106 | } 107 | const isNodeBuiltin = nodeBuiltins.some((builtinName) => { 108 | return name.toLowerCase() === builtinName 109 | }) 110 | if (isNodeBuiltin) { 111 | return invalid("Project name must not be a NodeJS built-in module name") 112 | } 113 | const isBlockedName = blockList.some((blockedName) => { 114 | return name.toLowerCase() === blockedName 115 | }) 116 | if (isBlockedName) { 117 | return invalid(`Project name '${name}' is blocked from use`) 118 | } 119 | if (encodeURIComponent(name) !== name) { 120 | // Check scoped packages 121 | const result = name.match(SCOPED_PACKAGE_REGEX) 122 | if (result) { 123 | const user = result[1] 124 | const pkg = result[2] 125 | if (encodeURIComponent(user) !== user || encodeURIComponent(pkg) !== pkg) { 126 | return invalid("Project name must only contain URL-friendly characters") 127 | } 128 | } 129 | } 130 | return Effect.succeed(name) 131 | } 132 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as CliConfig from "@effect/cli/CliConfig" 4 | import * as NodeContext from "@effect/platform-node/NodeContext" 5 | import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient" 6 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime" 7 | import * as Ansi from "@effect/printer-ansi/Ansi" 8 | import * as AnsiDoc from "@effect/printer-ansi/AnsiDoc" 9 | import * as Effect from "effect/Effect" 10 | import * as Layer from "effect/Layer" 11 | import * as Logger from "effect/Logger" 12 | import * as LogLevel from "effect/LogLevel" 13 | import { cli } from "./Cli.js" 14 | import { GitHub } from "./GitHub.js" 15 | import { AnsiDocLogger } from "./Logger.js" 16 | 17 | const MainLive = GitHub.Default.pipe( 18 | Layer.provideMerge(Layer.mergeAll( 19 | Logger.replace(Logger.defaultLogger, AnsiDocLogger), 20 | Logger.minimumLogLevel(LogLevel.Info), 21 | CliConfig.layer({ showBuiltIns: false }), 22 | NodeContext.layer, 23 | NodeHttpClient.layerUndici 24 | )) 25 | ) 26 | 27 | cli(process.argv).pipe( 28 | Effect.catchTags({ 29 | QuitException: () => 30 | Effect.logError(AnsiDoc.cat( 31 | AnsiDoc.hardLine, 32 | AnsiDoc.text("Exiting...").pipe(AnsiDoc.annotate(Ansi.red)) 33 | )) 34 | }), 35 | Effect.orDie, 36 | Effect.provide(MainLive), 37 | NodeRuntime.runMain({ 38 | disablePrettyLogger: true, 39 | disableErrorReporting: true 40 | }) 41 | ) 42 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/internal/examples.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export type Example = "http-server" 3 | 4 | /** @internal */ 5 | export const examples = ["http-server"] as const 6 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/internal/templates.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export type Template = "basic" | "cli" | "monorepo" 3 | 4 | /** @internal */ 5 | export const templates = ["basic", "cli", "monorepo"] as const 6 | -------------------------------------------------------------------------------- /packages/create-effect-app/src/internal/version.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const moduleVersion = "0.0.6" 3 | -------------------------------------------------------------------------------- /packages/create-effect-app/test/dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass") 5 | }) 6 | -------------------------------------------------------------------------------- /packages/create-effect-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/create-effect-app/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 7 | "rootDir": "src", 8 | "noEmit": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/create-effect-app/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "../../tsconfig.base.json", 4 | "include": ["test"], 5 | "compilerOptions": { 6 | "types": ["node"], 7 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 8 | "rootDir": "test", 9 | "noEmit": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/create-effect-app/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/bin.ts"], 5 | clean: true, 6 | publicDir: true, 7 | treeshake: "smallest", 8 | external: ["@parcel/watcher"] 9 | }) 10 | -------------------------------------------------------------------------------- /packages/create-effect-app/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /patches/@changesets__assemble-release-plan@6.0.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js 2 | index 60427457c887f2d72168fecec83d79088c68e3a4..b48899c83ca2bc5bfa0cbb65e0c098d5bb65fe3d 100644 3 | --- a/dist/changesets-assemble-release-plan.cjs.js 4 | +++ b/dist/changesets-assemble-release-plan.cjs.js 5 | @@ -189,7 +189,7 @@ function determineDependents({ 6 | preInfo, 7 | onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 8 | })) { 9 | - type = "major"; 10 | + type = "minor"; 11 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange))) { 12 | switch (depType) { 13 | case "dependencies": 14 | diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js 15 | index f6583cf3f639e1fe4df764a015689dea74127236..2b9dca9f460793596394484457a94a34bcc1d99a 100644 16 | --- a/dist/changesets-assemble-release-plan.esm.js 17 | +++ b/dist/changesets-assemble-release-plan.esm.js 18 | @@ -178,7 +178,7 @@ function determineDependents({ 19 | preInfo, 20 | onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 21 | })) { 22 | - type = "major"; 23 | + type = "minor"; 24 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange))) { 25 | switch (depType) { 26 | case "dependencies": 27 | -------------------------------------------------------------------------------- /patches/@changesets__get-github-info@0.6.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js 2 | index a74df59f8a5988f458a3476087399f5e6dfe4818..ce5e60ef9916eb0cb76ab1e9dd422abcad752bf6 100644 3 | --- a/dist/changesets-get-github-info.cjs.js 4 | +++ b/dist/changesets-get-github-info.cjs.js 5 | @@ -251,18 +251,13 @@ async function getInfo(request) { 6 | b = new Date(b.mergedAt); 7 | return a > b ? 1 : a < b ? -1 : 0; 8 | })[0] : null; 9 | - 10 | - if (associatedPullRequest) { 11 | - user = associatedPullRequest.author; 12 | - } 13 | - 14 | return { 15 | user: user ? user.login : null, 16 | pull: associatedPullRequest ? associatedPullRequest.number : null, 17 | links: { 18 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 19 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 20 | - user: user ? `[@${user.login}](${user.url})` : null 21 | + user: user ? `@${user.login}` : null 22 | } 23 | }; 24 | } 25 | diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js 26 | index 27e5c972ab1202ff16f5124b471f4bbcc46be2b5..3940a8fe86e10cb46d8ff6436dea1103b1839927 100644 27 | --- a/dist/changesets-get-github-info.esm.js 28 | +++ b/dist/changesets-get-github-info.esm.js 29 | @@ -242,18 +242,13 @@ async function getInfo(request) { 30 | b = new Date(b.mergedAt); 31 | return a > b ? 1 : a < b ? -1 : 0; 32 | })[0] : null; 33 | - 34 | - if (associatedPullRequest) { 35 | - user = associatedPullRequest.author; 36 | - } 37 | - 38 | return { 39 | user: user ? user.login : null, 40 | pull: associatedPullRequest ? associatedPullRequest.number : null, 41 | links: { 42 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 43 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 44 | - user: user ? `[@${user.login}](${user.url})` : null 45 | + user: user ? `@${user.login}` : null 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /scripts/examples.mjs: -------------------------------------------------------------------------------- 1 | import { createStreaming } from "@dprint/formatter" 2 | import * as Fs from "node:fs/promises" 3 | 4 | /** @type {import("@dprint/formatter").GlobalConfiguration} */ 5 | const globalConfig = { 6 | indentWidth: 2, 7 | lineWidth: 120 8 | } 9 | 10 | async function main() { 11 | const tsFormatter = await createStreaming( 12 | // check https://plugins.dprint.dev/ for latest plugin versions 13 | // eslint-disable-next-line no-undef 14 | fetch("https://plugins.dprint.dev/typescript-0.91.8.wasm") 15 | ) 16 | 17 | tsFormatter.setConfig(globalConfig, { 18 | semiColons: "asi", 19 | quoteStyle: "alwaysDouble", 20 | trailingCommas: "never", 21 | operatorPosition: "maintain", 22 | "arrowFunction.useParentheses": "force" 23 | }) 24 | 25 | const template = await Fs.readFile("./scripts/examples.template.txt") 26 | .then((buffer) => buffer.toString("utf8")) 27 | const examples = await Fs.readdir("./examples", { withFileTypes: true }) 28 | .then((entries) => 29 | entries 30 | .filter((entry) => entry.isDirectory()) 31 | .map((entry) => entry.name) 32 | ) 33 | 34 | await Fs.writeFile( 35 | "./packages/create-effect-app/src/internal/examples.ts", 36 | tsFormatter.formatText({ 37 | filePath: "file.ts", 38 | fileText: template 39 | .replace("EXAMPLE_VALUES", JSON.stringify(examples)) 40 | .replace("EXAMPLE_TYPE", examples.map(JSON.stringify).join(" | ")) 41 | }) 42 | ) 43 | } 44 | 45 | main() 46 | -------------------------------------------------------------------------------- /scripts/examples.template.txt: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export type Example = EXAMPLE_TYPE 3 | 4 | /** @internal */ 5 | export const examples = EXAMPLE_VALUES as const 6 | -------------------------------------------------------------------------------- /scripts/templates.mjs: -------------------------------------------------------------------------------- 1 | import { createStreaming } from "@dprint/formatter" 2 | import * as Fs from "node:fs/promises" 3 | 4 | /** @type {import("@dprint/formatter").GlobalConfiguration} */ 5 | const globalConfig = { 6 | indentWidth: 2, 7 | lineWidth: 120 8 | } 9 | 10 | async function main() { 11 | const tsFormatter = await createStreaming( 12 | // check https://plugins.dprint.dev/ for latest plugin versions 13 | // eslint-disable-next-line no-undef 14 | fetch("https://plugins.dprint.dev/typescript-0.91.8.wasm") 15 | ) 16 | 17 | tsFormatter.setConfig(globalConfig, { 18 | semiColons: "asi", 19 | quoteStyle: "alwaysDouble", 20 | trailingCommas: "never", 21 | operatorPosition: "maintain", 22 | "arrowFunction.useParentheses": "force" 23 | }) 24 | 25 | const template = await Fs.readFile("./scripts/templates.template.txt") 26 | .then((buffer) => buffer.toString("utf8")) 27 | const templates = await Fs.readdir("./templates", { withFileTypes: true }) 28 | .then((entries) => 29 | entries 30 | .filter((entry) => entry.isDirectory()) 31 | .map((entry) => entry.name) 32 | ) 33 | 34 | await Fs.writeFile( 35 | "./packages/create-effect-app/src/internal/templates.ts", 36 | tsFormatter.formatText({ 37 | filePath: "file.ts", 38 | fileText: template 39 | .replace("TEMPLATE_VALUES", JSON.stringify(templates)) 40 | .replace("TEMPLATE_TYPE", templates.map(JSON.stringify).join(" | ")) 41 | }) 42 | ) 43 | } 44 | 45 | main() 46 | -------------------------------------------------------------------------------- /scripts/templates.template.txt: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export type Template = TEMPLATE_TYPE 3 | 4 | /** @internal */ 5 | export const templates = TEMPLATE_VALUES as const 6 | -------------------------------------------------------------------------------- /scripts/version.mjs: -------------------------------------------------------------------------------- 1 | import * as Fs from "node:fs" 2 | import Package from "../packages/create-effect-app/package.json" assert { type: "json" } 3 | 4 | const tpl = Fs.readFileSync("./scripts/version.template.txt").toString("utf8") 5 | 6 | Fs.writeFileSync( 7 | "packages/create-effect-app/src/internal/version.ts", 8 | tpl.replace("VERSION", Package.version) 9 | ) 10 | -------------------------------------------------------------------------------- /scripts/version.template.txt: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const moduleVersion = "VERSION" 3 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import * as it from "@effect/vitest" 2 | 3 | it.addEqualityTesters() 4 | -------------------------------------------------------------------------------- /templates/basic/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "restricted", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /templates/basic/.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /templates/basic/.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Perform standard setup and install dependencies using pnpm. 3 | inputs: 4 | node-version: 5 | description: The version of Node.js to install 6 | required: true 7 | default: 20.16.0 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v3 14 | - name: Install node 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: pnpm 18 | node-version: ${{ inputs.node-version }} 19 | - name: Install dependencies 20 | shell: bash 21 | run: pnpm install 22 | -------------------------------------------------------------------------------- /templates/basic/.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install dependencies 24 | uses: ./.github/actions/setup 25 | - run: pnpm codegen 26 | - name: Check source state 27 | run: git add src && git diff-index --cached HEAD --exit-code src 28 | 29 | types: 30 | name: Types 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 10 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Install dependencies 36 | uses: ./.github/actions/setup 37 | - run: pnpm check 38 | 39 | lint: 40 | name: Lint 41 | runs-on: ubuntu-latest 42 | timeout-minutes: 10 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Install dependencies 46 | uses: ./.github/actions/setup 47 | - run: pnpm lint 48 | 49 | test: 50 | name: Test 51 | runs-on: ubuntu-latest 52 | timeout-minutes: 10 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Install dependencies 56 | uses: ./.github/actions/setup 57 | - run: pnpm test 58 | -------------------------------------------------------------------------------- /templates/basic/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | release: 14 | if: github.repository_owner == 'Effect-Ts' 15 | name: Release 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | permissions: 19 | contents: write 20 | id-token: write 21 | pull-requests: write 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install dependencies 25 | uses: ./.github/actions/setup 26 | - name: Create Release Pull Request or Publish 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | version: pnpm changeset-version 31 | publish: pnpm changeset-publish 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /templates/basic/.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | pull_request: 5 | branches: [main, next-minor, next-major] 6 | workflow_dispatch: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | snapshot: 12 | name: Snapshot 13 | if: github.repository_owner == 'Effect-Ts' 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install dependencies 19 | uses: ./.github/actions/setup 20 | - name: Build package 21 | run: pnpm build 22 | - name: Create snapshot 23 | id: snapshot 24 | run: pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off 25 | -------------------------------------------------------------------------------- /templates/basic/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.tsbuildinfo 3 | node_modules/ 4 | .DS_Store 5 | tmp/ 6 | dist/ 7 | build/ 8 | docs/ 9 | scratchpad/* 10 | !scratchpad/tsconfig.json 11 | .direnv/ 12 | .idea/ 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | -------------------------------------------------------------------------------- /templates/basic/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "effectful-tech.effect-vscode", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /templates/basic/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | } 6 | -------------------------------------------------------------------------------- /templates/basic/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/basic/README.md: -------------------------------------------------------------------------------- 1 | # Effect Package Template 2 | 3 | This template provides a solid foundation for building scalable and maintainable TypeScript package with Effect. 4 | 5 | ## Running Code 6 | 7 | This template leverages [tsx](https://tsx.is) to allow execution of TypeScript files via NodeJS as if they were written in plain JavaScript. 8 | 9 | To execute a file with `tsx`: 10 | 11 | ```sh 12 | pnpm tsx ./path/to/the/file.ts 13 | ``` 14 | 15 | ## Operations 16 | 17 | **Building** 18 | 19 | To build the package: 20 | 21 | ```sh 22 | pnpm build 23 | ``` 24 | 25 | **Testing** 26 | 27 | To test the package: 28 | 29 | ```sh 30 | pnpm test 31 | ``` 32 | -------------------------------------------------------------------------------- /templates/basic/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": ["error", { 65 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 66 | message: "Do not use spread arguments in Array.push" 67 | }], 68 | 69 | "no-unused-vars": "off", 70 | "prefer-rest-params": "off", 71 | "prefer-spread": "off", 72 | "import/first": "error", 73 | "import/newline-after-import": "error", 74 | "import/no-duplicates": "error", 75 | "import/no-unresolved": "off", 76 | "import/order": "off", 77 | "simple-import-sort/imports": "off", 78 | "sort-destructure-keys/sort-destructure-keys": "error", 79 | "deprecation/deprecation": "off", 80 | 81 | "@typescript-eslint/array-type": ["warn", { 82 | default: "generic", 83 | readonly: "generic" 84 | }], 85 | 86 | "@typescript-eslint/member-delimiter-style": 0, 87 | "@typescript-eslint/no-non-null-assertion": "off", 88 | "@typescript-eslint/ban-types": "off", 89 | "@typescript-eslint/no-explicit-any": "off", 90 | "@typescript-eslint/no-empty-interface": "off", 91 | "@typescript-eslint/consistent-type-imports": "warn", 92 | 93 | "@typescript-eslint/no-unused-vars": ["error", { 94 | argsIgnorePattern: "^_", 95 | varsIgnorePattern: "^_" 96 | }], 97 | 98 | "@typescript-eslint/ban-ts-comment": "off", 99 | "@typescript-eslint/camelcase": "off", 100 | "@typescript-eslint/explicit-function-return-type": "off", 101 | "@typescript-eslint/explicit-module-boundary-types": "off", 102 | "@typescript-eslint/interface-name-prefix": "off", 103 | "@typescript-eslint/no-array-constructor": "off", 104 | "@typescript-eslint/no-use-before-define": "off", 105 | "@typescript-eslint/no-namespace": "off", 106 | 107 | "@effect/dprint": ["error", { 108 | config: { 109 | indentWidth: 2, 110 | lineWidth: 120, 111 | semiColons: "asi", 112 | quoteStyle: "alwaysDouble", 113 | trailingCommas: "never", 114 | operatorPosition: "maintain", 115 | "arrowFunction.useParentheses": "force" 116 | } 117 | }] 118 | } 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /templates/basic/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | outputs = {nixpkgs, ...}: let 6 | forAllSystems = function: 7 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed 8 | (system: function nixpkgs.legacyPackages.${system}); 9 | in { 10 | formatter = forAllSystems (pkgs: pkgs.alejandra); 11 | devShells = forAllSystems (pkgs: { 12 | default = pkgs.mkShell { 13 | packages = with pkgs; [ 14 | corepack 15 | nodejs_22 16 | # For systems that do not ship with Python by default (required by `node-gyp`) 17 | python3 18 | ]; 19 | }; 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /templates/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@template/basic", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "packageManager": "pnpm@9.10.0", 6 | "license": "MIT", 7 | "description": "A basic Effect package", 8 | "repository": { 9 | "type": "git", 10 | "url": "" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "directory": "dist" 15 | }, 16 | "scripts": { 17 | "codegen": "build-utils prepare-v2", 18 | "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", 19 | "build-esm": "tsc -b tsconfig.build.json", 20 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 21 | "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", 22 | "check": "tsc -b tsconfig.json", 23 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 24 | "lint-fix": "pnpm lint --fix", 25 | "test": "vitest", 26 | "coverage": "vitest --coverage", 27 | "changeset-version": "changeset version && node scripts/version.mjs", 28 | "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish" 29 | }, 30 | "dependencies": { 31 | "effect": "latest" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.24.8", 35 | "@babel/core": "^7.25.2", 36 | "@babel/plugin-transform-export-namespace-from": "^7.24.7", 37 | "@babel/plugin-transform-modules-commonjs": "^7.24.8", 38 | "@changesets/changelog-github": "^0.5.0", 39 | "@changesets/cli": "^2.27.8", 40 | "@effect/build-utils": "^0.7.7", 41 | "@effect/eslint-plugin": "^0.2.0", 42 | "@effect/language-service": "^0.1.0", 43 | "@effect/vitest": "latest", 44 | "@eslint/compat": "1.1.1", 45 | "@eslint/eslintrc": "3.1.0", 46 | "@eslint/js": "9.10.0", 47 | "@types/node": "^22.5.2", 48 | "@typescript-eslint/eslint-plugin": "^8.4.0", 49 | "@typescript-eslint/parser": "^8.4.0", 50 | "babel-plugin-annotate-pure-calls": "^0.4.0", 51 | "eslint": "^9.10.0", 52 | "eslint-import-resolver-typescript": "^3.6.3", 53 | "eslint-plugin-codegen": "^0.28.0", 54 | "eslint-plugin-import": "^2.30.0", 55 | "eslint-plugin-simple-import-sort": "^12.1.1", 56 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 57 | "tsx": "^4.17.0", 58 | "typescript": "^5.6.2", 59 | "vitest": "^2.0.5" 60 | }, 61 | "effect": { 62 | "generateExports": { 63 | "include": [ 64 | "**/*.ts" 65 | ] 66 | }, 67 | "generateIndex": { 68 | "include": [ 69 | "**/*.ts" 70 | ] 71 | } 72 | }, 73 | "pnpm": { 74 | "patchedDependencies": { 75 | "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch", 76 | "babel-plugin-annotate-pure-calls@0.4.0": "patches/babel-plugin-annotate-pure-calls@0.4.0.patch" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /templates/basic/patches/@changesets__get-github-info@0.6.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js 2 | index a74df59f8a5988f458a3476087399f5e6dfe4818..ce5e60ef9916eb0cb76ab1e9dd422abcad752bf6 100644 3 | --- a/dist/changesets-get-github-info.cjs.js 4 | +++ b/dist/changesets-get-github-info.cjs.js 5 | @@ -251,18 +251,13 @@ async function getInfo(request) { 6 | b = new Date(b.mergedAt); 7 | return a > b ? 1 : a < b ? -1 : 0; 8 | })[0] : null; 9 | - 10 | - if (associatedPullRequest) { 11 | - user = associatedPullRequest.author; 12 | - } 13 | - 14 | return { 15 | user: user ? user.login : null, 16 | pull: associatedPullRequest ? associatedPullRequest.number : null, 17 | links: { 18 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 19 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 20 | - user: user ? `[@${user.login}](${user.url})` : null 21 | + user: user ? `@${user.login}` : null 22 | } 23 | }; 24 | } 25 | diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js 26 | index 27e5c972ab1202ff16f5124b471f4bbcc46be2b5..3940a8fe86e10cb46d8ff6436dea1103b1839927 100644 27 | --- a/dist/changesets-get-github-info.esm.js 28 | +++ b/dist/changesets-get-github-info.esm.js 29 | @@ -242,18 +242,13 @@ async function getInfo(request) { 30 | b = new Date(b.mergedAt); 31 | return a > b ? 1 : a < b ? -1 : 0; 32 | })[0] : null; 33 | - 34 | - if (associatedPullRequest) { 35 | - user = associatedPullRequest.author; 36 | - } 37 | - 38 | return { 39 | user: user ? user.login : null, 40 | pull: associatedPullRequest ? associatedPullRequest.number : null, 41 | links: { 42 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 43 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 44 | - user: user ? `[@${user.login}](${user.url})` : null 45 | + user: user ? `@${user.login}` : null 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /templates/basic/patches/babel-plugin-annotate-pure-calls@0.4.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/index.js b/lib/index.js 2 | index 2182884e21874ebb37261e2375eec08ad956fc9a..ef5630199121c2830756e00c7cc48cf1078c8207 100644 3 | --- a/lib/index.js 4 | +++ b/lib/index.js 5 | @@ -78,7 +78,7 @@ const isInAssignmentContext = path => { 6 | 7 | parentPath = _ref.parentPath; 8 | 9 | - if (parentPath.isVariableDeclaration() || parentPath.isAssignmentExpression()) { 10 | + if (parentPath.isVariableDeclaration() || parentPath.isAssignmentExpression() || parentPath.isClassDeclaration()) { 11 | return true; 12 | } 13 | } while (parentPath !== statement); 14 | -------------------------------------------------------------------------------- /templates/basic/scratchpad/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "declaration": false, 6 | "declarationMap": false, 7 | "composite": false, 8 | "incremental": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/basic/setupTests.ts: -------------------------------------------------------------------------------- 1 | import * as it from "@effect/vitest" 2 | 3 | it.addEqualityTesters() 4 | -------------------------------------------------------------------------------- /templates/basic/src/Program.ts: -------------------------------------------------------------------------------- 1 | import * as Effect from "effect/Effect" 2 | 3 | Effect.runPromise(Effect.log("Hello, World!")) 4 | -------------------------------------------------------------------------------- /templates/basic/test/Dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /templates/basic/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "NodeNext", 15 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 16 | "types": [], 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "declarationMap": true, 20 | "noImplicitReturns": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmitOnError": false, 25 | "noErrorTruncation": false, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "noImplicitAny": true, 30 | "noImplicitThis": true, 31 | "noUncheckedIndexedAccess": false, 32 | "strictNullChecks": true, 33 | "baseUrl": ".", 34 | "target": "ES2022", 35 | "module": "NodeNext", 36 | "incremental": true, 37 | "removeComments": false, 38 | "plugins": [{ "name": "@effect/language-service" }] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /templates/basic/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 6 | "outDir": "build/esm", 7 | "declarationDir": "build/dts", 8 | "stripInternal": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/basic/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "outDir": "build/src", 7 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 8 | "rootDir": "src" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/basic/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [ 5 | { "path": "tsconfig.src.json" } 6 | ], 7 | "compilerOptions": { 8 | "types": ["node"], 9 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 10 | "rootDir": "test", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/basic/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vitest/config" 3 | 4 | export default defineConfig({ 5 | plugins: [], 6 | test: { 7 | setupFiles: [path.join(__dirname, "setupTests.ts")], 8 | include: ["./test/**/*.test.ts"], 9 | globals: true 10 | }, 11 | resolve: { 12 | alias: { 13 | "@template/basic/test": path.join(__dirname, "test"), 14 | "@template/basic": path.join(__dirname, "src") 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /templates/cli/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "restricted", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /templates/cli/.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /templates/cli/.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Perform standard setup and install dependencies using pnpm. 3 | inputs: 4 | node-version: 5 | description: The version of Node.js to install 6 | required: true 7 | default: 20.16.0 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v3 14 | - name: Install node 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: pnpm 18 | node-version: ${{ inputs.node-version }} 19 | - name: Install dependencies 20 | shell: bash 21 | run: pnpm install 22 | -------------------------------------------------------------------------------- /templates/cli/.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Install dependencies 24 | uses: ./.github/actions/setup 25 | 26 | types: 27 | name: Types 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 10 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Install dependencies 33 | uses: ./.github/actions/setup 34 | - run: pnpm check 35 | 36 | lint: 37 | name: Lint 38 | runs-on: ubuntu-latest 39 | timeout-minutes: 10 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Install dependencies 43 | uses: ./.github/actions/setup 44 | - run: pnpm lint 45 | 46 | test: 47 | name: Test 48 | runs-on: ubuntu-latest 49 | timeout-minutes: 10 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Install dependencies 53 | uses: ./.github/actions/setup 54 | - run: pnpm test 55 | -------------------------------------------------------------------------------- /templates/cli/.github/workflows/release.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Effect-TS/examples/d88d8030518b9283bba9940b3e7b77c92475a0e3/templates/cli/.github/workflows/release.yml -------------------------------------------------------------------------------- /templates/cli/.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | pull_request: 5 | branches: [main, next-minor, next-major] 6 | workflow_dispatch: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | snapshot: 12 | name: Snapshot 13 | if: github.repository_owner == 'Effect-Ts' 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install dependencies 19 | uses: ./.github/actions/setup 20 | - name: Build package 21 | run: pnpm build 22 | - name: Create snapshot 23 | id: snapshot 24 | run: pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off 25 | -------------------------------------------------------------------------------- /templates/cli/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.tsbuildinfo 3 | node_modules/ 4 | yarn-error.log 5 | .ultra.cache.json 6 | .DS_Store 7 | tmp/ 8 | build/ 9 | dist/ 10 | .direnv/ 11 | .env 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | -------------------------------------------------------------------------------- /templates/cli/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "effectful-tech.effect-vscode", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /templates/cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "typescript.enablePromptUseWorkspaceTsdk": true, 5 | "editor.formatOnSave": true, 6 | "eslint.format.enable": true, 7 | "[json]": { 8 | "editor.defaultFormatter": "vscode.json-language-features" 9 | }, 10 | "[markdown]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "prettier.semi": false, 13 | "prettier.trailingComma": "none" 14 | }, 15 | "[javascript]": { 16 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 17 | }, 18 | "[javascriptreact]": { 19 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 20 | }, 21 | "[typescript]": { 22 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 23 | }, 24 | "[typescriptreact]": { 25 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 26 | }, 27 | "eslint.validate": ["markdown", "javascript", "typescript"], 28 | "editor.codeActionsOnSave": { 29 | "source.fixAll.eslint": "explicit" 30 | }, 31 | "editor.quickSuggestions": { 32 | "other": true, 33 | "comments": false, 34 | "strings": false 35 | }, 36 | "editor.acceptSuggestionOnCommitCharacter": true, 37 | "editor.acceptSuggestionOnEnter": "on", 38 | "editor.quickSuggestionsDelay": 10, 39 | "editor.suggestOnTriggerCharacters": true, 40 | "editor.tabCompletion": "off", 41 | "editor.suggest.localityBonus": true, 42 | "editor.suggestSelection": "recentlyUsed", 43 | "editor.wordBasedSuggestions": "matchingDocuments", 44 | "editor.parameterHints.enabled": true, 45 | "files.insertFinalNewline": true 46 | } 47 | -------------------------------------------------------------------------------- /templates/cli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/cli/README.md: -------------------------------------------------------------------------------- 1 | # Effect CLI Application Template 2 | 3 | This template provides a solid foundation for building scalable and maintainable command-line applications with Effect. 4 | 5 | ## Running Code 6 | 7 | This template leverages [tsx](https://tsx.is) to allow execution of TypeScript files via NodeJS as if they were written in plain JavaScript. 8 | 9 | To execute a file with `tsx`: 10 | 11 | ```sh 12 | pnpm tsx ./path/to/the/file.ts 13 | ``` 14 | 15 | ## Operations 16 | 17 | **Building** 18 | 19 | To build the package: 20 | 21 | ```sh 22 | pnpm build 23 | ``` 24 | 25 | **Testing** 26 | 27 | To test the package: 28 | 29 | ```sh 30 | pnpm test 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /templates/cli/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": [ 65 | "error", 66 | { 67 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 68 | message: "Do not use spread arguments in Array.push" 69 | } 70 | ], 71 | 72 | "no-unused-vars": "off", 73 | "prefer-rest-params": "off", 74 | "prefer-spread": "off", 75 | "import/first": "error", 76 | "import/newline-after-import": "error", 77 | "import/no-duplicates": "error", 78 | "import/no-unresolved": "off", 79 | "import/order": "off", 80 | "simple-import-sort/imports": "off", 81 | "sort-destructure-keys/sort-destructure-keys": "error", 82 | "deprecation/deprecation": "off", 83 | 84 | "@typescript-eslint/array-type": [ 85 | "warn", 86 | { 87 | default: "generic", 88 | readonly: "generic" 89 | } 90 | ], 91 | 92 | "@typescript-eslint/member-delimiter-style": 0, 93 | "@typescript-eslint/no-non-null-assertion": "off", 94 | "@typescript-eslint/ban-types": "off", 95 | "@typescript-eslint/no-explicit-any": "off", 96 | "@typescript-eslint/no-empty-interface": "off", 97 | "@typescript-eslint/consistent-type-imports": "warn", 98 | 99 | "@typescript-eslint/no-unused-vars": [ 100 | "error", 101 | { 102 | argsIgnorePattern: "^_", 103 | varsIgnorePattern: "^_" 104 | } 105 | ], 106 | 107 | "@typescript-eslint/ban-ts-comment": "off", 108 | "@typescript-eslint/camelcase": "off", 109 | "@typescript-eslint/explicit-function-return-type": "off", 110 | "@typescript-eslint/explicit-module-boundary-types": "off", 111 | "@typescript-eslint/interface-name-prefix": "off", 112 | "@typescript-eslint/no-array-constructor": "off", 113 | "@typescript-eslint/no-use-before-define": "off", 114 | "@typescript-eslint/no-namespace": "off", 115 | 116 | "@effect/dprint": [ 117 | "error", 118 | { 119 | config: { 120 | indentWidth: 2, 121 | lineWidth: 120, 122 | semiColons: "asi", 123 | quoteStyle: "alwaysDouble", 124 | trailingCommas: "never", 125 | operatorPosition: "maintain", 126 | "arrowFunction.useParentheses": "force" 127 | } 128 | } 129 | ] 130 | } 131 | } 132 | ] 133 | -------------------------------------------------------------------------------- /templates/cli/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | outputs = {nixpkgs, ...}: let 6 | forAllSystems = function: 7 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed 8 | (system: function nixpkgs.legacyPackages.${system}); 9 | in { 10 | formatter = forAllSystems (pkgs: pkgs.alejandra); 11 | devShells = forAllSystems (pkgs: { 12 | default = pkgs.mkShell { 13 | packages = with pkgs; [ 14 | corepack 15 | nodejs_22 16 | # For systems that do not ship with Python by default (required by `node-gyp`) 17 | python3 18 | ]; 19 | }; 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /templates/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@template/cli", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "packageManager": "pnpm@9.10.0", 6 | "license": "MIT", 7 | "description": "A basic Effect CLI application", 8 | "repository": { 9 | "type": "git", 10 | "url": "" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "directory": "dist" 15 | }, 16 | "scripts": { 17 | "build": "tsup && pnpm copy-package-json", 18 | "build:ts": "tsup", 19 | "clean": "rimraf dist/*", 20 | "check": "tsc -b tsconfig.json", 21 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 22 | "lint-fix": "pnpm lint --fix", 23 | "test": "vitest run", 24 | "coverage": "vitest run --coverage", 25 | "copy-package-json": "tsx scripts/copy-package-json.ts", 26 | "changeset-version": "changeset version && node scripts/version.mjs", 27 | "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish" 28 | }, 29 | "devDependencies": { 30 | "@changesets/changelog-github": "^0.5.0", 31 | "@changesets/cli": "^2.27.8", 32 | "@effect/cli": "latest", 33 | "@effect/eslint-plugin": "^0.2.0", 34 | "@effect/language-service": "^0.1.0", 35 | "@effect/platform": "latest", 36 | "@effect/platform-node": "latest", 37 | "@effect/vitest": "latest", 38 | "@eslint/compat": "1.1.1", 39 | "@eslint/eslintrc": "3.1.0", 40 | "@eslint/js": "9.10.0", 41 | "@types/node": "^22.5.2", 42 | "@typescript-eslint/eslint-plugin": "^8.4.0", 43 | "@typescript-eslint/parser": "^8.4.0", 44 | "effect": "latest", 45 | "eslint": "^9.10.0", 46 | "eslint-import-resolver-typescript": "^3.6.3", 47 | "eslint-plugin-codegen": "0.28.0", 48 | "eslint-plugin-deprecation": "^3.0.0", 49 | "eslint-plugin-import": "^2.30.0", 50 | "eslint-plugin-simple-import-sort": "^12.1.1", 51 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 52 | "tsup": "^8.2.4", 53 | "tsx": "^4.19.1", 54 | "typescript": "^5.6.2", 55 | "vitest": "^2.0.5" 56 | }, 57 | "pnpm": { 58 | "patchedDependencies": { 59 | "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /templates/cli/patches/@changesets__get-github-info@0.6.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js 2 | index a74df59f8a5988f458a3476087399f5e6dfe4818..ce5e60ef9916eb0cb76ab1e9dd422abcad752bf6 100644 3 | --- a/dist/changesets-get-github-info.cjs.js 4 | +++ b/dist/changesets-get-github-info.cjs.js 5 | @@ -251,18 +251,13 @@ async function getInfo(request) { 6 | b = new Date(b.mergedAt); 7 | return a > b ? 1 : a < b ? -1 : 0; 8 | })[0] : null; 9 | - 10 | - if (associatedPullRequest) { 11 | - user = associatedPullRequest.author; 12 | - } 13 | - 14 | return { 15 | user: user ? user.login : null, 16 | pull: associatedPullRequest ? associatedPullRequest.number : null, 17 | links: { 18 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 19 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 20 | - user: user ? `[@${user.login}](${user.url})` : null 21 | + user: user ? `@${user.login}` : null 22 | } 23 | }; 24 | } 25 | diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js 26 | index 27e5c972ab1202ff16f5124b471f4bbcc46be2b5..3940a8fe86e10cb46d8ff6436dea1103b1839927 100644 27 | --- a/dist/changesets-get-github-info.esm.js 28 | +++ b/dist/changesets-get-github-info.esm.js 29 | @@ -242,18 +242,13 @@ async function getInfo(request) { 30 | b = new Date(b.mergedAt); 31 | return a > b ? 1 : a < b ? -1 : 0; 32 | })[0] : null; 33 | - 34 | - if (associatedPullRequest) { 35 | - user = associatedPullRequest.author; 36 | - } 37 | - 38 | return { 39 | user: user ? user.login : null, 40 | pull: associatedPullRequest ? associatedPullRequest.number : null, 41 | links: { 42 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 43 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 44 | - user: user ? `[@${user.login}](${user.url})` : null 45 | + user: user ? `@${user.login}` : null 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /templates/cli/scripts/copy-package-json.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem, Path } from "@effect/platform" 2 | import { NodeContext } from "@effect/platform-node" 3 | import { Effect } from "effect" 4 | 5 | const program = Effect.gen(function*() { 6 | const fs = yield* FileSystem.FileSystem 7 | const path = yield* Path.Path 8 | yield* Effect.log("[Build] Copying package.json ...") 9 | const json: any = yield* fs.readFileString("package.json").pipe(Effect.map(JSON.parse)) 10 | const pkg = { 11 | name: json.name, 12 | version: json.version, 13 | type: json.type, 14 | description: json.description, 15 | main: "bin.cjs", 16 | bin: "bin.cjs", 17 | engines: json.engines, 18 | dependencies: json.dependencies, 19 | peerDependencies: json.peerDependencies, 20 | repository: json.repository, 21 | author: json.author, 22 | license: json.license, 23 | bugs: json.bugs, 24 | homepage: json.homepage, 25 | tags: json.tags, 26 | keywords: json.keywords 27 | } 28 | yield* fs.writeFileString(path.join("dist", "package.json"), JSON.stringify(pkg, null, 2)) 29 | yield* Effect.log("[Build] Build completed.") 30 | }).pipe(Effect.provide(NodeContext.layer)) 31 | 32 | Effect.runPromise(program).catch(console.error) 33 | -------------------------------------------------------------------------------- /templates/cli/src/Cli.ts: -------------------------------------------------------------------------------- 1 | import * as Command from "@effect/cli/Command" 2 | 3 | const command = Command.make("hello") 4 | 5 | export const run = Command.run(command, { 6 | name: "Hello World", 7 | version: "0.0.0" 8 | }) 9 | -------------------------------------------------------------------------------- /templates/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as NodeContext from "@effect/platform-node/NodeContext" 4 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime" 5 | import * as Effect from "effect/Effect" 6 | import { run } from "./Cli.js" 7 | 8 | run(process.argv).pipe( 9 | Effect.provide(NodeContext.layer), 10 | NodeRuntime.runMain({ disableErrorReporting: true }) 11 | ) 12 | -------------------------------------------------------------------------------- /templates/cli/test/Dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /templates/cli/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [], 3 | "compilerOptions": { 4 | "strict": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "exactOptionalPropertyTypes": true, 13 | "emitDecoratorMetadata": false, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "NodeNext", 16 | "lib": ["ES2022", "DOM"], 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "declarationMap": true, 20 | "noImplicitReturns": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmitOnError": false, 25 | "noErrorTruncation": false, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "stripInternal": true, 30 | "noImplicitAny": true, 31 | "noImplicitThis": true, 32 | "noUncheckedIndexedAccess": false, 33 | "strictNullChecks": true, 34 | "baseUrl": ".", 35 | "target": "ES2022", 36 | "module": "NodeNext", 37 | "incremental": true, 38 | "removeComments": false, 39 | "plugins": [{ "name": "@effect/language-service" }], 40 | "paths": { 41 | "@template/cli": ["./src/index.js"], 42 | "@template/cli/*": ["./src/*.js"] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "references": [ 4 | { "path": "tsconfig.src.json" }, 5 | { "path": "tsconfig.test.json" }, 6 | { "path": "tsconfig.scripts.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/cli/tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "scripts", 5 | "eslint.config.mjs", 6 | "tsup.config.ts", 7 | "vitest.config.ts" 8 | ], 9 | "compilerOptions": { 10 | "types": [ 11 | "node" 12 | ], 13 | "tsBuildInfoFile": ".tsbuildinfo/scripts.tsbuildinfo", 14 | "rootDir": ".", 15 | "noEmit": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /templates/cli/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 7 | "rootDir": "src", 8 | "noEmit": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/cli/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["test"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 7 | "rootDir": "test", 8 | "noEmit": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/bin.ts"], 5 | clean: true, 6 | publicDir: true, 7 | treeshake: "smallest", 8 | external: ["@parcel/watcher"] 9 | }) 10 | -------------------------------------------------------------------------------- /templates/cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["./test/**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], 6 | exclude: [], 7 | globals: true, 8 | coverage: { 9 | provider: "v8" 10 | } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /templates/monorepo/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "restricted", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /templates/monorepo/.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /templates/monorepo/.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Perform standard setup and install dependencies using pnpm. 3 | inputs: 4 | node-version: 5 | description: The version of Node.js to install 6 | required: true 7 | default: 20.14.0 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v3 14 | - name: Install node 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: pnpm 18 | node-version: ${{ inputs.node-version }} 19 | - name: Install dependencies 20 | shell: bash 21 | run: pnpm install 22 | -------------------------------------------------------------------------------- /templates/monorepo/.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | build: 17 | name: Build 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 10 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install dependencies 23 | uses: ./.github/actions/setup 24 | - run: pnpm codegen 25 | - name: Check source state 26 | run: git add packages/*/src && git diff-index --cached HEAD --exit-code packages/*/src 27 | 28 | types: 29 | name: Types 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 10 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Install dependencies 35 | uses: ./.github/actions/setup 36 | - run: pnpm check 37 | 38 | lint: 39 | name: Lint 40 | runs-on: ubuntu-latest 41 | timeout-minutes: 10 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Install dependencies 45 | uses: ./.github/actions/setup 46 | - run: pnpm lint 47 | 48 | test: 49 | name: Test 50 | runs-on: ubuntu-latest 51 | timeout-minutes: 10 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Install dependencies 55 | uses: ./.github/actions/setup 56 | - run: pnpm vitest 57 | env: 58 | NODE_OPTIONS: --max_old_space_size=8192 59 | -------------------------------------------------------------------------------- /templates/monorepo/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | permissions: 17 | contents: write 18 | id-token: write 19 | pull-requests: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install dependencies 23 | uses: ./.github/actions/setup 24 | - name: Create Release Pull Request or Publish 25 | uses: changesets/action@v1 26 | with: 27 | version: pnpm changeset-version 28 | publish: pnpm changeset-publish 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /templates/monorepo/.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | on: 3 | pull_request: 4 | branches: [main] 5 | workflow_dispatch: 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | snapshot: 11 | name: Snapshot 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install dependencies 17 | uses: ./.github/actions/setup 18 | - name: Build package 19 | run: pnpm build 20 | - name: Create snapshot 21 | id: snapshot 22 | run: pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off ./packages/* 23 | -------------------------------------------------------------------------------- /templates/monorepo/.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.tsbuildinfo 3 | node_modules/ 4 | .DS_Store 5 | tmp/ 6 | dist/ 7 | build/ 8 | docs/ 9 | scratchpad/* 10 | !scratchpad/tsconfig.json 11 | .direnv/ 12 | .idea/ 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | -------------------------------------------------------------------------------- /templates/monorepo/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "effectful-tech.effect-vscode", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /templates/monorepo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "typescript.enablePromptUseWorkspaceTsdk": true, 5 | "editor.formatOnSave": true, 6 | "eslint.format.enable": true, 7 | "[json]": { 8 | "editor.defaultFormatter": "vscode.json-language-features" 9 | }, 10 | "[markdown]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "prettier.semi": false, 13 | "prettier.trailingComma": "none" 14 | }, 15 | "[javascript]": { 16 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 17 | }, 18 | "[javascriptreact]": { 19 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 20 | }, 21 | "[typescript]": { 22 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 23 | }, 24 | "[typescriptreact]": { 25 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 26 | }, 27 | "eslint.validate": ["markdown", "javascript", "typescript"], 28 | "editor.codeActionsOnSave": { 29 | "source.fixAll.eslint": "explicit" 30 | }, 31 | "editor.quickSuggestions": { 32 | "other": true, 33 | "comments": false, 34 | "strings": false 35 | }, 36 | "editor.acceptSuggestionOnCommitCharacter": true, 37 | "editor.acceptSuggestionOnEnter": "on", 38 | "editor.quickSuggestionsDelay": 10, 39 | "editor.suggestOnTriggerCharacters": true, 40 | "editor.tabCompletion": "off", 41 | "editor.suggest.localityBonus": true, 42 | "editor.suggestSelection": "recentlyUsed", 43 | "editor.wordBasedSuggestions": "matchingDocuments", 44 | "editor.parameterHints.enabled": true, 45 | "files.insertFinalNewline": true 46 | } 47 | -------------------------------------------------------------------------------- /templates/monorepo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/monorepo/README.md: -------------------------------------------------------------------------------- 1 | # Effect Monorepo Template 2 | 3 | This template provides a solid foundation for building scalable and maintainable TypeScript applications with Effect. 4 | 5 | ## Running Code 6 | 7 | This template leverages [tsx](https://tsx.is) to allow execution of TypeScript files via NodeJS as if they were written in plain JavaScript. 8 | 9 | To execute a file with `tsx`: 10 | 11 | ```sh 12 | pnpm tsx ./path/to/the/file.ts 13 | ``` 14 | 15 | ## Operations 16 | 17 | **Building** 18 | 19 | To build all packages in the monorepo: 20 | 21 | ```sh 22 | pnpm build 23 | ``` 24 | 25 | **Testing** 26 | 27 | To test all packages in the monorepo: 28 | 29 | ```sh 30 | pnpm test 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /templates/monorepo/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": ["error", { 65 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 66 | message: "Do not use spread arguments in Array.push" 67 | }], 68 | 69 | "no-unused-vars": "off", 70 | "prefer-rest-params": "off", 71 | "prefer-spread": "off", 72 | "import/first": "error", 73 | "import/newline-after-import": "error", 74 | "import/no-duplicates": "error", 75 | "import/no-unresolved": "off", 76 | "import/order": "off", 77 | "simple-import-sort/imports": "off", 78 | "sort-destructure-keys/sort-destructure-keys": "error", 79 | "deprecation/deprecation": "off", 80 | 81 | "@typescript-eslint/array-type": ["warn", { 82 | default: "generic", 83 | readonly: "generic" 84 | }], 85 | 86 | "@typescript-eslint/member-delimiter-style": 0, 87 | "@typescript-eslint/no-non-null-assertion": "off", 88 | "@typescript-eslint/ban-types": "off", 89 | "@typescript-eslint/no-explicit-any": "off", 90 | "@typescript-eslint/no-empty-interface": "off", 91 | "@typescript-eslint/consistent-type-imports": "warn", 92 | 93 | "@typescript-eslint/no-unused-vars": ["error", { 94 | argsIgnorePattern: "^_", 95 | varsIgnorePattern: "^_" 96 | }], 97 | 98 | "@typescript-eslint/ban-ts-comment": "off", 99 | "@typescript-eslint/camelcase": "off", 100 | "@typescript-eslint/explicit-function-return-type": "off", 101 | "@typescript-eslint/explicit-module-boundary-types": "off", 102 | "@typescript-eslint/interface-name-prefix": "off", 103 | "@typescript-eslint/no-array-constructor": "off", 104 | "@typescript-eslint/no-use-before-define": "off", 105 | "@typescript-eslint/no-namespace": "off", 106 | 107 | "@effect/dprint": ["error", { 108 | config: { 109 | indentWidth: 2, 110 | lineWidth: 120, 111 | semiColons: "asi", 112 | quoteStyle: "alwaysDouble", 113 | trailingCommas: "never", 114 | operatorPosition: "maintain", 115 | "arrowFunction.useParentheses": "force" 116 | } 117 | }] 118 | } 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /templates/monorepo/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1730272153, 6 | "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /templates/monorepo/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | outputs = 6 | { nixpkgs, ... }: 7 | let 8 | forAllSystems = 9 | function: 10 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 11 | system: function nixpkgs.legacyPackages.${system} 12 | ); 13 | in 14 | { 15 | formatter = forAllSystems (pkgs: pkgs.alejandra); 16 | devShells = forAllSystems (pkgs: { 17 | default = pkgs.mkShell { 18 | packages = with pkgs; [ 19 | corepack 20 | nodejs 21 | # For systems that do not ship with Python by default (required by `node-gyp`) 22 | python3 23 | ]; 24 | }; 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /templates/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "packageManager": "pnpm@9.10.0", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "clean": "node scripts/clean.mjs", 10 | "codegen": "pnpm --recursive --parallel run codegen", 11 | "build": "tsc -b tsconfig.build.json && pnpm --recursive --parallel run build", 12 | "check": "tsc -b tsconfig.json", 13 | "check-recursive": "pnpm --recursive exec tsc -b tsconfig.json", 14 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 15 | "lint-fix": "pnpm lint --fix", 16 | "test": "vitest", 17 | "coverage": "vitest --coverage", 18 | "changeset-version": "changeset version && node scripts/version.mjs", 19 | "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.25.9", 23 | "@babel/core": "^7.26.0", 24 | "@babel/plugin-transform-export-namespace-from": "^7.25.9", 25 | "@babel/plugin-transform-modules-commonjs": "^7.25.9", 26 | "@changesets/changelog-github": "^0.5.0", 27 | "@changesets/cli": "^2.27.8", 28 | "@effect/build-utils": "^0.7.7", 29 | "@effect/eslint-plugin": "^0.2.0", 30 | "@effect/language-service": "^0.2.0", 31 | "@effect/vitest": "latest", 32 | "@eslint/compat": "1.2.2", 33 | "@eslint/eslintrc": "3.1.0", 34 | "@eslint/js": "9.13.0", 35 | "@types/node": "^22.8.5", 36 | "@typescript-eslint/eslint-plugin": "^8.12.2", 37 | "@typescript-eslint/parser": "^8.12.2", 38 | "babel-plugin-annotate-pure-calls": "^0.4.0", 39 | "effect": "^3.10.7", 40 | "eslint": "^9.13.0", 41 | "eslint-import-resolver-typescript": "^3.6.3", 42 | "eslint-plugin-codegen": "^0.29.0", 43 | "eslint-plugin-import": "^2.31.0", 44 | "eslint-plugin-simple-import-sort": "^12.1.1", 45 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 46 | "glob": "^11.0.0", 47 | "tsx": "^4.19.2", 48 | "typescript": "^5.6.3", 49 | "vitest": "^2.1.4" 50 | }, 51 | "pnpm": { 52 | "overrides": { 53 | "vitest": "^2.0.5" 54 | }, 55 | "patchedDependencies": { 56 | "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch", 57 | "@changesets/assemble-release-plan@6.0.5": "patches/@changesets__assemble-release-plan@6.0.5.patch", 58 | "babel-plugin-annotate-pure-calls@0.4.0": "patches/babel-plugin-annotate-pure-calls@0.4.0.patch" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # Effect Monorepo Template - CLI Package -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@template/cli", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "description": "The CLI template", 7 | "repository": { 8 | "type": "git", 9 | "url": "", 10 | "directory": "packages/cli" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "directory": "dist" 15 | }, 16 | "scripts": { 17 | "codegen": "build-utils prepare-v2", 18 | "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", 19 | "build-esm": "tsc -b tsconfig.build.json", 20 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 21 | "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", 22 | "check": "tsc -b tsconfig.json", 23 | "test": "vitest", 24 | "coverage": "vitest --coverage" 25 | }, 26 | "dependencies": { 27 | "@effect/cli": "latest", 28 | "@effect/platform": "latest", 29 | "@effect/platform-node": "latest", 30 | "@template/domain": "workspace:^", 31 | "effect": "latest" 32 | }, 33 | "devDependencies": { 34 | "@effect/cli": "latest", 35 | "@effect/platform": "latest", 36 | "@effect/platform-node": "latest", 37 | "@template/domain": "workspace:^", 38 | "effect": "latest" 39 | }, 40 | "effect": { 41 | "generateExports": { 42 | "include": [ 43 | "**/*.ts" 44 | ] 45 | }, 46 | "generateIndex": { 47 | "include": [ 48 | "**/*.ts" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/src/Cli.ts: -------------------------------------------------------------------------------- 1 | import { Args, Command, Options } from "@effect/cli" 2 | import { TodosClient } from "./TodosClient.js" 3 | 4 | const todoArg = Args.text({ name: "todo" }).pipe( 5 | Args.withDescription("The message associated with a todo") 6 | ) 7 | 8 | const todoId = Options.integer("id").pipe( 9 | Options.withDescription("The identifier of the todo") 10 | ) 11 | 12 | const add = Command.make("add", { todo: todoArg }).pipe( 13 | Command.withDescription("Add a new todo"), 14 | Command.withHandler(({ todo }) => TodosClient.create(todo)) 15 | ) 16 | 17 | const done = Command.make("done", { id: todoId }).pipe( 18 | Command.withDescription("Mark a todo as done"), 19 | Command.withHandler(({ id }) => TodosClient.complete(id)) 20 | ) 21 | 22 | const list = Command.make("list").pipe( 23 | Command.withDescription("List all todos"), 24 | Command.withHandler(() => TodosClient.list) 25 | ) 26 | 27 | const remove = Command.make("remove", { id: todoId }).pipe( 28 | Command.withDescription("Remove a todo"), 29 | Command.withHandler(({ id }) => TodosClient.remove(id)) 30 | ) 31 | 32 | const command = Command.make("todo").pipe( 33 | Command.withSubcommands([add, done, list, remove]) 34 | ) 35 | 36 | export const cli = Command.run(command, { 37 | name: "Todo CLI", 38 | version: "0.0.0" 39 | }) 40 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/src/TodosClient.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiClient } from "@effect/platform" 2 | import { TodosApi } from "@template/domain/TodosApi" 3 | import { Effect } from "effect" 4 | 5 | export class TodosClient extends Effect.Service()("cli/TodosClient", { 6 | accessors: true, 7 | effect: Effect.gen(function*() { 8 | const client = yield* HttpApiClient.make(TodosApi, { 9 | baseUrl: "http://localhost:3000" 10 | }) 11 | 12 | function create(text: string) { 13 | return client.todos.createTodo({ payload: { text } }).pipe( 14 | Effect.flatMap((todo) => Effect.logInfo("Created todo: ", todo)) 15 | ) 16 | } 17 | 18 | const list = client.todos.getAllTodos().pipe( 19 | Effect.flatMap((todos) => Effect.logInfo(todos)) 20 | ) 21 | 22 | function complete(id: number) { 23 | return client.todos.completeTodo({ path: { id } }).pipe( 24 | Effect.flatMap((todo) => Effect.logInfo("Marked todo completed: ", todo)), 25 | Effect.catchTag("TodoNotFound", () => Effect.logError(`Failed to find todo with id: ${id}`)) 26 | ) 27 | } 28 | 29 | function remove(id: number) { 30 | return client.todos.removeTodo({ path: { id } }).pipe( 31 | Effect.flatMap(() => Effect.logInfo(`Deleted todo with id: ${id}`)), 32 | Effect.catchTag("TodoNotFound", () => Effect.logError(`Failed to find todo with id: ${id}`)) 33 | ) 34 | } 35 | 36 | return { 37 | create, 38 | list, 39 | complete, 40 | remove 41 | } as const 42 | }) 43 | }) {} 44 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { NodeContext, NodeHttpClient, NodeRuntime } from "@effect/platform-node" 4 | import { Effect, Layer } from "effect" 5 | import { cli } from "./Cli.js" 6 | import { TodosClient } from "./TodosClient.js" 7 | 8 | const MainLive = TodosClient.Default.pipe( 9 | Layer.provide(NodeHttpClient.layerUndici), 10 | Layer.merge(NodeContext.layer) 11 | ) 12 | 13 | cli(process.argv).pipe( 14 | Effect.provide(MainLive), 15 | NodeRuntime.runMain 16 | ) 17 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/test/Dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "references": [ 4 | { "path": "../domain/tsconfig.build.json" } 5 | ], 6 | "compilerOptions": { 7 | "types": ["node"], 8 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 9 | "outDir": "build/esm", 10 | "declarationDir": "build/dts", 11 | "stripInternal": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "references": [ 5 | { "path": "../domain" } 6 | ], 7 | "compilerOptions": { 8 | "types": ["node"], 9 | "outDir": "build/src", 10 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 11 | "rootDir": "src" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "../domain" } 7 | ], 8 | "compilerOptions": { 9 | "types": ["node"], 10 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 11 | "rootDir": "test", 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/monorepo/packages/cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/README.md: -------------------------------------------------------------------------------- 1 | # Effect Monorepo Template - Domain Package -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@template/domain", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "description": "The domain template", 7 | "repository": { 8 | "type": "git", 9 | "url": "", 10 | "directory": "packages/domain" 11 | }, 12 | "publishConfig": { 13 | "access": "public", 14 | "directory": "dist" 15 | }, 16 | "scripts": { 17 | "codegen": "build-utils prepare-v2", 18 | "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", 19 | "build-esm": "tsc -b tsconfig.build.json", 20 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 21 | "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", 22 | "check": "tsc -b tsconfig.json", 23 | "test": "vitest", 24 | "coverage": "vitest --coverage" 25 | }, 26 | "dependencies": { 27 | "@effect/platform": "latest", 28 | "@effect/sql": "latest", 29 | "effect": "latest" 30 | }, 31 | "effect": { 32 | "generateExports": { 33 | "include": [ 34 | "**/*.ts" 35 | ] 36 | }, 37 | "generateIndex": { 38 | "include": [ 39 | "**/*.ts" 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/src/TodosApi.ts: -------------------------------------------------------------------------------- 1 | import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" 2 | import { Schema } from "effect" 3 | 4 | export const TodoId = Schema.Number.pipe(Schema.brand("TodoId")) 5 | export type TodoId = typeof TodoId.Type 6 | 7 | export const TodoIdFromString = Schema.NumberFromString.pipe( 8 | Schema.compose(TodoId) 9 | ) 10 | 11 | export class Todo extends Schema.Class("Todo")({ 12 | id: TodoId, 13 | text: Schema.NonEmptyTrimmedString, 14 | done: Schema.Boolean 15 | }) {} 16 | 17 | export class TodoNotFound extends Schema.TaggedError()("TodoNotFound", { 18 | id: Schema.Number 19 | }) {} 20 | 21 | export class TodosApiGroup extends HttpApiGroup.make("todos") 22 | .add(HttpApiEndpoint.get("getAllTodos", "/todos").addSuccess(Schema.Array(Todo))) 23 | .add( 24 | HttpApiEndpoint.get("getTodoById", "/todos/:id") 25 | .addSuccess(Todo) 26 | .addError(TodoNotFound, { status: 404 }) 27 | .setPath(Schema.Struct({ id: Schema.NumberFromString })) 28 | ) 29 | .add( 30 | HttpApiEndpoint.post("createTodo", "/todos") 31 | .addSuccess(Todo) 32 | .setPayload(Schema.Struct({ text: Schema.NonEmptyTrimmedString })) 33 | ) 34 | .add( 35 | HttpApiEndpoint.patch("completeTodo", "/todos/:id") 36 | .addSuccess(Todo) 37 | .addError(TodoNotFound, { status: 404 }) 38 | .setPath(Schema.Struct({ id: Schema.NumberFromString })) 39 | ) 40 | .add( 41 | HttpApiEndpoint.del("removeTodo", "/todos/:id") 42 | .addSuccess(Schema.Void) 43 | .addError(TodoNotFound, { status: 404 }) 44 | .setPath(Schema.Struct({ id: Schema.NumberFromString })) 45 | ) 46 | {} 47 | 48 | export class TodosApi extends HttpApi.make("api").add(TodosApiGroup) {} 49 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/test/Dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 6 | "outDir": "build/esm", 7 | "declarationDir": "build/dts", 8 | "stripInternal": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "outDir": "build/src", 7 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 8 | "rootDir": "src" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [ 5 | { "path": "tsconfig.src.json" } 6 | ], 7 | "compilerOptions": { 8 | "types": ["node"], 9 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 10 | "rootDir": "test", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/monorepo/packages/domain/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/README.md: -------------------------------------------------------------------------------- 1 | # Effect Monorepo Template - Server Package -------------------------------------------------------------------------------- /templates/monorepo/packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@template/server", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "description": "The server template", 7 | "repository": { 8 | "type": "git", 9 | "url": "", 10 | "directory": "packages/server" 11 | }, 12 | "scripts": { 13 | "codegen": "build-utils prepare-v2", 14 | "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2", 15 | "build-esm": "tsc -b tsconfig.build.json", 16 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 17 | "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", 18 | "check": "tsc -b tsconfig.json", 19 | "test": "vitest", 20 | "coverage": "vitest --coverage" 21 | }, 22 | "dependencies": { 23 | "@effect/platform": "latest", 24 | "@effect/platform-node": "latest", 25 | "@template/domain": "workspace:^", 26 | "effect": "latest" 27 | }, 28 | "effect": { 29 | "generateExports": { 30 | "include": [ 31 | "**/*.ts" 32 | ] 33 | }, 34 | "generateIndex": { 35 | "include": [ 36 | "**/*.ts" 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/src/Api.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder } from "@effect/platform" 2 | import { TodosApi } from "@template/domain/TodosApi" 3 | import { Effect, Layer } from "effect" 4 | import { TodosRepository } from "./TodosRepository.js" 5 | 6 | const TodosApiLive = HttpApiBuilder.group(TodosApi, "todos", (handlers) => 7 | Effect.gen(function*() { 8 | const todos = yield* TodosRepository 9 | return handlers 10 | .handle("getAllTodos", () => todos.getAll) 11 | .handle("getTodoById", ({ path: { id } }) => todos.getById(id)) 12 | .handle("createTodo", ({ payload: { text } }) => todos.create(text)) 13 | .handle("completeTodo", ({ path: { id } }) => todos.complete(id)) 14 | .handle("removeTodo", ({ path: { id } }) => todos.remove(id)) 15 | })) 16 | 17 | export const ApiLive = HttpApiBuilder.api(TodosApi).pipe( 18 | Layer.provide(TodosApiLive) 19 | ) 20 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/src/TodosRepository.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoId, TodoNotFound } from "@template/domain/TodosApi" 2 | import { Effect, HashMap, Ref } from "effect" 3 | 4 | export class TodosRepository extends Effect.Service()("api/TodosRepository", { 5 | effect: Effect.gen(function*() { 6 | const todos = yield* Ref.make(HashMap.empty()) 7 | 8 | const getAll = Ref.get(todos).pipe( 9 | Effect.map((todos) => Array.from(HashMap.values(todos))) 10 | ) 11 | 12 | function getById(id: number): Effect.Effect { 13 | return Ref.get(todos).pipe( 14 | Effect.flatMap(HashMap.get(id)), 15 | Effect.catchTag("NoSuchElementException", () => new TodoNotFound({ id })) 16 | ) 17 | } 18 | 19 | function create(text: string): Effect.Effect { 20 | return Ref.modify(todos, (map) => { 21 | const id = TodoId.make(HashMap.reduce(map, 0, (max, todo) => todo.id > max ? todo.id : max)) 22 | const todo = new Todo({ id, text, done: false }) 23 | return [todo, HashMap.set(map, id, todo)] 24 | }) 25 | } 26 | 27 | function complete(id: number): Effect.Effect { 28 | return getById(id).pipe( 29 | Effect.map((todo) => new Todo({ ...todo, done: true })), 30 | Effect.tap((todo) => Ref.update(todos, HashMap.set(todo.id, todo))) 31 | ) 32 | } 33 | 34 | function remove(id: number): Effect.Effect { 35 | return getById(id).pipe( 36 | Effect.flatMap((todo) => Ref.update(todos, HashMap.remove(todo.id))) 37 | ) 38 | } 39 | 40 | return { 41 | getAll, 42 | getById, 43 | create, 44 | complete, 45 | remove 46 | } as const 47 | }) 48 | }) {} 49 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { HttpApiBuilder, HttpMiddleware } from "@effect/platform" 2 | import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" 3 | import { Layer } from "effect" 4 | import { createServer } from "node:http" 5 | import { ApiLive } from "./Api.js" 6 | import { TodosRepository } from "./TodosRepository.js" 7 | 8 | const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( 9 | Layer.provide(ApiLive), 10 | Layer.provide(TodosRepository.Default), 11 | Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) 12 | ) 13 | 14 | Layer.launch(HttpLive).pipe( 15 | NodeRuntime.runMain 16 | ) 17 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/test/Dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | 3 | describe("Dummy", () => { 4 | it("should pass", () => { 5 | expect(true).toBe(true) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "references": [ 4 | { "path": "../domain/tsconfig.build.json" } 5 | ], 6 | "compilerOptions": { 7 | "types": ["node"], 8 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 9 | "outDir": "build/esm", 10 | "declarationDir": "build/dts", 11 | "stripInternal": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "references": [ 5 | { "path": "../domain" } 6 | ], 7 | "compilerOptions": { 8 | "types": ["node"], 9 | "outDir": "build/src", 10 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 11 | "rootDir": "src" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "../domain" } 7 | ], 8 | "compilerOptions": { 9 | "types": ["node"], 10 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 11 | "rootDir": "test", 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/monorepo/packages/server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /templates/monorepo/patches/@changesets__assemble-release-plan@6.0.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js 2 | index 4f7b5e5b37bb05874a5c1d8e583e29d4a9593ecf..980ead81554a8d925b6366e1644f3be03765398e 100644 3 | --- a/dist/changesets-assemble-release-plan.cjs.js 4 | +++ b/dist/changesets-assemble-release-plan.cjs.js 5 | @@ -237,7 +237,7 @@ function determineDependents({ 6 | preInfo, 7 | onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 8 | })) { 9 | - type = "major"; 10 | + type = "minor"; 11 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange))) { 12 | switch (depType) { 13 | case "dependencies": 14 | diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js 15 | index a327d9e4c709a6698f505d60d8bbf0046d4bde74..eb00fec7262280fb4876165c942212abc6b25efb 100644 16 | --- a/dist/changesets-assemble-release-plan.esm.js 17 | +++ b/dist/changesets-assemble-release-plan.esm.js 18 | @@ -226,7 +226,7 @@ function determineDependents({ 19 | preInfo, 20 | onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 21 | })) { 22 | - type = "major"; 23 | + type = "minor"; 24 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange))) { 25 | switch (depType) { 26 | case "dependencies": 27 | -------------------------------------------------------------------------------- /templates/monorepo/patches/@changesets__get-github-info@0.6.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js 2 | index a74df59f8a5988f458a3476087399f5e6dfe4818..ce5e60ef9916eb0cb76ab1e9dd422abcad752bf6 100644 3 | --- a/dist/changesets-get-github-info.cjs.js 4 | +++ b/dist/changesets-get-github-info.cjs.js 5 | @@ -251,18 +251,13 @@ async function getInfo(request) { 6 | b = new Date(b.mergedAt); 7 | return a > b ? 1 : a < b ? -1 : 0; 8 | })[0] : null; 9 | - 10 | - if (associatedPullRequest) { 11 | - user = associatedPullRequest.author; 12 | - } 13 | - 14 | return { 15 | user: user ? user.login : null, 16 | pull: associatedPullRequest ? associatedPullRequest.number : null, 17 | links: { 18 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 19 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 20 | - user: user ? `[@${user.login}](${user.url})` : null 21 | + user: user ? `@${user.login}` : null 22 | } 23 | }; 24 | } 25 | diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js 26 | index 27e5c972ab1202ff16f5124b471f4bbcc46be2b5..3940a8fe86e10cb46d8ff6436dea1103b1839927 100644 27 | --- a/dist/changesets-get-github-info.esm.js 28 | +++ b/dist/changesets-get-github-info.esm.js 29 | @@ -242,18 +242,13 @@ async function getInfo(request) { 30 | b = new Date(b.mergedAt); 31 | return a > b ? 1 : a < b ? -1 : 0; 32 | })[0] : null; 33 | - 34 | - if (associatedPullRequest) { 35 | - user = associatedPullRequest.author; 36 | - } 37 | - 38 | return { 39 | user: user ? user.login : null, 40 | pull: associatedPullRequest ? associatedPullRequest.number : null, 41 | links: { 42 | commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, 43 | pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, 44 | - user: user ? `[@${user.login}](${user.url})` : null 45 | + user: user ? `@${user.login}` : null 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /templates/monorepo/patches/babel-plugin-annotate-pure-calls@0.4.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/index.js b/lib/index.js 2 | index 2182884e21874ebb37261e2375eec08ad956fc9a..ef5630199121c2830756e00c7cc48cf1078c8207 100644 3 | --- a/lib/index.js 4 | +++ b/lib/index.js 5 | @@ -78,7 +78,7 @@ const isInAssignmentContext = path => { 6 | 7 | parentPath = _ref.parentPath; 8 | 9 | - if (parentPath.isVariableDeclaration() || parentPath.isAssignmentExpression()) { 10 | + if (parentPath.isVariableDeclaration() || parentPath.isAssignmentExpression() || parentPath.isClassDeclaration()) { 11 | return true; 12 | } 13 | } while (parentPath !== statement); 14 | -------------------------------------------------------------------------------- /templates/monorepo/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /templates/monorepo/scratchpad/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "declaration": false, 6 | "declarationMap": false, 7 | "composite": false, 8 | "incremental": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/monorepo/scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import * as Glob from "glob" 2 | import * as Fs from "node:fs" 3 | 4 | const dirs = [".", ...Glob.sync("packages/*/")] 5 | dirs.forEach((pkg) => { 6 | const files = [".tsbuildinfo", "build", "dist", "coverage"] 7 | 8 | files.forEach((file) => { 9 | Fs.rmSync(`${pkg}/${file}`, { recursive: true, force: true }, () => {}) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /templates/monorepo/setupTests.ts: -------------------------------------------------------------------------------- 1 | import * as it from "@effect/vitest" 2 | 3 | it.addEqualityTesters() 4 | -------------------------------------------------------------------------------- /templates/monorepo/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "NodeNext", 15 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 16 | "types": [], 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "declarationMap": true, 20 | "noImplicitReturns": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmitOnError": false, 25 | "noErrorTruncation": false, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "noImplicitAny": true, 30 | "noImplicitThis": true, 31 | "noUncheckedIndexedAccess": false, 32 | "strictNullChecks": true, 33 | "baseUrl": ".", 34 | "target": "ES2022", 35 | "module": "NodeNext", 36 | "incremental": true, 37 | "removeComments": false, 38 | "plugins": [{ "name": "@effect/language-service" }], 39 | "paths": { 40 | "@template/cli": ["./packages/cli/src/index.js"], 41 | "@template/cli/*": ["./packages/cli/src/*.js"], 42 | "@template/cli/test/*": ["./packages/cli/test/*.js"], 43 | "@template/domain": ["./packages/domain/src/index.js"], 44 | "@template/domain/*": ["./packages/domain/src/*.js"], 45 | "@template/domain/test/*": ["./packages/domain/test/*.js"], 46 | "@template/server": ["./packages/server/src/index.js"], 47 | "@template/server/*": ["./packages/server/src/*.js"], 48 | "@template/server/test/*": ["./packages/server/test/*.js"] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /templates/monorepo/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "packages/cli/tsconfig.build.json" }, 6 | { "path": "packages/domain/tsconfig.build.json" }, 7 | { "path": "packages/server/tsconfig.build.json" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /templates/monorepo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "packages/cli" }, 6 | { "path": "packages/domain" }, 7 | { "path": "packages/server" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /templates/monorepo/vitest.shared.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import type { UserConfig } from "vitest/config" 3 | 4 | const alias = (name: string) => { 5 | const target = process.env.TEST_DIST !== undefined ? "dist/dist/esm" : "src" 6 | return ({ 7 | [`${name}/test`]: path.join(__dirname, "packages", name, "test"), 8 | [`${name}`]: path.join(__dirname, "packages", name, target) 9 | }) 10 | } 11 | 12 | // This is a workaround, see https://github.com/vitest-dev/vitest/issues/4744 13 | const config: UserConfig = { 14 | esbuild: { 15 | target: "es2020" 16 | }, 17 | optimizeDeps: { 18 | exclude: ["bun:sqlite"] 19 | }, 20 | test: { 21 | setupFiles: [path.join(__dirname, "setupTests.ts")], 22 | fakeTimers: { 23 | toFake: undefined 24 | }, 25 | sequence: { 26 | concurrent: true 27 | }, 28 | include: ["test/**/*.test.ts"], 29 | alias: { 30 | ...alias("cli"), 31 | ...alias("domain"), 32 | ...alias("server") 33 | } 34 | } 35 | } 36 | 37 | export default config 38 | -------------------------------------------------------------------------------- /templates/monorepo/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import { defineWorkspace, type UserWorkspaceConfig } from "vitest/config" 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const project = ( 6 | config: UserWorkspaceConfig["test"] & { name: `${string}|${string}` }, 7 | root = config.root ?? path.join(__dirname, `packages/${config.name.split("|").at(0)}`) 8 | ) => ({ 9 | extends: "vitest.shared.ts", 10 | test: { root, ...config } 11 | }) 12 | 13 | export default defineWorkspace([ 14 | // Add specialized configuration for some packages. 15 | // project({ name: "my-package|browser", environment: "happy-dom" }), 16 | // Add the default configuration for all packages. 17 | "packages/*" 18 | ]) 19 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "baseUrl": ".", 5 | "checkJs": false, 6 | "composite": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "downlevelIteration": true, 10 | "emitDecoratorMetadata": true, 11 | "esModuleInterop": false, 12 | "exactOptionalPropertyTypes": true, 13 | "experimentalDecorators": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "lib": ["ES2022"], 18 | "module": "NodeNext", 19 | "moduleDetection": "force", 20 | "moduleResolution": "NodeNext", 21 | "noEmitOnError": false, 22 | "noErrorTruncation": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitAny": true, 25 | "noImplicitReturns": false, 26 | "noImplicitThis": true, 27 | "noUncheckedIndexedAccess": false, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": false, 30 | "removeComments": false, 31 | "resolveJsonModule": true, 32 | "skipLibCheck": true, 33 | "sourceMap": true, 34 | "strict": true, 35 | "strictNullChecks": true, 36 | "target": "ES2022", 37 | "types": [], 38 | "plugins": [{ "name": "@effect/language-service" }] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "packages/create-effect-app" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /vitest.shared.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import type { UserConfig } from "vitest/config" 3 | 4 | const alias = (name: string) => { 5 | const target = process.env.TEST_DIST !== undefined ? "dist/dist/esm" : "src" 6 | return ({ 7 | [`${name}/test`]: path.join(__dirname, "packages", name, "test"), 8 | [`${name}`]: path.join(__dirname, "packages", name, target) 9 | }) 10 | } 11 | 12 | // This is a workaround, see https://github.com/vitest-dev/vitest/issues/4744 13 | const config: UserConfig = { 14 | esbuild: { 15 | target: "es2020" 16 | }, 17 | optimizeDeps: { 18 | exclude: ["bun:sqlite"] 19 | }, 20 | test: { 21 | setupFiles: [path.join(__dirname, "setupTests.ts")], 22 | fakeTimers: { 23 | toFake: undefined 24 | }, 25 | sequence: { 26 | concurrent: true 27 | }, 28 | include: ["test/**/*.test.ts"], 29 | alias: { 30 | ...alias("create-effect-app") 31 | } 32 | } 33 | } 34 | 35 | export default config 36 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import { defineWorkspace, type UserWorkspaceConfig } from "vitest/config" 3 | 4 | // Remaining issues: 5 | // - Random failures (browser): https://github.com/vitest-dev/vitest/issues/4497 6 | // - Alias resolution (browser, has workaround): https://github.com/vitest-dev/vitest/issues/4744 7 | // - Workspace optimization: https://github.com/vitest-dev/vitest/issues/4746 8 | 9 | // TODO: Once https://github.com/vitest-dev/vitest/issues/4497 and https://github.com/vitest-dev/vitest/issues/4746 10 | // are resolved, we can create specialized workspace groups in separate workspace files to better control test groups 11 | // with different dependencies (e.g. playwright browser) in CI. 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | const project = ( 15 | config: UserWorkspaceConfig["test"] & { name: `${string}|${string}` }, 16 | root = config.root ?? path.join(__dirname, `packages/${config.name.split("|").at(0)}`) 17 | ) => ({ 18 | extends: "vitest.shared.ts", 19 | test: { root, ...config } 20 | }) 21 | 22 | export default defineWorkspace([ 23 | // Add specialized configuration for some packages. 24 | // project({ name: "effect|browser", environment: "happy-dom" }), 25 | // project({ name: "schema|browser", environment: "happy-dom" }), 26 | // Add the default configuration for all packages. 27 | "packages/*" 28 | ]) 29 | --------------------------------------------------------------------------------