Repository: founded-labs/react-native-reusables Branch: main Commit: 983cd67b0c55 Files: 452 Total size: 1.1 MB Directory structure: gitextract___gsboks/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── pull_request_template.md ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── COMMUNITY_RESOURCES.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── apps/ │ ├── cli/ │ │ ├── .changeset/ │ │ │ └── config.json │ │ ├── .github/ │ │ │ ├── actions/ │ │ │ │ └── setup/ │ │ │ │ └── action.yml │ │ │ └── workflows/ │ │ │ ├── check.yml │ │ │ ├── release.yml │ │ │ └── snapshot.yml │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── .vscode/ │ │ │ ├── extensions.json │ │ │ └── settings.json │ │ ├── LICENSE │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── patches/ │ │ │ └── @changesets__get-github-info@0.6.0.patch │ │ ├── scripts/ │ │ │ └── copy-package-json.ts │ │ ├── src/ │ │ │ ├── bin.ts │ │ │ ├── cli.ts │ │ │ ├── contexts/ │ │ │ │ └── cli-options.ts │ │ │ ├── project-manifest.ts │ │ │ ├── services/ │ │ │ │ ├── commands/ │ │ │ │ │ ├── add.ts │ │ │ │ │ ├── doctor.ts │ │ │ │ │ └── init.ts │ │ │ │ ├── git.ts │ │ │ │ ├── package-manager.ts │ │ │ │ ├── project-config.ts │ │ │ │ ├── required-files-checker.ts │ │ │ │ ├── spinner.ts │ │ │ │ └── template.ts │ │ │ └── utils/ │ │ │ ├── retry-with.ts │ │ │ └── run-command.ts │ │ ├── test/ │ │ │ └── Dummy.test.ts │ │ ├── tsconfig.base.json │ │ ├── tsconfig.json │ │ ├── tsconfig.scripts.json │ │ ├── tsconfig.src.json │ │ ├── tsconfig.test.json │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── docs/ │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── (home)/ │ │ │ │ ├── BlockSection.tsx │ │ │ │ ├── ComponentsGrid.tsx │ │ │ │ ├── TemplatesSection.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── api/ │ │ │ │ └── search/ │ │ │ │ └── route.ts │ │ │ ├── docs/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── global.css │ │ │ ├── layout.tsx │ │ │ ├── og/ │ │ │ │ └── route.tsx │ │ │ └── showcase/ │ │ │ ├── links/ │ │ │ │ └── [[...slug]]/ │ │ │ │ └── page.tsx │ │ │ ├── privacy-policy/ │ │ │ │ └── page.tsx │ │ │ └── support/ │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── app-store-button.tsx │ │ │ ├── auth-block-tabs.tsx │ │ │ ├── auth-integration-select.tsx │ │ │ ├── blocks-grid.tsx │ │ │ ├── blocks.tsx │ │ │ ├── callout.tsx │ │ │ ├── clerk-logo.tsx │ │ │ ├── command-tabs.tsx │ │ │ ├── cookies-provider.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── download-app-banner.tsx │ │ │ ├── examples.tsx │ │ │ ├── external-links.tsx │ │ │ ├── icons/ │ │ │ │ └── rnr-icon.tsx │ │ │ ├── installation-tabs.tsx │ │ │ ├── link-tabs.tsx │ │ │ ├── platform-select.tsx │ │ │ ├── play-store-button.tsx │ │ │ ├── portal-info-alert.tsx │ │ │ ├── preview-card.tsx │ │ │ ├── safe-area-provider.tsx │ │ │ ├── skip-navigation-button.tsx │ │ │ ├── styling-library-tabs.tsx │ │ │ ├── ui/ │ │ │ │ ├── alert.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── select.tsx │ │ │ │ └── tabs.tsx │ │ │ ├── use-styling-library.ts │ │ │ └── vercel-oss-badge.tsx │ │ ├── components.json │ │ ├── content/ │ │ │ └── docs/ │ │ │ ├── blocks/ │ │ │ │ └── authentication/ │ │ │ │ ├── forgot-password-form.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── reset-password-form.mdx │ │ │ │ ├── sign-in-form.mdx │ │ │ │ ├── sign-up-form.mdx │ │ │ │ ├── social-connections.mdx │ │ │ │ ├── user-menu.mdx │ │ │ │ └── verify-email-form.mdx │ │ │ ├── changelog.mdx │ │ │ ├── cli.mdx │ │ │ ├── components/ │ │ │ │ ├── accordion.mdx │ │ │ │ ├── alert-dialog.mdx │ │ │ │ ├── alert.mdx │ │ │ │ ├── aspect-ratio.mdx │ │ │ │ ├── avatar.mdx │ │ │ │ ├── badge.mdx │ │ │ │ ├── button.mdx │ │ │ │ ├── card.mdx │ │ │ │ ├── checkbox.mdx │ │ │ │ ├── collapsible.mdx │ │ │ │ ├── context-menu.mdx │ │ │ │ ├── dialog.mdx │ │ │ │ ├── dropdown-menu.mdx │ │ │ │ ├── hover-card.mdx │ │ │ │ ├── input.mdx │ │ │ │ ├── label.mdx │ │ │ │ ├── menubar.mdx │ │ │ │ ├── popover.mdx │ │ │ │ ├── progress.mdx │ │ │ │ ├── radio-group.mdx │ │ │ │ ├── select.mdx │ │ │ │ ├── separator.mdx │ │ │ │ ├── skeleton.mdx │ │ │ │ ├── switch.mdx │ │ │ │ ├── tabs.mdx │ │ │ │ ├── text.mdx │ │ │ │ ├── textarea.mdx │ │ │ │ ├── toggle-group.mdx │ │ │ │ ├── toggle.mdx │ │ │ │ └── tooltip.mdx │ │ │ ├── create-your-own-registry.mdx │ │ │ ├── customization.mdx │ │ │ ├── index.mdx │ │ │ ├── installation/ │ │ │ │ ├── index.mdx │ │ │ │ └── manual.mdx │ │ │ └── meta.json │ │ ├── global.d.ts │ │ ├── lib/ │ │ │ ├── FoundedLabsIcon.tsx │ │ │ ├── file-generator.ts │ │ │ ├── source.ts │ │ │ └── utils.ts │ │ ├── nativewind-env.d.ts │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── public/ │ │ │ ├── .well-known/ │ │ │ │ ├── apple-app-site-association │ │ │ │ └── assetlinks.json │ │ │ └── r/ │ │ │ ├── nativewind/ │ │ │ │ ├── accordion.json │ │ │ │ ├── alert-dialog.json │ │ │ │ ├── alert.json │ │ │ │ ├── aspect-ratio.json │ │ │ │ ├── avatar.json │ │ │ │ ├── badge.json │ │ │ │ ├── button.json │ │ │ │ ├── card.json │ │ │ │ ├── checkbox.json │ │ │ │ ├── collapsible.json │ │ │ │ ├── context-menu.json │ │ │ │ ├── dialog.json │ │ │ │ ├── dropdown-menu.json │ │ │ │ ├── forgot-password-form-clerk.json │ │ │ │ ├── forgot-password-form.json │ │ │ │ ├── hover-card.json │ │ │ │ ├── icon.json │ │ │ │ ├── input.json │ │ │ │ ├── label.json │ │ │ │ ├── menubar.json │ │ │ │ ├── native-only-animated-view.json │ │ │ │ ├── popover.json │ │ │ │ ├── progress.json │ │ │ │ ├── radio-group.json │ │ │ │ ├── reset-password-form-clerk.json │ │ │ │ ├── reset-password-form.json │ │ │ │ ├── select.json │ │ │ │ ├── separator.json │ │ │ │ ├── sign-in-form-clerk.json │ │ │ │ ├── sign-in-form.json │ │ │ │ ├── sign-up-form-clerk.json │ │ │ │ ├── sign-up-form.json │ │ │ │ ├── skeleton.json │ │ │ │ ├── social-connections-clerk.json │ │ │ │ ├── social-connections.json │ │ │ │ ├── switch.json │ │ │ │ ├── tabs.json │ │ │ │ ├── text.json │ │ │ │ ├── textarea.json │ │ │ │ ├── toggle-group.json │ │ │ │ ├── toggle.json │ │ │ │ ├── tooltip.json │ │ │ │ ├── user-menu-clerk.json │ │ │ │ ├── user-menu.json │ │ │ │ ├── verify-email-form-clerk.json │ │ │ │ └── verify-email-form.json │ │ │ └── uniwind/ │ │ │ ├── accordion.json │ │ │ ├── alert-dialog.json │ │ │ ├── alert.json │ │ │ ├── aspect-ratio.json │ │ │ ├── avatar.json │ │ │ ├── badge.json │ │ │ ├── button.json │ │ │ ├── card.json │ │ │ ├── checkbox.json │ │ │ ├── collapsible.json │ │ │ ├── context-menu.json │ │ │ ├── dialog.json │ │ │ ├── dropdown-menu.json │ │ │ ├── forgot-password-form-clerk.json │ │ │ ├── forgot-password-form.json │ │ │ ├── hover-card.json │ │ │ ├── icon.json │ │ │ ├── input.json │ │ │ ├── label.json │ │ │ ├── menubar.json │ │ │ ├── native-only-animated-view.json │ │ │ ├── popover.json │ │ │ ├── progress.json │ │ │ ├── radio-group.json │ │ │ ├── reset-password-form-clerk.json │ │ │ ├── reset-password-form.json │ │ │ ├── select.json │ │ │ ├── separator.json │ │ │ ├── sign-in-form-clerk.json │ │ │ ├── sign-in-form.json │ │ │ ├── sign-up-form-clerk.json │ │ │ ├── sign-up-form.json │ │ │ ├── skeleton.json │ │ │ ├── social-connections-clerk.json │ │ │ ├── social-connections.json │ │ │ ├── switch.json │ │ │ ├── tabs.json │ │ │ ├── text.json │ │ │ ├── textarea.json │ │ │ ├── toggle-group.json │ │ │ ├── toggle.json │ │ │ ├── tooltip.json │ │ │ ├── user-menu-clerk.json │ │ │ ├── user-menu.json │ │ │ ├── verify-email-form-clerk.json │ │ │ └── verify-email-form.json │ │ ├── registry/ │ │ │ ├── nativewind.json │ │ │ └── uniwind.json │ │ ├── scripts/ │ │ │ └── generate-local-registry.js │ │ ├── source.config.ts │ │ ├── src/ │ │ │ └── content/ │ │ │ └── docs/ │ │ │ └── showcase/ │ │ │ ├── links/ │ │ │ │ ├── button.mdx │ │ │ │ ├── input.mdx │ │ │ │ └── wrong.mdx │ │ │ ├── privacy-policy.mdx │ │ │ └── support.mdx │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ └── vercel.json │ └── showcase/ │ ├── .gitignore │ ├── app/ │ │ ├── +html.tsx │ │ ├── +not-found.tsx │ │ ├── _layout.tsx │ │ ├── components/ │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── text.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ ├── index.tsx │ │ └── showcase/ │ │ └── links/ │ │ └── [slug].tsx │ ├── app.config.ts │ ├── babel.config.js │ ├── components/ │ │ ├── header-right-view.tsx │ │ ├── preview-carousel.tsx │ │ └── theme-toggle.tsx │ ├── components.json │ ├── eas.json │ ├── global.css │ ├── hooks/ │ │ └── use-geist-font.tsx │ ├── index.js │ ├── lib/ │ │ ├── constants.ts │ │ ├── theme.ts │ │ └── utils.ts │ ├── metro.config.js │ ├── nativewind-env.d.ts │ ├── package.json │ ├── plugins/ │ │ └── geistFontPlugin.js │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vercel.json ├── package.json ├── packages/ │ └── registry/ │ ├── nativewind-env.d.ts │ ├── package.json │ ├── src/ │ │ ├── blocks/ │ │ │ ├── clerk/ │ │ │ │ ├── forgot-password-form.tsx │ │ │ │ ├── reset-password-form.tsx │ │ │ │ ├── sign-in-form.tsx │ │ │ │ ├── sign-up-form.tsx │ │ │ │ ├── social-connections.tsx │ │ │ │ ├── user-menu.tsx │ │ │ │ └── verify-email-form.tsx │ │ │ ├── forgot-password-form.tsx │ │ │ ├── index.ts │ │ │ ├── reset-password-form.tsx │ │ │ ├── sign-in-form.tsx │ │ │ ├── sign-up-form.tsx │ │ │ ├── social-connections.tsx │ │ │ ├── user-menu.tsx │ │ │ └── verify-email-form.tsx │ │ ├── examples/ │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button/ │ │ │ │ ├── button-destructive.tsx │ │ │ │ ├── button-ghost.tsx │ │ │ │ ├── button-icon.tsx │ │ │ │ ├── button-link.tsx │ │ │ │ ├── button-loading.tsx │ │ │ │ ├── button-outline.tsx │ │ │ │ ├── button-primary.tsx │ │ │ │ ├── button-secondary.tsx │ │ │ │ ├── button-with-icon.tsx │ │ │ │ └── index.ts │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── index.ts │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select/ │ │ │ │ ├── index.ts │ │ │ │ ├── scrollable-select.tsx │ │ │ │ └── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── text/ │ │ │ │ ├── index.ts │ │ │ │ ├── text-cascade.tsx │ │ │ │ ├── text-typorgaphy.tsx │ │ │ │ └── text.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ ├── nativewind/ │ │ │ ├── components/ │ │ │ │ └── ui/ │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── icon.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── menubar.tsx │ │ │ │ ├── native-only-animated-view.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── text.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── lib/ │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── uniwind/ │ │ ├── components/ │ │ │ └── ui/ │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── icon.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── native-only-animated-view.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── text.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── lib/ │ │ ├── index.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── uniwind-types.d.ts ├── pnpm-workspace.yaml └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: mrzachnugent ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '[ BUG ]' labels: bug assignees: '' --- **Reproduction link**: `` **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce the behavior:** 1. Start the '...' app with '...' 2. Go to '...' 3. Click on '....' 4. Scroll down to '....' 5. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Platform (please complete the following information):** - Type: [eg: Browser, Simulator, Emulator, Device] - OS: [e.g. iOS] - Browser (if applies) [e.g. chrome, safari] **CLI output (paste the full command output)** If applicable, paste the full command output by running it with the `--log-level all` flag. ```bash npx @react-native-reusables/cli@latest --log-level all [command] [args] [options] // example: // npx @react-native-reusables/cli@latest --log-level all init -t minimal ``` **Additional context** Add any other context about the problem here. ================================================ FILE: .github/pull_request_template.md ================================================ # Pull Request Template ## Description: Fixes issue # ## Tested Platforms: - [ ] Web - [ ] iOS - [ ] Android ## Affected Apps/Packages: - [ ] apps/docs - [ ] apps/showcase - [ ] apps/cli - [ ] packages/registry ### Screenshots: #### Notes: ================================================ FILE: .gitignore ================================================ # Monorepo apps/*/credentials.json apps/*/build packages/*/build # Turborepo .turbo # Expo .expo __generated__ web-build expo-env.d.ts packages/experimental # macOS .DS_Store # Node node_modules npm-debug.log yarn-error.log package-lock.json # Ruby .direnv # Emacs *~ # Vim .*.swp .*.swo .*.swn .*.swm # Sublime Text *.sublime-project *.sublime-workspace # Xcode *.pbxuser !default.pbxuser *.xccheckout *.xcscmblueprint xcuserdata # Android Studio *.iml .gradle .idea/libraries .idea/workspace.xml .idea/gradle.xml .idea/misc.xml .idea/modules.xml .idea/vcs.xml # Eclipse .project .settings # VSCode .history/ ================================================ FILE: .npmrc ================================================ node-linker=hoisted ================================================ FILE: .nvmrc ================================================ 20.11 ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "tabWidth": 2, "singleQuote": true, "bracketSameLine": true, "trailingComma": "es5", "plugins": ["prettier-plugin-tailwindcss"], "tailwindFunctions": ["cva"] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@foundedlabs.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: COMMUNITY_RESOURCES.md ================================================ # Community Resources Community-created components, libraries, and templates that extend React Native Reusables and fill in missing shadcn/ui elements. _Contributions are welcome! Submit a PR to add your resource._ ## Components & Libraries ### Calendar - [React Native Flash Calendar](https://github.com/MarceloPrado/flash-calendar) Incredibly fast and flexible library to build calendars in React Native. ### Carousel - [Animated.ScrollView](https://medium.com/timeless/building-a-gallery-carousel-in-react-native-using-reanimated-i-19b19e6b6b10) Article explaining how to create a carousel using the ScrollView component. ### Chart - [Victory Native](https://github.com/FormidableLabs/victory-native-xl) Charting library for React Native with a focus on performance and customization. ### Combobox _TBD_ ### Command _TBD_ ### Data Table - [Tanstack Table](https://tanstack.com/table/latest) Headless UI for building powerful tables and datagrids. ### Date Picker - [React Native DateTimePicker](https://github.com/react-native-datetimepicker/datetimepicker) Date and time picker component for iOS, Android, and Windows. ### Drawer - [Universal Bottom Sheet](https://github.com/adebayoileri/universal-bottom-sheet) by [adebayoileri](https://github.com/adebayoileri) Bottom sheet component that combines Gorhom Bottom Sheet and Vaul for a seamless, responsive experience across mobile and web. ### Form - [React Hook Form](https://react-hook-form.com/get-started#ReactNative) Performant, flexible, and extensible forms with easy-to-use validation. ### Input OTP - [input-otp-native](https://github.com/yjose/input-otp-native) 🔐 OTP input for React Native/Expo apps: unstyled, copy-paste examples that are fully tested and compatible with Nativewind. ### Resizable _TBD_ ### Scroll Area - [React Native ScrollView](https://reactnative.dev/docs/scrollview) Generic scrolling container that can host multiple components and views. ### Sheet (Drawer Navigation) - [Drawer Navigation](https://reactnavigation.org/docs/drawer-based-navigation/) Drawer navigation component that slides in from the side. ### Sonner - [Sonner Native](https://github.com/gunnartorfis/sonner-native) by [gunnartorfis](https://github.com/gunnartorfis) Opinionated toast component for React Native. Port of @emilkowalski's sonner. - [Burnt](https://www.npmjs.com/package/burnt) Cross-platform toasts for React Native, powered by native elements. Uses [Sonner](https://github.com/emilkowalski/sonner) on Web. --- ## Templates - [RNR Base Bare](https://github.com/a0m0rajab/rnr-base-bare) by [a0m0rajab](https://github.com/a0m0rajab) Supabase-powered starter app with sign-in, sign-up, and profile functionality. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to React Native Reusables Thank you for your interest in contributing to `react-native-reusables`! We welcome contributions from the community to improve and enhance this project. Before getting started, please take a moment to review the following guidelines. ## How to Contribute > ⚠️ **Important** > > If you want to propose a new feature: > > 1. Make sure to read the [project scope](https://github.com/founded-labs/react-native-reusables/discussions/229) to confirm your proposal fits within the vision and purpose of `react-native-reusables`. > 2. Before taking any action, please open a [new discussion](https://github.com/founded-labs/react-native-reusables/discussions). This allows us to collaborate, gather feedback, and ensure alignment with the project's goals.
1. Fork the repository to your GitHub account. 2. Clone the forked repository to your local machine: ```bash git clone https://github.com/your-username/react-native-reusables.git cd react-native-reusables ``` 3. Create a new branch: ```bash git checkout -b your-username/your-feature-name ``` 4. Make your changes and ensure that your code adheres to the existing coding standards. 5. Commit your changes with clear and concise commit messages: ```bash git commit -m "Add your commit message here" ``` 6. Push your changes to your forked repository: ```bash git push origin your-username/your-feature-name ``` 7. Open a pull request (PR) against the main branch of the original repository. ## Code Style Guidelines Please follow the coding style and guidelines used in the project. If there are specific coding conventions, linting rules, or documentation standards, make sure your contributions adhere to them. > _If they are not clear, please create an issue and we will create a CODING_STYLE_GUIDELINE.md_ ## Issue Tracker Check the [issue tracker](https://github.com/founded-labs/react-native-reusables/issues) for existing issues or open a new issue to discuss and coordinate your contribution with the maintainers. ## Code of Conduct Please review and adhere to our [Code of Conduct](https://github.com/founded-labs/react-native-reusables/blob/main/CODE_OF_CONDUCT.md). Be respectful and considerate towards others. ## License By contributing to this project, you agree that your contributions will be licensed under the [LICENSE](https://github.com/founded-labs/react-native-reusables/blob/main/LICENSE) file of this repository. ## Contact If you have any questions or need further assistance, feel free to contact us at hello@foundedlabs.com. **We appreciate your contributions and look forward to working with you!** ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Founded Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # React Native Reusables Bringing [shadcn/ui](https://ui.shadcn.com) to React Native. Beautifully crafted components with [Nativewind](https://www.nativewind.dev/), open source, and almost as easy to use. ![hero](apps/docs/public/og.png) ## Documentation Visit https://reactnativereusables.com/docs to view the documentation. ## Community Resources See the [community resources](./COMMUNITY_RESOURCES.md) for community-maintained components, libraries, and templates. ## Contributing Please read the [contributing guide](/CONTRIBUTING.md). ## License Licensed under the [MIT license](/LICENSE).

