├── .changeset └── config.json ├── .gitignore ├── LICENSE ├── README.md ├── assets └── banner.png ├── biome.json ├── package.json ├── packages ├── create-fatima │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── bin.ts │ │ ├── lib │ │ │ ├── create-config-file.ts │ │ │ ├── tweaks.ts │ │ │ └── types.ts │ │ ├── templates │ │ │ ├── adapters │ │ │ │ ├── custom │ │ │ │ │ ├── javascript.mdx │ │ │ │ │ └── typescript.mdx │ │ │ │ ├── infisical │ │ │ │ │ ├── javascript.mdx │ │ │ │ │ └── typescript.mdx │ │ │ │ ├── local │ │ │ │ │ ├── javascript.mdx │ │ │ │ │ └── typescript.mdx │ │ │ │ ├── trigger │ │ │ │ │ ├── javascript.mdx │ │ │ │ │ └── typescript.mdx │ │ │ │ └── vercel │ │ │ │ │ ├── javascript.mdx │ │ │ │ │ └── typescript.mdx │ │ │ ├── base │ │ │ │ ├── javascript.mdx │ │ │ │ └── typescript.mdx │ │ │ └── validators │ │ │ │ ├── class-validator │ │ │ │ └── typescript.mdx │ │ │ │ ├── custom │ │ │ │ ├── javascript.mdx │ │ │ │ └── typescript.mdx │ │ │ │ ├── typia │ │ │ │ └── typescript.mdx │ │ │ │ └── zod │ │ │ │ ├── javascript.mdx │ │ │ │ └── typescript.mdx │ │ ├── utils │ │ │ ├── check-package-json.ts │ │ │ ├── get-caller-location.ts │ │ │ ├── get-package-manager.ts │ │ │ ├── has-nested-package.ts │ │ │ ├── logger.ts │ │ │ └── tweak-user-config.ts │ │ └── wizard │ │ │ ├── prompts │ │ │ ├── adapter.ts │ │ │ ├── lang.ts │ │ │ ├── monorepo.ts │ │ │ ├── public.ts │ │ │ └── validator.ts │ │ │ └── wizard.ts │ ├── test │ │ ├── install-modules.test.ts │ │ ├── missing-package-json.test.ts │ │ └── utils │ │ │ └── ephemeral-folder.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.js ├── fatima │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── plugins │ │ └── esbuild-raw-import-plugin.ts │ ├── src │ │ ├── cli │ │ │ ├── actions │ │ │ │ ├── dev.ts │ │ │ │ ├── generate.ts │ │ │ │ ├── reload.ts │ │ │ │ ├── run.ts │ │ │ │ └── validate.ts │ │ │ ├── cli.ts │ │ │ ├── context │ │ │ │ └── env.ts │ │ │ └── utils │ │ │ │ └── create-action.ts │ │ ├── core │ │ │ ├── adapters │ │ │ │ ├── dotenv.ts │ │ │ │ ├── heroku.ts │ │ │ │ ├── index.ts │ │ │ │ ├── infisical.ts │ │ │ │ ├── local.ts │ │ │ │ ├── trigger-dev.ts │ │ │ │ └── vercel.ts │ │ │ ├── core.ts │ │ │ ├── linter │ │ │ │ ├── eslint.ts │ │ │ │ └── index.ts │ │ │ └── validators │ │ │ │ ├── class-validator.ts │ │ │ │ ├── index.ts │ │ │ │ ├── typia.ts │ │ │ │ └── zod.ts │ │ ├── global.d.ts │ │ └── lib │ │ │ ├── client │ │ │ ├── client.ts │ │ │ ├── generate-client.ts │ │ │ └── preamble │ │ │ │ ├── preamble.ts │ │ │ │ └── types.d.ts │ │ │ ├── config │ │ │ ├── config-language.ts │ │ │ ├── index.ts │ │ │ ├── read-config.ts │ │ │ ├── resolve-config-path.ts │ │ │ └── utils.ts │ │ │ ├── constants │ │ │ ├── envline.ts │ │ │ └── port.ts │ │ │ ├── debugger │ │ │ └── index.ts │ │ │ ├── env │ │ │ ├── create-heaven.ts │ │ │ ├── load-env.ts │ │ │ ├── parse-env.ts │ │ │ ├── patch-env.ts │ │ │ └── reload-env.ts │ │ │ ├── exec │ │ │ └── index.ts │ │ │ ├── heaven │ │ │ ├── create-heaven.ts │ │ │ ├── file-watcher.ts │ │ │ └── heaven-server.ts │ │ │ ├── lifecycle │ │ │ ├── error.ts │ │ │ └── index.ts │ │ │ ├── logger │ │ │ └── index.ts │ │ │ ├── store │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ ├── compare-arrays.ts │ │ │ ├── debounce.ts │ │ │ ├── format.ts │ │ │ ├── get-caller-location.ts │ │ │ ├── get-runtime.ts │ │ │ ├── is-server.ts │ │ │ ├── is-typescript.ts │ │ │ ├── parse-validation.ts │ │ │ ├── txt.ts │ │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── heaven │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── heaven.ts │ │ ├── lib │ │ │ ├── get-runtime.ts │ │ │ ├── logger.ts │ │ │ ├── store.ts │ │ │ └── types.ts │ │ └── watch.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── playground │ ├── .gitignore │ ├── LICENSE │ ├── env.config.js │ ├── env.config.ts │ ├── fatima-script.js │ ├── jsconfig.json │ ├── package.json │ ├── src │ │ ├── class-validator │ │ │ ├── env.config.ts │ │ │ └── env.ts │ │ ├── jsdoc │ │ │ ├── env.config.js │ │ │ ├── env.js │ │ │ └── src │ │ │ │ └── index.js │ │ ├── typia │ │ │ ├── env.config.ts │ │ │ ├── env.ts │ │ │ └── test.ts │ │ └── zod │ │ │ ├── env.config.ts │ │ │ └── env.ts │ └── tsconfig.json └── web │ ├── LICENSE │ ├── app │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ ├── open-graph │ │ │ └── [...slug] │ │ │ │ ├── load-google-fonts.ts │ │ │ │ └── route.tsx │ │ └── search │ │ │ └── route.ts │ ├── docs │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── global.css │ ├── layout.config.tsx │ ├── layout.tsx │ └── logo.tsx │ ├── components │ └── icon.tsx │ ├── content │ └── docs │ │ ├── adapters │ │ ├── (adapters) │ │ │ ├── dotenv.mdx │ │ │ ├── infisical.mdx │ │ │ ├── local.mdx │ │ │ ├── trigger.mdx │ │ │ └── vercel.mdx │ │ ├── meta.json │ │ └── own.mdx │ │ ├── article.mdx │ │ ├── frameworks │ │ ├── (frameworks) │ │ │ └── sveltekit.mdx │ │ └── meta.json │ │ ├── getting-started │ │ ├── deploy.mdx │ │ ├── eslint.mdx │ │ ├── heaven.mdx │ │ ├── meta.json │ │ └── quickstart.mdx │ │ ├── index.mdx │ │ ├── meta.json │ │ ├── public-secrets │ │ ├── additional-setup.mdx │ │ ├── meta.json │ │ └── public-secrets.mdx │ │ ├── security │ │ ├── environment-mixing.mdx │ │ ├── meta.json │ │ └── secret-leaking.mdx │ │ └── validators │ │ ├── (validators) │ │ ├── class-validator.mdx │ │ ├── typia.mdx │ │ └── zod.mdx │ │ ├── meta.json │ │ └── own.mdx │ ├── lib │ ├── metadata.ts │ └── source.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── banner.png │ ├── favicon │ │ ├── dark.svg │ │ └── light.svg │ └── fonts │ │ └── Inter-Bold.ttf │ ├── source.config.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "bumpVersionsWithWorkspaceProtocolOnly": false, 5 | "updateInternalDependencies": "patch", 6 | "commit": false, 7 | "prettier": false, 8 | "access": "restricted", 9 | "baseBranch": "fatima", 10 | "fixed": [], 11 | "linked": [], 12 | "ignore": ["@fatimajs/playground", "@fatimajs/web"] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # editor 3 | .vscode 4 | 5 | # deps 6 | node_modules 7 | 8 | # output 9 | dist 10 | .next 11 | .source 12 | *.tsbuildinfo 13 | out 14 | .turbo 15 | 16 | # env 17 | .env 18 | 19 | # nextjs 20 | .vercel 21 | next-env.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Fatima](./assets/banner.png "Fatima")](https://fatimajs.vercel.app) 2 | 3 | ## Documentation 4 | 5 | Visit https://fatimajs.vercel.app to see the documentation. 6 | 7 | ## NPM 8 | 9 | Visit [https://npmjs.com/package/fatima](https://www.npmjs.com/package/fatima) to see the registry. 10 | 11 | ## Init CLI 12 | 13 | ```bash 14 | npx init fatima@latest 15 | ``` 16 | 17 | ```bash 18 | pnpm init fatima@latest 19 | ``` 20 | 21 | ```bash 22 | yarn create fatima@latest 23 | ``` 24 | 25 | ## Manual Setup 26 | 27 | ```bash 28 | npm install -D fatima 29 | ``` 30 | 31 | ```bash 32 | pnpm add -D fatima 33 | ``` 34 | 35 | ```bash 36 | yarn add -D fatima 37 | ``` 38 | 39 | ## License 40 | 41 | Licensed under the [MIT license](https://github.com/Fgc17/fatima/blob/fatima/LICENSE). 42 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgcoelho/fatima/4c1239aa8ebb0f956a8266ffe84a21ed27a2fdc5/assets/banner.png -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab", 15 | "include": ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.css", "**/*.json"], 16 | "ignore": [".next", ".turbo", ".source", "node_modules"] 17 | }, 18 | "linter": { 19 | "enabled": true, 20 | "rules": { 21 | "recommended": true, 22 | "style": { 23 | "useTemplate": "off", 24 | "noParameterAssign": "off" 25 | } 26 | }, 27 | "include": ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.css", "**/*.json"], 28 | "ignore": [".next", ".turbo", ".source", "dist", "node_modules"] 29 | }, 30 | "organizeImports": { 31 | "enabled": true 32 | }, 33 | "javascript": { 34 | "formatter": { 35 | "quoteStyle": "double" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo", 3 | "private": true, 4 | "scripts": { 5 | "lint": "turbo lint", 6 | "format": "turbo format", 7 | "dev": "turbo dev", 8 | "build": "turbo build", 9 | "change": "changeset", 10 | "bump": "changeset version", 11 | "release": "turbo release" 12 | }, 13 | "devDependencies": { 14 | "@biomejs/biome": "^1.9.4", 15 | "@changesets/cli": "^2.28.1", 16 | "turbo": "^2.4.0", 17 | "typescript": "5.7.3" 18 | }, 19 | "packageManager": "pnpm@9.0.0", 20 | "engines": { 21 | "node": ">=18" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/create-fatima/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-fatima 2 | 3 | ## 0.0.5 4 | 5 | ### Patch Changes (2025/03/24) 6 | 7 | - fix config tweaking issues 8 | - fix some template issues 9 | 10 | ## 0.0.4 11 | 12 | ### Patch Changes 13 | 14 | - change templates to use new local adapter 15 | 16 | ## 0.0.3 17 | 18 | ### Patch Changes 19 | 20 | - fix error when reading json config with comments 21 | 22 | ## 0.0.2 23 | 24 | ### Patch Changes 25 | 26 | - add missing shebang 27 | 28 | ## 0.0.1 29 | 30 | ### Patch Changes 31 | 32 | - first version 33 | -------------------------------------------------------------------------------- /packages/create-fatima/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /packages/create-fatima/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-fatima", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.0.5", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/Fgc17/fatima.git" 11 | }, 12 | "files": ["dist"], 13 | "type": "module", 14 | "bin": { 15 | "create-fatima": "./dist/bin.cjs" 16 | }, 17 | "scripts": { 18 | "------------- toolchain -------------": "-------------", 19 | "format": "biome format --write", 20 | "lint": "biome lint --error-on-warnings", 21 | "typecheck": "tsc --noEmit", 22 | "check": "pnpm typecheck && pnpm lint", 23 | "test": "vitest", 24 | "------------- dev -------------": "-------------", 25 | "dev": "pnpm tsup --watch", 26 | "------------- build -------------": "-------------", 27 | "build": "rm -rf dist && pnpm tsup", 28 | "bundlesize": "pnpm build --metafile && npm pack --dry-run", 29 | "bundlesize:dev": "pnpm tsup --metafile && npm pack --dry-run", 30 | "------------- publishing -------------": "-------------", 31 | "release": "pnpm check && pnpm build && pnpm publish --no-git-checks", 32 | "release:rc": "pnpm check && pnpm build && pnpm version prerelease --preid=rc" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^22.9.1", 36 | "esbuild-plugin-copy": "^2.1.1", 37 | "tsup": "^8.3.5", 38 | "tsx": "4.19.2", 39 | "typescript": "5.6.3" 40 | }, 41 | "dependencies": { 42 | "@inquirer/prompts": "^7.2.4", 43 | "chalk": "^4", 44 | "comment-json": "^4.2.5", 45 | "log-symbols": "^3", 46 | "prettier": "3.5.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/create-fatima/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { createConfigFile } from "src/lib/create-config-file"; 4 | import { applyUserConfigTweaks } from "./lib/tweaks"; 5 | import { logger } from "./utils/logger"; 6 | import { checkPackageJson } from "./utils/check-package-json"; 7 | import { wizard } from "./wizard/wizard"; 8 | 9 | const form = async () => { 10 | checkPackageJson(); 11 | 12 | const { language, adapter, validator } = await wizard(); 13 | 14 | const modules = await createConfigFile({ 15 | adapter, 16 | language, 17 | validator, 18 | }); 19 | 20 | applyUserConfigTweaks(language); 21 | 22 | await logger.summary(language, modules); 23 | }; 24 | 25 | const runForm = async () => 26 | form().catch((e) => { 27 | if (!e.message.includes("force closed")) { 28 | console.error(e); 29 | } 30 | 31 | console.log("Exiting..."); 32 | }); 33 | 34 | runForm(); 35 | -------------------------------------------------------------------------------- /packages/create-fatima/src/lib/tweaks.ts: -------------------------------------------------------------------------------- 1 | import { tweakUserConfig } from "src/utils/tweak-user-config"; 2 | import type { Language } from "./types"; 3 | import { assign } from "comment-json"; 4 | 5 | const tweakTypescript = () => { 6 | tweakUserConfig("tsconfig.json", (config) => { 7 | if (!config.compilerOptions) { 8 | config.compilerOptions = { 9 | paths: { 10 | env: ["./env.ts"], 11 | }, 12 | }; 13 | 14 | return config; 15 | } 16 | 17 | config.compilerOptions = assign(config.compilerOptions, { 18 | paths: assign(config.compilerOptions?.paths, { 19 | env: ["./env.ts"], 20 | }), 21 | }); 22 | 23 | return config; 24 | }); 25 | 26 | tweakUserConfig(".gitignore", (content: string) => { 27 | const didIgnoreEnv = content.includes("env.ts"); 28 | 29 | if (didIgnoreEnv) return null; 30 | 31 | const additions = []; 32 | 33 | if (content.length > 0) { 34 | additions.push("\n"); 35 | } 36 | 37 | additions.push("# fatima", "\n", "env.ts"); 38 | 39 | return content + additions.join(""); 40 | }); 41 | }; 42 | 43 | const tweakJavascript = () => { 44 | tweakUserConfig("jsconfig.json", (config) => { 45 | if (!config.compilerOptions) { 46 | config.compilerOptions = { 47 | paths: { 48 | "#env": ["./env.js"], 49 | }, 50 | }; 51 | 52 | return config; 53 | } 54 | 55 | config.compilerOptions = assign(config.compilerOptions, { 56 | paths: assign(config.compilerOptions?.paths, { 57 | "#env": ["./env.js"], 58 | }), 59 | }); 60 | 61 | return config; 62 | }); 63 | 64 | tweakUserConfig("package.json", (config) => { 65 | config.imports = { 66 | ...config.imports, 67 | "#env": "./env.js", 68 | }; 69 | return config; 70 | }); 71 | 72 | tweakUserConfig(".gitignore", (content) => { 73 | const didIgnoreEnv = content.includes("env.js"); 74 | 75 | if (didIgnoreEnv) return null; 76 | 77 | const additions = ["\n", "# fatima", "\n", "env.js"]; 78 | 79 | return content + additions.join(""); 80 | }); 81 | }; 82 | 83 | export const applyUserConfigTweaks = (language: Language) => { 84 | const tweak = language === "typescript" ? tweakTypescript : tweakJavascript; 85 | 86 | tweak(); 87 | }; 88 | -------------------------------------------------------------------------------- /packages/create-fatima/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { select } from "@inquirer/prompts"; 2 | 3 | // biome-ignore lint/suspicious/noExplicitAny: 4 | export type AnyType = any; 5 | 6 | export type InquirerSelectChoice = Parameters[0]["choices"]; 7 | 8 | export type Language = "typescript" | "javascript"; 9 | 10 | export type Validator = "zod" | "class-validator" | "typia" | "custom"; 11 | 12 | export type Adapter = 13 | | "dotenv" 14 | | "infisical" 15 | | "vercel" 16 | | "triggerdev" 17 | | "heroku" 18 | | "custom"; 19 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/custom/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | module.exports = config({ 3 | load: { 4 | development: [ 5 | async () => { 6 | const fetch = async () => { 7 | return { NODE_ENV: "development" }; 8 | }; 9 | 10 | return await fetch(); 11 | }, 12 | ], 13 | }, 14 | }); 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/custom/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | export default config({ 3 | load: { 4 | development: [ 5 | async () => { 6 | const fetch = async () => { 7 | return { NODE_ENV: "development" }; 8 | }; 9 | 10 | return await fetch(); 11 | }, 12 | ], 13 | }, 14 | }); 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/infisical/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | const { adapters } = require("fatima"); 3 | const { InfisicalSDK } = require("@infisical/sdk"); 4 | 5 | module.exports = config({ 6 | load: { 7 | development: [ 8 | adapters.local.load(".env"), 9 | adapters.infisical.load(InfisicalSDK), 10 | ], 11 | }, 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/infisical/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { adapters } from "fatima"; 3 | import { InfisicalSDK } from "@infisical/sdk"; 4 | 5 | export default config({ 6 | load: { 7 | development: [ 8 | adapters.local.load(".env"), 9 | adapters.infisical.load(InfisicalSDK), 10 | ], 11 | }, 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/local/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | const { adapters } = require("fatima"); 3 | 4 | module.exports = config({ 5 | load: { 6 | development: [adapters.local.load(".env")], 7 | }, 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/local/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { adapters } from "fatima"; 3 | 4 | export default config({ 5 | load: { 6 | development: [adapters.local.load(".env")], 7 | }, 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/trigger/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | const { adapters } = require("fatima"); 3 | const trigger = require("@trigger.dev/sdk/v3"); 4 | 5 | module.exports = config({ 6 | load: { 7 | development: [ 8 | adapters.local.load(".env"), 9 | adapters.triggerDev.load(trigger), 10 | ], 11 | }, 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/trigger/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { adapters } from "fatima"; 3 | import * as trigger from "@trigger.dev/sdk/v3"; 4 | 5 | export default config({ 6 | load: { 7 | development: [adapters.local.load(".env"), adapters.trigger.load(trigger)], 8 | }, 9 | }); 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/vercel/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | const { adapters, parse } = require("fatima"); 3 | 4 | module.exports = config({ 5 | load: { 6 | development: [adapters.vercel.load(parse)], 7 | }, 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/adapters/vercel/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { adapters, parse } from "fatima"; 3 | 4 | export default config({ 5 | load: { 6 | development: [adapters.vercel.load(parse)], 7 | }, 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/base/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | const { config } = require("fatima"); 3 | 4 | module.exports = config({ 5 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 6 | }); 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/base/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { config } from "fatima"; 3 | 4 | type Environment = "development" | "staging" | "production"; 5 | 6 | export default config({ 7 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/class-validator/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import "reflect-metadata"; 3 | import { IsIn, validate } from "class-validator"; 4 | import { plainToInstance } from "class-transformer"; 5 | import { validators } from "fatima"; 6 | import type { EnvObject } from "env"; 7 | 8 | class Constraint implements Partial { 9 | @IsIn(["development", "staging", "production"]) 10 | NODE_ENV: string; 11 | } 12 | 13 | export default config({ 14 | validate: validators.classValidator(Constraint, { 15 | plainToInstance, 16 | validate, 17 | }), 18 | }); 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/custom/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | module.exports = config({ 3 | validate: async (processEnv) => { 4 | /** 5 | * @type {import('fatima').FatimaValidationResult} 6 | */ 7 | const validation = { 8 | isValid: true, 9 | errors: [], 10 | } 11 | 12 | if (!processEnv.NODE_ENV) { 13 | validation.errors.push({ 14 | key: "NODE_ENV", 15 | message: "NODE_ENV is required", 16 | }); 17 | } 18 | 19 | if (validation.errors.length) { 20 | validation.isValid = false; 21 | } 22 | 23 | return validation; 24 | },, 25 | }); 26 | ``` 27 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/custom/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import type { FatimaValidationResult } from "fatima"; 3 | 4 | export default config({ 5 | validate: async (processEnv) => { 6 | const validation = { 7 | isValid: true, 8 | errors: [], 9 | } as FatimaValidationResult; 10 | 11 | if (!processEnv.NODE_ENV) { 12 | validation.errors.push({ 13 | key: "NODE_ENV", 14 | message: "NODE_ENV is required", 15 | }); 16 | } 17 | 18 | if (validation.errors.length) { 19 | validation.isValid = false; 20 | } 21 | 22 | return validation; 23 | }, 24 | }); 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/typia/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { validators } from "fatima"; 3 | import typia, { tags } from "typia"; 4 | import type { EnvType } from "env"; 5 | 6 | type Constraint = EnvType<{ 7 | NODE_ENV: string; 8 | }>; 9 | 10 | export default config({ 11 | validate: validators.typia((env) => typia.validate(env)), 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/zod/javascript.mdx: -------------------------------------------------------------------------------- 1 | ```js 2 | const { validators } = require("fatima"); 3 | const { z } = require("zod"); 4 | 5 | /** 6 | * @type {import('#env').Constraint} 7 | */ 8 | const constraint = { 9 | NODE_ENV: z.enum(["development"]), 10 | }; 11 | 12 | module.exports = config({ 13 | validate: validators.zod(z.object(constraint)), 14 | }); 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/create-fatima/src/templates/validators/zod/typescript.mdx: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { validators } from "fatima"; 3 | import { z, type ZodType } from "zod"; 4 | import type { EnvRecord } from "env"; 5 | 6 | type Constraint = Partial>; 7 | 8 | const constraint: Constraint = { 9 | NODE_ENV: z.enum(["development"]), 10 | }; 11 | 12 | export default config({ 13 | validate: validators.zod(z.object(constraint)), 14 | }); 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/check-package-json.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import chalk from "chalk"; 3 | import path from "node:path"; 4 | 5 | export function checkPackageJson() { 6 | const currentDir = process.cwd(); 7 | 8 | if (!existsSync(path.join(currentDir, "package.json"))) { 9 | console.log( 10 | chalk.red( 11 | "'fatima init' must be executed in a directory with a package.json file.", 12 | ), 13 | ); 14 | 15 | process.exit(1); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/get-caller-location.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export function getCallerLocation(index = 3) { 4 | const callerStack = new Error().stack || ""; 5 | const callerLine = callerStack.split("\n")[index]; 6 | const callerFileMatch = 7 | callerLine.match(/\((.*):\d+:\d+\)$/) || 8 | callerLine.match(/at (.*):\d+:\d+/); 9 | const filePath = callerFileMatch ? callerFileMatch[1] : "unknown"; 10 | const folderPath = path.dirname(filePath); 11 | 12 | return { filePath, folderPath }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/get-package-manager.ts: -------------------------------------------------------------------------------- 1 | type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; 2 | 3 | export function getPackageManager() { 4 | const userAgent = process.env.npm_config_user_agent; 5 | 6 | if (userAgent?.includes("pnpm")) { 7 | return "pnpm"; 8 | } 9 | 10 | if (userAgent?.includes("yarn")) { 11 | return "yarn"; 12 | } 13 | 14 | if (userAgent?.includes("bun")) { 15 | return "bun"; 16 | } 17 | 18 | return "npm"; 19 | } 20 | 21 | export function getPackageManagerInstall(packageManager: PackageManager) { 22 | if (packageManager === "pnpm") { 23 | return "pnpm i"; 24 | } 25 | 26 | if (packageManager === "yarn") { 27 | return "yarn add"; 28 | } 29 | 30 | if (packageManager === "bun") { 31 | return "bun i"; 32 | } 33 | 34 | return "npm i"; 35 | } 36 | 37 | export function getPackageManagerExec(packageManager: PackageManager) { 38 | if (packageManager === "pnpm") { 39 | return "pnpm"; 40 | } 41 | 42 | if (packageManager === "yarn") { 43 | return "yarn"; 44 | } 45 | 46 | if (packageManager === "bun") { 47 | return "bun"; 48 | } 49 | 50 | return "npx"; 51 | } 52 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/has-nested-package.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | export function findNestedPackage(directory = process.cwd()): boolean { 5 | function search(dir: string, isRoot = true): boolean { 6 | const entries = fs.readdirSync(dir, { withFileTypes: true }); 7 | 8 | return entries.some((entry) => { 9 | if (entry.name === "node_modules" || entry.name.startsWith(".")) { 10 | return false; 11 | } 12 | if (entry.name === "package.json" && !isRoot) { 13 | return true; 14 | } 15 | if (entry.isDirectory()) { 16 | return search(path.resolve(dir, entry.name), false); 17 | } 18 | return false; 19 | }); 20 | } 21 | 22 | return search(directory); 23 | } 24 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import logSymbols from "log-symbols"; 3 | import type { Language } from "src/lib/types"; 4 | import { 5 | getPackageManager, 6 | getPackageManagerInstall, 7 | } from "./get-package-manager"; 8 | 9 | export const summary = async (language: Language, modules: string[]) => { 10 | const ext = language === "typescript" ? "ts" : "js"; 11 | const alias = 12 | language === "typescript" 13 | ? "'tsconfig.json'" 14 | : "'jsconfig.json' and 'package.json'"; 15 | 16 | const packageManager = getPackageManager(); 17 | const packageManagerInstall = getPackageManagerInstall(packageManager); 18 | 19 | const configFile = `env.config.${ext}`; 20 | const envFile = `env.${ext}`; 21 | 22 | const message = [ 23 | logSymbols.success + chalk.bold(" Done! 🎉"), 24 | logSymbols.warning + chalk.bold(" Here's a summary of what happened:"), 25 | ` ↪ Created '${configFile}'`, 26 | " ↪ Added path alias to " + alias, 27 | ` ↪ Added '${envFile}' to '.gitignore'`, 28 | logSymbols.info + chalk.bold(" Next steps:"), 29 | modules.length && 30 | ` ↪ Install dependencies: ${packageManagerInstall} ${modules.join(" ")}`, 31 | " ↪ Make it typesafe: 'fatima generate'", 32 | " ↪ Setup your dev script: 'fatima dev -- npm dev'", 33 | chalk.bold( 34 | " ↪ Check out docs for much more: https://fatimajs.vercel.app/docs", 35 | ), 36 | "Happy coding! 🚀", 37 | ].filter(Boolean); 38 | 39 | return console.log(message.map((x) => `\r${x}`).join("\n")); 40 | }; 41 | 42 | export const logger = { 43 | summary, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/create-fatima/src/utils/tweak-user-config.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from "node:fs"; 2 | import path from "node:path"; 3 | import type { AnyType } from "../lib/types"; 4 | import { parse, stringify, assign } from "comment-json"; 5 | 6 | function getConfigPath(filename: string): string | null { 7 | let currentDir = process.cwd(); 8 | 9 | while (currentDir !== path.parse(currentDir).root) { 10 | const configPath = path.join(currentDir, filename); 11 | 12 | if (existsSync(configPath)) { 13 | return configPath; 14 | } 15 | 16 | currentDir = path.dirname(currentDir); 17 | } 18 | 19 | return path.join(process.cwd(), filename); 20 | } 21 | 22 | export function tweakUserConfig( 23 | fileName: string, 24 | modifier: (config: AnyType) => AnyType, 25 | ): AnyType { 26 | const filePath = getConfigPath(fileName); 27 | 28 | let updatedFile = ""; 29 | 30 | if (!filePath) return; 31 | 32 | try { 33 | const fileExtension = path.extname(fileName); 34 | 35 | switch (fileExtension) { 36 | case ".json": { 37 | if (!existsSync(filePath)) { 38 | writeFileSync(filePath, "{}"); 39 | } 40 | 41 | const fileContent = readFileSync(filePath, "utf8"); 42 | 43 | const config = parse(fileContent, null, false); 44 | 45 | const changed = assign(config, modifier(config)); 46 | 47 | const newContent = stringify(changed, null, 2); 48 | 49 | updatedFile = newContent; 50 | 51 | break; 52 | } 53 | default: { 54 | if (!existsSync(filePath)) { 55 | writeFileSync(filePath, ""); 56 | } 57 | 58 | const currentContent = readFileSync(filePath, "utf8"); 59 | 60 | const newContent = modifier(currentContent); 61 | 62 | if (!newContent) return; 63 | 64 | updatedFile = newContent; 65 | 66 | break; 67 | } 68 | } 69 | } catch (e) { 70 | console.error(`Error reading ${fileName}:`, e); 71 | return; 72 | } 73 | 74 | console.log( 75 | `Tweaked ${filePath} with the following changes:\n${updatedFile}`, 76 | ); 77 | 78 | writeFileSync(filePath, updatedFile); 79 | } 80 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/prompts/adapter.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | 3 | export type Adapter = 4 | | "dotenv" 5 | | "infisical" 6 | | "vercel" 7 | | "triggerdev" 8 | | "heroku" 9 | | "custom"; 10 | 11 | export const askAdapter = async () => 12 | (await select({ 13 | message: "Select an adapter", 14 | choices: [ 15 | { 16 | name: "local (.env)", 17 | value: "local", 18 | }, 19 | { 20 | name: "infisical", 21 | value: "infisical", 22 | }, 23 | { 24 | name: "vercel", 25 | value: "vercel", 26 | }, 27 | { 28 | name: "trigger.dev", 29 | value: "triggerdev", 30 | }, 31 | { 32 | name: "I'll build my own adapter", 33 | value: "custom", 34 | description: "It is pretty easy.", 35 | }, 36 | ], 37 | })) as Adapter; 38 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/prompts/lang.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | 3 | export type Language = "typescript" | "javascript"; 4 | 5 | export const askLanguage = async () => 6 | (await select({ 7 | message: "Choose the language you want to use", 8 | choices: [ 9 | { 10 | name: "TypeScript (.ts)", 11 | value: "typescript", 12 | }, 13 | { 14 | name: "JavaScript (.js)", 15 | value: "javascript", 16 | description: "Fatima provides full type safety via JSDoc.", 17 | }, 18 | ], 19 | })) as Language; 20 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/prompts/monorepo.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | import { findNestedPackage } from "src/utils/has-nested-package"; 3 | 4 | export type UserIntent = "quit" | "continue"; 5 | 6 | export async function askUserIntent(): Promise { 7 | const hasNestedPackage = findNestedPackage(); 8 | 9 | if (!hasNestedPackage) return "continue"; 10 | 11 | const willQuit = await select({ 12 | message: 13 | "This will initialize fatima at your workspace root. Are you sure?", 14 | choices: [ 15 | { 16 | name: "Yes, continue.", 17 | value: false, 18 | }, 19 | { 20 | name: "Quit, I will run this at the package/app directory", 21 | value: true, 22 | }, 23 | ], 24 | }); 25 | 26 | if (willQuit) return "quit"; 27 | 28 | return "continue"; 29 | } 30 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/prompts/public.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | 3 | export const askPublic = async () => 4 | await select({ 5 | message: "Do you want to set up public secrets? (usually for frontend)", 6 | choices: [ 7 | { 8 | name: "No", 9 | value: "no-public", 10 | }, 11 | { 12 | name: "Yes", 13 | value: "public", 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/prompts/validator.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | import type { InquirerSelectChoice, Language } from "src/lib/types"; 3 | 4 | export type Validator = "zod" | "class-validator" | "typia" | "custom"; 5 | 6 | const tsChoices: InquirerSelectChoice = [ 7 | { 8 | name: "class-validator", 9 | value: "class-validator", 10 | }, 11 | { 12 | name: "typia", 13 | value: "typia", 14 | }, 15 | ]; 16 | 17 | const getTsChoices = (lang: Language) => 18 | lang === "typescript" ? tsChoices : []; 19 | 20 | export const askValidator = async (language: Language) => 21 | (await select({ 22 | message: "Select a validator", 23 | choices: [ 24 | { 25 | name: "zod", 26 | value: "zod", 27 | }, 28 | ...getTsChoices(language), 29 | { 30 | name: "I'll build my own validator", 31 | value: "custom", 32 | description: "It is pretty easy.", 33 | }, 34 | ], 35 | })) as string; 36 | -------------------------------------------------------------------------------- /packages/create-fatima/src/wizard/wizard.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter, Language, Validator } from "@/lib/types"; 2 | import { askUserIntent } from "./prompts/monorepo"; 3 | import { askAdapter } from "./prompts/adapter"; 4 | import { askLanguage } from "./prompts/lang"; 5 | import { askValidator } from "./prompts/validator"; 6 | 7 | export interface WizardResult { 8 | language: Language; 9 | adapter: Adapter; 10 | validator: Validator; 11 | } 12 | 13 | export async function wizard(): Promise { 14 | const userIntent = await askUserIntent(); 15 | 16 | if (userIntent === "quit") { 17 | throw "Exiting..."; 18 | } 19 | 20 | const language = (await askLanguage()) as Language; 21 | 22 | const adapter = (await askAdapter()) as Adapter; 23 | 24 | const validator = (await askValidator(language)) as Validator; 25 | 26 | return { language, adapter, validator }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/create-fatima/test/install-modules.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach, expect } from "vitest"; 2 | import * as fs from "node:fs/promises"; 3 | import * as path from "node:path"; 4 | import { create } from "src/create-fatima"; 5 | import type { Adapter } from "@/wizard/prompts/adapter"; 6 | import type { Language } from "@/wizard/prompts/language"; 7 | import type { Validator } from "@/wizard/prompts/validator"; 8 | import { 9 | cleanEphemeralFolder, 10 | openEphemeralFolder, 11 | } from "./utils/ephemeral-folder"; 12 | 13 | describe("install modules", () => { 14 | const projectDir = process.cwd(); 15 | let ephemeralFolder: string; 16 | 17 | beforeEach(async () => { 18 | ephemeralFolder = await openEphemeralFolder(); 19 | 20 | const pkgJson = { 21 | name: "temp-project", 22 | version: "1.0.0", 23 | }; 24 | 25 | await fs.writeFile("package.json", JSON.stringify(pkgJson, null, 2)); 26 | }); 27 | 28 | afterEach(async () => { 29 | await cleanEphemeralFolder(ephemeralFolder, projectDir); 30 | }); 31 | 32 | it("should install modules correctly", async () => { 33 | const dummyLanguage = "javascript" as Language; 34 | const dummyAdapter = "custom" as Adapter; 35 | const dummyValidator = "custom" as Validator; 36 | 37 | const modules = await create({ 38 | adapter: dummyAdapter, 39 | language: dummyLanguage, 40 | validator: dummyValidator, 41 | }); 42 | 43 | expect(Array.isArray(modules)).toBe(true); 44 | 45 | const pkgData = await fs.readFile("package.json", "utf-8"); 46 | 47 | const pkg = JSON.parse(pkgData); 48 | 49 | expect(pkg).toHaveProperty("dependencies"); 50 | expect(pkg.dependencies).toHaveProperty("fatima"); 51 | 52 | await fs.access("fatima.config.js"); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/create-fatima/test/missing-package-json.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, afterEach, test } from "vitest"; 2 | import { 3 | cleanEphemeralFolder, 4 | openEphemeralFolder, 5 | } from "./utils/ephemeral-folder"; 6 | import { checkPackageJson } from "@/utils/check-package-json"; 7 | 8 | describe("Missing package.json", () => { 9 | const projectDir = process.cwd(); 10 | let ephemeralFolder: string; 11 | 12 | beforeEach(async () => { 13 | ephemeralFolder = await openEphemeralFolder(); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanEphemeralFolder(ephemeralFolder, projectDir); 18 | }); 19 | 20 | test("should throw an error if package.json is missing", async ({ 21 | expect, 22 | }) => { 23 | expect(checkPackageJson).toThrowError(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/create-fatima/test/utils/ephemeral-folder.ts: -------------------------------------------------------------------------------- 1 | import type { PathLike } from "node:fs"; 2 | import { promises as fs } from "node:fs"; 3 | import os from "node:os"; 4 | import path from "node:path"; 5 | 6 | const BASE_EPHEMERAL_DIR = path.join(os.tmpdir(), "EPHEMERAL"); 7 | 8 | async function ensureBaseEphemeralDir() { 9 | try { 10 | await fs.access(BASE_EPHEMERAL_DIR); 11 | } catch { 12 | await fs.mkdir(BASE_EPHEMERAL_DIR, { recursive: true }); 13 | } 14 | } 15 | 16 | export async function getTempEphemeralFolder() { 17 | await ensureBaseEphemeralDir(); 18 | 19 | const uniqueDir = path.join(BASE_EPHEMERAL_DIR, crypto.randomUUID()); 20 | 21 | try { 22 | await fs.mkdir(uniqueDir); 23 | } catch (err) { 24 | console.error("Error creating unique EPHEMERAL directory:", err); 25 | } 26 | 27 | return uniqueDir; 28 | } 29 | 30 | export async function openEphemeralFolder() { 31 | await ensureBaseEphemeralDir(); 32 | 33 | const ephemeralDir = await getTempEphemeralFolder(); 34 | 35 | process.chdir(ephemeralDir); 36 | 37 | return ephemeralDir; 38 | } 39 | 40 | export async function cleanEphemeralFolder( 41 | tempDir: PathLike, 42 | originalCwd: string, 43 | ) { 44 | try { 45 | process.chdir(originalCwd); 46 | await fs.rm(tempDir, { recursive: true, force: true }); 47 | } catch (err) { 48 | console.error("Error cleaning up temporary ephemeral directory:", err); 49 | } 50 | } 51 | 52 | openEphemeralFolder(); 53 | -------------------------------------------------------------------------------- /packages/create-fatima/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationDir": "./", 10 | "incremental": true, 11 | "tsBuildInfoFile": ".tsbuildinfo", 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "useUnknownInCatchVariables": false, 16 | "baseUrl": "./", 17 | "outDir": "./dist", 18 | "rootDir": "./", 19 | "noEmit": true, 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["./src/**/*.ts", "./test/**/*.ts"], 25 | "exclude": ["node_modules", "./test/**/*.ts", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/create-fatima/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | import copyPlugin from "esbuild-plugin-copy"; 3 | 4 | export default defineConfig({ 5 | entry: ["src/bin.ts"], 6 | shims: true, 7 | clean: true, 8 | esbuildPlugins: [ 9 | copyPlugin({ 10 | resolveFrom: "cwd", 11 | assets: { 12 | from: ["./src/templates/**/*"], 13 | to: ["./dist/templates"], 14 | }, 15 | }), 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/create-fatima/vitest.config.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default { 4 | test: { 5 | globals: true, 6 | }, 7 | resolve: { 8 | alias: { 9 | "@": path.resolve(__dirname, "./src"), 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/fatima/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fatima 2 | 3 | ## 0.0.24 4 | ### Patch Changes (2025/03/31) 5 | 6 | - fix broken public secrets typesafety 7 | 8 | ## 0.0.22 9 | 10 | ### Patch Changes (2025/03/24) 11 | 12 | - expose env parse function 13 | - add global cli options 14 | - implement --process-env flag for skipping custom load functions 15 | - implement --environment flag for overwriting the environment function result 16 | 17 | ## 0.0.21 18 | 19 | ### Patch Changes (2025/03/15) 20 | 21 | - remove "fatima/tools" and "jiti" install requirements, now 0kb on production. 22 | 23 | ## 0.0.20 24 | 25 | ### Patch Changes (2025/02/22) 26 | 27 | - add command aliases 28 | - add jiti as automatically installed dependency 29 | 30 | ## 0.0.19 31 | 32 | ### Patch Changes (2025/02/22) 33 | 34 | - remove vercel loader parse option 35 | - add 'fatima install' command 36 | - turn fatima into a real dev dependency 37 | 38 | ## 0.0.18 39 | 40 | ### Patch Changes 41 | 42 | - reduce 40kb on bundlesize by implementing terser 43 | 44 | ## 0.0.17 45 | 46 | ### Patch Changes 47 | 48 | - implement heaven, runtime env reloading solution 49 | 50 | ## 0.0.16 51 | 52 | ### Patch Changes 53 | 54 | - disable removeNodeProtocol 55 | 56 | ## 0.0.15 57 | 58 | ### Patch Changes 59 | 60 | - improve instrumentation compatibility 61 | 62 | ## 0.0.14 63 | 64 | ### Patch Changes 65 | 66 | - improve logging and make instrumentation safer 67 | 68 | ## 0.0.13 69 | 70 | ### Patch Changes 71 | 72 | - add complete environment reloading support 73 | 74 | ## 0.0.12 75 | 76 | ### Patch Changes 77 | 78 | - add type abstraction and fix type errors 79 | 80 | ## 0.0.11 81 | 82 | ### Patch Changes 83 | 84 | - remove inquirer dependency 85 | 86 | ## 0.0.10 87 | 88 | ### Patch Changes 89 | 90 | - remove transform decorators plugin depemdency 91 | 92 | ## 0.0.9 93 | 94 | ### Patch Changes 95 | 96 | - add full lite mode support and performance improvements. 97 | 98 | ## 0.0.8 99 | 100 | ### Patch Changes 101 | 102 | - ignore .env files starting with .tmp 103 | 104 | ## 0.0.7 105 | 106 | ### Patch Changes 107 | 108 | - improve publishing method 109 | 110 | ## 0.0.6 111 | 112 | ### Patch Changes 113 | 114 | - implement underlying type api, a reload command and general fixes 115 | 116 | ## 0.0.5 117 | 118 | ### Patch Changes 119 | 120 | - implement environment configuration 121 | 122 | ## 0.0.4 123 | 124 | ### Patch Changes 125 | 126 | - make vercel load function more consistent 127 | 128 | ## 0.0.3 129 | 130 | ### Patch Changes 131 | 132 | - improve warn for wrong environment 133 | - improve warn when NODE_ENV is undefined 134 | 135 | ## 0.0.2 136 | 137 | ### Patch Changes 138 | 139 | - fix undefined environments in initializers 140 | 141 | ## 0.0.1 142 | 143 | ### Patch Changes 144 | 145 | - first fatima version 146 | -------------------------------------------------------------------------------- /packages/fatima/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /packages/fatima/README.md: -------------------------------------------------------------------------------- 1 | [![Fatima](./assets/banner.png "Fatima")](https://fatimajs.vercel.app) 2 | 3 | ## Documentation 4 | 5 | Visit https://fatimajs.vercel.app to see the documentation. 6 | 7 | ## NPM 8 | 9 | Visit [https://npmjs.com/package/fatima](https://www.npmjs.com/package/fatima) to see the registry. 10 | 11 | ## Init CLI 12 | 13 | ```bash 14 | npx init fatima@latest 15 | ``` 16 | 17 | ```bash 18 | pnpm init fatima@latest 19 | ``` 20 | 21 | ```bash 22 | yarn create fatima@latest 23 | ``` 24 | 25 | ## Manual Setup 26 | 27 | ```bash 28 | npm install -D fatima 29 | ``` 30 | 31 | ```bash 32 | pnpm add -D fatima 33 | ``` 34 | 35 | ```bash 36 | yarn add -D fatima 37 | ``` 38 | 39 | ## License 40 | 41 | Licensed under the [MIT license](https://github.com/Fgc17/fatima/blob/fatima/LICENSE). 42 | -------------------------------------------------------------------------------- /packages/fatima/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fatima", 3 | "description": "typesafe secrets for the javascript ecosystem", 4 | "author": "Fernando Coelho", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "version": "0.0.24", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Fgc17/fatima.git" 13 | }, 14 | "homepage": "https://fatimajs.vercel.app", 15 | "files": ["dist"], 16 | "keywords": ["env", "typesafe", "typescript", "environment", "variable"], 17 | "type": "module", 18 | "main": "./dist/core/core.cjs", 19 | "types": "./dist/core/core.d.cts", 20 | "exports": { 21 | ".": "./dist/core/core.cjs" 22 | }, 23 | "typesVersions": { 24 | "*": { 25 | ".": ["./dist/core/core.d.cts"] 26 | } 27 | }, 28 | "bin": { 29 | "fatima": "./dist/cli/cli.cjs" 30 | }, 31 | "scripts": { 32 | "------------- toolchain -------------": "-------------", 33 | "format": "biome format --write", 34 | "lint": "biome lint --error-on-warnings", 35 | "typecheck": "tsc --noEmit", 36 | "check": "pnpm typecheck && pnpm lint", 37 | "test": "vitest", 38 | "------------- dev -------------": "-------------", 39 | "dev": "pnpm tsup --env.mode dev", 40 | "------------- build -------------": "-------------", 41 | "build": "rm -rf dist && pnpm tsup --env.mode release", 42 | "bundlesize": "pnpm build --metafile && npm pack --dry-run", 43 | "bundlesize:dev": "pnpm tsup --metafile && npm pack --dry-run", 44 | "------------- publishing -------------": "-------------", 45 | "release": "pnpm check && pnpm build && pnpm publish --no-git-checks", 46 | "release:rc": "pnpm check && pnpm build && pnpm version prerelease --preid=rc" 47 | }, 48 | "devDependencies": { 49 | "@types/node": "^22.9.1", 50 | "esbuild": "^0.25.1", 51 | "jiti": "2.4.2", 52 | "terser": "^5.39.0", 53 | "tsup": "^8.3.5", 54 | "typescript": "5.6.3", 55 | "prettier": "3.5.3", 56 | "@biomejs/biome": "1.9.4" 57 | }, 58 | "dependencies": { 59 | "commander": "13.1.0", 60 | "jiti": "2.4.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/fatima/plugins/esbuild-raw-import-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, PluginBuild } from "esbuild"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | export interface RawImportConfig { 6 | extensions?: string[]; 7 | textExtensions?: string[]; 8 | loader?: "text" | "base64" | "dataurl" | "file" | "binary" | "default"; 9 | } 10 | 11 | export function rawImportPlugin(config: RawImportConfig = {}) { 12 | const extList = config.extensions ?? [ 13 | "ts", 14 | "tsx", 15 | "js", 16 | "jsx", 17 | "mjs", 18 | "mts", 19 | "module.css", 20 | "module.scss", 21 | "css", 22 | "scss", 23 | "d.ts", 24 | ]; 25 | const specifiedLoader = config.loader; 26 | const textExts = config.textExtensions ?? []; 27 | 28 | const plugin: Plugin = { 29 | name: "raw-import-plugin", 30 | setup(builder: PluginBuild) { 31 | builder.onResolve({ filter: /\?raw$/ }, (resolveArgs) => { 32 | const rawPath = resolveArgs.path.replace(/\?raw$/, ""); 33 | const absolutePath = path.resolve(resolveArgs.resolveDir, rawPath); 34 | return { 35 | path: resolveArgs.path, 36 | pluginData: { targetPath: absolutePath }, 37 | namespace: "raw-content", 38 | }; 39 | }); 40 | 41 | builder.onLoad( 42 | { filter: /\?raw$/, namespace: "raw-content" }, 43 | (loadArgs) => { 44 | const { targetPath } = loadArgs.pluginData as { targetPath: string }; 45 | 46 | if (specifiedLoader && specifiedLoader !== "text") { 47 | if (!fs.existsSync(targetPath)) { 48 | throw new Error(`Cannot locate file: ${targetPath}`); 49 | } 50 | const fileContent = fs.readFileSync(targetPath, "utf8"); 51 | return { contents: fileContent, loader: specifiedLoader }; 52 | } 53 | 54 | let finalPath = targetPath; 55 | const stat = fs.existsSync(finalPath) 56 | ? fs.lstatSync(finalPath) 57 | : null; 58 | if (stat?.isDirectory()) { 59 | finalPath = path.join(finalPath, "index"); 60 | } 61 | 62 | if (!fs.existsSync(finalPath)) { 63 | let found = false; 64 | for (const ext of extList) { 65 | const testPath = `${finalPath}.${ext}`; 66 | if (fs.existsSync(testPath)) { 67 | finalPath = testPath; 68 | found = true; 69 | break; 70 | } 71 | } 72 | if (!found) { 73 | throw new Error( 74 | `No matching file found for: ${targetPath}\nTried extensions: ${extList.join(", ")}`, 75 | ); 76 | } 77 | } 78 | 79 | const fileContent = fs.readFileSync(finalPath, "utf8"); 80 | return { contents: fileContent, loader: "text" }; 81 | }, 82 | ); 83 | 84 | if (textExts.length > 0) { 85 | const textPattern = new RegExp( 86 | `\\.(${textExts.map((ext) => ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`, 87 | ); 88 | builder.onLoad({ filter: textPattern }, (loadArgs) => { 89 | const fileContent = fs.readFileSync(loadArgs.path, "utf8"); 90 | return { contents: fileContent, loader: "text" }; 91 | }); 92 | } 93 | }, 94 | }; 95 | 96 | // biome-ignore lint/suspicious/noExplicitAny: fine here. 97 | return plugin as any; 98 | } 99 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/actions/dev.ts: -------------------------------------------------------------------------------- 1 | import { generateClient } from "lib/client/generate-client"; 2 | import { createHeaven } from "lib/env/create-heaven"; 3 | import { createInjectableEnv } from "lib/env/patch-env"; 4 | import { exec } from "lib/exec"; 5 | import { logger } from "lib/logger"; 6 | import { fatimaStore } from "lib/store"; 7 | import { createAction } from "../utils/create-action"; 8 | import { type EnvActionContext, envActionContext } from "../context/env"; 9 | 10 | const environmentBlacklist = [ 11 | "production", 12 | "prod", 13 | "staging", 14 | "stg", 15 | "preview", 16 | "pre", 17 | "prev", 18 | "preprod", 19 | ]; 20 | 21 | export const devService = async ({ 22 | config, 23 | env, 24 | envCount, 25 | args, 26 | }: EnvActionContext) => { 27 | fatimaStore.set("devMode", true); 28 | 29 | const environment = fatimaStore.get("environment") as string; 30 | 31 | if (environmentBlacklist.includes(environment)) { 32 | logger.error( 33 | `Your 'config.environment()' function returned '${environment}', you can't run 'fatima dev' in this environment.`, 34 | ); 35 | process.exit(1); 36 | } 37 | 38 | if (!fatimaStore.get("liteMode")) { 39 | await generateClient(config, env); 40 | } 41 | 42 | logger.success(`Loaded ${envCount} environment variables`); 43 | 44 | const injectableEnv = createInjectableEnv(env); 45 | 46 | const child = exec(args, { 47 | env: injectableEnv, 48 | shell: false, 49 | }); 50 | 51 | const { closeHeaven } = createHeaven(config); 52 | 53 | child.on("error", (error) => { 54 | console.error(`Error: ${error.message}`); 55 | closeHeaven(); 56 | process.exit(1); 57 | }); 58 | 59 | child.on("close", (code) => { 60 | if (code !== 0) { 61 | console.error(`Command exited with code ${code}`); 62 | closeHeaven(); 63 | process.exit(code); 64 | } 65 | }); 66 | }; 67 | 68 | export const devAction = createAction(devService, envActionContext); 69 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/actions/generate.ts: -------------------------------------------------------------------------------- 1 | import { generateClient } from "lib/client/generate-client"; 2 | import { validateService } from "./validate"; 3 | import { logger } from "lib/logger"; 4 | import { createAction } from "../utils/create-action"; 5 | import { type EnvActionContext, envActionContext } from "../context/env"; 6 | 7 | export const generateService = async (ctx: EnvActionContext) => { 8 | const { env, config, envCount } = ctx; 9 | 10 | if (config.validate) { 11 | await validateService(ctx); 12 | } 13 | 14 | await generateClient(config, env); 15 | 16 | logger.success( 17 | `Successfully generated env.ts with ${envCount} environment variables`, 18 | ); 19 | }; 20 | 21 | export const generateAction = createAction(generateService, envActionContext); 22 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/actions/reload.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "lib/logger"; 2 | import { createAction, type ActionContext } from "../utils/create-action"; 3 | import { debug } from "lib/debugger"; 4 | 5 | export const reloadService = async ({ config }: ActionContext) => { 6 | const port = config.heaven; 7 | 8 | if (!port) { 9 | logger.error( 10 | "Failed to reload environment variables, missing port, please set 'ports.reload' in your fatima config.", 11 | ); 12 | process.exit(1); 13 | } 14 | 15 | await fetch(`http://localhost:${port}/fatima`, { 16 | method: "POST", 17 | }) 18 | .then((res) => { 19 | if (res.status !== 200) { 20 | throw "err"; 21 | } 22 | 23 | logger.success("Successfully reloaded environment variables"); 24 | }) 25 | .catch((e) => { 26 | debug.error(e); 27 | 28 | logger.error( 29 | `Failed to reload environment variables, did you run 'fatima dev'?`, 30 | ); 31 | 32 | process.exit(1); 33 | }); 34 | }; 35 | 36 | export const reloadAction = createAction(reloadService); 37 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/actions/run.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "lib/exec"; 2 | import { createInjectableEnv } from "lib/env/patch-env"; 3 | import { createAction } from "../utils/create-action"; 4 | import { type EnvActionContext, envActionContext } from "../context/env"; 5 | import { logger } from "lib/logger"; 6 | 7 | export const runService = async ({ env, envCount, args }: EnvActionContext) => { 8 | const injectableEnv = createInjectableEnv(env); 9 | 10 | logger.success(`Loaded ${envCount} environment variables`); 11 | 12 | const child = exec(args, { 13 | env: injectableEnv, 14 | shell: true, 15 | }); 16 | 17 | child.on("error", (error) => { 18 | console.error(`Error: ${error.message}`); 19 | process.exit(1); 20 | }); 21 | 22 | child.on("close", (code) => { 23 | if (code !== 0) { 24 | console.error(`Command exited with code ${code}`); 25 | process.exit(code); 26 | } 27 | }); 28 | }; 29 | 30 | export const runAction = createAction(runService, envActionContext); 31 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/actions/validate.ts: -------------------------------------------------------------------------------- 1 | import { parseValidationErrors } from "lib/utils/parse-validation"; 2 | import { lifecycle } from "lib/lifecycle"; 3 | import { logger } from "lib/logger"; 4 | import { createAction } from "../utils/create-action"; 5 | import { type EnvActionContext, envActionContext } from "../context/env"; 6 | 7 | export const validateService = async ({ 8 | env, 9 | config: { validate }, 10 | }: EnvActionContext) => { 11 | if (!validate) { 12 | logger.error( 13 | "Validate command was called but no validator was provided in the config.", 14 | ); 15 | process.exit(1); 16 | } 17 | 18 | const { isValid, errors } = await validate(env); 19 | 20 | if (!isValid && errors) { 21 | const parsedErrors = parseValidationErrors(errors); 22 | 23 | return lifecycle.error.invalidEnvironmentVariables(parsedErrors); 24 | } 25 | 26 | logger.success("Successfully validated environment variables"); 27 | }; 28 | 29 | export const validateAction = createAction(validateService, envActionContext); 30 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { program } from "commander"; 3 | import { generateAction } from "./actions/generate"; 4 | import { devAction } from "./actions/dev"; 5 | import { validateAction } from "./actions/validate"; 6 | import { runAction } from "./actions/run"; 7 | import { reloadAction } from "./actions/reload"; 8 | import { initializeEnv } from "lib/env/patch-env"; 9 | import { finishLog } from "lib/logger"; 10 | 11 | initializeEnv(); 12 | 13 | program 14 | .name("fatima") 15 | .version("0.0.22") 16 | .description("safe environment variables for the js ecosystem") 17 | .configureHelp({ 18 | showGlobalOptions: true, 19 | }) 20 | .option("-d, --debug", "turn on debug mode") 21 | .option( 22 | "-c, --config , --config=", 23 | "customize config file path", 24 | ) 25 | .option( 26 | "-e, --environment , --environment=", 27 | "overwrite your environment function", 28 | ) 29 | .option("--process-env", "load only from process.env") 30 | .argument("", "the script to execute after --") 31 | .action(runAction); 32 | 33 | program.command("generate").alias("g").action(generateAction); 34 | 35 | program.command("validate").alias("v").action(validateAction); 36 | 37 | program 38 | .command("run") 39 | .argument("", "The script to execute after --") 40 | .action(runAction); 41 | 42 | program 43 | .command("dev") 44 | .alias("d") 45 | .option("-l, --lite", "Lite mode, won't generate client") 46 | .argument("", "The command to execute after --") 47 | .action(devAction); 48 | 49 | program.command("reload").action(reloadAction); 50 | 51 | program.command("help").alias("h").action(program.help); 52 | 53 | process.on("exit", finishLog); 54 | 55 | program.parse(); 56 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/context/env.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from "lib/env/load-env"; 2 | import type { ActionContext } from "../utils/create-action"; 3 | 4 | export const envActionContext = async ({ 5 | args, 6 | options, 7 | config, 8 | }: ActionContext) => { 9 | const { env: loadedEnv, envCount } = await loadEnv(config); 10 | 11 | const env = loadedEnv; 12 | 13 | return { config, env, envCount, options, args }; 14 | }; 15 | 16 | export type EnvActionContext = Awaited>; 17 | -------------------------------------------------------------------------------- /packages/fatima/src/cli/utils/create-action.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "lib/config"; 2 | import { readConfig } from "lib/config/read-config"; 3 | import { resolveConfigPath } from "lib/config/resolve-config-path"; 4 | import { fatimaStore } from "lib/store"; 5 | import type { AnyType, Promisable } from "lib/utils/types"; 6 | 7 | export interface ActionContext { 8 | options: Record; 9 | args: string[]; 10 | config: FatimaConfig; 11 | } 12 | 13 | export const createAction = ( 14 | action: (ctx: T) => Promisable, 15 | contextFn?: (payload: ActionContext) => Promisable, 16 | ) => { 17 | return async (param1: AnyType, param2: AnyType, param3: AnyType) => { 18 | const hasPositionalArgs = Array.isArray(param1); 19 | 20 | const program = hasPositionalArgs ? param3 : param2; 21 | 22 | const args = hasPositionalArgs ? param1 : param2; 23 | 24 | const options = program.optsWithGlobals(); 25 | 26 | fatimaStore.earlyInitialize(); 27 | 28 | const configPath = resolveConfigPath(options.config); 29 | 30 | const config = await readConfig(configPath); 31 | 32 | fatimaStore.initialize(config, options); 33 | 34 | const baseContext: ActionContext = { options, args, config }; 35 | 36 | const context = contextFn 37 | ? await contextFn(baseContext) 38 | : (baseContext as T); 39 | 40 | await action(context); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/dotenv.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaBuiltInLoadFunction, 3 | UnsafeEnvironmentVariables, 4 | } from "lib/types"; 5 | import type { AnyType } from "lib/utils/types"; 6 | 7 | export type DotenvConfigOptionsMock = { 8 | path: string | string[] | URL; 9 | }; 10 | 11 | export interface DotenvMock { 12 | config: (config?: DotenvConfigOptionsMock) => { 13 | parsed?: AnyType; 14 | }; 15 | } 16 | 17 | const load = 18 | ( 19 | dotenv: DotenvMock, 20 | config?: DotenvConfigOptionsMock, 21 | ): FatimaBuiltInLoadFunction => 22 | async () => { 23 | const env = (dotenv.config(config).parsed ?? 24 | {}) as UnsafeEnvironmentVariables; 25 | 26 | return env; 27 | }; 28 | 29 | export const dotenv = { 30 | load, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/heroku.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaBuiltInLoadFunction, 3 | UnsafeEnvironmentVariables, 4 | } from "lib/types"; 5 | import { lifecycle } from "lib/lifecycle"; 6 | 7 | export interface HerokuLoadOptions { 8 | app_id_or_name: string; 9 | bearer_token?: string; 10 | } 11 | 12 | const load = 13 | (config: HerokuLoadOptions): FatimaBuiltInLoadFunction => 14 | async () => { 15 | const auth = { 16 | bearer_token: config.bearer_token ?? process.env.HEROKU_API_TOKEN, 17 | }; 18 | 19 | if (!auth.bearer_token) { 20 | return lifecycle.error.missingConfig("HEROKU_API_TOKEN"); 21 | } 22 | 23 | const headers = new Headers(); 24 | headers.append("Authorization", `Bearer ${auth.bearer_token}`); 25 | headers.append("Accept", "application/vnd.heroku+json; version=3"); 26 | 27 | const env = await fetch( 28 | `https://api.heroku.com/apps/${config.app_id_or_name}/config-vars`, 29 | { 30 | headers, 31 | }, 32 | ) 33 | .then(async (res) => { 34 | if (!res.ok) { 35 | const errorText = await res.text(); 36 | throw new Error( 37 | `Failed to fetch Heroku config vars. Status: ${res.status}, Response: ${errorText}`, 38 | ); 39 | } 40 | 41 | return await res.json(); 42 | }) 43 | .then((env) => env as UnsafeEnvironmentVariables); 44 | 45 | return env; 46 | }; 47 | 48 | export const heroku = { 49 | load, 50 | }; 51 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/index.ts: -------------------------------------------------------------------------------- 1 | import { dotenv } from "./dotenv"; 2 | import { heroku } from "./heroku"; 3 | import { infisical } from "./infisical"; 4 | import { local } from "./local"; 5 | import { triggerDev } from "./trigger-dev"; 6 | import { vercel } from "./vercel"; 7 | 8 | export const adapters = { 9 | infisical, 10 | triggerDev, 11 | vercel, 12 | dotenv, 13 | heroku, 14 | local, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/infisical.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaBuiltInLoadFunction, 3 | FatimaLoadConfig, 4 | UnsafeEnvironmentVariables, 5 | } from "lib/types"; 6 | import type { GenericClass, AnyType } from "lib/utils/types"; 7 | 8 | import { lifecycle } from "lib/lifecycle"; 9 | 10 | type InfisicalClientMock = GenericClass<{ 11 | auth: () => { 12 | universalAuth: { 13 | login: (args: { 14 | clientId: string; 15 | clientSecret: string; 16 | }) => Promise; 17 | }; 18 | }; 19 | secrets: () => { 20 | listSecrets: (args: { environment: string; projectId: string }) => Promise<{ 21 | secrets: Array<{ 22 | secretKey: string; 23 | secretValue: string; 24 | }>; 25 | }>; 26 | }; 27 | }>; 28 | 29 | const load = 30 | ( 31 | infisicalClient: InfisicalClientMock, 32 | config?: FatimaLoadConfig<{ 33 | clientId?: string; 34 | clientSecret?: string; 35 | projectId?: string; 36 | environment?: string; 37 | }>, 38 | ): FatimaBuiltInLoadFunction => 39 | async () => { 40 | const client = new infisicalClient(); 41 | 42 | const configObject = config 43 | ? config(process.env as UnsafeEnvironmentVariables) 44 | : {}; 45 | 46 | const auth = { 47 | clientId: process.env.INFISICAL_CLIENT_ID, 48 | clientSecret: process.env.INFISICAL_CLIENT_SECRET, 49 | projectId: process.env.INFISICAL_PROJECT_ID, 50 | environment: "dev", 51 | ...configObject, 52 | }; 53 | 54 | if (!auth.clientId) { 55 | return lifecycle.error.missingConfig("INFISICAL_CLIENT_ID"); 56 | } 57 | 58 | if (!auth.clientSecret) { 59 | return lifecycle.error.missingConfig("INFISICAL_CLIENT_SECRET"); 60 | } 61 | 62 | if (!auth.projectId) { 63 | return lifecycle.error.missingConfig("INFISICAL_PROJECT_ID"); 64 | } 65 | 66 | await client.auth().universalAuth.login({ 67 | clientId: auth.clientId, 68 | clientSecret: auth.clientSecret, 69 | }); 70 | 71 | const { secrets } = await client.secrets().listSecrets({ 72 | environment: auth.environment, 73 | projectId: auth.projectId, 74 | }); 75 | 76 | const env = secrets.reduce((acc, { secretKey, secretValue }) => { 77 | acc[secretKey] = secretValue; 78 | return acc; 79 | }, {} as UnsafeEnvironmentVariables); 80 | 81 | return env; 82 | }; 83 | 84 | export const infisical = { 85 | load, 86 | }; 87 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/local.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { resolve } from "node:path"; 3 | import { parseEnvLines } from "lib/env/parse-env"; 4 | 5 | const parse = parseEnvLines; 6 | 7 | const load = 8 | (...files: string[]) => 9 | async () => { 10 | const base = process.cwd(); 11 | 12 | const envPaths = files.map((file) => resolve(base, file)); 13 | 14 | const envContents = await Promise.all( 15 | envPaths.map(async (envPath) => { 16 | try { 17 | const file = readFileSync(envPath); 18 | 19 | const content = file.toString("utf-8"); 20 | 21 | const env = parseEnvLines(content); 22 | 23 | return env; 24 | } catch (error) { 25 | return {}; 26 | } 27 | }), 28 | ); 29 | 30 | let env = {}; 31 | 32 | for (const content of envContents) { 33 | env = { ...env, ...content }; 34 | } 35 | 36 | return env; 37 | }; 38 | 39 | export const local = { 40 | load, 41 | parse, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/trigger-dev.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaBuiltInLoadFunction, 3 | UnsafeEnvironmentVariables, 4 | } from "lib/types"; 5 | import { resolve } from "node:path"; 6 | import { spawn } from "node:child_process"; 7 | import { lifecycle } from "lib/lifecycle"; 8 | import { getRuntime } from "lib/utils/get-runtime"; 9 | 10 | type TriggerDevClientMock = { 11 | envvars: { 12 | list: ( 13 | projectId: string, 14 | environment: string, 15 | ) => Promise< 16 | Array<{ 17 | name: string; 18 | value: string; 19 | }> 20 | >; 21 | }; 22 | configure: (config: { accessToken: string }) => void; 23 | }; 24 | 25 | const load = 26 | ( 27 | trigger: TriggerDevClientMock, 28 | config?: { 29 | projectId: string; 30 | accessToken: string; 31 | environment?: string; 32 | }, 33 | ): FatimaBuiltInLoadFunction => 34 | async () => { 35 | const auth = { 36 | projectId: process.env.TRIGGER_PROJECT_ID, 37 | accessToken: process.env.TRIGGER_ACCESS_TOKEN, 38 | environment: "dev", 39 | ...config, 40 | }; 41 | 42 | if (!auth.projectId) { 43 | return lifecycle.error.missingConfig("TRIGGER_PROJECT_ID"); 44 | } 45 | 46 | if (!auth.accessToken) { 47 | return lifecycle.error.missingConfig("TRIGGER_ACCESS_TOKEN"); 48 | } 49 | 50 | trigger.configure({ 51 | accessToken: auth.accessToken, 52 | }); 53 | 54 | const secrets = await trigger.envvars.list( 55 | auth.projectId, 56 | auth.environment, 57 | ); 58 | 59 | return secrets.reduce((acc, { name, value }) => { 60 | acc[name] = value; 61 | return acc; 62 | }, {} as UnsafeEnvironmentVariables); 63 | }; 64 | 65 | type TriggerDevPluginMock = { 66 | name: string; 67 | setup: (build: { onStart: (callback: () => void) => void }) => void; 68 | }; 69 | 70 | type TriggerDevBuildContextMock = { 71 | registerPlugin: (plugin: TriggerDevPluginMock) => void; 72 | target: string; 73 | }; 74 | 75 | type TriggerDevExtensionMock = { 76 | name: string; 77 | onBuildStart: (context: TriggerDevBuildContextMock) => void; 78 | }; 79 | 80 | export const extension = (configPath?: string): TriggerDevExtensionMock => ({ 81 | name: "fatima-trigger-dev", 82 | onBuildStart(context) { 83 | if (context.target === "dev") return; 84 | 85 | context.registerPlugin({ 86 | name: "fatima-trigger-dev", 87 | async setup(build) { 88 | build.onStart(async () => { 89 | const runtime = getRuntime(); 90 | 91 | const cmdPath = resolve( 92 | process.cwd(), 93 | "node_modules/fatima/dist/cli/cli.cjs", 94 | ); 95 | 96 | await new Promise((resolve, reject) => { 97 | const child = spawn( 98 | runtime, 99 | [cmdPath, "generate", configPath ? `--config=${configPath}` : ""], 100 | { 101 | shell: true, 102 | stdio: "inherit", 103 | }, 104 | ); 105 | 106 | child.on("error", (error) => { 107 | console.error(`Error: ${error.message}`); 108 | reject(error); 109 | }); 110 | 111 | child.on("close", (code) => { 112 | if (code !== 0) { 113 | console.error(`Command exited with code ${code}`); 114 | reject(new Error(`Command exited with code ${code}`)); 115 | } else { 116 | resolve(); 117 | } 118 | }); 119 | }); 120 | }); 121 | }, 122 | }); 123 | }, 124 | }); 125 | 126 | export const triggerDev = { 127 | load, 128 | extension, 129 | }; 130 | -------------------------------------------------------------------------------- /packages/fatima/src/core/adapters/vercel.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaBuiltInLoadFunction, 3 | UnsafeEnvironmentVariables, 4 | } from "lib/types"; 5 | import { spawn } from "node:child_process"; 6 | import { existsSync, promises as fs } from "node:fs"; 7 | import { lifecycle } from "lib/lifecycle"; 8 | import { parseEnvLines } from "lib/env/parse-env"; 9 | import { createInjectableEnv } from "lib/env/patch-env"; 10 | import { logger } from "lib/logger"; 11 | 12 | export type VercelParseFunction = ( 13 | envFileContent: string, 14 | ) => UnsafeEnvironmentVariables; 15 | 16 | export interface VercelLoadConfig { 17 | /** 18 | * The Vercel token from your Vercel account settings. 19 | * @link https://vercel.com/account/settings 20 | */ 21 | vercelToken?: string; 22 | /** 23 | * The Vercel project ID. 24 | * 25 | * Use the following URL with your own values to get the project ID: 26 | * @link https://vercel.com/your-team-name/your-project-name/settings#project-id 27 | */ 28 | vercelProjectId?: string; 29 | /** 30 | * The Vercel organization ID. 31 | * 32 | * Use the following URL with your own values to get the org ID: 33 | * @link https://vercel.com/teams/your-team-name/settings#team-id 34 | */ 35 | vercelOrgId?: string; 36 | /** 37 | * The environment to pull the variables from. 38 | * 39 | * @default "development" 40 | */ 41 | vercelEnvironment?: string; 42 | } 43 | 44 | const load = 45 | (config?: VercelLoadConfig): FatimaBuiltInLoadFunction => 46 | async () => { 47 | try { 48 | const isProjectLinked = existsSync("./.vercel"); 49 | 50 | const auth = { 51 | VERCEL_ORG_ID: 52 | config?.vercelOrgId ?? (process.env.VERCEL_ORG_ID as string), 53 | VERCEL_PROJECT_ID: 54 | config?.vercelProjectId ?? (process.env.VERCEL_PROJECT_ID as string), 55 | VERCEL_TOKEN: 56 | config?.vercelToken ?? (process.env.VERCEL_TOKEN as string), 57 | VERCEL_PROJECT_ENV: config?.vercelEnvironment ?? "development", 58 | } as const satisfies Record; 59 | 60 | const injectableEnv = createInjectableEnv(auth); 61 | 62 | const args = [ 63 | "env", 64 | "pull", 65 | ".tmp.vercel.env", 66 | `--environment=${auth.VERCEL_PROJECT_ENV}`, 67 | ]; 68 | 69 | if (!isProjectLinked) { 70 | if (!auth.VERCEL_ORG_ID) { 71 | return lifecycle.error.missingConfig("VERCEL_ORG_ID"); 72 | } 73 | 74 | if (!auth.VERCEL_PROJECT_ID) { 75 | return lifecycle.error.missingConfig("VERCEL_PROJECT_ID"); 76 | } 77 | 78 | if (!auth.VERCEL_TOKEN) { 79 | return lifecycle.error.missingConfig("VERCEL_TOKEN"); 80 | } 81 | 82 | args.push(`--token=${auth.VERCEL_TOKEN}`); 83 | } 84 | 85 | await new Promise((resolve, reject) => { 86 | const child = spawn("vercel", args, { 87 | env: injectableEnv, 88 | }); 89 | 90 | child.on("error", (e) => { 91 | reject(e); 92 | }); 93 | 94 | child.on("close", (code) => { 95 | if (code === 0) { 96 | resolve(); 97 | } else { 98 | reject(new Error(`Process exited with code ${code}`)); 99 | } 100 | }); 101 | }); 102 | 103 | const envFileContent = await fs.readFile(".tmp.vercel.env", "utf-8"); 104 | 105 | const envVariables = parseEnvLines(envFileContent); 106 | 107 | await fs.unlink(".tmp.vercel.env"); 108 | 109 | return envVariables; 110 | } catch (error) { 111 | logger.error( 112 | "Fatima could not load secrets from Vercel, here are some possible reasons:\n", 113 | "1. You didn't install the Vercel CLI: 'npm i -g vercel'", 114 | "", 115 | "2. You didn't authenticate correctly:", 116 | " - Authenticate by CLI: run 'vercel login' and then 'vercel link'", 117 | " - * you can delete the .vercel folder and try again", 118 | "", 119 | " - Authenticate by TOKEN: VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_ORG_ID", 120 | " - * you can auth by token via .env or passing down the options to the loader", 121 | "", 122 | `3. You don't have the '${config?.vercelEnvironment ?? "development"}' environment in your project.`, 123 | ); 124 | process.exit(1); 125 | } 126 | }; 127 | 128 | export const vercel = { 129 | load, 130 | }; 131 | -------------------------------------------------------------------------------- /packages/fatima/src/core/core.ts: -------------------------------------------------------------------------------- 1 | import { parseEnvLines as parse } from "lib/env/parse-env"; 2 | import { adapters } from "./adapters"; 3 | import { linter } from "./linter"; 4 | import { validators } from "./validators"; 5 | 6 | export { validators, linter, adapters, parse }; 7 | 8 | export * from "lib/config"; 9 | export * from "lib/types"; 10 | -------------------------------------------------------------------------------- /packages/fatima/src/core/linter/eslint.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "lib/utils/types"; 2 | 3 | const noEnvRuleObject = { 4 | meta: { 5 | type: "problem", 6 | docs: { 7 | description: "Prevents unsafe access of the 'env' object.", 8 | recommended: false, 9 | }, 10 | schema: [], 11 | messages: { 12 | noEnv: "Access to the 'env' object is not allowed here.", 13 | }, 14 | }, 15 | 16 | create(context: AnyType) { 17 | return { 18 | MemberExpression(node: AnyType) { 19 | if (node.object && node.object.name === "env") { 20 | context.report({ 21 | node, 22 | messageId: "noEnv", 23 | }); 24 | } 25 | }, 26 | 27 | CallExpression(node: AnyType) { 28 | if (node.callee?.object?.name === "env") { 29 | context.report({ 30 | node, 31 | messageId: "noEnv", 32 | }); 33 | } 34 | }, 35 | }; 36 | }, 37 | }; 38 | 39 | const noProcessEnvRule = { 40 | meta: { 41 | type: "problem", 42 | docs: { 43 | description: "Prevents unsafe access of the 'process.env' object.", 44 | recommended: false, 45 | }, 46 | schema: [], 47 | messages: { 48 | noProcessEnv: 49 | "Access to the 'process.env' object is not allowed, use 'env' or 'publicEnv'.", 50 | }, 51 | }, 52 | 53 | create(context: AnyType) { 54 | return { 55 | MemberExpression(node: AnyType) { 56 | if (node.object?.name === "process" && node.property?.name === "env") { 57 | context.report({ 58 | node, 59 | messageId: "noProcessEnv", 60 | }); 61 | } 62 | }, 63 | 64 | CallExpression(node: AnyType) { 65 | if ( 66 | node.callee?.object?.name === "process" && 67 | node.callee.property?.name === "env" 68 | ) { 69 | context.report({ 70 | node, 71 | messageId: "noProcessEnv", 72 | }); 73 | } 74 | }, 75 | }; 76 | }, 77 | }; 78 | 79 | export const plugin = { 80 | plugins: { 81 | "@fatima": { 82 | rules: { 83 | "no-env": noEnvRuleObject, 84 | "no-process-env": noProcessEnvRule, 85 | }, 86 | }, 87 | }, 88 | rules: { 89 | "@fatima/no-process-env": "error", 90 | }, 91 | } as const; 92 | 93 | export const noEnvRule = (...files: string[]) => 94 | ({ 95 | files, 96 | rules: { 97 | "@fatima/no-env": "error", 98 | }, 99 | }) as const; 100 | -------------------------------------------------------------------------------- /packages/fatima/src/core/linter/index.ts: -------------------------------------------------------------------------------- 1 | import * as eslint from "./eslint"; 2 | 3 | export const linter = { 4 | eslint, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/fatima/src/core/validators/class-validator.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaValidator, UnsafeEnvironmentVariables } from "lib/types"; 2 | import { lifecycle } from "lib/lifecycle"; 3 | import type { AnyType } from "lib/utils/types"; 4 | 5 | export type ClassValidatorValidateMock = (instance: AnyType) => Promise< 6 | Array<{ 7 | property: string; 8 | constraints?: { 9 | [type: string]: string; 10 | }; 11 | }> 12 | >; 13 | 14 | export type ClassTransformerPlainToInstance = ( 15 | constraint: AnyType, 16 | object: AnyType, 17 | ) => AnyType; 18 | 19 | export const classValidator = ( 20 | constraint: AnyType, 21 | helpers: { 22 | validate: ClassValidatorValidateMock; 23 | plainToInstance: ClassTransformerPlainToInstance; 24 | }, 25 | ): FatimaValidator => { 26 | return async (env: UnsafeEnvironmentVariables) => { 27 | try { 28 | const instance = helpers.plainToInstance(constraint, env); 29 | 30 | const classValidatorErrors = await helpers.validate(instance); 31 | 32 | const isValid = classValidatorErrors.length === 0; 33 | 34 | const errors = isValid 35 | ? [] 36 | : classValidatorErrors.flatMap((error) => 37 | Object.values(error.constraints ?? {}).map((value) => ({ 38 | key: error.property, 39 | message: value, 40 | })), 41 | ); 42 | 43 | return { 44 | isValid, 45 | errors, 46 | }; 47 | } catch { 48 | return lifecycle.error.missingBabelTransformClassProperties(); 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/fatima/src/core/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { classValidator } from "./class-validator"; 2 | import { typia } from "./typia"; 3 | import { zod } from "./zod"; 4 | 5 | export const validators = { 6 | zod, 7 | classValidator, 8 | typia, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/fatima/src/core/validators/typia.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaValidator, UnsafeEnvironmentVariables } from "lib/types"; 2 | import type { AnyType } from "lib/utils/types"; 3 | import { fatimaStore } from "lib/store"; 4 | import { spawn } from "node:child_process"; 5 | import { readConfig } from "lib/config/read-config"; 6 | import fs from "node:fs/promises"; 7 | import path from "node:path"; 8 | 9 | export type TypiaFunction = (env: UnsafeEnvironmentVariables) => { 10 | success: boolean; 11 | data?: AnyType; 12 | errors?: IError[]; 13 | }; 14 | 15 | interface IError { 16 | path: string; 17 | expected: string; 18 | value: AnyType; 19 | } 20 | 21 | const typiaTempFolderName = "eba76a2e-684c-4377-a31f-e433246df2f5"; 22 | 23 | export const typia = (fn: TypiaFunction): FatimaValidator => { 24 | return async (env: AnyType) => { 25 | let configPath = 26 | fatimaStore.get("transformedConfigPath") ?? fatimaStore.get("configPath"); 27 | 28 | const isReadingTransformedConfig = configPath.includes(typiaTempFolderName); 29 | 30 | if (isReadingTransformedConfig) { 31 | const result = fn(env); 32 | 33 | const isValid = result.success; 34 | 35 | const errors = 36 | result.errors?.map((error) => ({ 37 | key: error.path.replace("$input.", ""), 38 | message: error.expected, 39 | })) ?? []; 40 | 41 | return { 42 | isValid, 43 | errors, 44 | }; 45 | } 46 | 47 | const configDir = path.dirname(configPath); 48 | 49 | const tempFolderPath = path.join( 50 | configDir, 51 | `${typiaTempFolderName}-${Date.now()}`, 52 | ); 53 | await fs.mkdir(tempFolderPath); 54 | 55 | const tempConfigPath = path.join(tempFolderPath, path.basename(configPath)); 56 | 57 | await fs.copyFile(configPath, tempConfigPath); 58 | 59 | const tempTypiaOutputPath = path.join(tempFolderPath, "dist"); 60 | await fs.mkdir(tempTypiaOutputPath); 61 | 62 | await new Promise((resolve, reject) => { 63 | const typiaCommand = "typia"; 64 | const args = [ 65 | "generate", 66 | "--input", 67 | tempFolderPath, 68 | "--output", 69 | tempTypiaOutputPath, 70 | "--project", 71 | "tsconfig.json", 72 | ]; 73 | 74 | const process = spawn(typiaCommand, args, { shell: true }); 75 | 76 | process.stdout.on("data", () => {}); 77 | 78 | process.stderr.on("data", () => {}); 79 | 80 | process.on("close", (code) => { 81 | if (code === 0) { 82 | resolve(); 83 | } else { 84 | reject(new Error(`Process exited with code ${code}`)); 85 | } 86 | }); 87 | }); 88 | 89 | configPath = path.join(tempTypiaOutputPath, path.basename(configPath)); 90 | 91 | const transformedConfig = await readConfig(configPath); 92 | 93 | fatimaStore.set("transformedConfigPath", configPath); 94 | 95 | const validate = transformedConfig.validate as FatimaValidator; 96 | 97 | const result = validate(env); 98 | 99 | await fs.rm(tempFolderPath, { recursive: true }); 100 | 101 | return result; 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /packages/fatima/src/core/validators/zod.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaValidator, UnsafeEnvironmentVariables } from "lib/types"; 2 | 3 | export type ZodSchemaMock = { 4 | safeParse: (env: unknown) => { 5 | success: boolean; 6 | error?: { 7 | errors: Array<{ 8 | path: (string | number)[]; 9 | message: string; 10 | }>; 11 | }; 12 | data?: unknown; 13 | }; 14 | }; 15 | 16 | export const zod = (constraint: ZodSchemaMock): FatimaValidator => { 17 | return (env: UnsafeEnvironmentVariables) => { 18 | const result = constraint.safeParse(env); 19 | 20 | const isValid = result.success; 21 | 22 | const errors = 23 | result.error?.errors.map((error) => ({ 24 | key: error.path.join("."), 25 | message: error.message, 26 | })) ?? []; 27 | 28 | return { 29 | isValid, 30 | errors, 31 | }; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/fatima/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" { 2 | const module: string; 3 | export default module; 4 | } 5 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/client/client.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaClientOptions } from "lib/types"; 2 | import { txt } from "../utils/txt"; 3 | import preamble from "./preamble/preamble?raw"; 4 | import preambleTypes from "./preamble/types?raw"; 5 | 6 | const ifstring = (condition: T, Then = "", Else = "") => 7 | condition ? Then : Else; 8 | 9 | const content = (blueprint: { 10 | envs: string[]; 11 | createEnvArg: string; 12 | module: "cjs" | "esm"; 13 | lang: "ts" | "js"; 14 | publicPrefix?: string; 15 | }) => { 16 | const privateEnvs = blueprint.envs.filter( 17 | (key) => !blueprint.publicPrefix || !key.startsWith(blueprint.publicPrefix), 18 | ); 19 | 20 | const publicEnvs = blueprint.envs.filter( 21 | (key) => blueprint.publicPrefix && key.startsWith(blueprint.publicPrefix), 22 | ); 23 | 24 | const isJs = blueprint.lang === "js"; 25 | 26 | const isTs = !isJs; 27 | 28 | const exportDeclaration = 29 | blueprint.module === "cjs" ? "module.exports = " : "export"; 30 | 31 | const exportObject = `{ env${ifstring(publicEnvs.length, ", publicEnv")} }`; 32 | 33 | const preambleTypesContent = (preambleTypes as string) 34 | .replace( 35 | "export type EnvObject = AnyType;", 36 | `export interface EnvObject {${blueprint.envs.map((key) => `"${key}": string;`).join("\n ")}}\n`, 37 | ) 38 | .replaceAll("", blueprint.publicPrefix ?? "PUBLIC_"); 39 | 40 | const preambleContent = preamble.split("\n").slice(1).join("\n"); 41 | 42 | return [ 43 | ifstring(isTs, "// @ts-nocheck"), 44 | 45 | ifstring( 46 | isJs, 47 | txt( 48 | "/** @typedef {Object} Env", 49 | privateEnvs.map((key) => ` * @property {string} ${key}`).join("\n"), 50 | " */", 51 | "", 52 | ), 53 | ), 54 | 55 | ifstring( 56 | isJs && publicEnvs.length, 57 | txt( 58 | "/** @typedef {Object} PublicEnv", 59 | publicEnvs.map((key) => ` * @property {string} ${key}`).join("\n"), 60 | " */", 61 | "", 62 | ), 63 | ), 64 | 65 | ifstring( 66 | isJs, 67 | txt( 68 | "/** @typedef {Object} Constraint", 69 | blueprint.envs.map((key) => ` * @property {any} ${key}`).join("\n"), 70 | " */", 71 | "", 72 | ), 73 | ), 74 | 75 | ifstring(isTs, preambleTypesContent), 76 | 77 | preambleContent, 78 | "", 79 | 80 | ifstring(isJs, "/** @type { Env } */"), 81 | `const env = createEnv(${blueprint.createEnvArg}) ${ifstring(isTs, " as Env")}`, 82 | "", 83 | 84 | ifstring( 85 | publicEnvs.length, 86 | txt( 87 | ifstring(isJs, "/** @type { PublicEnv } */"), 88 | `const publicEnv = createPublicEnv(${txt( 89 | " {", 90 | ` publicPrefix: "${blueprint.publicPrefix}",`, 91 | ` publicVariables: {${publicEnvs 92 | .map((key) => ` ${key}: processEnv.${key}`) 93 | .join(",\n")}}`, 94 | "}", 95 | )}) ${ifstring(isTs, " as PublicEnv")}`, 96 | ), 97 | ), 98 | "", 99 | 100 | `${exportDeclaration} ${exportObject}`, 101 | ]; 102 | }; 103 | 104 | export function client( 105 | envs: string[], 106 | options: FatimaClientOptions & { 107 | module: "cjs" | "esm"; 108 | lang: "ts" | "js"; 109 | }, 110 | ) { 111 | const createEnvArg = `{ isServer: ${options.isServer?.toString() ?? "undefined"} }`; 112 | 113 | return content({ 114 | createEnvArg, 115 | envs, 116 | publicPrefix: options.publicPrefix, 117 | lang: options.lang, 118 | module: options.module, 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/client/generate-client.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "lib/config"; 2 | import type { UnsafeEnvironmentVariables } from "lib/types"; 3 | import path from "node:path"; 4 | import { writeFileSync } from "node:fs"; 5 | import { client } from "./client"; 6 | import { getConfigLanguage } from "lib/config/config-language"; 7 | import { format } from "lib/utils/format"; 8 | 9 | export async function generateClient( 10 | config: FatimaConfig, 11 | env: UnsafeEnvironmentVariables, 12 | ) { 13 | const clientPath = path.resolve( 14 | config.file.folderPath, 15 | `env${config.file.extension}`, 16 | ); 17 | 18 | const envs = Object.keys(env ?? {}); 19 | 20 | const { module, lang } = getConfigLanguage(config.file.path); 21 | 22 | const clientContent = client(envs, { 23 | lang, 24 | module, 25 | ...config.client, 26 | }); 27 | 28 | writeFileSync(clientPath, await format(clientContent.join("\n"))); 29 | } 30 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/client/preamble/preamble.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | // eslint-disable-next-line @fatima/no-process-env 4 | const processEnv = process.env; 5 | 6 | const createLogLine = (message) => { 7 | const env = 8 | processEnv.fatima_environment?.split(":")[1] ?? "EnvironmentNotFound"; 9 | 10 | return `🔒 [fatima] (${env}) ` + message.join("\n"); 11 | }; 12 | 13 | const logError = (...messages) => { 14 | const message = createLogLine(messages); 15 | 16 | const currentLogCount = Number(processEnv.fatima_logs?.split(2) ?? "0"); 17 | 18 | processEnv.fatima_logs = "i:" + (currentLogCount + 1).toString(); 19 | 20 | console.log(`\u001B[31m ${message} \u001B[39m`); 21 | }; 22 | 23 | const undefinedEnvironment = (key) => { 24 | logError(`Environment variable ${key} not found.`); 25 | 26 | throw "Environment variable not found"; 27 | }; 28 | 29 | const undefinedEnvironmentAndStore = (key) => { 30 | logError( 31 | `Environment variable ${key} not found.`, 32 | "You might have forgotten to run: fatima dev -g -- 'your-command'", 33 | ); 34 | 35 | throw "Environment variable not found"; 36 | }; 37 | 38 | const createEnv = (options) => { 39 | const isServer = options.isServer || (() => typeof window === "undefined"); 40 | 41 | /** @returns {boolean} */ 42 | const isAccessForbidden = () => !isServer(); 43 | 44 | /** @param {string} key @returns {boolean} */ 45 | const isUndefined = (key) => !processEnv[key]; 46 | 47 | /** @param {string} key */ 48 | const handleUndefined = (key) => { 49 | if (!processEnv.fatima_storeMarker) { 50 | return undefinedEnvironmentAndStore(key); 51 | } 52 | return undefinedEnvironment(key); 53 | }; 54 | 55 | /** @param {string} key */ 56 | const handleForbiddenAccess = (key) => { 57 | const error = [ 58 | `Environment variable ${key} not allowed on the client.`, 59 | "Here are some possible fixes:", 60 | "\n 1. Add the public prefix to your variable if you want to expose it to the client.", 61 | "\n 2. Check if your public prefix is correct by assigning 'env.publicPrefix' to your fatima configuration.", 62 | ]; 63 | 64 | logError(...error); 65 | 66 | throw new Error( 67 | `🔒 [fatima] Environment variable ${key} not allowed on the client.` + 68 | error.join("\n"), 69 | ); 70 | }; 71 | 72 | const fatimaEnv = new Proxy(processEnv, { 73 | /** @param {Object} target @param {string} key @returns {string} */ 74 | get(target, key) { 75 | if (typeof key !== "string" || !/^[A-Z0-9_]+$/.test(key)) { 76 | return undefined; 77 | } 78 | 79 | if (isAccessForbidden()) { 80 | handleForbiddenAccess(key); 81 | } 82 | 83 | if (isUndefined(key)) { 84 | handleUndefined(key); 85 | } 86 | 87 | return Reflect.get(target, key); 88 | }, 89 | }); 90 | 91 | return fatimaEnv; 92 | }; 93 | 94 | const createPublicEnv = (options) => { 95 | /** @param {string} key @returns {boolean} */ 96 | const isUndefined = (key) => { 97 | if (!options.publicVariables[key]) { 98 | return true; 99 | } 100 | }; 101 | 102 | /** @param {string} key */ 103 | const handleUndefined = (key) => { 104 | throw new Error(`🔒 [fatima] Environment variable ${key} not found.`); 105 | }; 106 | 107 | const fatimaPublicEnv = new Proxy(options.publicVariables, { 108 | /** @param {Object} target @param {string} key @returns {string} */ 109 | get(target, key) { 110 | if (isUndefined(key)) { 111 | handleUndefined(key); 112 | } 113 | 114 | return Reflect.get(target, key); 115 | }, 116 | }); 117 | 118 | return fatimaPublicEnv; 119 | }; 120 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/client/preamble/types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ // For ESLint 2 | // biome-ignore lint/suspicious/noExplicitAny: For Biome 3 | type AnyType = any; 4 | 5 | export type EnvObject = AnyType; 6 | 7 | export type FatimaEnvRecord = { 8 | [K in keyof EnvObject as K extends string ? K : never]: EnvValues; 9 | }; 10 | 11 | export type FatimaPrimitiveEnvType = { 12 | [K in keyof EnvObject as K extends string ? K : never]?: AnyType; 13 | }; 14 | 15 | export type FatimaEnvType< 16 | EnvObject, 17 | Type extends 18 | FatimaPrimitiveEnvType = FatimaPrimitiveEnvType, 19 | > = Type; 20 | 21 | export type ServerEnvRecord = { 22 | [K in Keys as K extends `${Prefix}${string}` ? never : K]: string; 23 | }; 24 | 25 | export type PublicEnvRecord = { 26 | [K in Keys as K extends `${Prefix}${string}` ? K : never]: string; 27 | }; 28 | 29 | export type EnvKeys = keyof EnvObject; 30 | 31 | export type EnvRecord = FatimaEnvRecord; 32 | 33 | type PrimitiveEnvType = FatimaPrimitiveEnvType; 34 | 35 | export type EnvType = FatimaEnvType; 36 | 37 | export type Env = ServerEnvRecord">; 38 | 39 | export type PublicEnv = PublicEnvRecord">; 40 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/config/config-language.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { isTypescriptFile } from "lib/utils/is-typescript"; 3 | 4 | export const getConfigLanguage = (configPath: string) => { 5 | const lang = isTypescriptFile(configPath) ? "ts" : "js"; 6 | 7 | const configContent = readFileSync(configPath, "utf-8"); 8 | 9 | const isCJS = 10 | configContent.includes("exports.") || 11 | configContent.includes("module.exports"); 12 | 13 | return { 14 | lang, 15 | module: isCJS ? "cjs" : "esm", 16 | } as const; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FatimaClientOptions, 3 | FatimaEnvironment, 4 | FatimaEnvironmentFunction, 5 | FatimaLoadObject, 6 | FatimaValidator, 7 | } from "lib/types"; 8 | import { FATIMA_DEFAULT_HEAVEN_PORT } from "../constants/port"; 9 | import { lifecycle } from "../lifecycle"; 10 | import { extname } from "node:path"; 11 | import { getCallerLocation } from "lib/utils/get-caller-location"; 12 | import { markConfig } from "./utils"; 13 | 14 | export type FatimaOptions< 15 | Environments extends FatimaEnvironment = FatimaEnvironment, 16 | > = { 17 | /** 18 | * Defines how Fatima loads environment variables. 19 | * 20 | * This should be an object where keys represent different environments, and values 21 | * are arrays of functions responsible for loading the environment variables. 22 | * 23 | * Fatima determines the current environment using the `environment` function 24 | * and then loads variables accordingly. 25 | * 26 | * @type {Record} 27 | */ 28 | load: FatimaLoadObject; 29 | 30 | /** 31 | * A function responsible for determining the current environment. 32 | * 33 | * The return value of this function should match one of the environments 34 | * defined in the `load` object. 35 | * 36 | * @type {FatimaEnvironmentFunction} 37 | */ 38 | environment: FatimaEnvironmentFunction; 39 | 40 | /** 41 | * Optional configuration settings for the generated client. 42 | * 43 | * @type {FatimaClientOptions | undefined} 44 | */ 45 | client?: FatimaClientOptions; 46 | 47 | /** 48 | * An optional function to validate the loaded environment variables. 49 | * 50 | * @type {FatimaValidator | undefined} 51 | */ 52 | validate?: FatimaValidator; 53 | 54 | /** 55 | * Configures the "heaven" feature, which listens on a specified port. 56 | * 57 | * - If set to a number, the heaven service will be enabled on that port. 58 | * - If set to `false`, the feature will be disabled. 59 | * 60 | * @default false 61 | */ 62 | heaven?: number | false; 63 | }; 64 | 65 | export type FatimaConfig = ReturnType; 66 | 67 | export function config({ 68 | load, 69 | environment, 70 | validate, 71 | client, 72 | heaven, 73 | }: FatimaOptions) { 74 | if (!environment) { 75 | return lifecycle.error.missingEnvironmentConfig(); 76 | } 77 | 78 | const { filePath: configFilePath, folderPath: configFolderPath } = 79 | getCallerLocation(); 80 | 81 | const configExtension = extname(configFilePath); 82 | 83 | if (typeof heaven === "undefined") { 84 | heaven = FATIMA_DEFAULT_HEAVEN_PORT; 85 | } 86 | 87 | return markConfig({ 88 | validate, 89 | environment, 90 | client, 91 | load, 92 | heaven, 93 | file: { 94 | extension: configExtension, 95 | path: configFilePath, 96 | folderPath: configFolderPath, 97 | }, 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/config/read-config.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "."; 2 | import { createRequire } from "node:module"; 3 | import { isTypescriptFile } from "src/lib/utils/is-typescript"; 4 | import { isFatimaConfig } from "./utils"; 5 | import { lifecycle } from "lib/lifecycle"; 6 | import { logger } from "lib/logger"; 7 | 8 | const require = createRequire(import.meta.url); 9 | 10 | export async function readConfig(configPath: string): Promise { 11 | try { 12 | const originalEnv = { ...process.env }; 13 | 14 | const isTypescript = isTypescriptFile(configPath); 15 | 16 | let config: FatimaConfig; 17 | 18 | if (!isTypescript) { 19 | config = require(configPath); 20 | } else { 21 | const plugins = []; 22 | try { 23 | const pluginPath = require.resolve( 24 | "@babel/plugin-transform-class-properties", 25 | ); 26 | 27 | const pluginTransformClassProperties = await import(pluginPath) 28 | .then((mod) => mod.default) 29 | .catch(() => {}); 30 | 31 | plugins.push(pluginTransformClassProperties); 32 | } catch {} 33 | 34 | const jitiPath = require.resolve("jiti"); 35 | 36 | const jitiModule = await import(jitiPath); 37 | 38 | if (!jitiModule) { 39 | return lifecycle.error.missinJitiModule(); 40 | } 41 | 42 | const createJiti = jitiModule.createJiti; 43 | 44 | const jiti = createJiti(import.meta.url, { 45 | interopDefault: true, 46 | fsCache: false, 47 | transformOptions: { 48 | ts: true, 49 | babel: { 50 | plugins, 51 | }, 52 | }, 53 | }); 54 | 55 | config = await jiti.import(configPath, { 56 | default: true, 57 | }); 58 | } 59 | 60 | process.env = originalEnv; 61 | 62 | if (!isFatimaConfig(config)) { 63 | logger.error( 64 | "Config file must be created with the fatima config function.", 65 | ); 66 | process.exit(1); 67 | } 68 | 69 | return config; 70 | } catch (error) { 71 | logger.error(error.message); 72 | logger.error("Failed to read config file, check if it exists."); 73 | process.exit(1); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/config/resolve-config-path.ts: -------------------------------------------------------------------------------- 1 | import { resolve, parse } from "node:path"; 2 | import { existsSync, readdirSync, statSync } from "node:fs"; 3 | import { logger } from "lib/logger"; 4 | 5 | const searchBlacklist = [ 6 | "node_modules", 7 | ".git", 8 | "dist", 9 | "out", 10 | "build", 11 | ".next", 12 | ".nuxt", 13 | ".cache", 14 | ".tmp", 15 | ".temp", 16 | ".vscode", 17 | "logs", 18 | ]; 19 | 20 | export function resolveConfigPath(configPath?: string): string { 21 | const baseDir = process.cwd(); 22 | const extensions = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]; 23 | 24 | if (configPath) { 25 | const parsed = parse(configPath); 26 | 27 | if (!parsed.ext) { 28 | logger.error( 29 | `No extension found in given config file path: ${configPath}`, 30 | ); 31 | process.exit(1); 32 | } 33 | 34 | if (!extensions.includes(parsed.ext)) { 35 | logger.error(`Invalid given config file extension: ${parsed.ext}`); 36 | process.exit(1); 37 | } 38 | 39 | const fullPath = resolve(baseDir, configPath); 40 | 41 | if (!existsSync(fullPath)) { 42 | logger.error(`Config file doesn't exist: ${fullPath}`); 43 | process.exit(1); 44 | } 45 | 46 | return fullPath; 47 | } 48 | 49 | const baseName = "env.config"; 50 | 51 | function searchConfig(dir: string): string | null { 52 | const foundPaths: string[] = []; 53 | 54 | for (const file of readdirSync(dir)) { 55 | const fullPath = resolve(dir, file); 56 | 57 | const workspacePath = fullPath.replace(baseDir, ""); 58 | 59 | const pathCrumbs = workspacePath.split("/").map((c) => c.trim()); 60 | 61 | if ( 62 | searchBlacklist.some((blacklisted) => pathCrumbs.includes(blacklisted)) 63 | ) { 64 | continue; 65 | } 66 | 67 | if (extensions.some((ext) => file === baseName + ext)) { 68 | foundPaths.push(fullPath); 69 | } 70 | 71 | if (statSync(fullPath).isDirectory()) { 72 | const nestedConfig = searchConfig(fullPath); 73 | if (nestedConfig) foundPaths.push(nestedConfig); 74 | } 75 | } 76 | 77 | return foundPaths[0] || null; 78 | } 79 | 80 | const configPathFound = searchConfig(baseDir); 81 | 82 | if (!configPathFound) { 83 | logger.error( 84 | "No 'env.config.{js|ts|etc}' file found in the current directory or its subdirectories.", 85 | ); 86 | process.exit(1); 87 | } 88 | 89 | return configPathFound; 90 | } 91 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/config/utils.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "."; 2 | 3 | export const markConfig = (config: T) => { 4 | return { 5 | ...config, 6 | fatimaConfigMarker: true, 7 | }; 8 | }; 9 | 10 | export const isFatimaConfig = ( 11 | config: FatimaConfig, 12 | ): config is FatimaConfig => { 13 | return config.fatimaConfigMarker ?? false; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/constants/envline.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Scott Motte 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | /** 28 | * Source Code: @link https://github.com/motdotla/dotenv/blob/aa03dcad1002027390dac1e8d96ac236274de354/lib/main.js#L9 29 | * 30 | * Regex101: @link https://regex101.com/library/ciNvMc 31 | */ 32 | export const ENVLINE = 33 | /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; 34 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/constants/port.ts: -------------------------------------------------------------------------------- 1 | export const FATIMA_DEFAULT_HEAVEN_PORT = 15781; 2 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/debugger/index.ts: -------------------------------------------------------------------------------- 1 | import { fatimaStore } from "lib/store"; 2 | 3 | const utils = { 4 | error: console.error, 5 | log: console.log, 6 | }; 7 | 8 | export const debug = new Proxy(utils, { 9 | get(target, key: string) { 10 | if (!fatimaStore.get("debug")) return () => null; 11 | 12 | return Reflect.get(target, key); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/env/create-heaven.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "../config"; 2 | import { createFileWatcher } from "lib/heaven/file-watcher"; 3 | import { createHeavenServer } from "lib/heaven/heaven-server"; 4 | import { reloadEnv } from "./reload-env"; 5 | 6 | export function createHeaven(config: FatimaConfig) { 7 | let closeHeaven = () => {}; 8 | 9 | let reload = async () => { 10 | await reloadEnv(config); 11 | }; 12 | 13 | if (config.heaven) { 14 | const { send, server: heavenServer } = createHeavenServer( 15 | config.heaven, 16 | () => reloadEnv(config), 17 | ); 18 | 19 | reload = async () => { 20 | await send(); 21 | }; 22 | 23 | closeHeaven = heavenServer.close; 24 | } 25 | 26 | createFileWatcher(reload); 27 | 28 | return { closeHeaven }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/env/load-env.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "lib/config"; 2 | import type { FatimaLoadFunction, UnsafeEnvironmentVariables } from "lib/types"; 3 | import { lifecycle } from "../lifecycle"; 4 | import { logger } from "lib/logger"; 5 | import { fatimaStore } from "lib/store"; 6 | 7 | export async function loadEnv(config: FatimaConfig) { 8 | try { 9 | const initialSecretsEnviornment = fatimaStore.get("environment"); 10 | 11 | if (!initialSecretsEnviornment || initialSecretsEnviornment === "") { 12 | return lifecycle.error.undefinedEnvironmentFunctionReturn(); 13 | } 14 | 15 | const load = config.load[initialSecretsEnviornment]; 16 | 17 | if (fatimaStore.get("skipLoading") || !load) { 18 | if (!load) { 19 | logger.warn( 20 | `No environment loading function found for the environment "${initialSecretsEnviornment}"`, 21 | ); 22 | } 23 | 24 | logger.info( 25 | "Skipping environment loading, loading system process.env object.", 26 | ); 27 | 28 | return { 29 | env: process.env as UnsafeEnvironmentVariables, 30 | envCount: Object.keys(process.env).length, 31 | }; 32 | } 33 | 34 | let loadChain = load as FatimaLoadFunction[]; 35 | 36 | if (!load.length) { 37 | const loadFunction = load as FatimaLoadFunction; 38 | 39 | loadChain = [loadFunction]; 40 | } 41 | 42 | let env = {} as UnsafeEnvironmentVariables; 43 | 44 | for (const load of loadChain) { 45 | const loadedEnvs = await load(process.env as UnsafeEnvironmentVariables); 46 | 47 | process.env = { ...process.env, ...loadedEnvs }; 48 | 49 | env = { ...env, ...loadedEnvs }; 50 | } 51 | 52 | const finalSecretsEnvironment = config.environment( 53 | process.env as UnsafeEnvironmentVariables, 54 | ); 55 | 56 | if (finalSecretsEnvironment !== initialSecretsEnviornment) { 57 | return lifecycle.error.environmentMixing( 58 | initialSecretsEnviornment, 59 | finalSecretsEnvironment, 60 | ); 61 | } 62 | 63 | fatimaStore.set("envNames", Object.keys(env)); 64 | 65 | return { 66 | env, 67 | envCount: Object.keys(env).length, 68 | }; 69 | } catch (err) { 70 | logger.error(`Failed to load environment variables: ${err.message}`); 71 | process.exit(1); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/env/parse-env.ts: -------------------------------------------------------------------------------- 1 | // Special thanks to motdotla (dotenv) for the regex pattern and how to parse env lines. 2 | 3 | import { ENVLINE } from "../constants/envline"; 4 | 5 | const normalizeEnvValue = (value = "") => { 6 | let normalizedValue = value; 7 | 8 | normalizedValue = normalizedValue.trim(); 9 | 10 | normalizedValue = normalizedValue.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); 11 | 12 | const isDoubleQuoted = value[0] === '"'; 13 | 14 | if (isDoubleQuoted) { 15 | normalizedValue = normalizedValue.replace(/\\n/g, "\n"); 16 | normalizedValue = normalizedValue.replace(/\\r/g, "\r"); 17 | } 18 | 19 | return normalizedValue; 20 | }; 21 | 22 | export function parseEnvLines(lines: string) { 23 | const env = {} as Record; 24 | 25 | const lineList = lines.replace(/\r\n?/gm, "\n").split("\n"); 26 | 27 | for (const line of lineList) { 28 | const match = ENVLINE.exec(line); 29 | 30 | if (!match) { 31 | continue; 32 | } 33 | 34 | const [_, envKey, envValue] = match; 35 | 36 | env[envKey] = normalizeEnvValue(envValue); 37 | 38 | ENVLINE.lastIndex = 0; 39 | } 40 | 41 | return env; 42 | } 43 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/env/patch-env.ts: -------------------------------------------------------------------------------- 1 | import type { UnsafeEnvironmentVariables } from "lib/types"; 2 | 3 | export function createInjectableEnv(env?: UnsafeEnvironmentVariables) { 4 | return { 5 | ...process.env, 6 | ...env, 7 | } as UnsafeEnvironmentVariables; 8 | } 9 | 10 | export function populateEnv(env: UnsafeEnvironmentVariables = {}) { 11 | Object.assign(process.env, env); 12 | } 13 | 14 | export function initializeEnv(env: UnsafeEnvironmentVariables = {}) { 15 | process.env = { 16 | ...process.env, 17 | ...env, 18 | FORCE_COLOR: "1", 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/env/reload-env.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "../config"; 2 | import { generateClient } from "../client/generate-client"; 3 | import { lifecycle } from "../lifecycle"; 4 | import { compareArrays } from "../utils/compare-arrays"; 5 | import { parseValidationErrors } from "../utils/parse-validation"; 6 | import { loadEnv } from "./load-env"; 7 | import { logger } from "lib/logger"; 8 | import { fatimaStore } from "lib/store"; 9 | 10 | export const reloadEnv = async (config: FatimaConfig) => { 11 | const isClientGenerationEnabled = !fatimaStore.get("liteMode"); 12 | 13 | const previousEnvNames = fatimaStore.get("envNames"); 14 | 15 | const { env, envCount } = await loadEnv(config); 16 | 17 | const currentEnvNames = Object.keys(env); 18 | 19 | const didChangeEnv = !compareArrays(previousEnvNames, currentEnvNames); 20 | 21 | if (isClientGenerationEnabled && didChangeEnv) { 22 | await generateClient(config, env); 23 | logger.success(`Updated types with ${envCount} environment variables`); 24 | } 25 | 26 | if (config.validate) { 27 | const { errors } = await config.validate(env); 28 | 29 | if (errors?.length) { 30 | const parsedErrors = parseValidationErrors(errors); 31 | 32 | lifecycle.error.invalidEnvironmentVariables(parsedErrors, false); 33 | } 34 | } 35 | 36 | return { env, envCount }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/exec/index.ts: -------------------------------------------------------------------------------- 1 | import type { UnsafeEnvironmentVariables } from "lib/types"; 2 | import { spawn } from "node:child_process"; 3 | 4 | export const exec = ( 5 | command: string[], 6 | options: { 7 | env: UnsafeEnvironmentVariables; 8 | shell?: boolean; 9 | }, 10 | ) => { 11 | const [cmd, ...args] = command; 12 | 13 | return spawn(cmd, args, { 14 | env: options.env, 15 | shell: options.shell, 16 | stdio: "inherit", 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/heaven/create-heaven.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "../config"; 2 | import { reloadEnv } from "../env/reload-env"; 3 | import { createFileWatcher } from "./file-watcher"; 4 | import { createHeavenServer } from "./heaven-server"; 5 | 6 | export function createHeaven(config: FatimaConfig) { 7 | let closeHeaven = () => {}; 8 | 9 | let reload = async () => { 10 | await reloadEnv(config); 11 | }; 12 | 13 | if (config.heaven) { 14 | const { send, server: heavenServer } = createHeavenServer( 15 | config.heaven, 16 | () => reloadEnv(config), 17 | ); 18 | 19 | reload = async () => { 20 | await send(); 21 | }; 22 | 23 | closeHeaven = heavenServer.close; 24 | } 25 | 26 | createFileWatcher(reload); 27 | 28 | return { closeHeaven }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/heaven/file-watcher.ts: -------------------------------------------------------------------------------- 1 | import type { Promisable } from "lib/utils/types"; 2 | import { watch } from "node:fs"; 3 | import { debounce } from "../utils/debounce"; 4 | 5 | export function createFileWatcher(reload?: () => Promisable) { 6 | return watch( 7 | process.cwd(), 8 | debounce(async (_, filename: string | null) => { 9 | if ( 10 | !filename || 11 | !filename.endsWith(".env") || 12 | filename.startsWith(".tmp") || 13 | filename === ".example.env" 14 | ) 15 | return; 16 | 17 | await reload?.(); 18 | }, 100), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/heaven/heaven-server.ts: -------------------------------------------------------------------------------- 1 | import type { Promisable } from "lib/utils/types"; 2 | import { lifecycle } from "../lifecycle"; 3 | import net from "node:net"; 4 | 5 | export const createHeavenServer = ( 6 | port: number | string, 7 | getSendEventData: () => Promisable, 8 | ) => { 9 | const clients = new Set(); 10 | 11 | const sendDataToHeavenClient = async () => { 12 | const data = await getSendEventData(); 13 | 14 | return Promise.all( 15 | Array.from(clients).map((c) => { 16 | try { 17 | c.write(JSON.stringify(data) + "\n"); 18 | } catch (error) { 19 | console.error("Error writing to client:", error); 20 | } 21 | }), 22 | ); 23 | }; 24 | 25 | const server = net.createServer((socket) => { 26 | socket.on("data", async (data) => { 27 | const dataString = data.toString().trim(); 28 | 29 | const dataLines = dataString.split("\n"); 30 | 31 | let isHttpRequest = false; 32 | 33 | if (dataLines[0].includes("HTTP")) { 34 | isHttpRequest = true; 35 | } 36 | 37 | if (isHttpRequest) { 38 | clients.delete(socket); 39 | 40 | await sendDataToHeavenClient(); 41 | 42 | socket.write( 43 | "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nOK", 44 | ); 45 | 46 | return socket.end(); 47 | } 48 | }); 49 | 50 | socket.on("error", (err) => { 51 | console.error("Socket error:", err); 52 | }); 53 | 54 | clients.add(socket); 55 | }); 56 | 57 | server.listen(Number(port)); 58 | 59 | server.on("error", (error) => { 60 | if ((error as NodeJS.ErrnoException).code === "EADDRINUSE") { 61 | lifecycle.error.heavenPortAlreadyInUse(port); 62 | } 63 | }); 64 | 65 | return { 66 | send: sendDataToHeavenClient, 67 | server, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/lifecycle/error.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "lib/logger"; 2 | import type { FatimaParsedValidationErrors } from "../types"; 3 | 4 | const missingEnvironmentVariable = (env: string): never => { 5 | logger.error(`Missing environment variable: ${env}`); 6 | 7 | process.exit(1); 8 | }; 9 | 10 | const missingConfig = (config: string): never => { 11 | logger.error(`Missing configuration: ${config}`); 12 | 13 | process.exit(1); 14 | }; 15 | 16 | const missingEnvironmentConfig = () => { 17 | logger.error( 18 | `No 'config.environment' found. Please set the environment config in your fatima.config.ts file.`, 19 | ); 20 | 21 | process.exit(1); 22 | }; 23 | 24 | const undefinedEnvironmentFunctionReturn = () => { 25 | logger.error( 26 | `The 'config.environment' function returned undefined or "". Please return a filled string.`, 27 | ); 28 | 29 | process.exit(1); 30 | }; 31 | 32 | const environmentMixing = (initial: string, final: string) => { 33 | logger.error( 34 | `You tried to load "${initial}" variables, but ended up loading "${final}" variables, be careful.\n`, 35 | "The environment must be consistent, otherwise you risk loading secrets from the wrong environment (e.g prod -> dev).", 36 | ); 37 | 38 | process.exit(1); 39 | }; 40 | 41 | const invalidEnvironmentVariables = ( 42 | parsedErrors: FatimaParsedValidationErrors, 43 | exit = true, 44 | ) => { 45 | logger.error( 46 | "Validation failed, here's the error list:" + 47 | "\n\n" + 48 | Object.entries(parsedErrors) 49 | ?.map(([key, messages]) => `❌ ${key}\n • ${messages.join("\n • ")}`) 50 | .join("\n"), 51 | ); 52 | 53 | if (exit) { 54 | process.exit(1); 55 | } 56 | }; 57 | 58 | const missingBabelTransformClassProperties = () => { 59 | logger.error( 60 | "You need to install '@babel/plugin-transform-class-properties' to use 'class-validator' with Fatima.", 61 | ); 62 | 63 | process.exit(1); 64 | }; 65 | 66 | const missinJitiModule = () => { 67 | logger.error( 68 | "You need to install 'jiti' as a dev dependency to use typescript config with Fatima.", 69 | 'Run: "pnpm i -D jiti", "yarn add -D jiti", "npm i -D jiti"', 70 | ); 71 | 72 | process.exit(1); 73 | }; 74 | 75 | const missingWatchPort = () => { 76 | logger.error( 77 | "You need to set 'config.reload.watch' to use the watch feature.", 78 | ); 79 | 80 | process.exit(1); 81 | }; 82 | 83 | const heavenPortAlreadyInUse = (port: number | string) => { 84 | logger.error( 85 | `Couldn't run Heaven, port ${port} is already in use.`, 86 | `Please specify a different one under config option 'heaven' or kill the current process.`, 87 | ); 88 | 89 | process.exit(1); 90 | }; 91 | 92 | const undefinedEnvironment = (key: string) => { 93 | logger.error(`Environment variable ${key} not found.`); 94 | 95 | process.exit(1); 96 | }; 97 | 98 | const undefinedEnvironmentAndStore = (key: string) => { 99 | logger.error( 100 | `Environment variable ${key} not found.`, 101 | "You might have forgotten to run: fatima dev -g -- 'your-command'", 102 | ); 103 | 104 | process.exit(1); 105 | }; 106 | 107 | export const error = { 108 | missingConfig, 109 | missingEnvironmentConfig, 110 | missingEnvironmentVariable, 111 | environmentMixing, 112 | invalidEnvironmentVariables, 113 | missingBabelTransformClassProperties, 114 | missinJitiModule, 115 | missingWatchPort, 116 | heavenPortAlreadyInUse, 117 | undefinedEnvironmentFunctionReturn, 118 | undefinedEnvironment, 119 | undefinedEnvironmentAndStore, 120 | }; 121 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/lifecycle/index.ts: -------------------------------------------------------------------------------- 1 | import { error } from "./error"; 2 | 3 | export const lifecycle = { 4 | error, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { fatimaStore } from "../store"; 2 | 3 | const join = (message: string[]) => { 4 | const env = fatimaStore.get("environment") ?? "EnvironmentNotFound"; 5 | 6 | return `🔒 [fatima] (${env}) ` + message.join("\n"); 7 | }; 8 | 9 | const block = (message: string) => { 10 | const isFirstLog = fatimaStore.get("logs") === 1; 11 | 12 | let block = message; 13 | 14 | if (isFirstLog) { 15 | block = "\r" + block; 16 | } 17 | 18 | if (!isFirstLog || !process.env.npm_package_version) { 19 | block = "\n" + block; 20 | } 21 | 22 | return block; 23 | }; 24 | 25 | const colors = { 26 | error: [31, 39], 27 | success: [32, 39], 28 | warn: [33, 39], 29 | info: [34, 39], 30 | }; 31 | 32 | type LogTheme = keyof typeof colors; 33 | 34 | export const logger: Record void> = 35 | Object.keys(colors).reduce( 36 | (acc, level) => { 37 | acc[level as LogTheme] = (...messages: string[]) => { 38 | const [open, close] = colors[level as LogTheme]; 39 | const message = join(messages); 40 | 41 | fatimaStore.set("logs", fatimaStore.get("logs") + 1); 42 | 43 | console.log(block(`\u001B[${open}m ${message} \u001B[${close}m`)); 44 | }; 45 | return acc; 46 | }, 47 | {} as Record void>, 48 | ); 49 | 50 | export const finishLog = () => { 51 | if (fatimaStore.get("logs") !== 0) { 52 | console.log(""); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaConfig } from "lib/config"; 2 | import type { UnsafeEnvironmentVariables } from "lib/types"; 3 | 4 | type FatimaStore = { 5 | environment: string; 6 | configPath: string; 7 | logs: number; 8 | transformedConfigPath: string; 9 | heavenPort: string; 10 | storeMarker: boolean; 11 | envNames: string[]; 12 | liteMode: boolean; 13 | debug: boolean; 14 | devMode: boolean; 15 | skipLoading: boolean; 16 | }; 17 | 18 | type Prefix = "u:" | "n:" | "b:" | "i:" | "s:" | "a:"; 19 | 20 | type SerializedValue = `${Prefix}${string}`; 21 | 22 | const serialize = (value: T): string => { 23 | if (value === undefined) return "u:"; 24 | 25 | if (value === null) return "n:" + String(value); 26 | 27 | if (typeof value === "boolean") return value ? "b:t" : "b:f"; 28 | 29 | if (typeof value === "number") return "i:" + String(value); 30 | 31 | if (Array.isArray(value)) 32 | return "a:" + value.map(encodeURIComponent).join("&"); 33 | 34 | return "s:" + String(value).trim(); 35 | }; 36 | 37 | const deserialize = (serializedValue: SerializedValue | undefined): T => { 38 | const type = serializedValue?.slice(0, 2) as Prefix; 39 | 40 | const value = serializedValue?.slice(2); 41 | 42 | if (value === undefined || type === "u:") return undefined as T; 43 | 44 | if (type === "n:") return null as T; 45 | 46 | if (type === "b:") return (value === "t") as T; 47 | 48 | if (type === "i:") return Number(value) as T; 49 | 50 | if (type === "s:") return value as T; 51 | 52 | if (type === "a:") return value.split("&").map(decodeURIComponent) as T; 53 | 54 | throw new Error(`Invalid serialized value: ${value}`); 55 | }; 56 | 57 | export const fatimaStore = { 58 | earlyInitialize() { 59 | this.set("storeMarker", true); 60 | this.set("logs", 0); 61 | this.set("envNames", []); 62 | }, 63 | initialize(config: FatimaConfig, options: Record) { 64 | this.set( 65 | "environment", 66 | (options.environment as string) ?? 67 | config.environment(process.env as UnsafeEnvironmentVariables), 68 | ); 69 | 70 | this.set("configPath", config.file.path); 71 | 72 | this.set("heavenPort", String(config.heaven)); 73 | 74 | this.set("liteMode", Boolean(options.lite)); 75 | this.set("debug", Boolean(options.debug)); 76 | this.set("devMode", Boolean(options.devMode)); 77 | this.set("skipLoading", Boolean(options.processEnv)); 78 | }, 79 | get(key: K): FatimaStore[K] { 80 | const rawValue = process.env[`fatima_${key}`] as SerializedValue; 81 | return deserialize(rawValue); 82 | }, 83 | 84 | set(key: K, value?: FatimaStore[K]) { 85 | const serializedValue = serialize(value); 86 | 87 | Object.assign(process.env, { 88 | [`fatima_${key}`]: serializedValue, 89 | }); 90 | }, 91 | 92 | exists() { 93 | return this.get("storeMarker"); 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Promisable } from "./utils/types"; 2 | 3 | export type UnsafeEnvironmentVariables = Record; 4 | 5 | export type FatimaEnvironment = string; 6 | 7 | export type FatimaBuiltInLoadFunction = 8 | () => Promisable; 9 | 10 | export type FatimaCustomLoadFunction = ( 11 | processEnv: UnsafeEnvironmentVariables, 12 | ) => Promisable; 13 | 14 | export type FatimaLoadFunction = 15 | | FatimaBuiltInLoadFunction 16 | | FatimaCustomLoadFunction; 17 | 18 | export type FatimaLoadConfig = ( 19 | processEnv: UnsafeEnvironmentVariables, 20 | ) => Promisable; 21 | 22 | export type FatimaLoaderChain = FatimaLoadFunction[] | FatimaLoadFunction; 23 | 24 | export type FatimaLoadObject = { 25 | [env in Environments]?: FatimaLoaderChain; 26 | }; 27 | 28 | export type FatimaValidatorError = { 29 | key: string; 30 | message: string; 31 | }; 32 | 33 | export type FatimaValidationResult = { 34 | isValid: boolean; 35 | errors: FatimaValidatorError[]; 36 | }; 37 | 38 | export type FatimaValidator = ( 39 | env: UnsafeEnvironmentVariables, 40 | ) => Promisable; 41 | 42 | export type FatimaParsedValidationErrors = Record; 43 | 44 | export type FatimaEnvironmentFunction = ( 45 | processEnv: UnsafeEnvironmentVariables, 46 | ) => string; 47 | 48 | export interface FatimaClientOptions { 49 | /** 50 | * Prefix for public secrets 51 | */ 52 | publicPrefix?: string; 53 | /** 54 | * Function to verify server environment 55 | */ 56 | isServer?: () => boolean; 57 | } 58 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/compare-arrays.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "./types"; 2 | 3 | export function compareArrays(arr1: AnyType[], arr2: AnyType[]) { 4 | if (arr1.length !== arr2.length) return false; 5 | return arr1.every((value: AnyType, index: number) => value === arr2[index]); 6 | } 7 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "./types"; 2 | 3 | export function debounce void>( 4 | func: T, 5 | delay: number, 6 | ): (...args: Parameters) => void { 7 | let timeout: NodeJS.Timeout; 8 | return (...args: Parameters): void => { 9 | clearTimeout(timeout); 10 | timeout = setTimeout(() => func(...args), delay); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "lib/debugger"; 2 | import { execSync } from "node:child_process"; 3 | import path from "node:path"; 4 | 5 | export const format = async (content: string) => { 6 | let formatCode = async () => content; 7 | 8 | const formatters = ["@biomejs/biome", "prettier"]; 9 | 10 | let userFormatter = { 11 | name: "", 12 | path: "", 13 | }; 14 | 15 | for (const formatter of formatters) { 16 | try { 17 | const formatterPath = path.dirname( 18 | require.resolve(formatter + "/package.json"), 19 | ); 20 | 21 | userFormatter = { 22 | name: formatter, 23 | path: formatterPath, 24 | }; 25 | 26 | break; 27 | } catch (e) { 28 | debug.error(e); 29 | } 30 | } 31 | 32 | switch (userFormatter.name) { 33 | case "prettier": { 34 | const prettier = await import(userFormatter.path).then( 35 | (mod) => mod.default, 36 | ); 37 | 38 | formatCode = () => { 39 | return prettier.format(content, { 40 | parser: "typescript", 41 | }); 42 | }; 43 | 44 | break; 45 | } 46 | 47 | case "@biomejs/biome": { 48 | const biomeBinary = path.join(userFormatter.path, "bin/biome"); 49 | 50 | formatCode = async () => 51 | execSync(`${biomeBinary} format --stdin-file-path=tmp.env.ts`, { 52 | input: content, 53 | }).toString(); 54 | 55 | break; 56 | } 57 | } 58 | 59 | return await formatCode(); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/get-caller-location.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export function getCallerLocation(index = 0) { 4 | const callerStack = new Error().stack || ""; 5 | 6 | const callerLine = callerStack.split("\n")[3 + index]; 7 | const callerFileMatch = 8 | callerLine.match(/\((.*):\d+:\d+\)$/) || 9 | callerLine.match(/at (.*):\d+:\d+/); 10 | const filePath = callerFileMatch ? callerFileMatch[1] : "unknown"; 11 | const folderPath = path.dirname(filePath); 12 | 13 | return { filePath, folderPath }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/get-runtime.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "./types"; 2 | 3 | declare const Bun: AnyType; 4 | declare const Deno: AnyType; 5 | 6 | export const getRuntime = () => { 7 | let runtime = "" as "node" | "bun" | "deno"; 8 | 9 | if (process.release.name === "node") { 10 | runtime = "node"; 11 | } 12 | 13 | if (typeof Bun !== "undefined") { 14 | runtime = "bun"; 15 | } 16 | 17 | if (typeof Deno !== "undefined") { 18 | runtime = "deno"; 19 | } 20 | 21 | const supportedRuntimes = ["node", "bun"]; 22 | 23 | if (!supportedRuntimes.includes(runtime)) { 24 | throw new Error(`Unsupported runtime: ${runtime}`); 25 | } 26 | 27 | return runtime; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/is-server.ts: -------------------------------------------------------------------------------- 1 | export const isServer = () => typeof window === "undefined"; 2 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/is-typescript.ts: -------------------------------------------------------------------------------- 1 | export const isTypescriptFile = (path: string) => path.endsWith("ts"); 2 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/parse-validation.ts: -------------------------------------------------------------------------------- 1 | import type { FatimaValidatorError } from "../types"; 2 | 3 | export function parseValidationErrors(errors: FatimaValidatorError[]) { 4 | const groupedErrors = errors.reduce( 5 | (acc, error) => { 6 | const key = error.key; 7 | 8 | if (!acc[key]) { 9 | acc[key] = []; 10 | } 11 | 12 | acc[key].push(error.message); 13 | 14 | return acc; 15 | }, 16 | {} as Record, 17 | ); 18 | 19 | return groupedErrors; 20 | } 21 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/txt.ts: -------------------------------------------------------------------------------- 1 | export function txt(...message: string[]) { 2 | return message.join("\n"); 3 | } 4 | 5 | export const BLANK_LINE = txt(""); 6 | -------------------------------------------------------------------------------- /packages/fatima/src/lib/utils/types.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/suspicious/noExplicitAny: I need a AnyType 2 | export type AnyType = any; 3 | 4 | // biome-ignore lint/complexity/noBannedTypes: Let me use the Function type 5 | export interface GenericClass extends Function { 6 | new (...args: AnyType[]): T; 7 | } 8 | 9 | export type Promisable = T | Promise; 10 | -------------------------------------------------------------------------------- /packages/fatima/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationDir": "./", 10 | "incremental": true, 11 | "tsBuildInfoFile": ".tsbuildinfo", 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "useUnknownInCatchVariables": false, 16 | "noEmit": true, 17 | "paths": { 18 | "lib/*": ["./src/lib/*"] 19 | }, 20 | "baseUrl": "./", 21 | "outDir": "./dist", 22 | "rootDir": "./" 23 | }, 24 | "include": ["./src/**/*.ts", "./plugins/**/*.ts"], 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/fatima/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | import { rawImportPlugin } from "./plugins/esbuild-raw-import-plugin"; 3 | 4 | const createEntries = (...entries: string[]) => { 5 | return entries.map((entry) => { 6 | return `src/${entry}/${entry}.ts`; 7 | }); 8 | }; 9 | 10 | export default defineConfig((opts) => { 11 | const config: Options = { 12 | entry: createEntries("core", "cli"), 13 | dts: true, 14 | shims: true, 15 | clean: true, 16 | platform: "node", 17 | removeNodeProtocol: false, 18 | esbuildPlugins: [rawImportPlugin()], 19 | external: ["prettier", "@biomejs/biome"], 20 | }; 21 | 22 | const release: Options = { 23 | minify: "terser", 24 | treeshake: true, 25 | terserOptions: { 26 | compress: { 27 | passes: 3, 28 | }, 29 | }, 30 | }; 31 | 32 | const dev: Options = { 33 | watch: true, 34 | sourcemap: true, 35 | }; 36 | 37 | if (opts.env?.mode === "release") { 38 | Object.assign(config, release); 39 | } 40 | 41 | if (opts.env?.mode === "dev") { 42 | Object.assign(config, dev); 43 | } 44 | 45 | return config; 46 | }); 47 | -------------------------------------------------------------------------------- /packages/heaven/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @fatimajs/heaven 2 | 3 | ## 0.0.2 (2025/03/24) 4 | 5 | ### Patch Changes 6 | 7 | - fix missing types 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - first fatima heaven version 14 | -------------------------------------------------------------------------------- /packages/heaven/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /packages/heaven/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fatimajs/heaven", 3 | "description": "the heaven client for fatima", 4 | "author": "Fernando Coelho", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "version": "0.0.2", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Fgc17/fatima.git" 13 | }, 14 | "files": ["dist"], 15 | "type": "module", 16 | "types": "./dist/heaven.d.cts", 17 | "main": "./dist/heaven.cjs", 18 | "scripts": { 19 | "------------- toolchain -------------": "-------------", 20 | "format": "biome format --write", 21 | "lint": "biome lint --error-on-warnings", 22 | "typecheck": "tsc --noEmit", 23 | "check": "pnpm typecheck && pnpm lint", 24 | "test": "vitest", 25 | "------------- dev -------------": "-------------", 26 | "dev": "pnpm tsup --env.mode dev", 27 | "------------- build -------------": "-------------", 28 | "build": "rm -rf dist && pnpm tsup --env.mode release", 29 | "bundlesize": "pnpm build --metafile && npm pack --dry-run", 30 | "bundlesize:dev": "pnpm tsup --metafile && npm pack --dry-run", 31 | "------------- publishing -------------": "-------------", 32 | "release": "pnpm check && pnpm build && pnpm publish --no-git-checks", 33 | "release:rc": "pnpm check && pnpm build && pnpm version prerelease --preid=rc" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^22.9.1", 37 | "terser": "^5.39.0", 38 | "tsup": "^8.3.5", 39 | "typescript": "5.6.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/heaven/src/client.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "lib/types"; 2 | import { getRuntime } from "./lib/get-runtime"; 3 | import { logger } from "lib/logger"; 4 | 5 | declare global { 6 | namespace Bun { 7 | interface Socket { 8 | data: Promise; 9 | } 10 | function connect(options: { 11 | hostname: string; 12 | port: number; 13 | }): Promise; 14 | function listen(options: { 15 | hostname: string; 16 | port: number; 17 | }): Promise; 18 | } 19 | } 20 | 21 | declare global { 22 | namespace Deno { 23 | interface Conn { 24 | readable: ReadableStream; 25 | } 26 | function connect(options: { 27 | hostname: string; 28 | port: number; 29 | }): Promise; 30 | } 31 | } 32 | 33 | interface EnvData { 34 | env: Record; 35 | envCount: number; 36 | } 37 | 38 | interface CrossPlatformSocket { 39 | connect: ( 40 | port: number, 41 | host: string, 42 | cb?: () => void, 43 | ) => void | Promise; 44 | on: (event: string, callback: (data?: AnyType) => void) => void; 45 | write?: (data: string) => void; 46 | } 47 | 48 | export function createHeavenClient(host: string, portLike: string | number) { 49 | let client: CrossPlatformSocket; 50 | const port = Number(portLike); 51 | const runtime = getRuntime(); 52 | 53 | switch (runtime) { 54 | case "node": { 55 | const net = require("node:net"); 56 | client = new net.Socket(); 57 | break; 58 | } 59 | case "bun": { 60 | client = { 61 | connect: async (port: number, host: string, cb?: () => void) => { 62 | const conn = await Bun.connect({ hostname: host, port }); 63 | if (cb) cb(); 64 | }, 65 | on: (event: string, callback: (data?: AnyType) => void) => { 66 | if (event === "data") { 67 | Bun.listen({ hostname: host, port }).then((socket) => { 68 | socket.data.then((data) => callback(data)); 69 | }); 70 | } else if (event === "close") { 71 | callback(); 72 | } else if (event === "error") { 73 | callback(new Error("Bun connection error")); 74 | } 75 | }, 76 | }; 77 | break; 78 | } 79 | case "deno": { 80 | let conn: Deno.Conn; 81 | client = { 82 | connect: async (port: number, host: string, cb?: () => void) => { 83 | conn = await Deno.connect({ hostname: host, port }); 84 | if (cb) cb(); 85 | }, 86 | on: (event: string, callback: (data?: AnyType) => void) => { 87 | if (event === "data") { 88 | (async () => { 89 | const decoder = new TextDecoder(); 90 | for await (const chunk of conn.readable) { 91 | callback({ toString: () => decoder.decode(chunk) }); 92 | } 93 | })(); 94 | } else if (event === "close") { 95 | callback(); 96 | } else if (event === "error") { 97 | callback(new Error("Deno connection error")); 98 | } 99 | }, 100 | }; 101 | break; 102 | } 103 | default: 104 | throw new Error(`Unsupported runtime: ${runtime}`); 105 | } 106 | 107 | client.connect(port, host); 108 | 109 | client.on("data", (data: { toString: () => string }) => { 110 | try { 111 | const dataString = data.toString().trim(); 112 | const [envString] = dataString.split("\n"); 113 | const { env, envCount } = JSON.parse(envString) as EnvData; 114 | 115 | Object.assign(process.env, env); 116 | 117 | logger.success(`Successfully reloaded process.env with ${envCount} envs`); 118 | } catch (err) { 119 | logger.error("Heaven error:", err); 120 | } 121 | }); 122 | 123 | client.on("close", () => { 124 | logger.error("Heaven disconnected"); 125 | }); 126 | 127 | client.on("error", (err: Error) => { 128 | logger.error("Heaven error:", err.message); 129 | }); 130 | 131 | return client; 132 | } 133 | -------------------------------------------------------------------------------- /packages/heaven/src/heaven.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "./watch"; 2 | 3 | export const heaven = { 4 | watch, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/heaven/src/lib/get-runtime.ts: -------------------------------------------------------------------------------- 1 | import type { AnyType } from "./types"; 2 | 3 | declare const Bun: AnyType; 4 | declare const Deno: AnyType; 5 | 6 | export const getRuntime = () => { 7 | let runtime = "" as "node" | "bun" | "deno"; 8 | 9 | if (process.release.name === "node") { 10 | runtime = "node"; 11 | } 12 | 13 | if (typeof Bun !== "undefined") { 14 | runtime = "bun"; 15 | } 16 | 17 | if (typeof Deno !== "undefined") { 18 | runtime = "deno"; 19 | } 20 | 21 | const supportedRuntimes = ["node", "bun"]; 22 | 23 | if (!supportedRuntimes.includes(runtime)) { 24 | throw new Error(`Unsupported runtime: ${runtime}`); 25 | } 26 | 27 | return runtime; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/heaven/src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import { fatimaStore } from "./store"; 2 | 3 | const join = (message: string[]) => { 4 | const env = fatimaStore.get("environment") ?? "EnvironmentNotFound"; 5 | 6 | return `🔒 [fatima] (${env}) ` + message.join("\n"); 7 | }; 8 | 9 | const block = (message: string) => { 10 | const isFirstLog = fatimaStore.get("logs") === 1; 11 | 12 | let block = message; 13 | 14 | if (isFirstLog) { 15 | block = "\r" + block; 16 | } 17 | 18 | if (!isFirstLog || !process.env.npm_package_version) { 19 | block = "\n" + block; 20 | } 21 | 22 | return block; 23 | }; 24 | 25 | const colors = { 26 | error: [31, 39], 27 | success: [32, 39], 28 | warn: [33, 39], 29 | info: [34, 39], 30 | }; 31 | 32 | type LogTheme = keyof typeof colors; 33 | 34 | export const logger: Record void> = 35 | Object.keys(colors).reduce( 36 | (acc, level) => { 37 | acc[level as LogTheme] = (...messages: string[]) => { 38 | const [open, close] = colors[level as LogTheme]; 39 | const message = join(messages); 40 | 41 | fatimaStore.set("logs", fatimaStore.get("logs") + 1); 42 | 43 | console.log(`\u001B[${open}m ${block(message)} \u001B[${close}m`); 44 | }; 45 | return acc; 46 | }, 47 | {} as Record void>, 48 | ); 49 | -------------------------------------------------------------------------------- /packages/heaven/src/lib/store.ts: -------------------------------------------------------------------------------- 1 | type FatimaStore = { 2 | logs: number; 3 | environment: string; 4 | storeMarker: boolean; 5 | heavenPort: string; 6 | devMode: boolean; 7 | }; 8 | 9 | const serialize = (value: T): string => { 10 | if (value === undefined) return "u:"; 11 | 12 | if (value === null) return "n:" + String(value); 13 | 14 | if (typeof value === "boolean") return value ? "b:t" : "b:f"; 15 | 16 | if (typeof value === "number") return "i:"; 17 | 18 | return "s:" + String(value).trim(); 19 | }; 20 | 21 | const deserialize = (value: string | undefined): T => { 22 | if (value === undefined || value === "u:") return undefined as T; 23 | 24 | if (value.startsWith("n:")) return null as T; 25 | 26 | if (value.startsWith("b:")) return (value === "b:t") as T; 27 | 28 | if (value.startsWith("i:")) return Number(value) as T; 29 | 30 | if (value.startsWith("s:")) return value.slice(2) as T; 31 | 32 | throw new Error(`Invalid serialized value: ${value}`); 33 | }; 34 | 35 | export const fatimaStore = { 36 | get(key: K): FatimaStore[K] { 37 | const rawValue = process.env[`fatima_${key}`]; 38 | return deserialize(rawValue); 39 | }, 40 | 41 | set(key: K, value?: FatimaStore[K]) { 42 | process.env[`fatima_${key}`] = serialize(value); 43 | }, 44 | 45 | exists() { 46 | return this.get("storeMarker"); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/heaven/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/suspicious/noExplicitAny: I need a AnyType 2 | export type AnyType = any; 3 | -------------------------------------------------------------------------------- /packages/heaven/src/watch.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "lib/logger"; 2 | import { createHeavenClient } from "./client"; 3 | import { fatimaStore } from "lib/store"; 4 | 5 | export function watch(host = "localhost") { 6 | const isDevMode = Boolean(process.env.fatima_devMode); 7 | 8 | if (!isDevMode) return; 9 | 10 | const port = fatimaStore.get("heavenPort"); 11 | 12 | if (!port) { 13 | logger.error("You need to set 'config.heaven' to use the watch feature."); 14 | 15 | console.log(""); 16 | 17 | process.exit(1); 18 | } 19 | 20 | createHeavenClient(host, port); 21 | } 22 | -------------------------------------------------------------------------------- /packages/heaven/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationDir": "./", 10 | "incremental": true, 11 | "tsBuildInfoFile": ".tsbuildinfo", 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "useUnknownInCatchVariables": false, 16 | "baseUrl": "./", 17 | "outDir": "./dist", 18 | "rootDir": "./", 19 | "noEmit": true, 20 | "paths": { 21 | "lib/*": ["src/lib/*"] 22 | } 23 | }, 24 | "include": ["src/**/*.ts"], 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/heaven/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | 3 | export default defineConfig((opts) => { 4 | const config: Options = { 5 | entry: ["./src/heaven.ts"], 6 | dts: true, 7 | clean: true, 8 | shims: true, 9 | removeNodeProtocol: false, 10 | }; 11 | 12 | const release: Options = { 13 | minify: "terser", 14 | treeshake: true, 15 | terserOptions: { 16 | compress: { 17 | passes: 3, 18 | }, 19 | }, 20 | }; 21 | 22 | const dev: Options = { 23 | watch: true, 24 | sourcemap: true, 25 | }; 26 | 27 | if (opts.env?.mode === "release") { 28 | Object.assign(config, release); 29 | } 30 | 31 | if (opts.env?.mode === "dev") { 32 | Object.assign(config, dev); 33 | } 34 | 35 | return config; 36 | }); 37 | -------------------------------------------------------------------------------- /packages/playground/.gitignore: -------------------------------------------------------------------------------- 1 | xdxd 2 | 3 | node_modules 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | # fatima 17 | env.ts 18 | # fatima 19 | env.ts -------------------------------------------------------------------------------- /packages/playground/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /packages/playground/env.config.js: -------------------------------------------------------------------------------- 1 | const { config } = require("fatima"); 2 | const { adapters } = require("fatima"); 3 | const { validators } = require("fatima"); 4 | const { z } = require("zod"); 5 | 6 | /** 7 | * @type {import('#env').Constraint} 8 | */ 9 | const constraint = { 10 | NODE_ENV: z.enum(["development"]), 11 | }; 12 | 13 | module.exports = config({ 14 | load: { 15 | development: [adapters.local.load(".env")], 16 | }, 17 | validate: validators.zod(z.object(constraint)), 18 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 19 | }); 20 | -------------------------------------------------------------------------------- /packages/playground/env.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "fatima"; 2 | import { adapters } from "fatima"; 3 | import { validators } from "fatima"; 4 | import { z, type ZodType } from "zod"; 5 | import type { EnvRecord } from "env"; 6 | 7 | type Environment = "development" | "staging" | "production"; 8 | type Constraint = Partial>; 9 | const constraint: Constraint = { 10 | NODE_ENV: z.enum(["development"]), 11 | }; 12 | 13 | export default config({ 14 | load: { 15 | development: [adapters.local.load(".env")], 16 | }, 17 | validate: validators.zod(z.object(constraint)), 18 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 19 | }); 20 | -------------------------------------------------------------------------------- /packages/playground/fatima-script.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | 3 | const args = process.argv.slice(2); 4 | const command = args[0]; 5 | const toolArgIndex = args.findIndex((arg) => arg === "--tool" || arg === "-t"); 6 | const langArgIndex = args.findIndex((arg) => arg === "--lang" || arg === "-l"); 7 | 8 | if (!command || toolArgIndex === -1 || toolArgIndex + 1 >= args.length) { 9 | console.error("Error: command and --tool/-t argument are required"); 10 | process.exit(1); 11 | } 12 | 13 | const tool = args[toolArgIndex + 1]; 14 | const lang = args[langArgIndex + 1] || "ts" 15 | const fullCommand = `fatima ${command} --config ./src/${tool}/env.config.${lang}`; 16 | 17 | try { 18 | execSync(fullCommand, { stdio: "inherit" }); 19 | } catch { 20 | process.exit(1); 21 | } 22 | -------------------------------------------------------------------------------- /packages/playground/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "#jsdoc-env": ["./src/jsdoc/env.js"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fatimajs/playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "fatimax": "node fatima-script.js" 7 | }, 8 | "imports": { 9 | "#jsdoc-env": "./src/jsdoc/env.js" 10 | }, 11 | "devDependencies": { 12 | "tsx": "4.19.2", 13 | "create-fatima": "workspace:*", 14 | "reflect-metadata": "0.2.2", 15 | "arktype": "^2.0.3", 16 | "class-transformer": "^0.5.1", 17 | "class-validator": "^0.14.1", 18 | "dotenv": "16.4.7", 19 | "fatima": "workspace:*", 20 | "typia": "7.6.0", 21 | "zod": "3.24.1", 22 | "@babel/plugin-transform-class-properties": "7.25.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/playground/src/class-validator/env.config.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import dotenv from "dotenv"; 3 | import { config, adapters, validators } from "fatima"; 4 | import { IsString, IsTimeZone, validate } from "class-validator"; 5 | import { plainToInstance } from "class-transformer"; 6 | 7 | import type { EnvObject } from "./env"; 8 | 9 | class Constraint implements Partial { 10 | @IsString() 11 | NODE_ENV: string; 12 | 13 | @IsTimeZone() 14 | TZ?: string | undefined; 15 | } 16 | 17 | type Environment = "development" | "staging" | "production"; 18 | 19 | export default config({ 20 | client: { 21 | publicPrefix: "NEST_PUBLIC", 22 | }, 23 | load: { 24 | development: [adapters.dotenv.load(dotenv)], 25 | }, 26 | validate: validators.classValidator(Constraint, { 27 | plainToInstance, 28 | validate, 29 | }), 30 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 31 | }); 32 | -------------------------------------------------------------------------------- /packages/playground/src/class-validator/env.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createEnv, 3 | type ServerEnvRecord, 4 | type EnvType as FatimaEnvType, 5 | type EnvRecord as FatimaEnvRecord, 6 | type PrimitiveEnvType as FatimaPrimitiveEnvType, 7 | } from 'fatima/env'; 8 | 9 | export interface EnvObject { 10 | "NODE_ENV": string; 11 | "TZ": string; 12 | "NEXT_PUBLIC_API_URL": string; 13 | "TEST": string; 14 | } 15 | 16 | export type EnvKeys = keyof EnvObject; 17 | 18 | export type EnvRecord = FatimaEnvRecord; 19 | 20 | type PrimitiveEnvType = FatimaPrimitiveEnvType; 21 | export type EnvType = FatimaEnvType; 22 | 23 | type Env = ServerEnvRecord 24 | 25 | export const env = createEnv({ isServer: undefined }) as Env; 26 | 27 | -------------------------------------------------------------------------------- /packages/playground/src/jsdoc/env.config.js: -------------------------------------------------------------------------------- 1 | const { config, adapters, validators } = require("fatima"); 2 | const { z } = require("zod"); 3 | 4 | const dotenv = require("dotenv"); 5 | 6 | /** 7 | * @type {import('#jsdoc-env').Constraint} 8 | */ 9 | const constraint = { 10 | NODE_ENV: z.string(), 11 | TZ: z.string(), 12 | NEXT_PUBLIC_API_URL: z.string(), 13 | }; 14 | 15 | module.exports = config({ 16 | load: { 17 | development: [adapters.dotenv.load(dotenv)], 18 | }, 19 | validate: validators.zod(z.object(constraint)), 20 | environment: () => process.env.NODE_ENV ?? "development", 21 | client: { 22 | publicPrefix: "PUBLIC_" 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /packages/playground/src/jsdoc/env.js: -------------------------------------------------------------------------------- 1 | /** @typedef {Object} Env 2 | * @property {string} NODE_ENV 3 | * @property {string} TZ 4 | * @property {string} NEXT_PUBLIC_API_URL 5 | * @property {string} TEST 6 | */ 7 | 8 | /** @typedef {Object} PublicEnv 9 | * @property {string} PUBLIC_TEST 10 | */ 11 | 12 | /** @typedef {Object} Constraint 13 | * @property {any} NODE_ENV 14 | * @property {any} TZ 15 | * @property {any} NEXT_PUBLIC_API_URL 16 | * @property {any} TEST 17 | * @property {any} PUBLIC_TEST 18 | */ 19 | 20 | // eslint-disable-next-line @fatima/no-process-env 21 | const processEnv = process.env; 22 | 23 | const createLogLine = (message) => { 24 | const env = 25 | processEnv.fatima_environment?.split(":")[1] ?? "EnvironmentNotFound"; 26 | 27 | return `🔒 [fatima] (${env}) ` + message.join("\n"); 28 | }; 29 | 30 | const logError = (...messages) => { 31 | const message = createLogLine(messages); 32 | 33 | const currentLogCount = Number(processEnv.fatima_logs?.split(2) ?? "0"); 34 | 35 | processEnv.fatima_logs = "i:" + (currentLogCount + 1).toString(); 36 | 37 | console.log(`\u001B[31m ${message} \u001B[39m`); 38 | }; 39 | 40 | const undefinedEnvironment = (key) => { 41 | logError(`Environment variable ${key} not found.`); 42 | process.exit(1); 43 | }; 44 | 45 | const undefinedEnvironmentAndStore = (key) => { 46 | logError( 47 | `Environment variable ${key} not found.`, 48 | "You might have forgotten to run: fatima dev -g -- 'your-command'", 49 | ); 50 | process.exit(1); 51 | }; 52 | 53 | const createEnv = (options) => { 54 | const isServer = options.isServer || (() => typeof window === "undefined"); 55 | 56 | /** @returns {boolean} */ 57 | const isAccessForbidden = () => !isServer(); 58 | 59 | /** @param {string} key @returns {boolean} */ 60 | const isUndefined = (key) => !processEnv[key]; 61 | 62 | /** @param {string} key */ 63 | const handleUndefined = (key) => { 64 | if (!processEnv.fatima_storeMarker) { 65 | return undefinedEnvironmentAndStore(key); 66 | } 67 | return undefinedEnvironment(key); 68 | }; 69 | 70 | /** @param {string} key */ 71 | const handleForbiddenAccess = (key) => { 72 | const error = [ 73 | "Here are some possible fixes:", 74 | "\n 1. Add the public prefix to your variable if you want to expose it to the client.", 75 | "\n 2. Check if your public prefix is correct by assigning 'env.publicPrefix' to your fatima configuration.", 76 | ]; 77 | 78 | logError(...error); 79 | 80 | throw new Error( 81 | `🔒 [fatima] Environment variable ${key} not allowed on the client.` + 82 | error.join("\n"), 83 | ); 84 | }; 85 | 86 | const fatimaEnv = new Proxy(processEnv, { 87 | /** @param {Object} target @param {string} key @returns {string} */ 88 | get(target, key) { 89 | if (isAccessForbidden()) { 90 | handleForbiddenAccess(key); 91 | } 92 | 93 | if (isUndefined(key)) { 94 | handleUndefined(key); 95 | } 96 | 97 | return Reflect.get(target, key); 98 | }, 99 | }); 100 | 101 | return fatimaEnv; 102 | }; 103 | 104 | const createPublicEnv = (options) => { 105 | /** @param {string} key @returns {boolean} */ 106 | const isUndefined = (key) => { 107 | if (!options.publicVariables[key]) { 108 | return true; 109 | } 110 | }; 111 | 112 | /** @param {string} key */ 113 | const handleUndefined = (key) => { 114 | throw new Error(`🔒 [fatima] Environment variable ${key} not found.`); 115 | }; 116 | 117 | const fatimaPublicEnv = new Proxy(options.publicVariables, { 118 | /** @param {Object} target @param {string} key @returns {string} */ 119 | get(target, key) { 120 | if (isUndefined(key)) { 121 | handleUndefined(key); 122 | } 123 | 124 | return Reflect.get(target, key); 125 | }, 126 | }); 127 | 128 | return fatimaPublicEnv; 129 | }; 130 | 131 | /** @type { Env } */ 132 | const env = createEnv({ isServer: undefined }); 133 | 134 | /** @type { PublicEnv } */ 135 | const publicEnv = createPublicEnv({ 136 | publicPrefix: "PUBLIC_", 137 | publicVariables: { PUBLIC_TEST: processEnv.PUBLIC_TEST }, 138 | }); 139 | 140 | module.exports = { env, publicEnv }; 141 | -------------------------------------------------------------------------------- /packages/playground/src/jsdoc/src/index.js: -------------------------------------------------------------------------------- 1 | const { env } = require("#jsdoc-env"); 2 | -------------------------------------------------------------------------------- /packages/playground/src/typia/env.config.ts: -------------------------------------------------------------------------------- 1 | import { config, adapters, validators } from "fatima"; 2 | import type { EnvType } from "@/typia/env"; 3 | import typia, { type tags } from "typia"; 4 | import dotenv from "dotenv"; 5 | 6 | type Constraint = EnvType<{ 7 | NODE_ENV: string; 8 | }>; 9 | 10 | export default config({ 11 | client: { 12 | publicPrefix: "NEXT_PUBLIC_", 13 | }, 14 | load: { 15 | development: [adapters.dotenv.load(dotenv)], 16 | }, 17 | validate: validators.typia((env) => typia.validate(env)), 18 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 19 | }); 20 | -------------------------------------------------------------------------------- /packages/playground/src/typia/env.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | export interface EnvObject { 4 | NODE_ENV: string; 5 | TZ: string; 6 | NEXT_PUBLIC_API_URL: string; 7 | TEST: string; 8 | PUBLIC_TEST: string; 9 | } 10 | 11 | /* eslint-disable @typescript-eslint/no-explicit-any */ // For ESLint 12 | // biome-ignore lint/suspicious/noExplicitAny: For Biome 13 | type AnyType = any; 14 | 15 | export type FatimaEnvRecord = { 16 | [K in keyof EnvObject as K extends string ? K : never]: EnvValues; 17 | }; 18 | 19 | export type FatimaPrimitiveEnvType = { 20 | [K in keyof EnvObject as K extends string ? K : never]?: AnyType; 21 | }; 22 | 23 | export type FatimaEnvType< 24 | EnvObject, 25 | Type extends 26 | FatimaPrimitiveEnvType = FatimaPrimitiveEnvType, 27 | > = Type; 28 | 29 | export type ServerEnvRecord = { 30 | [K in Keys as K extends `${Prefix}${string}` ? never : K]: string; 31 | }; 32 | 33 | export type PublicEnvRecord = { 34 | [K in Keys as K extends `${Prefix}${string}` ? K : never]: string; 35 | }; 36 | 37 | export type EnvKeys = keyof EnvObject; 38 | 39 | export type EnvRecord = FatimaEnvRecord; 40 | 41 | type PrimitiveEnvType = FatimaPrimitiveEnvType; 42 | 43 | export type EnvType = FatimaEnvType; 44 | 45 | export type Env = ServerEnvRecord; 46 | export type PublicEnv = PublicEnvRecord; 47 | 48 | const colors = { 49 | error: [31, 39], 50 | success: [32, 39], 51 | warn: [33, 39], 52 | info: [34, 39], 53 | }; 54 | 55 | const createLogLine = (message) => { 56 | const env = process.env.fatima_env ?? "EnvironmentNotFound"; 57 | return `🔒 [fatima] (${env}) ` + message.join("\n"); 58 | }; 59 | 60 | const logError = (...messages) => { 61 | const [open, close] = colors.error; 62 | const message = createLogLine(messages); 63 | console.log(`\u001B[${open}m ${message} \u001B[${close}m`); 64 | }; 65 | 66 | const undefinedEnvironment = (key) => { 67 | logError(`Environment variable ${key} not found.`); 68 | process.exit(1); 69 | }; 70 | 71 | const undefinedEnvironmentAndStore = (key) => { 72 | logError( 73 | `Environment variable ${key} not found.`, 74 | "You might have forgotten to run: fatima dev -g -- 'your-command'", 75 | ); 76 | process.exit(1); 77 | }; 78 | 79 | const createEnv = (options) => { 80 | const isServer = options.isServer || (() => typeof window === "undefined"); 81 | 82 | /** @returns {boolean} */ 83 | const isAccessForbidden = () => !isServer(); 84 | 85 | /** @param {string} key @returns {boolean} */ 86 | const isUndefined = (key) => !process.env[key]; 87 | 88 | /** @param {string} key */ 89 | const handleUndefined = (key) => { 90 | if (!process.env.fatima_storeMarker) { 91 | return undefinedEnvironmentAndStore(key); 92 | } 93 | return undefinedEnvironment(key); 94 | }; 95 | 96 | /** @param {string} key */ 97 | const handleForbiddenAccess = (key) => { 98 | const error = [ 99 | "Here are some possible fixes:", 100 | "\n 1. Add the public prefix to your variable if you want to expose it to the client.", 101 | "\n 2. Check if your public prefix is correct by assigning 'env.publicPrefix' to your fatima configuration.", 102 | ]; 103 | 104 | logError(...error); 105 | 106 | throw new Error( 107 | `🔒 [fatima] Environment variable ${key} not allowed on the client.` + 108 | error.join("\n"), 109 | ); 110 | }; 111 | 112 | const fatimaEnv = new Proxy(process.env, { 113 | /** @param {Object} target @param {string} key @returns {string} */ 114 | get(target, key) { 115 | if (isAccessForbidden()) { 116 | handleForbiddenAccess(key); 117 | } 118 | 119 | if (isUndefined(key)) { 120 | handleUndefined(key); 121 | } 122 | 123 | return Reflect.get(target, key); 124 | }, 125 | }); 126 | 127 | return fatimaEnv; 128 | }; 129 | 130 | const createPublicEnv = (options) => { 131 | /** @param {string} key @returns {boolean} */ 132 | const isUndefined = (key) => { 133 | if (!options.publicVariables[key]) { 134 | return true; 135 | } 136 | }; 137 | 138 | /** @param {string} key */ 139 | const handleUndefined = (key) => { 140 | throw new Error(`🔒 [fatima] Environment variable ${key} not found.`); 141 | }; 142 | 143 | const fatimaPublicEnv = new Proxy(options.publicVariables, { 144 | /** @param {Object} target @param {string} key @returns {string} */ 145 | get(target, key) { 146 | if (isUndefined(key)) { 147 | handleUndefined(key); 148 | } 149 | 150 | return Reflect.get(target, key); 151 | }, 152 | }); 153 | 154 | return fatimaPublicEnv; 155 | }; 156 | 157 | const env = createEnv({ isServer: undefined }) as Env; 158 | 159 | const publicEnv = createPublicEnv({ 160 | publicPrefix: "NEXT_PUBLIC_", 161 | publicVariables: { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL }, 162 | }) as PublicEnv; 163 | 164 | export { env, publicEnv }; 165 | -------------------------------------------------------------------------------- /packages/playground/src/typia/test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgcoelho/fatima/4c1239aa8ebb0f956a8266ffe84a21ed27a2fdc5/packages/playground/src/typia/test.ts -------------------------------------------------------------------------------- /packages/playground/src/zod/env.config.ts: -------------------------------------------------------------------------------- 1 | import { config, adapters, validators } from "fatima"; 2 | import { z, type ZodType } from "zod"; 3 | import dotenv from "dotenv"; 4 | 5 | import type { EnvRecord } from "./env"; 6 | 7 | type Constraint = Partial>; 8 | 9 | const constraint = { 10 | NODE_ENV: z.string(), 11 | } satisfies Constraint; 12 | 13 | export default config({ 14 | client: { 15 | publicPrefix: "NEXT_PUBLIC_", 16 | }, 17 | load: { 18 | development: [adapters.dotenv.load(dotenv)], 19 | }, 20 | validate: validators.zod(z.object(constraint)), 21 | environment: (processEnv) => processEnv.NODE_ENV ?? "development", 22 | }); 23 | -------------------------------------------------------------------------------- /packages/playground/src/zod/env.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createEnv, 3 | type ServerEnvRecord, 4 | type EnvType as FatimaEnvType, 5 | type EnvRecord as FatimaEnvRecord, 6 | type PrimitiveEnvType as FatimaPrimitiveEnvType, 7 | } from 'fatima/env'; 8 | 9 | export interface EnvObject { 10 | "NODE_ENV": string; 11 | "TZ": string; 12 | "NEXT_PUBLIC_API_URL": string; 13 | "TEST": string; 14 | } 15 | 16 | export type EnvKeys = keyof EnvObject; 17 | 18 | export type EnvRecord = FatimaEnvRecord; 19 | 20 | type PrimitiveEnvType = FatimaPrimitiveEnvType; 21 | export type EnvType = FatimaEnvType; 22 | 23 | type Env = ServerEnvRecord 24 | 25 | export const env = createEnv({ isServer: undefined }) as Env; 26 | 27 | import { createPublicEnv, type PublicEnvRecord } from "fatima/env"; 28 | type PublicEnv = PublicEnvRecord 29 | export const publicEnv = createPublicEnv( { 30 | publicPrefix: "NEXT_PUBLIC_", 31 | publicVariables: { 32 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL as string 33 | } 34 | }) as PublicEnv; -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationDir": "./", 10 | "incremental": true, 11 | "tsBuildInfoFile": ".tsbuildinfo", 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | "strictPropertyInitialization": false, 18 | "allowJs": false, 19 | "baseUrl": "./", 20 | "outDir": "./dist", 21 | "rootDir": "./", 22 | // xd 23 | "paths": { 24 | "@/*": [ 25 | "src/*" 26 | ], 27 | "env": [ 28 | "./env.ts" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "./src/**/*.ts", 34 | "./src/**/*.js" 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "dist" 39 | ] 40 | } -------------------------------------------------------------------------------- /packages/web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [Fernando Coelho] 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 | -------------------------------------------------------------------------------- /packages/web/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { HomeLayout } from "fumadocs-ui/layouts/home"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | 5 | export default function Layout({ children }: { children: ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /packages/web/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FatimaLogo } from "../logo"; 3 | import { ArrowUpRightIcon } from "@heroicons/react/16/solid"; 4 | 5 | export default function HomePage() { 6 | return ( 7 |
8 | 9 |

fatima

10 |

11 | safe secrets for the javascript ecosystem 12 |

13 |

14 | 18 | Get started 19 | {" "} 20 |

21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/app/api/open-graph/[...slug]/load-google-fonts.ts: -------------------------------------------------------------------------------- 1 | import type { ImageResponseOptions } from "next/server"; 2 | 3 | type FontOptions = ImageResponseOptions["fonts"]; 4 | 5 | async function loadGoogleFont(font: string, weight: number) { 6 | const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`; 7 | const css = await (await fetch(url)).text(); 8 | const resource = css.match( 9 | /src: url\((.+)\) format\('(opentype|truetype)'\)/, 10 | ); 11 | 12 | if (resource) { 13 | const response = await fetch(resource[1]); 14 | if (response.status === 200) { 15 | return { 16 | buffer: await response.arrayBuffer(), 17 | font: { family: font, weight: weight }, 18 | }; 19 | } 20 | } 21 | 22 | throw new Error("❌ Failed to load font data!"); 23 | } 24 | 25 | export const getFonts = async () => { 26 | const fonts = await Promise.all([ 27 | loadGoogleFont("Inter", 400), 28 | loadGoogleFont("Inter", 600), 29 | loadGoogleFont("Inter", 700), 30 | ]); 31 | 32 | return fonts.map(({ font, buffer }) => ({ 33 | name: font.family, 34 | data: buffer, 35 | style: "normal", 36 | weight: font.weight, 37 | })) as FontOptions; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/web/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { createFromSource } from "fumadocs-core/search/server"; 3 | 4 | export const { GET } = createFromSource(source); 5 | -------------------------------------------------------------------------------- /packages/web/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { 3 | DocsPage, 4 | DocsBody, 5 | DocsDescription, 6 | DocsTitle, 7 | } from "fumadocs-ui/page"; 8 | import { notFound } from "next/navigation"; 9 | import defaultMdxComponents from "fumadocs-ui/mdx"; 10 | import { Tab, Tabs } from "fumadocs-ui/components/tabs"; 11 | import { Callout } from "fumadocs-ui/components/callout"; 12 | import { metadataImage } from "@/lib/metadata"; 13 | import { Icon } from "@/components/icon"; 14 | 15 | export default async function Page(props: { 16 | params: Promise<{ slug?: string[] }>; 17 | }) { 18 | const params = await props.params; 19 | const page = source.getPage(params.slug); 20 | if (!page) notFound(); 21 | 22 | const MDX = page.data.body; 23 | 24 | const V = () => ( 25 | 26 | ); 27 | 28 | const X = () => ; 29 | 30 | return ( 31 | 32 | {page.data.title} 33 | {page.data.description} 34 | 35 | 46 | 47 | 48 | ); 49 | } 50 | 51 | export async function generateStaticParams() { 52 | return source.generateParams(); 53 | } 54 | 55 | export async function generateMetadata(props: { 56 | params: Promise<{ slug?: string[] }>; 57 | }) { 58 | const params = await props.params; 59 | const page = source.getPage(params.slug); 60 | if (!page) notFound(); 61 | 62 | return metadataImage.withImage(page.slugs, { 63 | title: page.data.title, 64 | description: page.data.description, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /packages/web/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from "fumadocs-ui/layouts/docs"; 2 | import type { ReactNode } from "react"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | import { source } from "@/lib/source"; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/app/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "fumadocs-ui/css/neutral.css"; 3 | @import "fumadocs-ui/css/preset.css"; 4 | 5 | @source '../node_modules/fumadocs-ui/dist/**/*.js'; 6 | -------------------------------------------------------------------------------- /packages/web/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 2 | import { FatimaLogo } from "./logo"; 3 | 4 | /** 5 | * Shared layout configurations 6 | * 7 | * you can configure layouts individually from: 8 | * Home Layout: app/(home)/layout.tsx 9 | * Docs Layout: app/docs/layout.tsx 10 | */ 11 | export const baseOptions: BaseLayoutProps = { 12 | githubUrl: "https://github.com/Fgc17/fatima", 13 | nav: { 14 | title: ( 15 | <> 16 | 17 | fatima 18 | 19 | ), 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import { Inter } from "next/font/google"; 4 | import type { ReactNode } from "react"; 5 | import { createMetadata } from "@/lib/metadata"; 6 | 7 | const inter = Inter({ 8 | subsets: ["latin"], 9 | }); 10 | 11 | export const metadata = createMetadata({ 12 | title: { 13 | template: "%s | Fatima", 14 | default: "Fatima", 15 | }, 16 | description: "safe secrets for the javascript ecosystem", 17 | icons: { 18 | icon: [ 19 | { 20 | media: "(prefers-color-scheme: light)", 21 | url: "/favicon/light.svg", 22 | href: "/favicon/light.svg", 23 | }, 24 | { 25 | media: "(prefers-color-scheme: dark)", 26 | url: "/favicon/dark.svg", 27 | href: "/favicon/dark.svg", 28 | }, 29 | ], 30 | }, 31 | }); 32 | 33 | export default function Layout({ children }: { children: ReactNode }) { 34 | return ( 35 | 36 | 37 | {children} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/web/components/icon.tsx: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | import * as icons from "@heroicons/react/16/solid"; 3 | 4 | export function Icon({ 5 | icon, 6 | className, 7 | }: { 8 | icon: string | undefined; 9 | className?: string; 10 | }) { 11 | if (!icon) { 12 | return; 13 | } 14 | 15 | if (!icon.endsWith("Icon")) { 16 | icon = `${icon}Icon`; 17 | } 18 | 19 | if (icon && !(icon in icons)) { 20 | console.log(`Icon not found: ${icon}`); 21 | 22 | return; 23 | } 24 | 25 | if (icon in icons) 26 | return createElement(icons[icon as keyof typeof icons], { 27 | className, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/(adapters)/dotenv.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: dotenv 3 | description: '"Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env"' 4 | --- 5 | 6 | This loader is a dumb wrapper around `dotenv`, it is useful to load envs from local files. 7 | 8 | ## Dependencies 9 | 10 | 11 | 12 | ```bash tab="npm" 13 | npm install dotenv 14 | ``` 15 | 16 | ```bash tab="pnpm" 17 | pnpm add dotenv 18 | ``` 19 | 20 | ```bash tab="yarn" 21 | yarn add dotenv 22 | ``` 23 | 24 | 25 | 26 | ## Importing the loader 27 | 28 | ```ts title="env.config.ts" 29 | import { config, adapters } from "fatima"; 30 | import dotenv from "dotenv"; 31 | 32 | export default config({ 33 | load: { 34 | development: [adapters.dotenv.load(dotenv)], 35 | }, 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/(adapters)/infisical.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: infisical 3 | description: '"All-in-one platform to securely manage application configuration and secrets..."' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install @infisical/sdk 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm add @infisical/sdk 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn add @infisical/sdk 20 | ``` 21 | 22 | 23 | 24 | ## Importing the loader 25 | 26 | ```ts title="env.config.ts" 27 | import { config, adapters } from "fatima"; 28 | import { InfisicalSDK } from "@infisical/sdk"; 29 | 30 | export default config({ 31 | load: { 32 | development: [ 33 | adapters.local.load(".env"), 34 | adapters.infisical.load(InfisicalSDK, { 35 | // You can check here the needed options, they can be loaded through the previous loader 36 | }), 37 | ], 38 | }, 39 | }); 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/(adapters)/local.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: local 3 | description: Let's remove the dotenv dependency. 4 | --- 5 | 6 | This adapter will load environment variables from `.env` files, with no dependencies. 7 | 8 | ## Dependencies 9 | 10 | Zero. 11 | 12 | ## Importing the loader 13 | 14 | ```ts title="env.config.ts" 15 | import { config, adapters } from "fatima"; 16 | 17 | export default config({ 18 | load: { 19 | development: [adapters.load.load(".env", ".env.local")], 20 | }, 21 | }); 22 | ``` 23 | 24 | ## Special thanks 25 | 26 | Special thanks to [dotenv](https://github.com/motdotla/dotenv) for the regex that reads `.env` lines, [source code can be found here.](https://github.com/motdotla/dotenv/blob/master/lib/main.js) 27 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/(adapters)/trigger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: trigger.dev 3 | description: '"The open source background jobs platform"' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install trigger.dev @trigger.dev/sdk 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm add trigger.dev @trigger.dev/sdk 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn add trigger.dev @trigger.dev/sdk 20 | ``` 21 | 22 | 23 | 24 | ## The extension 25 | 26 | When deploying to trigger you may not always deploy from your machine, where `env.ts` is already generated. 27 | 28 | In this case, to avoid build errors, you can use the provided extension: 29 | 30 | ```ts title="trigger.config.ts" 31 | import { defineConfig } from "@trigger.dev/sdk/v3"; 32 | import { adapters } from "fatima"; 33 | 34 | export default defineConfig({ 35 | project: "my-project", 36 | build: { 37 | extensions: [adapters.triggerDev.extension()], 38 | }, 39 | }); 40 | ``` 41 | 42 | ## Importing the loader 43 | 44 | ```ts title="env.config.ts" 45 | import { config, adapters } from "fatima"; 46 | import * as trigger from "@trigger.dev/sdk/v3"; 47 | 48 | export default config({ 49 | load: { 50 | development: [ 51 | adapters.local.load(".env"), 52 | adapters.triggerDev.load(trigger, { 53 | // Check out here with intellisense 54 | }), 55 | ], 56 | }, 57 | }); 58 | ``` 59 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/(adapters)/vercel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: vercel 3 | description: '"Your complete platform for the web..."' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install -g vercel 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm i -g vercel 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn global add vercel 20 | ``` 21 | 22 | 23 | 24 | ## Authenticating 25 | 26 | First, log in: 27 | 28 | ```bash 29 | vercel login 30 | ``` 31 | 32 | Then, link your project: 33 | 34 | ```bash 35 | vercel link 36 | ``` 37 | 38 | ### Token Authenticating 39 | 40 | Fatima also supports token-based authentication, which can be useful for people running containers. 41 | 42 | Just grab your `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` from your Vercel account and set them as environment variables, or pass down as loader options. 43 | 44 | ## Importing the loader 45 | 46 | ```ts title="env.config.ts" 47 | import { config, adapters } from "fatima"; 48 | 49 | export default config({ 50 | load: { 51 | development: [ 52 | adapters.local.load(".env"), 53 | adapters.vercel.load({ 54 | // Check out here with intellisense 55 | }), 56 | // You may want to overwrite some values from vercel 57 | adapters.local.load(".env.local"), 58 | ], 59 | }, 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Adapters", 3 | "pages": [ 4 | "own", 5 | "./(adapters)/local", 6 | "./(adapters)/infisical", 7 | "./(adapters)/vercel", 8 | "./(adapters)/trigger" 9 | ], 10 | "defaultOpen": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/web/content/docs/adapters/own.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building your own loader 3 | description: Fatima is completely agnostic on how you load your secrets. 4 | --- 5 | 6 | There's currently some built-in loaders that you can use, but I will show you how to create your own. 7 | 8 | 9 | Unless you know what you're doing, I highly recommend using the built-in loaders for the following providers: 10 | 11 | - Vercel 12 | - Trigger.dev (the extension is the hard part) 13 | 14 | 15 | 16 | ## How the load object works 17 | 18 | Inside your `env.config.ts` there's an available `load` key that you can fill with an object of the following type: 19 | 20 | ```ts title="types.d.ts" 21 | export type UnsafeEnvironmentVariables = Record; 22 | 23 | export type FatimaLoadFunction = () => Promisable< 24 | UnsafeEnvironmentVariables | null | undefined 25 | >; 26 | 27 | export type FatimaLoaderChain = FatimaLoadFunction[] | FatimaLoadFunction; 28 | 29 | export type FatimaLoader = { 30 | development: FatimaLoaderChain; 31 | [nodeEnv: string]: FatimaLoaderChain; 32 | }; 33 | ``` 34 | 35 | This means you can pass multiple loaders for specific environments based on the environment key. 36 | 37 | When chain loading your load functions, Fatima will execute them in order, and progressively merge the results, so what's loaded before will be available for the next loader. 38 | 39 | The development key is mandatory, it will be used when running `fatima dev`. 40 | 41 | And yeah, this means you can load secrets from **any** source, .env or cloud. 42 | 43 | ## Creating a custom load function with the InfisicalSDK 44 | 45 | 46 | Fatima alredy comes with [a built-in infisical 47 | loader](/docs/loaders/infisical), this is just an example. 48 | 49 | 50 | We will be using the built-in `local` loader to load from `.env`. 51 | 52 | ```ts title="env.config.ts" 53 | import { config, adapters, UnsafeEnvironmentVariables } from "fatima"; 54 | 55 | export default config({ 56 | load: { 57 | development: [ 58 | adapters.local.load(".env"), 59 | async () => { 60 | // All of the process.env were loaded by the previous local loader 61 | 62 | const client = new infisicalClient(); 63 | 64 | await client.auth().universalAuth.login({ 65 | clientId: process.env.INFISICAL_CLIENT_ID!,, 66 | clientSecret: process.env.INFISICAL_CLIENT_SECRET!, 67 | }); 68 | 69 | const { secrets } = await client.secrets().listSecrets({ 70 | projectId: process.env.INFISICAL_PROJECT_ID!, 71 | environment: "dev", 72 | }); 73 | 74 | const env = secrets.reduce((acc, { secretKey, secretValue }) => { 75 | acc[secretKey] = secretValue; 76 | return acc; 77 | }, {} as UnsafeEnvironmentVariables); 78 | 79 | return env; 80 | }, 81 | ], 82 | }, 83 | }); 84 | ``` 85 | 86 | Any loader will work as long as it returns a `UnsafeEnvironmentVariables` object. 87 | -------------------------------------------------------------------------------- /packages/web/content/docs/frameworks/(frameworks)/sveltekit.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sveltekit 3 | description: '"web development for the rest of us"' 4 | --- 5 | 6 | ## What should I know? 7 | 8 | Here's a list of things SvelveKit does natively: 9 | 10 | - Environments type safety (no need for Fatima's client). 11 | - Private and public secrets (again no need for Fatima's client) 12 | - [Secret leaking](/docs/security/secret-leaking) (no need for Fatima's ESLint rule). 13 | 14 | ## How to integrate with Fatima 15 | 16 | Things are pretty straight forward, but as mentioned, you won't need Fatima's generated client neither the provided ESLint rule. 17 | 18 | As we won't be generating the client, Fatima provides underlying type API so that you can generate your validation constraints. 19 | 20 | ## Dev script 21 | 22 | ```json title="package.json" 23 | { 24 | "scripts": { 25 | "dev": "fatima dev --lite -- vite dev" 26 | } 27 | } 28 | ``` 29 | 30 | The `--lite` flag will disable the client generation. 31 | 32 | ## Config file 33 | 34 | ```ts 35 | import { env } from "$env/dynamic/private"; 36 | import { EnvRecord } from "@fatimajs/tools/env"; 37 | import { validate, config, adapters } from "fatima"; 38 | import { z, ZodType } from "zod"; 39 | 40 | type DynamicPrivateEnvObject = typeof env; 41 | 42 | type Constraint = EnvRecord; 43 | 44 | export const constraint: Constraint = { 45 | NODE_ENV: z.enum(["development", "staging", "production"]), 46 | PORT: z.number().int().positive(), 47 | API_URL: z.string().url(), 48 | }; 49 | 50 | type Environment = 'development' | 'staging' | 'production' 51 | 52 | export default config({ 53 | environment: (processEnv) => processEnv.NODE_ENV ?? "development" 54 | validate: validate.zod(constraint), 55 | load: { 56 | development: [adapters.local.load(".env")] 57 | } 58 | }); 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/web/content/docs/frameworks/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Frameworks", 3 | "pages": ["./(frameworks)/sveltekit"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/web/content/docs/getting-started/deploy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deploy (CI/CD) 3 | description: Deploying with Fatima is straightforward. 4 | --- 5 | 6 | ## How 7 | 8 | Currently, the only command you need for deploying is `fatima generate`. 9 | 10 | Before building, make sure to run: 11 | 12 | ```sh 13 | npx fatima@latest --process-env generate 14 | ``` 15 | 16 | Don't be scared if it shows you a high count of envs in your `env.ts`, those are the system variables. 17 | 18 | ## Why `--process-env`? 19 | 20 | This flag is used to load environments only from `process.env`, it will tell fatima to skip load functions declared in your config. 21 | 22 | Ideally, every environment variable should be injected by your hosting provider, so you don't need to load them, they are already exposed in `process.env`. 23 | 24 | This flag is useless 99% of the time, because fatima automatically loads from `process.env` if it doesn't find a matching load function in your config, it even warns you about what's going on. 25 | 26 | However, let's say you want to load from `.env.test` locally, but from `process.env` in your CI/CD, then you can use this flag to make sure your client gets generated correctly. 27 | 28 | At the end, keep it, won't hurt if you don't need custom loading in your pipeline. 29 | 30 | ## Bundling 31 | 32 | Fatima is a development dependency, it doesn't need to be bundled with your application. 33 | 34 | This is specially useful for frontend, as you will get 0kb into your website bundle. 35 | -------------------------------------------------------------------------------- /packages/web/content/docs/getting-started/eslint.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ESLint 3 | description: Enable ESLint in your project with fatima. 4 | --- 5 | 6 | Check out [Secret Leaking](/docs/security/secret-leaking) to enable fatima's ESLint plugin. 7 | -------------------------------------------------------------------------------- /packages/web/content/docs/getting-started/heaven.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Heaven (reloading) 3 | description: Heaven powers up env runtime reloading. 4 | --- 5 | 6 | Restarting a process is really annoying, especially when dealing with slow builds, fatima helps you with that by providing "Heaven", a runtime env reloading solution. 7 | 8 | ## Heaven, runtime reloading 9 | 10 | To enable runtime reloading, we will use `@fatimajs/heaven`, make sure to install it: 11 | 12 | 13 | 14 | ```bash tab="npm" 15 | npm install @fatimajs/heaven 16 | ``` 17 | 18 | ```bash tab="pnpm" 19 | pnpm add @fatimajs/heaven 20 | ``` 21 | 22 | ```bash tab="yarn" 23 | yarn add @fatimajs/heaven 24 | ``` 25 | 26 | 27 | 28 | 29 | 30 | ```typescript title="instrumentation.ts" tab="nextjs" 31 | // Here we are using the 'instrumentation' entry hook to watch for changes 32 | export async function register() { 33 | if (process.env.NEXT_RUNTIME === "nodejs") { 34 | const { heaven } = await import("@fatimajs/heaven"); 35 | 36 | heaven.watch(); 37 | } 38 | } 39 | ``` 40 | 41 | ```typescript title="main.ts" tab="nestjs" 42 | // Here we are directly tweaking the app entry file to watch for changes 43 | import { NestFactory } from "@nestjs/core"; 44 | import { AppModule } from "./app.module"; 45 | import { heaven } from "@fatimajs/heaven"; 46 | 47 | async function bootstrap() { 48 | heaven.watch(); 49 | 50 | const app = await NestFactory.create(AppModule); 51 | 52 | await app.listen(3000); 53 | } 54 | bootstrap(); 55 | ``` 56 | 57 | 58 | 59 | 60 | `heaven.watch()` only runs stuff if you execute your app with `fatima dev`, 61 | otherwise it will be ignored. 62 | 63 | 64 | 65 | If your framework does not support hooks, or if for some reason you can't 66 | tweak the app entry file, you can't use Heaven runtime reloading. 67 | 68 | 69 | ## Watching local changes 70 | 71 | Local changes are automatically detected by Heaven, you don't need to do anything. 72 | 73 | 74 | `.env.example` is ignored by Heaven watch feature, you can use it to document 75 | your environment variables. 76 | 77 | 78 | ## Watching cloud changes 79 | 80 | For watching cloud changes, you need to setup a webhook in your secret manager. 81 | 82 | Heaven natively runs on port `15781`, but you can explicitly set the `heaven` option in your `fatima.config.ts`. 83 | 84 | ```typescript title="fatima.config.ts" 85 | export default config({ 86 | heaven: 15781, 87 | }); 88 | ``` 89 | 90 | This will spin up an endpoint at `http://localhost:15781/fatima` accepting any HTTP method to trigger a reload. 91 | 92 | It is your job to provide the tunneling service, here's a list of services you can use: 93 | 94 | - [zrok](https://zrok.io/) 95 | - [ngrok](https://ngrok.com/) 96 | - [localtunnel](https://localtunnel.github.io/www/) 97 | - [localhost.run](https://localhost.run/) 98 | 99 | For much more, check the [Awesome Tunneling repo](https://github.com/anderspitman/awesome-tunneling) 100 | 101 | After setting up the tunneling you just need to set the webhook URL in your secret manager to the endpoint URL. 102 | 103 | ## My secret manager does not support webhooks 104 | 105 | This usually happens with tools that _have_ a secret manager but aren't secret managers themselves: vercel, railway, trigger.dev, etc. 106 | 107 | In this case, you can use the `fatima reload` command to trigger a reload. 108 | 109 | ```bash 110 | fatima reload 111 | ``` 112 | 113 | You can run this command on a second terminal without stopping your process. 114 | -------------------------------------------------------------------------------- /packages/web/content/docs/getting-started/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Getting Started", 3 | "pages": ["quickstart", "quickstart-js", "heaven", "eslint", "deploy"], 4 | "defaultOpen": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/web/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why Fatima? 3 | description: In case you're wondering, the name refers to "Our Lady of Fátima". 4 | icon: QuestionMarkCircle 5 | --- 6 | 7 | ## Comparison 8 | 9 | | | Fatima | [t3-env][t3env] | [typedotenv][typedotenv] | [dotenvx][dotenvx] | 10 | | ------------------------- | ------ | --------------- | ------------------------ | ------------------ | 11 | | CommonJS support | | | | | 12 | | Javascript support | | | | | 13 | | Typescript setup file | | | CLI Only | CLI Only | 14 | | Type Safety | | | | | 15 | | End-to-end type safety | | | | | 16 | | Load from cloud to local | | | | | 17 | | Validate with any library | | | | | 18 | | Public and server secrets | | | | | 19 | | ESlint helper plugin | | | | | 20 | | Development reloading | | | | | 21 | | Native .env support | | | | | 22 | | No Codegen | | | | | 23 | | Files to setup | 1 | 1 -> 2 | 0 | 0 | 24 | | Unpacked (production) | 0kb | 45kb | 0kb | 0kb | 25 | 26 | [t3env]: https://github.com/t3-oss/t3-env 27 | [typedotenv]: https://github.com/ssssota/typedotenv 28 | [dotenvx]: https://github.com/dotenvx/dotenvx 29 | -------------------------------------------------------------------------------- /packages/web/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "index", 4 | "getting-started", 5 | "security", 6 | "public-secrets", 7 | "frameworks", 8 | "environment-reloading", 9 | "adapters", 10 | "validators" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/content/docs/public-secrets/additional-setup.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Additional setup 3 | description: Additional setup for client secrets. 4 | --- 5 | 6 | ### Modifying the `isServer` function 7 | 8 | The default way fatima checks if you are on the server is currently the following: 9 | 10 | ```ts title="fatima/src/env/env.ts" 11 | () => typeof window === "undefined"; 12 | ``` 13 | 14 | This works for 99% of the cases, but there are other ways to make the check. 15 | 16 | There's actually a library called [browser-or-node](https://www.npmjs.com/package/browser-or-node) that can help you with that. 17 | 18 | If you want to use this library or already do so, you can modify the `isServer` function anyway you want: 19 | 20 | ```ts title="env.config.ts" 21 | import { config } from "fatima"; 22 | import * as jsEnv from "browser-or-node"; 23 | 24 | const isServer = !jsEnv.isBrowser; 25 | 26 | export default config({ 27 | client: { 28 | isServer: () => isServer, 29 | publicPrefix: "NEXT_PUBLIC_", 30 | }, 31 | }); 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/web/content/docs/public-secrets/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Public Secrets", 3 | "pages": ["public-secrets", "additional-setup"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/web/content/docs/public-secrets/public-secrets.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting up public secrets 3 | description: Public secrets should be easy. 4 | --- 5 | 6 | ## How they work 7 | 8 | People get confused on how public secrets works, it is easy: they are variables that get defined through `.env` content instead of hard coding. 9 | 10 | Your server variables will never be defined on the frontend, your SSR framework won't allow it. 11 | 12 | 13 | Your framework will not ship server secrets to the frontend, but no one said you 14 | can't mess up and pass them by yourself through routes or html server 15 | rendering. 16 | 17 | Check out the [fatima eslint rule](/docs/security/secret-leaking) for partially helping you with that. 18 | 19 | 20 | 21 | ## How to set them up 22 | 23 | You can start using public secrets by specifying a public prefix in your config file: 24 | 25 | ```ts title="env.config.ts" 26 | import { config } from "fatima"; 27 | 28 | export default config({ 29 | client: { 30 | publicPrefix: "NEXT_PUBLIC_", 31 | }, 32 | }); 33 | ``` 34 | 35 | From now on, as soon as you hit `fatima generate`, all your `.env` variables prefixed with `NEXT_PUBLIC_` will be available under `publicEnv`. 36 | 37 | ## Calling `publicEnv` 38 | 39 | So you generated your public secrets, here's how you would call them: 40 | 41 | ```tsx 42 | import { publicEnv } from "env"; 43 | 44 | export default function Home() { 45 | return
{publicEnv.MY_SECRET}
; 46 | } 47 | ``` 48 | 49 | 50 | 51 | Even though fatima uses a single file for accessing both server and public secrets, we do not leak your server secret names to the frontend, that's because the server secret names are only types, not values. 52 | 53 | 54 | -------------------------------------------------------------------------------- /packages/web/content/docs/security/environment-mixing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Environment mixing 3 | description: Environment mixing is dangerous, and Fatima can help you prevent it. 4 | icon: "ExclamationTriangle" 5 | --- 6 | 7 | ## What is environment mixing? 8 | 9 | Environment mixing happens when you accidentally load secrets from the wrong environment. For example, you might load production secrets in a development environment, or vice versa. This can lead to serious security/application issues, and it's important to prevent it. 10 | 11 | ## How Fatima helps 12 | 13 | Fatima will require you to explicitly define a `environment` function in your `env.config.ts` file 14 | 15 | ```ts title="env.config.ts" 16 | export default config({ 17 | environment: (processEnv) => processEnv.NODE_ENV, 18 | }); 19 | ``` 20 | 21 | When Fatima tries to load secrets, it uses the `environment` function to determine which environment to load variables from. 22 | 23 | However, right after all secrets are loaded, Fatima injects them into `process.env` and runs the `environment` function again. 24 | 25 | If for some reason the `environment` function returns a different environment than the one loaded, Fatima will throw an error. 26 | 27 | That's why it is important to keep the return value of this function somehow linked to your environment variables. Everyone likes a double layer of security. 28 | 29 | 30 | The callback argument "processEnv" is useful to avoid using 'process.env' 31 | directly, which depending on your setup can cause some problems with types and 32 | ESLint. 33 | 34 | 35 | 36 | Although `fatima generate` temporarily loads secrets names and values to generate types, 37 | it doesn't inject them anywhere in your application. 38 | 39 | It's a safe command to run, and you don't need to worry about environment mixing when running it. 40 | 41 | 42 | 43 | ## How you could mess up 44 | 45 | ### 1. Using `WHATEVER_ENV=production fatima run` in development 46 | 47 | Few people will define a `load.production` function in config, that's because `fatima run` generally should not be used in production. 48 | 49 | Production variables should be loaded in CI/CD pipelines, directly into the hosting machine `process.env`. 50 | 51 | But if you actually define `load.production` and then run the command described above, you will load production secrets into your local process. Not much to do here, Fatima can't help you with that. 52 | 53 | As for the existance of the `fatima run` command, I've also questioned it, but it can be useful in some cases, like running a script that needs access to secrets. 54 | 55 | ### 2. Setting the wrong environments in your secret storage 56 | 57 | Unfortunately, there's not much to do if you set your production database URL in your secret storage development environment. Fatima can't help you with that. 58 | -------------------------------------------------------------------------------- /packages/web/content/docs/security/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Security", 3 | "pages": ["environment-mixing", "secret-leaking"], 4 | "defaultOpen": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/web/content/docs/security/secret-leaking.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Secret leaking 3 | description: Fatima helps you prevent secret leaking in your codebase. 4 | icon: "ExclamationTriangle" 5 | --- 6 | 7 | # ESlint 8 | 9 | ## The Plugin 10 | 11 | Fatima provides an ESLint plugin containing two rules: `no-process-env` and `no-env`. 12 | 13 | The former is enabled by default, and it will prevent you from accessing `process.env` in your codebase. 14 | 15 | This helps you avoid accidentally leaking secrets, as `process.env` does not provide any kind of safety. It also keeps your codebase consistent. 16 | 17 | As for the latter, it prevents you from acessing the `env` object generated by fatima, and it needs to be enabled manually as it requires you to specify the files you don't want to access the object. 18 | 19 | ## Setup 20 | 21 | ```ts title="eslint.config.ts" 22 | import { linter as fatima } from "fatima"; 23 | 24 | export default [ 25 | fatima.eslint.plugin, 26 | fatima.eslint.noEnvRule("**/*.tsx"), 27 | ] satisfies ESLintConfig; 28 | ``` 29 | 30 | In the example above, we're enabling the `no-env` rule for all `.tsx` files. 31 | -------------------------------------------------------------------------------- /packages/web/content/docs/validators/(validators)/class-validator.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: class-validator 3 | description: '"Decorator-based property validation for classes."' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install class-validator class-transformer reflect-metadata @babel/plugin-transform-class-properties 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm add class-validator class-transformer reflect-metadata @babel/plugin-transform-class-properties 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn add class-validator class-transformer reflect-metadata @babel/plugin-transform-class-properties 20 | ``` 21 | 22 | 23 | 24 | ## Setup typescript 25 | 26 | To use class-validator, you need to tweak your `tsconfig.json`: 27 | 28 | ```json title="tsconfig.json" 29 | { 30 | "compilerOptions": { 31 | "experimentalDecorators": true, 32 | "emitDecoratorMetadata": true, 33 | "strictPropertyInitialization": false // you probably want this 34 | } 35 | } 36 | ``` 37 | 38 | ## Importing the validator 39 | 40 | 41 | If you generate your types before writing validation, they will be available 42 | to help you define the constraint. 43 | 44 | 45 | 46 | You need to import `reflect-metadata` at the top of your file to use 47 | class-validator. 48 | 49 | 50 | ```ts title="env.config.ts" 51 | import "reflect-metadata"; 52 | import { IsEmail, IsTimeZone, validate } from "class-validator"; 53 | import { plainToInstance } from "class-transformer"; 54 | 55 | import { config, validators } from "fatima"; 56 | import { EnvObject } from "env"; 57 | 58 | class Constraint implements Partial { 59 | @IsEmail() 60 | NODE_ENV: string; 61 | 62 | @IsTimeZone() 63 | TZ?: string | undefined; 64 | } 65 | 66 | export default config({ 67 | validate: validators.classValidator(Constraint, { 68 | plainToInstance, 69 | validate, 70 | }), 71 | }); 72 | ``` 73 | 74 | ## Validate 75 | 76 | To validate, just run the validate command: 77 | 78 | 79 | 80 | ```bash tab="npm" 81 | npm fatima validate 82 | ``` 83 | 84 | ```bash tab="pnpm" 85 | pnpm fatima validate 86 | ``` 87 | 88 | ```bash tab="yarn" 89 | yarn fatima validate 90 | ``` 91 | 92 | 93 | -------------------------------------------------------------------------------- /packages/web/content/docs/validators/(validators)/typia.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: typia 3 | description: '"Super-fast/easy runtime validators and serializers via transformation"' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install typia 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm add typia 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn add typia 20 | ``` 21 | 22 | 23 | 24 | ## Importing the validator 25 | 26 | 27 | If you generate your types before writing validation, they will be available 28 | to help you define the constraint. 29 | 30 | 31 | 32 | The current validator transforms the config file, so the schema must be inside 33 | it. 34 | 35 | 36 | 37 | Do not pass the function to the validator without executing it, typia will not 38 | read it correctly. 39 | 40 | ```ts title="env.config.ts" 41 | ❌ validators.typia(typia.validate); 42 | 43 | ✅ validators.typia((env) => typia.validate(env)); 44 | ``` 45 | 46 | 47 | 48 | ```ts title="env.config.ts" 49 | import { config, validators } from "fatima"; 50 | import typia, { tags } from "typia"; 51 | import { EnvRecord } from "env"; 52 | 53 | type Constraint = EnvType<{ 54 | NODE_ENV: string & tags.Format<"email">; 55 | TZ: string; 56 | }>; 57 | 58 | export default config({ 59 | // WARNING: Do not pass the validate function without executing it here. 60 | validate: validators.typia((env) => typia.validate(env)), 61 | }); 62 | ``` 63 | 64 | ## Validate 65 | 66 | To validate, just run the validate command: 67 | 68 | 69 | 70 | ```bash tab="npm" 71 | npm fatima validate 72 | ``` 73 | 74 | ```bash tab="pnpm" 75 | pnpm fatima validate 76 | ``` 77 | 78 | ```bash tab="yarn" 79 | yarn fatima validate 80 | ``` 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/web/content/docs/validators/(validators)/zod.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: zod 3 | description: '"TypeScript-first schema validation with static type inference"' 4 | --- 5 | 6 | ## Dependencies 7 | 8 | 9 | 10 | ```bash tab="npm" 11 | npm install zod 12 | ``` 13 | 14 | ```bash tab="pnpm" 15 | pnpm add zod 16 | ``` 17 | 18 | ```bash tab="yarn" 19 | yarn add zod 20 | ``` 21 | 22 | 23 | 24 | ## Importing the validator 25 | 26 | 27 | If you generate your types before writing validation, they will be available 28 | to help you define the constraint. 29 | 30 | 31 | ```ts title="env.config.ts" 32 | import { config, validators } from "fatima"; 33 | import { z, ZodType } from "zod"; 34 | import { EnvRecord } from "env"; 35 | 36 | type Constraint = Partial>; 37 | 38 | const schema = z.object({ 39 | NODE_ENV: z.string().email() 40 | TZ: z.string() 41 | }); 42 | 43 | export default config({ 44 | validate: validators.zod(schema), 45 | }); 46 | ``` 47 | 48 | ## Validate 49 | 50 | To validate, just run the validate command: 51 | 52 | 53 | 54 | ```bash tab="npm" 55 | npm fatima validate 56 | ``` 57 | 58 | ```bash tab="pnpm" 59 | pnpm fatima validate 60 | ``` 61 | 62 | ```bash tab="yarn" 63 | yarn fatima validate 64 | ``` 65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/web/content/docs/validators/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Validators", 3 | "pages": [ 4 | "own", 5 | "./(validators)/zod", 6 | "./(validators)/class-validator", 7 | "./(validators)/typia" 8 | ], 9 | "defaultOpen": true 10 | } 11 | -------------------------------------------------------------------------------- /packages/web/content/docs/validators/own.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building your own validator 3 | description: Fatima is completely agnostic on how you validate your secrets. 4 | --- 5 | 6 | There's currently some built-in validators that you can use, but I will show you how to create your own. 7 | 8 | 9 | Unless you know what you're doing, I highly recommend using the built-in validators for the following libraries: 10 | 11 | - Typia 12 | 13 | 14 | 15 | ## How the validate function works 16 | 17 | Inside your `env.config.ts` there's an available `validate` key that you can fill with a function of the following type: 18 | 19 | ```ts title="types.d.ts" 20 | export type FatimaValidator = ( 21 | env: UnsafeEnvironmentVariables, 22 | context: FatimaContext 23 | ) => Promisable<{ 24 | isValid: boolean; 25 | errors?: Array<{ 26 | key: string; 27 | message: string; 28 | }>; 29 | }>; 30 | ``` 31 | 32 | ## Creating a custom validate function with zod 33 | 34 | 35 | Fatima alredy comes with [a built-in zod validator](/docs/validators/zod), 36 | this is just an example. 37 | 38 | 39 | Here's an example using `zod`: 40 | 41 | ```ts title="env.config.ts" 42 | import { config, validators } from "fatima"; 43 | import { z, ZodType } from "zod"; 44 | import { EnvKeys } from "env"; 45 | 46 | type ZodEnv = Partial>; 47 | 48 | const schema = z.object({ 49 | ADMIN_PASSWORD: z.string().min(12), 50 | }); 51 | 52 | export default config({ 53 | validate: ({ env }) => { 54 | const result = schema.safeParse(env); 55 | 56 | const isValid = result.success; 57 | 58 | const errors = result.error?.errors.map((error) => ({ 59 | key: error.path.join("."), 60 | message: error.message, 61 | })); 62 | 63 | return { 64 | isValid, 65 | errors, 66 | }; 67 | }, 68 | }); 69 | ``` 70 | 71 | ## Validate 72 | 73 | To validate, just run the validate command: 74 | 75 | 76 | 77 | ```bash tab="npm" 78 | npm fatima validate 79 | ``` 80 | 81 | ```bash tab="pnpm" 82 | pnpm fatima validate 83 | ``` 84 | 85 | ```bash tab="yarn" 86 | yarn fatima validate 87 | ``` 88 | 89 | 90 | -------------------------------------------------------------------------------- /packages/web/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next/types"; 2 | import { createMetadataImage } from "fumadocs-core/server"; 3 | import { source } from "@/lib/source"; 4 | 5 | export const baseUrl = 6 | process.env.NODE_ENV === "development" || !process.env.NEXT_PUBLIC_APP_URL 7 | ? new URL(`https://${process.env.NEXT_PUBLIC_TUNNEL_URL}`) 8 | : new URL(`https://${process.env.NEXT_PUBLIC_APP_URL}`); 9 | 10 | export const metadataImage = createMetadataImage({ 11 | imageRoute: "/api/open-graph", 12 | source, 13 | }); 14 | 15 | export function createMetadata(override: Metadata): Metadata { 16 | return { 17 | ...override, 18 | openGraph: { 19 | title: override.title ?? undefined, 20 | description: override.description ?? undefined, 21 | url: baseUrl, 22 | images: "/banner.png", 23 | siteName: "Fatima", 24 | ...override.openGraph, 25 | }, 26 | twitter: { 27 | card: "summary_large_image", 28 | creator: "@fernando_coelho", 29 | title: override.title ?? undefined, 30 | description: override.description ?? undefined, 31 | images: "/banner.png", 32 | ...override.twitter, 33 | }, 34 | metadataBase: baseUrl, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/web/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs, meta } from "@/.source"; 2 | import { createMDXSource } from "fumadocs-mdx"; 3 | import { loader } from "fumadocs-core/source"; 4 | import { Icon } from "@/components/icon"; 5 | 6 | export const source = loader({ 7 | baseUrl: "/docs", 8 | source: createMDXSource(docs, meta), 9 | icon(iconString) { 10 | return Icon({ 11 | icon: iconString, 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | }; 9 | 10 | export default withMDX(config); 11 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fatimajs/web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "------------- toolchain -------------": "-------------", 7 | "format": "biome format --write", 8 | "lint": "biome lint --error-on-warnings", 9 | "typecheck": "tsc --noEmit", 10 | "--------------------------": "-------------", 11 | "build": "next build", 12 | "dev": "next dev --turbopack", 13 | "tunnel": "lt --port 3000 --subdomain fatima-tunnel", 14 | "dev-tunnel": "concurrently --raw --kill-others \"npm run dev\" \"npm run tunnel\"", 15 | "start": "next start", 16 | "postinstall": "fumadocs-mdx" 17 | }, 18 | "dependencies": { 19 | "@heroicons/react": "^2.2.0", 20 | "fumadocs-core": "15.0.17", 21 | "fumadocs-docgen": "^1.3.5", 22 | "fumadocs-mdx": "11.5.6", 23 | "fumadocs-ui": "15.0.17", 24 | "next": "15.2.2", 25 | "react": "^19.0.0", 26 | "react-dom": "^19.0.0", 27 | "shiki": "3.2.1" 28 | }, 29 | "devDependencies": { 30 | "@fumadocs/cli": "0.0.7", 31 | "@types/mdx": "^2.0.13", 32 | "@types/node": "22.10.7", 33 | "@types/react": "^19.0.7", 34 | "@types/react-dom": "^19.0.3", 35 | "autoprefixer": "^10.4.20", 36 | "concurrently": "9.1.2", 37 | "localtunnel": "2.0.2", 38 | "postcss": "^8.5.1", 39 | "tailwindcss": "4.0.14", 40 | "@tailwindcss/postcss": "4.0.14", 41 | "typescript": "^5.7.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/web/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgcoelho/fatima/4c1239aa8ebb0f956a8266ffe84a21ed27a2fdc5/packages/web/public/banner.png -------------------------------------------------------------------------------- /packages/web/public/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgcoelho/fatima/4c1239aa8ebb0f956a8266ffe84a21ed27a2fdc5/packages/web/public/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /packages/web/source.config.ts: -------------------------------------------------------------------------------- 1 | import { defineDocs, defineConfig } from "fumadocs-mdx/config"; 2 | 3 | export const { docs, meta } = defineDocs({ 4 | dir: "content/docs", 5 | }); 6 | 7 | export default defineConfig({ 8 | mdxOptions: { 9 | remarkPlugins: [], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": ["./*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "typecheck": { 6 | "dependsOn": ["^typecheck"] 7 | }, 8 | "lint": { 9 | "dependsOn": ["^lint"] 10 | }, 11 | "test": { 12 | "dependsOn": ["^test"] 13 | }, 14 | "format": { 15 | "dependsOn": ["^format"] 16 | }, 17 | "dev": { 18 | "cache": false, 19 | "persistent": true 20 | }, 21 | "build": { 22 | "dependsOn": ["^build"], 23 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 24 | "outputs": [".next/**", "!.next/cache/**", "dist"] 25 | }, 26 | "release": { 27 | "dependsOn": [ 28 | "@fatimajs/heaven#build", 29 | "fatima#build", 30 | "create-fatima#build" 31 | ] 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------