Vercel OSS Program ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Security Issue If you discover a security issue in this repository, please help us by responsibly disclosing it to us. We appreciate your efforts and will work with you to address and resolve the problem. Please follow these guidelines when reporting security issues: ### Responsible Disclosure 1. **Do not create public issues for security-related concerns.** 2. Email us at [hello@foundedlabs.com](mailto:hello@foundedlabs.com) with details about the issue. 3. Allow us a reasonable amount of time to address the issue before disclosing it publicly. ### Information to Include When reporting a security issue, please provide the following information: - Description of the issue, including steps to reproduce. - Any specific details or context that can help us understand and address the problem. - Your contact information for further communication. ## Code Copy-Pasting Disclaimer This repository allows users to copy and paste code snippets. While we aim to ensure the security of the code, users are responsible for reviewing and using the code responsibly in their own projects. The repository maintainers are not liable for any security vulnerabilities or issues that may arise from code usage. ## Updates and Security Notices Security updates and notices related to this repository will be communicated through the repository's README or other appropriate channels. Users are encouraged to stay informed about security-related announcements. ## Support If you have any questions or need further assistance, you can reach out to us at [hello@foundedlabs.com](mailto:hello@foundedlabs.com). Thank you for helping us keep this repository secure! ================================================ FILE: apps/cli/.changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", "changelog": [ "@changesets/changelog-github", { "repo": "founded-labs/react-native-reusables" } ], "commit": false, "fixed": [], "linked": [], "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: apps/cli/.github/actions/setup/action.yml ================================================ name: Setup description: Perform standard setup and install dependencies using pnpm. inputs: node-version: description: The version of Node.js to install required: true default: 20.16.0 runs: using: composite steps: - name: Install pnpm uses: pnpm/action-setup@v3 - name: Install node uses: actions/setup-node@v4 with: cache: pnpm node-version: ${{ inputs.node-version }} - name: Install dependencies shell: bash run: pnpm install ================================================ FILE: apps/cli/.github/workflows/check.yml ================================================ name: Check on: workflow_dispatch: pull_request: branches: [main] push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: build: name: Build runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup types: name: Types runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - run: pnpm check lint: name: Lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - run: pnpm lint test: name: Test runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - run: pnpm test ================================================ FILE: apps/cli/.github/workflows/release.yml ================================================ ================================================ FILE: apps/cli/.github/workflows/snapshot.yml ================================================ name: Snapshot on: pull_request: branches: [main, next-minor, next-major] workflow_dispatch: permissions: {} jobs: snapshot: name: Snapshot if: github.repository_owner == 'Effect-Ts' runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/actions/setup - name: Build package run: pnpm build - name: Create snapshot id: snapshot run: pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off ================================================ FILE: apps/cli/.gitignore ================================================ coverage/ *.tsbuildinfo node_modules/ yarn-error.log .ultra.cache.json .DS_Store tmp/ build/ dist/ .direnv/ .env .env.local .env.development.local .env.test.local .env.production.local ================================================ FILE: apps/cli/.prettierrc ================================================ { "tabWidth": 2, "printWidth": 120, "semi": false, "singleQuote": false, "trailingComma": "none", "arrowParens": "always" } ================================================ FILE: apps/cli/.vscode/extensions.json ================================================ { "recommendations": [ "effectful-tech.effect-vscode", "dbaeumer.vscode-eslint" ] } ================================================ FILE: apps/cli/.vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib", "typescript.preferences.importModuleSpecifier": "relative", "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, "eslint.format.enable": true, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.semi": false, "prettier.trailingComma": "none" }, "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[javascriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[typescriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "eslint.validate": ["markdown", "javascript", "typescript"], "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.quickSuggestions": { "other": true, "comments": false, "strings": false }, "editor.acceptSuggestionOnCommitCharacter": true, "editor.acceptSuggestionOnEnter": "on", "editor.quickSuggestionsDelay": 10, "editor.suggestOnTriggerCharacters": true, "editor.tabCompletion": "off", "editor.suggest.localityBonus": true, "editor.suggestSelection": "recentlyUsed", "editor.wordBasedSuggestions": "matchingDocuments", "editor.parameterHints.enabled": true, "files.insertFinalNewline": true } ================================================ FILE: apps/cli/LICENSE ================================================ MIT License Copyright (c) 2024-present Founded Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: apps/cli/README.md ================================================ # React Native Reusables CLI A command-line toolkit to streamline the integration, setup, and maintenance of reusable React Native components in your projects. ## Features - **Add**: Quickly add reusable React Native components to your project, with style and path options. - **Doctor**: Diagnose your project for missing files, misconfigurations, and required dependencies. Optionally auto-install missing dependencies. - **Init**: Bootstrap a new React Native project pre-configured for reusables, or inspect/repair an existing setup. ## Getting Started ### Installation You can run the CLI directly with your favorite package manager: ```sh npx @react-native-reusables/cli@latest pnpm dlx @react-native-reusables/cli@latest yarn dlx @react-native-reusables/cli@latest bunx --bun @react-native-reusables/cli@latest ``` ## Commands --- ### `@react-native-reusables/cli@latest add [options] [components...]` Add one or more React Native components to your project. | Argument | Description | | ---------- | ----------------------------------------------------------------------------- | | components | Name of component(s) to add. (e.g. `button`, `input`, `card`, `avatar`, etc.) | | Option | Description | Default | | ------------------- | ------------------------------------ | --------------- | | `-y, --yes` | Skip confirmation prompts. | false | | `-o, --overwrite` | Overwrite existing files. | false | | `-c, --cwd ` | The working directory. | . (current dir) | | `-a, --all` | Add all available components. | false | | `-p, --path ` | The path to add the component(s) to. | | | `--styling-library ` | Override detected styling library (`nativewind` or `uniwind`). | auto-detect | | `-h, --help` | Display help for command. | | --- ### `rnr doctor [options]` Check your project setup and diagnose issues. | Option | Description | Default | | ----------------- | ------------------------------------------------------ | --------------- | | `-y, --yes` | Skip confirmation prompts for installing dependencies. | false | | `-c, --cwd ` | The working directory. | . (current dir) | | `--summary` | Output a summary only. | false | | `-h, --help` | Display help for command. | | --- ### `rnr init [options]` Initialize a new React Native project with reusables. | Option | Description | Default | | ----------------- | ------------------------- | --------------- | | `-c, --cwd ` | The working directory. | . (current dir) | | `-h, --help` | Display help for command. | | --- ## Development ### Scripts - `pnpm build` – Build the CLI for production. - `pnpm dev` – Run the CLI in development mode. > **Note:** If you are developing locally and want to use the `add` command in development mode, you must have the `apps/docs` app running. Start it from the root with: > > ```sh > pnpm dev:docs > ``` > > This serves the component registry required for local development. ### Structure - `src/cli.ts` – Main CLI entrypoint and command definitions. - `src/bin.ts` – Node boot-strapper. - `src/services/commands/` – Command implementations (`add`, `doctor`, `init`). - `src/contexts/` – CLI option/context definitions. - `src/utils/` – Utility functions. ## Contributing See the [main repo README](../../README.md) for guidelines. ================================================ FILE: apps/cli/eslint.config.mjs ================================================ import { fixupPluginRules } from "@eslint/compat" import { FlatCompat } from "@eslint/eslintrc" import js from "@eslint/js" import tsParser from "@typescript-eslint/parser" import codegen from "eslint-plugin-codegen" import _import from "eslint-plugin-import" import simpleImportSort from "eslint-plugin-simple-import-sort" import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" import path from "node:path" import { fileURLToPath } from "node:url" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all }) export default [ { ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] }, ...compat.extends( "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:@effect/recommended" ), { plugins: { import: fixupPluginRules(_import), "sort-destructure-keys": sortDestructureKeys, "simple-import-sort": simpleImportSort, codegen }, languageOptions: { parser: tsParser, ecmaVersion: 2018, sourceType: "module" }, settings: { "import/parsers": { "@typescript-eslint/parser": [".ts", ".tsx"] }, "import/resolver": { typescript: { alwaysTryTypes: true } } }, rules: { "codegen/codegen": "error", "no-fallthrough": "off", "no-irregular-whitespace": "off", "object-shorthand": "error", "prefer-destructuring": "off", "sort-imports": "off", "no-restricted-syntax": [ "error", { selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", message: "Do not use spread arguments in Array.push" } ], "no-unused-vars": "off", "prefer-rest-params": "off", "prefer-spread": "off", "import/first": "error", "import/newline-after-import": "error", "import/no-duplicates": "error", "import/no-unresolved": "off", "import/order": "off", "simple-import-sort/imports": "off", "sort-destructure-keys/sort-destructure-keys": "error", "deprecation/deprecation": "off", "@typescript-eslint/array-type": [ "warn", { default: "generic", readonly: "generic" } ], "@typescript-eslint/member-delimiter-style": 0, "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/consistent-type-imports": "warn", "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" } ], "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/camelcase": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/no-array-constructor": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-namespace": "off", "@effect/dprint": "off" } } ] ================================================ FILE: apps/cli/package.json ================================================ { "name": "@react-native-reusables/cli", "version": "0.7.1", "type": "module", "license": "MIT", "description": "A CLI for React Native Reusables", "repository": { "type": "git", "url": "https://github.com/founded-labs/react-native-reusables.git", "directory": "apps/cli" }, "publishConfig": { "access": "public", "directory": "dist" }, "scripts": { "build": "tsup && pnpm copy-package-json", "build:ts": "tsup", "clean": "rimraf dist/*", "check": "tsc -b tsconfig.json", "dev": "INTERNAL_ENV=development tsx src/bin.ts", "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", "lint-fix": "pnpm lint --fix", "test": "vitest run", "coverage": "vitest run --coverage", "copy-package-json": "tsx scripts/copy-package-json.ts", "changeset-version": "changeset version && node scripts/version.mjs", "changeset-publish": "pnpm build && TEST_DIST= pnpm vitest && changeset publish", "pub:beta": "pnpm publish --no-git-checks --access public --tag beta", "pub:next": "pnpm publish --no-git-checks --access public --tag next", "pub:release": "pnpm publish --access public" }, "dependencies": { "tsconfig-paths": "^4.2.0" }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.8", "@effect/cli": "latest", "@effect/eslint-plugin": "^0.2.0", "@effect/language-service": "^0.1.0", "@effect/platform": "latest", "@effect/platform-node": "latest", "@effect/vitest": "latest", "@eslint/compat": "1.1.1", "@eslint/eslintrc": "3.1.0", "@eslint/js": "9.10.0", "@types/node": "22.10.5", "@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/parser": "^8.4.0", "effect": "latest", "eslint": "^9.10.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-codegen": "0.28.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-import": "^2.30.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-sort-destructure-keys": "^2.0.0", "execa": "^7.2.0", "log-symbols": "^7.0.1", "ora": "^6.1.2", "package-manager-detector": "^1.3.0", "tsup": "^8.2.4", "tsx": "^4.19.1", "typescript": "^5.9.3", "vitest": "^2.0.5" }, "pnpm": { "patchedDependencies": { "@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch" } } } ================================================ FILE: apps/cli/patches/@changesets__get-github-info@0.6.0.patch ================================================ diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js index a74df59f8a5988f458a3476087399f5e6dfe4818..ce5e60ef9916eb0cb76ab1e9dd422abcad752bf6 100644 --- a/dist/changesets-get-github-info.cjs.js +++ b/dist/changesets-get-github-info.cjs.js @@ -251,18 +251,13 @@ async function getInfo(request) { b = new Date(b.mergedAt); return a > b ? 1 : a < b ? -1 : 0; })[0] : null; - - if (associatedPullRequest) { - user = associatedPullRequest.author; - } - return { user: user ? user.login : null, pull: associatedPullRequest ? associatedPullRequest.number : null, links: { commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, - user: user ? `[@${user.login}](${user.url})` : null + user: user ? `@${user.login}` : null } }; } diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js index 27e5c972ab1202ff16f5124b471f4bbcc46be2b5..3940a8fe86e10cb46d8ff6436dea1103b1839927 100644 --- a/dist/changesets-get-github-info.esm.js +++ b/dist/changesets-get-github-info.esm.js @@ -242,18 +242,13 @@ async function getInfo(request) { b = new Date(b.mergedAt); return a > b ? 1 : a < b ? -1 : 0; })[0] : null; - - if (associatedPullRequest) { - user = associatedPullRequest.author; - } - return { user: user ? user.login : null, pull: associatedPullRequest ? associatedPullRequest.number : null, links: { commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, - user: user ? `[@${user.login}](${user.url})` : null + user: user ? `@${user.login}` : null } }; } ================================================ FILE: apps/cli/scripts/copy-package-json.ts ================================================ import { FileSystem, Path } from "@effect/platform" import { NodeContext } from "@effect/platform-node" import { Effect } from "effect" const program = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path yield* Effect.log("[Build] Copying package.json ...") const json: any = yield* fs.readFileString("package.json").pipe(Effect.map(JSON.parse)) const pkg = { name: json.name, version: json.version, type: json.type, description: json.description, main: "bin.cjs", bin: "bin.cjs", engines: json.engines, dependencies: json.dependencies, peerDependencies: json.peerDependencies, repository: json.repository, author: json.author, license: json.license, bugs: json.bugs, homepage: json.homepage, tags: json.tags, keywords: json.keywords } yield* fs.writeFileString(path.join("dist", "package.json"), JSON.stringify(pkg, null, 2)) yield* Effect.log("[Build] Build completed.") }).pipe(Effect.provide(NodeContext.layer)) Effect.runPromise(program).catch(console.error) ================================================ FILE: apps/cli/src/bin.ts ================================================ #!/usr/bin/env node import * as NodeContext from "@effect/platform-node/NodeContext" import * as NodeRuntime from "@effect/platform-node/NodeRuntime" import * as Effect from "effect/Effect" import * as Cli from "./cli.js" Effect.suspend(Cli.run).pipe( Effect.provide(NodeContext.layer), Effect.catchAll((error) => { if (error instanceof Error) { Effect.logDebug(error) return Effect.logError(error.message) } return Effect.logError(error) }), NodeRuntime.runMain({ disableErrorReporting: true }) ) ================================================ FILE: apps/cli/src/cli.ts ================================================ import { all, cwd, overwrite, path, stylingLibrary, summary, template, yes } from "@cli/contexts/cli-options.js" import * as Add from "@cli/services/commands/add.js" import * as Doctor from "@cli/services/commands/doctor.js" import * as Init from "@cli/services/commands/init.js" import { Args, Command, Prompt } from "@effect/cli" import { Effect, pipe } from "effect" const addArgs = Args.all({ components: Args.text({ name: "components" }).pipe(Args.repeated) }) const AddCommand = Command.make("add", { args: addArgs, cwd, yes, overwrite, all, path, stylingLibrary }) .pipe(Command.withDescription("Add React Native components to your project")) .pipe(Command.withHandler(Add.make)) const DoctorCommand = Command.make("doctor", { cwd, summary, yes }) .pipe(Command.withDescription("Check your project setup and diagnose issues")) .pipe(Command.withHandler(Doctor.make)) const InitCommand = Command.make("init", { cwd, template }) .pipe(Command.withDescription("Initialize a new React Native project with reusables")) .pipe(Command.withHandler(Init.make)) const Cli = Command.make("react-native-reusables/cli", { cwd }) .pipe(Command.withDescription("React Native Reusables CLI - A powerful toolkit for React Native development")) .pipe( Command.withHandler((options) => Effect.gen(function* () { yield* Effect.log("React Native Reusables CLI - A powerful toolkit for React Native development") const choice = yield* Prompt.select({ message: "What would you like to do?", choices: [ { title: "Add a component", value: "add" }, { title: "Inspect project configuration", value: "doctor" }, { title: "Initialize a new project", value: "init" } ] }) if (choice === "add") { yield* Add.make({ cwd: options.cwd, yes: true, overwrite: false, all: false, path: "", stylingLibrary: undefined, args: { components: [] } }) } else if (choice === "doctor") { yield* Doctor.make({ cwd: options.cwd, summary: false, yes: false }) } else if (choice === "init") { yield* Init.make({ cwd: options.cwd, template: "" }) } }) ) ) .pipe(Command.withSubcommands([AddCommand, DoctorCommand, InitCommand])) export const run = () => pipe( process.argv, Command.run(Cli, { name: "@react-native-reusables/cli", version: "1.0.0" }) ) ================================================ FILE: apps/cli/src/contexts/cli-options.ts ================================================ import { Options } from "@effect/cli" import { Context, Option } from "effect" type StylingLibrary = "nativewind" | "uniwind" class CliOptions extends Context.Tag("CommandOptions")< CliOptions, Readonly<{ cwd: string yes: boolean stylingLibrary: StylingLibrary | undefined }> >() { } const cwd = Options.directory("cwd", { exists: "yes" }).pipe(Options.withDefault("."), Options.withAlias("c")) const yes = Options.boolean("yes", { aliases: ["y"] }) const summary = Options.boolean("summary").pipe(Options.withAlias("s")) const overwrite = Options.boolean("overwrite", { aliases: ["o"] }) const all = Options.boolean("all", { aliases: ["a"] }) const path = Options.text("path").pipe(Options.withDefault(""), Options.withAlias("p")) const stylingLibrary = Options.choice("styling-library", ["nativewind", "uniwind"] as const).pipe( Options.optional, Options.map(Option.getOrUndefined), Options.withDescription("Override the detected styling library for this command"), ) const template = Options.text("template").pipe(Options.withAlias("t"), Options.withDefault("")) export { CliOptions, cwd, summary, yes, overwrite, all, path, stylingLibrary, template } export type { StylingLibrary } ================================================ FILE: apps/cli/src/project-manifest.ts ================================================ interface FileCheck { name: string fileNames: Array docs: string includes: Array<{ content: Array message: string docs: string }> stylingLibraries: Array<"nativewind" | "uniwind"> } type CustomFileCheck = Omit & { defaultFileNames?: ReadonlyArray } interface FileWithContent extends FileCheck { content: string } interface MissingInclude { fileName: string content: ReadonlyArray message: string docs: string } const CORE_DEPENDENCIES = [ "expo", "react-native-reanimated", "react-native-safe-area-context", "tailwindcss-animate", "class-variance-authority", "clsx", "tailwind-merge" ] const DEPENDENCIES = { nativewind: [...CORE_DEPENDENCIES, "nativewind"], uniwind: [...CORE_DEPENDENCIES, "uniwind"] } const DEV_DEPENDENCIES = ["tailwindcss@^3.4.14"] const FILE_CHECKS: Array = [ { name: "Babel Config", fileNames: ["babel.config.js", "babel.config.ts"], docs: "https://www.nativewind.dev/docs/getting-started/installation#3-add-the-babel-preset", includes: [ { content: ["nativewind/babel", "jsxImportSource"], message: "jsxImportSource or nativewind/babel is missing", docs: "https://www.nativewind.dev/docs/getting-started/installation#3-add-the-babel-preset" } ], stylingLibraries: ["nativewind"] as const }, { name: "Metro Config", fileNames: ["metro.config.js", "metro.config.ts"], docs: "https://www.nativewind.dev/docs/getting-started/installation#4-create-or-modify-your-metroconfigjs", includes: [ { content: ["withNativeWind("], message: "The withNativeWind function is missing", docs: "https://www.nativewind.dev/docs/getting-started/installation#4-create-or-modify-your-metroconfigjs" }, { content: ["inlineRem", "16"], message: "The 'inlineRem: 16' is missing", docs: "https://reactnativereusables.com/docs/installation/manual#update-the-default-inlined-rem-value" } ], stylingLibraries: ["nativewind"] as const }, { name: "Metro Config", fileNames: ["metro.config.js", "metro.config.ts"], docs: "https://www.nativewind.dev/docs/getting-started/installation#4-create-or-modify-your-metroconfigjs", includes: [ { content: ["withUniwindConfig("], message: "The withUniwindConfig function is missing", docs: "https://docs.uniwind.dev/api/metro-config#metro-config-js" } ], stylingLibraries: ["uniwind"] as const }, { name: "Root Layout", fileNames: ["app/_layout.tsx", "src/app/_layout.tsx"], docs: "https://reactnativereusables.com/docs/installation/manual#add-the-portal-host-to-your-root-layout", // includes: [ { content: [".css"], message: "The css file import is missing", docs: "https://www.nativewind.dev/docs/getting-started/installation#5-import-your-css-file" }, { content: ["> = [ { name: "Icons", fileNames: ["icons/iconWithClassName.ts"], includes: [ { content: ["iconWithClassName"], message: "lib/icons and its contents are deprecated. Use the new icon wrapper from components/ui/icon.", docs: "https://reactnativereusables.com/docs/changelog#august-2025-deprecated" } ] }, { name: "Constants", fileNames: ["constants.ts"], includes: [ { content: ["NAV_THEME"], message: "Usage of lib/constants for NAV_THEME is deprecated. Use lib/theme instead.", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" } ] }, { name: "useColorScheme", fileNames: ["useColorScheme.tsx"], includes: [ { content: ["useColorScheme"], message: "lib/useColorScheme is deprecated. Use Nativewind's color scheme hook instead.", docs: "https://www.nativewind.dev/docs/api/use-color-scheme" } ] } ] const DEPRECATED_FROM_UI: Array> = [ { name: "Typography", fileNames: ["typography.tsx"], includes: [ { content: [ "function H1({", "function H2({", "function H3({", "function H4({", "function P({", "function BlockQuote({", "function Code({", "function Lead({", "function Large({", "function Small({", "function Muted({" ], message: "Typography is deprecated. Instead, use the Text component with its variant prop (e.g. Title)", docs: "https://reactnativereusables.com/docs/components/text#typography" } ] } ] // Excludes foreground colors since it is formatted differently in all 3 styling files (tailwind config, global.css, theme.ts) const CSS_VARIABLE_NAMES = [ "background", "foreground", "card", "popover", "primary", "secondary", "muted", "accent", "destructive", "border", "input", "ring", "radius" ] const CUSTOM_FILE_CHECKS: Record = { tailwindConfig: { name: "Tailwind Config", defaultFileNames: ["tailwind.config.js", "tailwind.config.ts"], docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles", includes: [ { content: ["nativewind/preset"], message: "The nativewind preset is missing", docs: "https://www.nativewind.dev/docs/getting-started/installation#2-setup-tailwind-css" }, { content: CSS_VARIABLE_NAMES, message: "At least one of the color css variables is missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" } ], stylingLibraries: ["nativewind"] as const }, theme: { name: "Theme", defaultFileNames: ["lib/theme.ts"], docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles", includes: [ { content: CSS_VARIABLE_NAMES, message: "At least one of the color variables is missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" }, { content: ["NAV_THEME"], message: "The NAV_THEME is missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" } ], stylingLibraries: ["nativewind", "uniwind"] as const }, nativewindEnv: { name: "Nativewind Env", docs: "https://www.nativewind.dev/docs/getting-started/installation#7-typescript-setup-optional", includes: [ { content: ["nativewind/types"], message: "The nativewind types are missing", docs: "https://www.nativewind.dev/docs/getting-started/installation#7-typescript-setup-optional" } ], stylingLibraries: ["nativewind"] as const }, uniwindTypes: { name: "Uniwind Types", defaultFileNames: ["uniwind-types.d.ts"], docs: "https://docs.uniwind.dev/api/metro-config#dtsfile", includes: [ { content: ["uniwind/types"], message: "The uniwind types are missing", docs: "https://docs.uniwind.dev/api/metro-config#dtsfile" } ], stylingLibraries: ["uniwind"] as const }, utils: { name: "Utils", defaultFileNames: ["lib/utils.ts"], docs: "https://reactnativereusables.com/docs/installation/manual#add-a-cn-helper", includes: [ { content: ["function cn("], message: "The cn function is missing", docs: "https://reactnativereusables.com/docs/installation/manual#add-a-cn-helper" } ], stylingLibraries: ["nativewind", "uniwind"] as const }, css: { name: "CSS", defaultFileNames: ["globals.css", "src/global.css"], docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles", includes: [ { content: ["@tailwind base", "@tailwind components", "@tailwind utilities"], message: "The tailwind layer directives are missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" }, { content: CSS_VARIABLE_NAMES, message: "At least one of the color css variables is missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" } ], stylingLibraries: ["nativewind"] as const }, uniwindCss: { name: "CSS", defaultFileNames: ["globals.css", "src/global.css"], docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles", includes: [ { content: ["tailwindcss", "uniwind"], message: "The tailwind layer directives are missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" }, { content: CSS_VARIABLE_NAMES, message: "At least one of the color css variables is missing", docs: "https://reactnativereusables.com/docs/installation/manual#configure-your-styles" } ], stylingLibraries: ["uniwind"] as const } } const NATIVEWIND_ENV_FILE = "nativewind-env.d.ts" const UNIWIND_TYPES_FILE = "uniwind-types.d.ts" const COMPONENTS = [ "accordion", "alert-dialog", "alert", "aspect-ratio", "avatar", "badge", "button", "card", "checkbox", "collapsible", "context-menu", "dialog", "dropdown-menu", "hover-card", "input", "label", "menubar", "popover", "progress", "radio-group", "select", "separator", "skeleton", "switch", "tabs", "text", "textarea", "toggle-group", "toggle", "tooltip" ] const TEMPLATES = [ { name: "Minimal (Nativewind)", url: "https://github.com/founded-labs/react-native-reusables-templates.git", subPath: "minimal" }, { name: "Minimal (Uniwind)", url: "https://github.com/founded-labs/react-native-reusables-templates.git", subPath: "minimal-uniwind" }, { name: "Clerk auth (Nativewind)", url: "https://github.com/founded-labs/react-native-reusables-templates.git", subPath: "clerk-auth" } ] const PROJECT_MANIFEST = { dependencies: DEPENDENCIES, devDependencies: DEV_DEPENDENCIES, fileChecks: FILE_CHECKS, deprecatedFromLib: DEPRECATED_FROM_LIB, deprecatedFromUi: DEPRECATED_FROM_UI, customFileChecks: CUSTOM_FILE_CHECKS, nativewindEnvFile: NATIVEWIND_ENV_FILE, uniwindTypesFile: UNIWIND_TYPES_FILE, components: COMPONENTS, templates: TEMPLATES } export { PROJECT_MANIFEST } export type { FileCheck, CustomFileCheck, FileWithContent, MissingInclude } ================================================ FILE: apps/cli/src/services/commands/add.ts ================================================ import { CliOptions, type StylingLibrary } from "@cli/contexts/cli-options.js" import { PROJECT_MANIFEST } from "@cli/project-manifest.js" import { Doctor } from "@cli/services/commands/doctor.js" import { runCommand } from "@cli/utils/run-command.js" import { Prompt } from "@effect/cli" import { Effect, Layer } from "effect" import { PackageManager } from "../package-manager.js" import { ProjectConfig } from "../project-config.js" type AddOptions = { cwd: string args: { components: Array } yes: boolean overwrite: boolean all: boolean path: string stylingLibrary: StylingLibrary | undefined } class Add extends Effect.Service()("Add", { dependencies: [PackageManager.Default], effect: Effect.gen(function* () { const doctor = yield* Doctor const projectConfig = yield* ProjectConfig const packageManager = yield* PackageManager return { run: (options: AddOptions) => Effect.gen(function* () { yield* Effect.logDebug(`Add options: ${JSON.stringify(options, null, 2)}`) yield* projectConfig.getComponentJson() // ensure components.json config is valid and prompt if not const components = options.all ? PROJECT_MANIFEST.components : (options.args?.components ?? []) if (components.length === 0) { const selectedComponents = yield* Prompt.multiSelect({ message: "Select components to add", choices: PROJECT_MANIFEST.components.map((component) => ({ title: component, value: component })) }) for (const component of selectedComponents) { components.push(component) } } if (components.length === 0) { yield* Effect.fail(new Error("No components selected.")) } yield* Effect.logDebug(`Selected components: ${components.join(", ")}`) const stylingLibrary = yield* projectConfig.getStylingLibrary() const registry = stylingLibrary === "uniwind" ? "uniwind" : "nativewind" const baseUrl = process.env.INTERNAL_ENV === "development" ? `http://localhost:3000/local/r/${registry}` : `https://reactnativereusables.com/r/${registry}` const componentUrls = components.map((component) => { const lowerCaseComponent = component.toLocaleLowerCase() return lowerCaseComponent.startsWith("http") ? lowerCaseComponent : `${baseUrl}/${lowerCaseComponent}.json` }) const shadcnOptions = toShadcnOptions(options) const binaryRunner = yield* packageManager.getBinaryRunner(options.cwd) const commandArgs = [ ...binaryRunner.slice(1), "shadcn@latest", "add", ...shadcnOptions, ...componentUrls ].filter((option) => option !== undefined) yield* Effect.logDebug(`Running command: ${binaryRunner[0]} ${commandArgs.join(" ")}`) yield* runCommand(binaryRunner[0], commandArgs, { cwd: options.cwd, stdio: "inherit" }) yield* doctor.run({ ...options, summary: true }) }) } }) }) {} function make(options: AddOptions) { const optionsLayer = Layer.succeed(CliOptions, { ...options, yes: true }) // For the project config return Effect.gen(function* () { const add = yield* Add return yield* add.run(options) }).pipe( Effect.provide(Add.Default), Effect.provide(Doctor.Default), Effect.provide(ProjectConfig.Default), Effect.provide(optionsLayer) ) } export { make } function toShadcnOptions(options: AddOptions) { const shadcnOptions = [] if (options.overwrite) { shadcnOptions.push("--overwrite") } if (options.yes) { shadcnOptions.push("--yes") } if (options.path) { shadcnOptions.push("--path") shadcnOptions.push(options.path) } return shadcnOptions } ================================================ FILE: apps/cli/src/services/commands/doctor.ts ================================================ import { CliOptions } from "@cli/contexts/cli-options.js" import { type CustomFileCheck, type FileCheck, type MissingInclude, PROJECT_MANIFEST } from "@cli/project-manifest.js" import { ProjectConfig } from "@cli/services/project-config.js" import { RequiredFilesChecker } from "@cli/services/required-files-checker.js" import { Spinner } from "@cli/services/spinner.js" import { runCommand } from "@cli/utils/run-command.js" import { Prompt } from "@effect/cli" import { FileSystem, Path } from "@effect/platform" import { Data, Effect, Layer, Schema } from "effect" import logSymbols from "log-symbols" const packageJsonSchema = Schema.Struct({ dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), devDependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })) }) class PackageJsonError extends Data.TaggedError("PackageJsonError")<{ cause?: unknown message?: string }> {} type DoctorOptions = { cwd: string summary: boolean yes: boolean } class Doctor extends Effect.Service()("Doctor", { dependencies: [RequiredFilesChecker.Default, Spinner.Default], effect: Effect.gen(function* () { const options = yield* CliOptions const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const requiredFileChecker = yield* RequiredFilesChecker const spinner = yield* Spinner const projectConfig = yield* ProjectConfig const stylingLibrary = yield* projectConfig.getStylingLibrary() if (stylingLibrary !== "unknown"){ console.log( `\x1b[2m${logSymbols.info} Styling Library: ${stylingLibrary === "uniwind" ? "Uniwind" : "Nativewind"}\x1b[0m` ) } const checkRequiredDependencies = ({ dependencies, devDependencies }: { dependencies: Array devDependencies: Array }) => Effect.gen(function* () { const packageJsonExists = yield* fs.exists(path.join(options.cwd, "package.json")) if (!packageJsonExists) { return yield* Effect.fail(new PackageJsonError({ message: "A package.json was not found and is required." })) } const packageJson = yield* fs.readFileString(path.join(options.cwd, "package.json")).pipe( Effect.flatMap(Schema.decodeUnknown(Schema.parseJson())), Effect.flatMap(Schema.decodeUnknown(packageJsonSchema)), Effect.catchTags({ ParseError: () => Effect.fail(new PackageJsonError({ message: "Failed to parse package.json" })) }) ) const uninstalledDependencies: Array = [] const uninstalledDevDependencies: Array = [] for (const dependency of dependencies) { if ( !packageJson.dependencies?.[dependency.split("@")[0]] && !packageJson.devDependencies?.[dependency.split("@")[0]] ) { uninstalledDependencies.push(dependency) continue } yield* Effect.logDebug( `${logSymbols.success} ${dependency}@${packageJson.dependencies?.[dependency.split("@")[0]]} is installed` ) } for (const devDependency of devDependencies) { if ( !packageJson.devDependencies?.[devDependency.split("@")[0]] && !packageJson.dependencies?.[devDependency.split("@")[0]] ) { uninstalledDevDependencies.push(devDependency) continue } yield* Effect.logDebug( `${logSymbols.success} ${devDependency}@${packageJson.devDependencies?.[devDependency]} is installed` ) } return { uninstalledDependencies, uninstalledDevDependencies } }) const registry = stylingLibrary === "uniwind" ? "uniwind" : "nativewind" return { run: (options: DoctorOptions) => Effect.gen(function* () { yield* Effect.logDebug(`Doctor options: ${JSON.stringify(options, null, 2)}`) const { uninstalledDependencies, uninstalledDevDependencies } = yield* checkRequiredDependencies({ dependencies: PROJECT_MANIFEST.dependencies[registry], devDependencies: PROJECT_MANIFEST.devDependencies }) const { customFileResults, deprecatedFileResults, fileResults } = yield* requiredFileChecker.run({ customFileChecks: PROJECT_MANIFEST.customFileChecks, deprecatedFromLib: PROJECT_MANIFEST.deprecatedFromLib, deprecatedFromUi: PROJECT_MANIFEST.deprecatedFromUi, fileChecks: PROJECT_MANIFEST.fileChecks, stylingLibrary: registry }) const result = { missingFiles: [...fileResults.missingFiles, ...customFileResults.missingFiles], uninstalledDependencies, uninstalledDevDependencies, missingIncludes: [...fileResults.missingIncludes, ...customFileResults.missingIncludes], deprecatedFileResults } let total = Object.values(result).reduce((sum, cat) => sum + cat.length, 0) if (!options.summary) { const dependenciesToInstall: Array = [] for (const dep of result.uninstalledDependencies) { const confirmsInstall = options.yes ? true : yield* Prompt.confirm({ message: `The ${dep} dependency is missing. Do you want to install it?`, initial: true }) if (confirmsInstall) { if (uninstalledDependencies.includes("expo")) { continue } total-- yield* Effect.logDebug(`Adding ${dep} to dependencies to install`) dependenciesToInstall.push(dep) result.uninstalledDependencies = result.uninstalledDependencies.filter((d) => d !== dep) } } if (dependenciesToInstall.length > 0) { yield* Effect.logDebug(`Installing ${dependenciesToInstall.join(", ")}`) if (process.env.INTERNAL_ENV !== "development") { spinner.start("Installing dependencies") yield* runCommand("npx", ["expo", "install", ...dependenciesToInstall], { cwd: options.cwd }) spinner.stop() } } const devDependenciesToInstall: Array = [] for (const dep of result.uninstalledDevDependencies) { const confirmsInstall = options.yes ? true : yield* Prompt.confirm({ message: `The ${dep} dependency is missing. Do you want to install it?`, initial: true }) if (confirmsInstall) { if (uninstalledDependencies.includes("expo")) { continue } total-- yield* Effect.logDebug(`Adding ${dep} to devDependencies to install`) devDependenciesToInstall.push(dep) result.uninstalledDevDependencies = result.uninstalledDevDependencies.filter((d) => d !== dep) } } if (devDependenciesToInstall.length > 0) { yield* Effect.logDebug(`Installing ${devDependenciesToInstall.join(", ")}`) if (process.env.INTERNAL_ENV !== "development") { spinner.start("Installing dev dependencies") yield* runCommand("npx", ["expo", "install", ...devDependenciesToInstall], { cwd: options.cwd }) spinner.stop() } } } if (total === 0) { console.log(`\x1b[2m${logSymbols.success} All checks passed.\x1b[0m\n`) return yield* Effect.succeed(true) } const analysis = analyzeResult(result) if (options.summary) { console.log( `\x1b[2m${logSymbols.warning} ${total} Potential issue${ total > 1 ? "s" : "" } found. For more info, run: 'npx @react-native-reusables/cli doctor${ options.cwd !== "." ? ` -c ${options.cwd}` : "" }'\x1b[0m\n` ) } else { yield* Effect.log("\n\n🩺 Diagnosis") for (const item of analysis) { console.group(`\n${item.title}`) item.logs.forEach((line) => console.log(line)) console.groupEnd() } console.log(`\n`) } }) } }) }) {} function make(options: DoctorOptions) { const optionsLayer = Layer.succeed(CliOptions, { ...options, stylingLibrary: undefined }) return Effect.gen(function* () { const doctor = yield* Doctor return yield* doctor.run(options) }).pipe(Effect.provide(Doctor.Default), Effect.provide(ProjectConfig.Default), Effect.provide(optionsLayer)) } export { Doctor, make } interface Result { missingFiles: Array missingIncludes: Array uninstalledDependencies: Array uninstalledDevDependencies: Array deprecatedFileResults: Array> } function analyzeResult(result: Result) { const categories: Array<{ title: string; logs: Array; count: number }> = [] if (result.missingFiles.length > 0) { categories.push({ title: `${logSymbols.error} Missing Files (${result.missingFiles.length})`, count: result.missingFiles.length, logs: result.missingFiles.flatMap((f) => [`• ${f.name}`, ` 📘 Docs: ${f.docs}`]) }) } if (result.missingIncludes.length > 0) { categories.push({ title: `${logSymbols.error} Potentially Misconfigured Files (${result.missingIncludes.length})`, count: result.missingIncludes.length, logs: result.missingIncludes.flatMap((inc) => [ `• ${inc.fileName}`, ` ↪ ${inc.message}`, ` 📘 Docs: ${inc.docs}` ]) }) } if (result.uninstalledDependencies.length > 0) { categories.push({ title: `${logSymbols.error} Missing Dependencies (${result.uninstalledDependencies.length})`, count: result.uninstalledDependencies.length, logs: ["• Install with:", ` ↪ npx expo install ${result.uninstalledDependencies.join(" ")}`] }) } if (result.uninstalledDevDependencies.length > 0) { categories.push({ title: `${logSymbols.error} Missing Dev Dependencies (${result.uninstalledDevDependencies.length})`, count: result.uninstalledDevDependencies.length, logs: ["• Install with:", ` ↪ npx expo install -- -D ${result.uninstalledDevDependencies.join(" ")}`] }) } if (result.deprecatedFileResults.length > 0) { categories.push({ title: `${logSymbols.warning} Deprecated (${result.deprecatedFileResults.length})`, count: result.deprecatedFileResults.length, logs: result.deprecatedFileResults.flatMap((deprecatedFile) => [ `• ${deprecatedFile.name}`, ...deprecatedFile.includes.map((item) => ` ↪ ${item.message}\n 📘 Docs: ${item.docs}`) ]) }) } return categories } ================================================ FILE: apps/cli/src/services/commands/init.ts ================================================ import { CliOptions } from "@cli/contexts/cli-options.js" import { PROJECT_MANIFEST } from "@cli/project-manifest.js" import { Doctor } from "@cli/services/commands/doctor.js" import { ProjectConfig } from "@cli/services/project-config.js" import { Template } from "@cli/services/template.js" import { Prompt } from "@effect/cli" import { FileSystem, Path } from "@effect/platform" import { Effect, Layer } from "effect" import logSymbols from "log-symbols" type InitOptions = { cwd: string template: string } class Init extends Effect.Service()("Init", { dependencies: [Template.Default], effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const doctor = yield* Doctor const template = yield* Template return { run: (options: InitOptions) => Effect.gen(function* () { yield* Effect.logDebug(`Init options: ${JSON.stringify(options, null, 2)}`) const packageJsonExists = yield* fs.exists(path.join(options.cwd, "package.json")) yield* Effect.logDebug(`Does package.json exist: ${packageJsonExists ? "yes" : "no"}`) if (packageJsonExists) { yield* Effect.logWarning(`${logSymbols.warning} A project already exists in this directory.`) const choice = yield* Prompt.select({ message: "How would you like to proceed?", choices: [ { title: "Initialize a new project here anyway", value: "init-new" }, { title: "Inspect project configuration", value: "doctor" }, { title: "Cancel and exit", value: "cancel" } ] }) yield* Effect.logDebug(`Init choice: ${choice}`) if (choice === "cancel") { return yield* Effect.succeed(true) } if (choice === "doctor") { console.log("") return yield* doctor.run({ ...options, summary: false, yes: false }) } } const projectName = yield* Prompt.text({ message: "What is the name of your project? (e.g. my-app)", default: "my-app" }) const templateFromFlag = PROJECT_MANIFEST.templates.find((t) => t.subPath === options.template) const selectedTemplate = templateFromFlag ? templateFromFlag : yield* Prompt.select({ message: "Select a template", choices: PROJECT_MANIFEST.templates.map((template) => ({ title: template.name, value: template })) }) yield* template.clone({ cwd: options.cwd, name: projectName, repo: selectedTemplate }) }) } }) }) {} function make(options: InitOptions) { const optionsLayer = Layer.succeed(CliOptions, { ...options, yes: true, stylingLibrary: undefined }) return Effect.gen(function* () { const init = yield* Init return yield* init.run(options) }).pipe( Effect.provide(Init.Default), Effect.provide(Doctor.Default), Effect.provide(ProjectConfig.Default), Effect.provide(optionsLayer) ) } export { make } ================================================ FILE: apps/cli/src/services/git.ts ================================================ import { Data, Effect } from "effect" import { Command } from "@effect/platform" import { Prompt } from "@effect/cli" import logSymbols from "log-symbols" export class GitError extends Data.TaggedError("GitError")<{ cause?: unknown message?: string }> {} const COMMANDS = { status: Command.make("git", "status", "--porcelain") } as const export class Git extends Effect.Service()("Git", { succeed: { promptIfDirty: () => Effect.gen(function* () { const gitStatus = yield* COMMANDS.status.pipe( Command.string, Effect.catchAll(() => Effect.succeed("")) // Not a git repository ) const isDirty = gitStatus.trim().length > 0 if (!isDirty) { return false } const result = yield* Prompt.confirm({ message: `${logSymbols.warning} The Git repository is dirty (uncommitted changes). Continue anyway?`, initial: true }) if (!result) { return yield* Effect.fail(new GitError({ message: "Aborted due to uncommitted changes." })) } return result }) } }) {} ================================================ FILE: apps/cli/src/services/package-manager.ts ================================================ import { Effect } from "effect" import { detect } from "package-manager-detector" const PACKAGE_MANAGERS = ["npm", "bun", "pnpm", "yarn@berry", "yarn"] as const const BINARY_RUNNERS = { npm: ["npx"], bun: ["bunx", "--bun"], pnpm: ["pnpm", "dlx"], yarn: ["npx"], "yarn@berry": ["npx"] } as const const detectPackageManager = (cwd: string) => Effect.tryPromise({ try: () => { return detect({ cwd, strategies: ["install-metadata", "lockfile", "packageManager-field", "devEngines-field"] }) }, catch: () => new Error("Failed to get package manager") }) const getPackageManager = (cwd: string) => Effect.gen(function* () { const pm = yield* detectPackageManager(cwd) if (!pm) { return "npm" } const name = PACKAGE_MANAGERS.find((name) => pm.agent.startsWith(name) || pm.name.startsWith(name)) ?? "npm" return name }) const getBinaryRunner = (cwd: string) => Effect.gen(function* () { const pm = yield* getPackageManager(cwd) return BINARY_RUNNERS[pm] }) class PackageManager extends Effect.Service()("PackageManager", { succeed: { detectPackageManager, getPackageManager, getBinaryRunner } }) {} export { PackageManager } ================================================ FILE: apps/cli/src/services/project-config.ts ================================================ import { CliOptions } from "@cli/contexts/cli-options.js" import { retryWith } from "@cli/utils/retry-with.js" import { Prompt } from "@effect/cli" import { FileSystem, Path } from "@effect/platform" import { Effect, Schema } from "effect" import { Git } from "./git.js" import { type ConfigLoaderSuccessResult, createMatchPath, loadConfig as loadTypescriptConfig } from "tsconfig-paths" import { PROJECT_MANIFEST } from "@cli/project-manifest.js" const componentJsonSchema = Schema.Struct({ $schema: Schema.optional(Schema.String), style: Schema.String, rsc: Schema.Boolean, tsx: Schema.Boolean, tailwind: Schema.Struct({ config: Schema.optional(Schema.String), css: Schema.String, baseColor: Schema.String, cssVariables: Schema.Boolean, prefix: Schema.optional(Schema.String) }), aliases: Schema.Struct({ components: Schema.String, utils: Schema.String, ui: Schema.optional(Schema.String), lib: Schema.optional(Schema.String), hooks: Schema.optional(Schema.String) }), iconLibrary: Schema.optional(Schema.String) }) const supportedExtensions = [".ts", ".tsx", ".jsx", ".js", ".css"] class ProjectConfig extends Effect.Service()("ProjectConfig", { dependencies: [Git.Default], effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const options = yield* CliOptions const git = yield* Git let componentJsonConfig: typeof componentJsonSchema.Type | null = null let tsConfig: ConfigLoaderSuccessResult | null = null const getComponentJson = () => Effect.gen(function* () { if (componentJsonConfig) { return componentJsonConfig } const componentJsonExists = yield* fs.exists(path.join(options.cwd, "components.json")) if (!componentJsonExists) { return yield* handleInvalidComponentJson(false) } const config = yield* fs.readFileString(path.join(options.cwd, "components.json")).pipe( Effect.flatMap(Schema.decodeUnknown(Schema.parseJson())), Effect.flatMap(Schema.decodeUnknown(componentJsonSchema)), Effect.catchTags({ ParseError: () => handleInvalidComponentJson(true) }) ) componentJsonConfig = config yield* Effect.logDebug(`componentJsonConfig: ${JSON.stringify(componentJsonConfig, null, 2)}`) return config }) const getUniwindDtsPath = () => Effect.gen(function* () { const metroConfigPaths = ["metro.config.js", "metro.config.ts"].map((p) => path.join(options.cwd, p)) as [ string, ...Array ] const metroContent = yield* retryWith((filePath: string) => fs.readFileString(filePath), metroConfigPaths).pipe( Effect.catchAll(() => Effect.succeed(null)) ) if (!metroContent?.includes("withUniwindConfig")) { return null } const dtsFileMatch = metroContent.match(/dtsFile\s*:\s*["']([^"']+)["']/) if (dtsFileMatch?.[1]) { const dtsPath = dtsFileMatch[1].replace(/^\.\//, "") return path.join(options.cwd, dtsPath) } return path.join(options.cwd, PROJECT_MANIFEST.uniwindTypesFile) }) const getStylingLibrary = () => Effect.gen(function* () { if (options.stylingLibrary) { return options.stylingLibrary } const metroConfigPaths = ["metro.config.js", "metro.config.ts"].map((p) => path.join(options.cwd, p)) as [ string, ...Array ] const metroContent = yield* retryWith((filePath: string) => fs.readFileString(filePath), metroConfigPaths).pipe( Effect.catchAll(() => Effect.succeed(null)) ) if (metroContent?.includes("withUniwindConfig")) { return "uniwind" as const } // v4 uses withNativeWind // v5 uses withNativewind if (metroContent?.toLowerCase().includes("withnativewind")) { return "nativewind" as const } return "unknown" as const }) const handleInvalidComponentJson = (exists: boolean) => Effect.gen(function* () { yield* Effect.logWarning( `${exists ? "Invalid components.json" : "Missing components.json"}${" (required to continue)"}` ) const agreeToWrite = yield* Prompt.confirm({ message: `Would you like to ${exists ? "update the" : "write a"} components.json file?`, label: { confirm: "y", deny: "n" }, initial: true, placeholder: { defaultConfirm: "y/n" } }) if (!agreeToWrite) { return yield* Effect.fail(new Error("Unable to continue without a valid components.json file.")) } const baseColor = options.yes ? "neutral" : yield* Prompt.select({ message: "Which color would you like to use as the base color?", choices: [ { title: "neutral", value: "neutral" }, { title: "stone", value: "stone" }, { title: "zinc", value: "zinc" }, { title: "gray", value: "gray" }, { title: "slate", value: "slate" } ] as const }) const hasRootGlobalCss = yield* fs.exists(path.join(options.cwd, "global.css")) const hasSrcGlobalCss = hasRootGlobalCss ? false : yield* fs.exists(path.join(options.cwd, "src/global.css")) const detectedCss = hasRootGlobalCss ? "global.css" : hasSrcGlobalCss ? "src/global.css" : "" const css = options.yes && detectedCss ? detectedCss : yield* Prompt.text({ message: "What is the name of the CSS file and path to it? (e.g. global.css or src/global.css)", default: detectedCss }) const stylingLibrary = yield* getStylingLibrary() const hasTailwindConfig = yield* fs.exists(path.join(options.cwd, "tailwind.config.js")) const tailwindConfig = stylingLibrary === "uniwind" ? "" : options.yes && hasTailwindConfig ? "tailwind.config.js" : yield* Prompt.text({ message: "What is the name of the Tailwind config file and path to it? (e.g. tailwind.config.js or src/tailwind.config.js)", default: "tailwind.config.js" }) const tsConfig = yield* getTsConfig() const aliasSymbol = `${(Object.keys(tsConfig.paths ?? {})[0] ?? "@/*").split("/*")[0]}` const detectedAliases = { components: `${aliasSymbol}/components`, utils: `${aliasSymbol}/lib/utils`, ui: `${aliasSymbol}/components/ui`, lib: `${aliasSymbol}/lib`, hooks: `${aliasSymbol}/hooks` } let aliases = detectedAliases if (!options.yes) { const useDetectedAliases = yield* Prompt.confirm({ message: `Use detected alias (${aliasSymbol}/*) in your setup?`, initial: true }) if (!useDetectedAliases) { const [componentsAlias, utilsAlias, uiAlias, libAlias, hooksAlias] = yield* Prompt.all([ Prompt.text({ message: "What is the name of the components alias?", default: detectedAliases.components }), Prompt.text({ message: "What is the name of the utils alias?", default: detectedAliases.utils }), Prompt.text({ message: "What is the name of the ui alias?", default: detectedAliases.ui }), Prompt.text({ message: "What is the name of the lib alias?", default: detectedAliases.lib }), Prompt.text({ message: "What is the name of the hooks alias?", default: detectedAliases.hooks }) ]) aliases = { components: componentsAlias, utils: utilsAlias, ui: uiAlias, lib: libAlias, hooks: hooksAlias } } } const newComponentJson = yield* Schema.encode(componentJsonSchema)({ $schema: "https://ui.shadcn.com/schema.json", style: "new-york", aliases, rsc: false, tsx: true, tailwind: { css, baseColor, cssVariables: true, config: tailwindConfig } }) yield* git.promptIfDirty() yield* fs.writeFileString(path.join(options.cwd, "components.json"), JSON.stringify(newComponentJson, null, 2)) yield* Effect.logDebug(`newComponentJson: ${JSON.stringify(newComponentJson, null, 2)}`) return newComponentJson }) const getTsConfig = () => Effect.try({ try: () => { if (tsConfig) { return tsConfig } const configResult = loadTypescriptConfig(options.cwd) if (configResult.resultType === "failed") { throw new Error("Error loading tsconfig.json", { cause: configResult.message }) } tsConfig = configResult return configResult }, catch: (error) => new Error("Error loading {ts,js}config.json", { cause: String(error) }) }) const resolvePathFromAlias = (aliasPath: string) => Effect.gen(function* () { const config = yield* getTsConfig() return yield* Effect.try({ try: () => { const matchPath = createMatchPath(config.absoluteBaseUrl, config.paths)( aliasPath, undefined, () => true, supportedExtensions ) if (!matchPath) { throw new Error("Path not found", { cause: aliasPath }) } return matchPath }, catch: (error) => new Error("Path not found", { cause: String(error) }) }) }) return { getComponentJson, getTsConfig, resolvePathFromAlias, getStylingLibrary, getUniwindDtsPath } }) }) {} export { ProjectConfig } ================================================ FILE: apps/cli/src/services/required-files-checker.ts ================================================ import { CliOptions } from "@cli/contexts/cli-options.js" import type { CustomFileCheck, FileCheck, FileWithContent, MissingInclude } from "@cli/project-manifest.js" import { PROJECT_MANIFEST } from "@cli/project-manifest.js" import { ProjectConfig } from "@cli/services/project-config.js" import { retryWith } from "@cli/utils/retry-with.js" import { FileSystem, Path } from "@effect/platform" import { Data, Effect } from "effect" import logSymbols from "log-symbols" class RequiredFileError extends Data.TaggedError("RequiredFileError")<{ file: string message?: string }> { } class RequiredFilesChecker extends Effect.Service()("RequiredFilesChecker", { effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path const options = yield* CliOptions const projectConfig = yield* ProjectConfig const checkFiles = (fileChecks: Array, stylingLibrary: "nativewind" | "uniwind") => Effect.gen(function* () { const missingFiles: Array = [] const missingIncludes: Array = [] const filesWithContent = yield* Effect.forEach( fileChecks.filter((file) => file.stylingLibraries.includes(stylingLibrary)), (file) => retryWith( (filePath: string) => Effect.gen(function* () { const fileContents = yield* fs.readFileString(filePath) yield* Effect.logDebug(`${logSymbols.success} ${file.name} found`) return { ...file, content: fileContents } as FileWithContent }), file.fileNames.map((p) => path.join(options.cwd, p)) as [string, ...Array] ).pipe( Effect.catchAll(() => { missingFiles.push(file) return Effect.logDebug(`${logSymbols.error} ${file.name} not found`).pipe(() => Effect.succeed(null)) }) ), { concurrency: "unbounded" } ).pipe(Effect.map((files) => files.filter((file): file is FileWithContent => file !== null))) yield* Effect.forEach(filesWithContent, (file) => Effect.gen(function* () { const { content, includes, name } = file for (const include of includes) { if (include.content.every((str) => content.includes(str))) { yield* Effect.logDebug(`${logSymbols.success} ${name} has ${include.content.join(", ")}`) continue } yield* Effect.logDebug(`${logSymbols.error} ${name} missing ${include.content.join(", ")}`) missingIncludes.push({ ...include, fileName: name }) } }) ) return { missingFiles, missingIncludes } }) const checkDeprecatedFiles = ( deprecatedFromLib: Array>, deprecatedFromUi: Array> ) => Effect.gen(function* () { const componentJson = yield* projectConfig.getComponentJson() const aliasForLib = componentJson.aliases.lib ?? `${componentJson.aliases.utils}/lib` const existingDeprecatedFromLibs = yield* Effect.forEach( deprecatedFromLib, (file) => projectConfig.resolvePathFromAlias(`${aliasForLib}/${file.fileNames[0]}`).pipe( Effect.flatMap((fullPath) => Effect.gen(function* () { const exists = yield* fs.exists(fullPath) if (!exists) { yield* Effect.logDebug( `${logSymbols.success} Deprecated ${aliasForLib}/${file.fileNames[0]} not found` ) return { ...file, hasIncludes: false } } yield* Effect.logDebug(`${logSymbols.error} Deprecated ${aliasForLib}/${file.fileNames[0]} found`) const fileContent = yield* fs.readFileString(fullPath) return { ...file, hasIncludes: file.includes.some((include) => include.content.some((content) => fileContent.includes(content)) ) } }) ) ), { concurrency: "unbounded" } ).pipe( Effect.map((results) => results.filter((result) => result.hasIncludes).map(({ hasIncludes: _hasIncludes, ...result }) => result) ) ) const aliasForUi = componentJson.aliases.ui ?? `${componentJson.aliases.components}/ui` const existingDeprecatedFromUi = yield* Effect.forEach( deprecatedFromUi, (file) => projectConfig.resolvePathFromAlias(`${aliasForUi}/${file.fileNames[0]}`).pipe( Effect.flatMap((fullPath) => Effect.gen(function* () { const exists = yield* fs.exists(fullPath) if (!exists) { yield* Effect.logDebug( `${logSymbols.success} Deprecated ${aliasForUi}/${file.fileNames[0]} not found` ) return { ...file, hasIncludes: false } } yield* Effect.logDebug(`${logSymbols.error} Deprecated ${aliasForUi}/${file.fileNames[0]} found`) const fileContent = yield* fs.readFileString(fullPath) return { ...file, hasIncludes: file.includes.some((include) => include.content.some((content) => fileContent.includes(content)) ) } }) ) ), { concurrency: "unbounded" } ).pipe( Effect.map((results) => results.filter((result) => result.hasIncludes).map(({ hasIncludes: _hasIncludes, ...result }) => result) ) ) return [...existingDeprecatedFromLibs, ...existingDeprecatedFromUi] }) const checkCustomFiles = ( customFileChecks: Record, stylingLibrary: "nativewind" | "uniwind" ) => Effect.gen(function* () { const componentJson = yield* projectConfig.getComponentJson() const aliasForLib = componentJson.aliases.lib ?? `${componentJson.aliases.utils}/lib` const missingFiles: Array = [] const missingIncludes: Array = [] // Check CSS files const cssPaths = [componentJson.tailwind.css, "global.css", "src/global.css"].filter((p) => p != null) const cssContent = yield* retryWith( (filePath: string) => Effect.gen(function* () { const content = yield* fs.readFileString(filePath) yield* Effect.logDebug(`${logSymbols.success} ${customFileChecks.css.name} found`) return content }), cssPaths.map((p) => path.join(options.cwd, p)) as [string, ...Array] ).pipe( Effect.catchAll(() => Effect.fail( new RequiredFileError({ file: "CSS", message: "CSS file not found. Please follow the instructions at https://www.nativewind.dev/docs/getting-started/installation#installation-with-expo" }) ) ) ) const cssShouldInclude = stylingLibrary === "uniwind" ? customFileChecks.uniwindCss.includes : customFileChecks.css.includes for (const include of cssShouldInclude) { if (include.content.every((str) => cssContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.css.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.css.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.css.name }) } // Check Nativewind env or Uniwind types file if (componentJson.tsx !== false) { const missingTypeFiles: Array = [] if (stylingLibrary === "nativewind") { const nativewindEnvContent = yield* fs .readFileString(path.join(options.cwd, PROJECT_MANIFEST.nativewindEnvFile)) .pipe( Effect.catchAll(() => { missingTypeFiles.push(customFileChecks.nativewindEnv) return Effect.succeed(null) }) ) if (nativewindEnvContent) { for (const include of customFileChecks.nativewindEnv.includes) { if (include.content.every((str) => nativewindEnvContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.nativewindEnv.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.nativewindEnv.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.nativewindEnv.name }) } } } if (stylingLibrary === "uniwind") { // Get uniwind dts path from metro config (supports custom dtsFile) const uniwindDtsPath = yield* projectConfig.getUniwindDtsPath() const uniwindTypesContent: string | null = uniwindDtsPath ? yield* fs.readFileString(uniwindDtsPath).pipe( Effect.catchAll(() => { missingTypeFiles.push(customFileChecks.uniwindTypes) return Effect.succeed(null) }) ) : null if (uniwindTypesContent) { for (const include of customFileChecks.uniwindTypes.includes) { if (include.content.every((str) => uniwindTypesContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.uniwindTypes.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.uniwindTypes.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.uniwindTypes.name }) } } } if (missingTypeFiles.length === 2) { missingFiles.push(missingTypeFiles[0], missingTypeFiles[1]) } } if (stylingLibrary === "nativewind") { // Check Tailwind config const tailwindConfigPaths = [ componentJson.tailwind.config, "tailwind.config.js", "tailwind.config.ts" ].filter((p) => p != null) const tailwindConfigContent = yield* retryWith( (filePath: string) => Effect.gen(function* () { const content = yield* fs.readFileString(filePath) yield* Effect.logDebug(`${logSymbols.success} ${customFileChecks.tailwindConfig.name} found`) return content }), tailwindConfigPaths.map((p) => path.join(options.cwd, p)) as [string, ...Array] ).pipe( Effect.catchAll(() => { console.warn( `${logSymbols.warning} Tailwind config not found, Please follow the instructions at https://www.nativewind.dev/docs/getting-started/installation#installation-with-expo`); return Effect.succeed("") }) ) for (const include of customFileChecks.tailwindConfig.includes) { if (include.content.every((str) => tailwindConfigContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.tailwindConfig.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.tailwindConfig.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.tailwindConfig.name }) } } // Check theme file const themeAliasPath = yield* projectConfig.resolvePathFromAlias(`${aliasForLib}/theme.ts`) const themeContent = yield* fs.readFileString(themeAliasPath).pipe( Effect.catchAll(() => { missingFiles.push(customFileChecks.theme) return Effect.succeed(null) }) ) if (themeContent) { for (const include of customFileChecks.theme.includes) { if (include.content.every((str) => themeContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.theme.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.theme.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.theme.name }) } } else { yield* Effect.logDebug(`${logSymbols.error} ${customFileChecks.theme.name} not found`) } // Check utils file const utilsPath = yield* projectConfig.resolvePathFromAlias(`${aliasForLib}/utils.ts`) const utilsContent = yield* fs.readFileString(utilsPath).pipe( Effect.catchAll(() => { missingFiles.push(customFileChecks.utils) return Effect.succeed(null) }) ) if (utilsContent) { for (const include of customFileChecks.utils.includes) { if (include.content.every((str) => utilsContent.includes(str))) { yield* Effect.logDebug( `${logSymbols.success} ${customFileChecks.utils.name} has ${include.content.join(", ")}` ) continue } yield* Effect.logDebug( `${logSymbols.error} ${customFileChecks.utils.name} missing ${include.content.join(", ")}` ) missingIncludes.push({ ...include, fileName: customFileChecks.utils.name }) } } else { yield* Effect.logDebug(`${logSymbols.error} ${customFileChecks.utils.name} not found`) } return { missingFiles, missingIncludes } }) return { run: ({ customFileChecks, deprecatedFromLib, deprecatedFromUi, fileChecks, stylingLibrary }: { fileChecks: Array customFileChecks: Record deprecatedFromLib: Array> deprecatedFromUi: Array> stylingLibrary: "nativewind" | "uniwind" }) => Effect.gen(function* () { const [fileResults, customFileResults, deprecatedFileResults] = yield* Effect.all([ checkFiles(fileChecks, stylingLibrary), checkCustomFiles(customFileChecks, stylingLibrary), checkDeprecatedFiles(deprecatedFromLib, deprecatedFromUi) ]) return { fileResults, customFileResults, deprecatedFileResults } }) } }) }) { } export { RequiredFilesChecker } ================================================ FILE: apps/cli/src/services/spinner.ts ================================================ import { Effect } from "effect" import ora from "ora" class Spinner extends Effect.Service()("Spinner", { effect: Effect.gen(function* () { const spinner = yield* Effect.try({ try: () => ora(), catch: () => new Error("Failed to create spinner") }) return spinner }) }) {} export { Spinner } ================================================ FILE: apps/cli/src/services/template.ts ================================================ import { runCommand } from "@cli/utils/run-command.js" import { Prompt } from "@effect/cli" import { FileSystem, Path } from "@effect/platform" import { Effect } from "effect" import { Spinner } from "@cli/services/spinner.js" import logSymbols from "log-symbols" class Template extends Effect.Service