├── .editorconfig ├── .eslintignore ├── .github ├── actions │ └── yarn-install │ │ └── action.yml └── workflows │ ├── cd-chromatic.yml │ ├── cd-eas-update-preview.yml │ ├── cd-eas-update-staging.yml │ ├── ci-build-test.yml │ ├── ci-monorepo-integrity.yml │ └── ci-semantic-release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .watchmanconfig ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-constraints.cjs │ │ ├── plugin-version.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.2.3.cjs ├── .yarnrc.yml ├── README.md ├── apps ├── expo │ ├── .env.development │ ├── .env.preview │ ├── .env.production │ ├── .env.staging │ ├── .eslintrc.js │ ├── .gitignore │ ├── .versionrc.js │ ├── .watchmanconfig │ ├── CHANGELOG.md │ ├── app.config.ts │ ├── app.json │ ├── app │ │ ├── _layout.tsx │ │ ├── index.tsx │ │ └── user │ │ │ └── [id].tsx │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon-dev.png │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ ├── eas.json │ ├── env.d.ts │ ├── index.js │ ├── metro.config.js │ ├── package.json │ ├── release.config.js │ ├── tamagui.config.ts │ └── tsconfig.json ├── next │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── index.tsx │ │ └── user │ │ │ └── [id].tsx │ ├── tamagui.config.ts │ └── tsconfig.json ├── storybook │ ├── .eslintrc.js │ ├── .gitignore │ ├── .storybook │ │ ├── main.ts │ │ ├── preview-head.html │ │ └── preview.tsx │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── woff │ │ │ │ ├── SpaceGrotesk-Bold-subset.zopfli.woff │ │ │ │ └── SpaceGrotesk-Regular-subset.zopfli.woff │ │ │ └── woff2 │ │ │ │ ├── SpaceGrotesk-Bold-subset.woff2 │ │ │ │ ├── SpaceGrotesk-Bold.woff2 │ │ │ │ ├── SpaceGrotesk-Regular-subset.woff2 │ │ │ │ └── SpaceGrotesk-Regular.woff2 │ │ └── vercel.svg │ ├── stories │ │ └── .gitkeep │ ├── tamagui.config.ts │ └── tsconfig.json └── tests │ ├── .gitignore │ ├── __tests__ │ └── ui │ │ └── MyButton.test.tsx │ ├── babel.config.js │ ├── jest.config.js │ ├── jest.setup-after-env.js │ ├── jest.setup.js │ ├── package.json │ ├── src │ ├── index.ts │ └── test-utils.tsx │ └── tamagui.config.ts ├── commitlint.config.js ├── constraints.pro ├── docs ├── CI-CD-Workflow.jam ├── apps-screenshot.jpg ├── ci-cd-flow.png └── dep-graph.png ├── my-turborepo.code-workspace ├── package.json ├── packages ├── app │ ├── .eslintrc.js │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── features │ │ │ ├── home │ │ │ │ └── HomeScreen.tsx │ │ │ └── user │ │ │ │ └── detail-screen.tsx │ │ ├── i18n │ │ │ ├── i18n.ts │ │ │ └── locales │ │ │ │ ├── de.json │ │ │ │ └── en.json │ │ ├── index.ts │ │ ├── provider │ │ │ ├── index.tsx │ │ │ └── tamagui-provider.tsx │ │ ├── react-i18n.d.ts │ │ ├── rnw-overrides.tsx │ │ ├── state │ │ │ ├── appState.ts │ │ │ └── themeState.ts │ │ └── tamagui.config.ts │ └── tsconfig.json ├── eslint-config-custom │ ├── index.js │ └── package.json ├── tsconfig │ ├── README.md │ ├── app.json │ ├── base.json │ ├── expo.json │ ├── nextjs.json │ ├── package.json │ ├── storybook.json │ ├── tests.json │ └── ui.json └── ui │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierignore │ ├── package.json │ ├── src │ ├── animations.ts │ ├── components │ │ └── my-button │ │ │ ├── MyButton.stories.tsx │ │ │ └── MyButton.tsx │ ├── global.ts │ ├── index.ts │ ├── screens │ │ └── home-screen │ │ │ ├── HomeScreen.stories.tsx │ │ │ └── HomeScreen.tsx │ └── tamagui.config.ts │ └── tsconfig.json ├── turbo.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/build/** 3 | **/node_modules/** 4 | **/types/** 5 | **/*.test.{js,jsx,ts,tsx} 6 | **/*.spec.{js,jsx,ts,tsx} 7 | **/config-overrides.js 8 | **/.eslintrc.js 9 | -------------------------------------------------------------------------------- /.github/actions/yarn-install/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Monorepo install (yarn)' 2 | description: 'Run yarn install with node_modules linked and cache enabled' 3 | inputs: 4 | yarn-download-cache-key: 5 | description: 'Yarn rotates downloaded cache archives. In case of issue you can update the key.' 6 | required: false 7 | default: 'v1' 8 | 9 | runs: 10 | using: 'composite' 11 | 12 | steps: 13 | - name: Get yarn cache directory path 14 | id: yarn-cache-dir-path 15 | shell: bash 16 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 17 | 18 | # Yarn rotates the downloaded cache archives. 19 | # Keep one global cache-entry through action/cache 20 | - name: Restore yarn cache 21 | uses: actions/cache@v3 22 | id: yarn-download-cache 23 | with: 24 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 25 | key: yarn-download-cache-${{ inputs.yarn-download-cache-key }} 26 | 27 | # Invalidated on yarn.lock changes 28 | - name: Restore yarn install state 29 | id: yarn-install-state-cache 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | .ci-cache/yarn-state 34 | key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} 35 | 36 | - name: Install dependencies 37 | shell: bash 38 | run: | 39 | yarn install --immutable --inline-builds 40 | env: 41 | # CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action. 42 | YARN_ENABLE_GLOBAL_CACHE: 'false' # Use local cache folder to keep downloaded archives 43 | YARN_NM_MODE: 'hardlinks-local' # Hardlinks-local reduces io / node_modules size 44 | YARN_INSTALL_STATE_PATH: .ci-cache/yarn-state/install-state.gz # Small speedup when lock does not change 45 | HUSKY: '0' # By default do not run HUSKY install 46 | -------------------------------------------------------------------------------- /.github/workflows/cd-chromatic.yml: -------------------------------------------------------------------------------- 1 | name: Storybook 2 | 3 | on: 4 | workflow_call: 5 | 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'packages/ui/**' 11 | 12 | pull_request: 13 | types: 14 | - opened 15 | - synchronize 16 | - reopened 17 | paths: 18 | - 'packages/ui/**' 19 | 20 | jobs: 21 | publish-storybook: 22 | name: Publish 23 | runs-on: ubuntu-latest 24 | env: 25 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 26 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 27 | defaults: 28 | run: 29 | working-directory: ./ 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 2 36 | 37 | - name: Set Node.js 18.14.0 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 18.14.0 41 | 42 | - name: 📥 Monorepo install 43 | uses: ./.github/actions/yarn-install 44 | 45 | - name: Publish to Chromatic 46 | uses: chromaui/action@v1 47 | with: 48 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 49 | workingDir: apps/storybook 50 | -------------------------------------------------------------------------------- /.github/workflows/cd-eas-update-preview.yml: -------------------------------------------------------------------------------- 1 | name: CD-EAS-Update-Preview 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'expo-app-*' 7 | 8 | jobs: 9 | eas-update-preview: 10 | name: EAS Update Preview 11 | runs-on: ubuntu-latest 12 | environment: preview 13 | env: 14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 15 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 16 | APP_VARIANT: ${{ vars.APP_VARIANT }} 17 | defaults: 18 | run: 19 | working-directory: ./ 20 | 21 | steps: 22 | - name: Get tag name 23 | id: get_tag_name 24 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 25 | 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | with: 29 | fetch-depth: 2 30 | ref: ${{ steps.get_tag_name.outputs.VERSION }} 31 | 32 | - name: Set Node.js 18.14.0 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: 18.14.0 36 | 37 | - name: Setup Expo 38 | uses: expo/expo-github-action@v7 39 | with: 40 | expo-version: latest 41 | eas-version: latest 42 | token: ${{ secrets.EXPO_TOKEN }} 43 | 44 | - name: 📥 Monorepo install 45 | uses: ./.github/actions/yarn-install 46 | 47 | - name: Publish preview update 48 | run: cd apps/expo && npx dotenv -c preview -- eas update --branch preview --non-interactive --message "update preview to ${{ steps.get_tag_name.outputs.VERSION }}" 49 | 50 | - name: Slack Notification 51 | uses: rtCamp/action-slack-notify@v2 52 | env: 53 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 54 | SLACK_CHANNEL: REPLACE-ME 55 | SLACK_MESSAGE: "Updated PREVIEW app to ${{ steps.get_tag_name.outputs.VERSION }} :rocket:" 56 | -------------------------------------------------------------------------------- /.github/workflows/cd-eas-update-staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Staging 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | paths: 10 | - 'packages/ui/**' 11 | - 'packages/app/**' 12 | - 'apps/expo/**' 13 | 14 | jobs: 15 | ci-build-test: 16 | uses: ./.github/workflows/ci-build-test.yml 17 | 18 | eas-update-staging: 19 | name: EAS Update 20 | environment: staging 21 | runs-on: ubuntu-latest 22 | needs: [ci-build-test] 23 | env: 24 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 25 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 26 | APP_VARIANT: ${{ vars.APP_VARIANT }} 27 | defaults: 28 | run: 29 | working-directory: ./ 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 2 36 | 37 | - name: Set Node.js 18.14.0 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 18.14.0 41 | 42 | - name: Setup Expo 43 | uses: expo/expo-github-action@v7 44 | with: 45 | expo-version: latest 46 | eas-version: latest 47 | token: ${{ secrets.EXPO_TOKEN }} 48 | 49 | - name: 📥 Monorepo install 50 | uses: ./.github/actions/yarn-install 51 | 52 | - name: 🚀 Publish to Expo 53 | run: cd apps/expo && npx dotenv -c staging -- eas update --branch=staging --non-interactive --message "update staging channel to ${GITHUB_HEAD_REF}" 54 | 55 | - name: 💬 Comment in preview 56 | uses: expo/expo-github-action/preview-comment@v7 57 | with: 58 | channel: staging 59 | 60 | - name: Slack Notification 61 | uses: rtCamp/action-slack-notify@v2 62 | env: 63 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 64 | SLACK_CHANNEL: REPLACE-ME 65 | SLACK_MESSAGE: "Updated STAGING app to ${{env.GITHUB_HEAD_REF}} :rocket:" 66 | -------------------------------------------------------------------------------- /.github/workflows/ci-build-test.yml: -------------------------------------------------------------------------------- 1 | name: CI-Build-Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | # pull_request: 7 | # types: 8 | # - opened 9 | # - synchronize 10 | # - reopened 11 | # paths: 12 | # - 'apps/**' 13 | # - 'packages/**' 14 | # - 'package.json' 15 | # - '*.lock' 16 | # - '.yarnrc.yml' 17 | # - '.prettier*' 18 | # - '.github/**' 19 | 20 | jobs: 21 | build-and-test: 22 | name: Build and Test 23 | runs-on: ubuntu-latest 24 | env: 25 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 26 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 27 | defaults: 28 | run: 29 | working-directory: ./ 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 2 36 | 37 | - name: Set Node.js 18.14.0 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 18.14.0 41 | 42 | - name: 📥 Monorepo install 43 | uses: ./.github/actions/yarn-install 44 | 45 | - name: 🕵️ Typecheck 46 | run: yarn g:typecheck 47 | 48 | - name: 🔬 Lint 49 | run: yarn g:lint 50 | 51 | - name: 📐 Format check 52 | run: yarn g:format:check 53 | 54 | - name: 🏗 Build 55 | run: yarn g:build 56 | 57 | - name: 🧪 Unit tests 58 | run: yarn g:test 59 | -------------------------------------------------------------------------------- /.github/workflows/ci-monorepo-integrity.yml: -------------------------------------------------------------------------------- 1 | name: Monorepo 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'package.json' 9 | - '*.lock' 10 | - '.yarnrc.yml' 11 | - '.github/**' 12 | 13 | pull_request: 14 | types: 15 | - opened 16 | - synchronize 17 | - reopened 18 | paths: 19 | - 'package.json' 20 | - '*.lock' 21 | - '.yarnrc.yml' 22 | - '.github/**' 23 | 24 | jobs: 25 | monorepo-integrity: 26 | name: Check integrity 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 2 33 | 34 | - name: Set Node.js 18.14.0 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: 18.14.0 38 | 39 | - name: 📥 Monorepo install 40 | uses: ./.github/actions/yarn-install 41 | 42 | - name: 👬🏽 Check for duplicate dependencies in lock file 43 | run: yarn dedupe --check 44 | 45 | - name: 🦺 Check for yarn constraints.pro 46 | run: yarn constraints 47 | -------------------------------------------------------------------------------- /.github/workflows/ci-semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: CI-Semantic-Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | ci-build-test: 10 | name: Build and Test 11 | uses: ./.github/workflows/ci-build-test.yml 12 | 13 | semantic-release: 14 | name: Semantic Release 15 | needs: [ci-build-test] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 2 22 | token: ${{ secrets.EXTENDED_GITHUB_TOKEN }} 23 | persist-credentials: false 24 | 25 | - name: Set Node.js 18.14.0 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18.14.0 29 | 30 | - name: 📥 Monorepo install 31 | uses: ./.github/actions/yarn-install 32 | 33 | - name: Run semantic-release 34 | uses: borales/actions-yarn@v4 35 | with: 36 | cmd: semantic-release:expo-app 37 | env: 38 | GH_TOKEN: ${{ secrets.EXTENDED_GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .yarn/cache 36 | .yarn/versions 37 | .yarn/install-state.gz 38 | dist/out-tsc/* 39 | 40 | **/.env 41 | 42 | .idea 43 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged --allow-empty && yarn g:run-all-checks 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*+(.ts?|.tsx)": ["turbo staged:lint:fix staged:format --filter=// --"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # used in tandem with package.json engines section to only use yarn 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.14.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | 3 | apps/expo/.expo 4 | apps/expo/android 5 | apps/expo/ios 6 | 7 | apps/next/.next 8 | apps/next/.tamagui 9 | apps/next/.turbo 10 | apps/next/public 11 | 12 | apps/storybook-react/.storybook 13 | apps/storybook-react/.turbo 14 | apps/storybook-react/dist 15 | apps/storybook-react/public 16 | 17 | packages/ui/.turbo 18 | packages/ui/dist 19 | packages/ui/types 20 | 21 | dist 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "arrowParens": "always", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var wr=Object.create,me=Object.defineProperty,Sr=Object.defineProperties,vr=Object.getOwnPropertyDescriptor,Hr=Object.getOwnPropertyDescriptors,$r=Object.getOwnPropertyNames,et=Object.getOwnPropertySymbols,kr=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty,Tr=Object.prototype.propertyIsEnumerable;var rt=(e,t,r)=>t in e?me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,B=(e,t)=>{for(var r in t||(t={}))tt.call(t,r)&&rt(e,r,t[r]);if(et)for(var r of et(t))Tr.call(t,r)&&rt(e,r,t[r]);return e},Q=(e,t)=>Sr(e,Hr(t)),Lr=e=>me(e,"__esModule",{value:!0});var K=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Or=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of $r(t))!tt.call(e,n)&&n!=="default"&&me(e,n,{get:()=>t[n],enumerable:!(r=vr(t,n))||r.enumerable});return e},X=e=>Nr(Lr(me(e!=null?wr(kr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var $e=K(te=>{"use strict";te.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;te.find=(e,t)=>e.nodes.find(r=>r.type===t);te.exceedsLimit=(e,t,r=1,n)=>n===!1||!te.isInteger(e)||!te.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=n;te.escapeNode=(e,t=0,r)=>{let n=e.nodes[t];!n||(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};te.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;te.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0==0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;te.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;te.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);te.flatten=(...e)=>{let t=[],r=n=>{for(let s=0;s{"use strict";var it=$e();at.exports=(e,t={})=>{let r=(n,s={})=>{let a=t.escapeInvalid&&it.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o="";if(n.value)return(a||i)&&it.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let h of n.nodes)o+=r(h);return o};return r(e)}});var ct=K((os,ot)=>{"use strict";ot.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var At=K((cs,ut)=>{"use strict";var lt=ct(),pe=(e,t,r)=>{if(lt(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(lt(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n=B({relaxZeros:!0},r);typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),a=String(n.shorthand),i=String(n.capture),o=String(n.wrap),h=e+":"+t+"="+s+a+i+o;if(pe.cache.hasOwnProperty(h))return pe.cache[h].result;let g=Math.min(e,t),f=Math.max(e,t);if(Math.abs(g-f)===1){let R=e+"|"+t;return n.capture?`(${R})`:n.wrap===!1?R:`(?:${R})`}let A=ft(e)||ft(t),p={min:e,max:t,a:g,b:f},k=[],y=[];if(A&&(p.isPadded=A,p.maxLen=String(p.max).length),g<0){let R=f<0?Math.abs(f):1;y=pt(R,Math.abs(g),p,n),g=p.a=0}return f>=0&&(k=pt(g,f,p,n)),p.negatives=y,p.positives=k,p.result=Ir(y,k,n),n.capture===!0?p.result=`(${p.result})`:n.wrap!==!1&&k.length+y.length>1&&(p.result=`(?:${p.result})`),pe.cache[h]=p,p.result};function Ir(e,t,r){let n=Pe(e,t,"-",!1,r)||[],s=Pe(t,e,"",!1,r)||[],a=Pe(e,t,"-?",!0,r)||[];return n.concat(a).concat(s).join("|")}function Mr(e,t){let r=1,n=1,s=ht(e,r),a=new Set([t]);for(;e<=s&&s<=t;)a.add(s),r+=1,s=ht(e,r);for(s=dt(t+1,n)-1;e1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+gt(o.count),i=g+1;continue}r.isPadded&&(A=Gr(g,r,n)),f.string=A+f.pattern+gt(f.count),a.push(f),i=g+1,o=f}return a}function Pe(e,t,r,n,s){let a=[];for(let i of e){let{string:o}=i;!n&&!mt(t,"string",o)&&a.push(r+o),n&&mt(t,"string",o)&&a.push(r+o)}return a}function Pr(e,t){let r=[];for(let n=0;nt?1:t>e?-1:0}function mt(e,t,r){return e.some(n=>n[t]===r)}function ht(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function dt(e,t){return e-e%Math.pow(10,t)}function gt(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function Dr(e,t,r){return`[${e}${t-e==1?"":"-"}${t}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Gr(e,t,r){if(!t.isPadded)return e;let n=Math.abs(t.maxLen-String(e).length),s=r.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}pe.cache={};pe.clearCache=()=>pe.cache={};ut.exports=pe});var Ge=K((us,Rt)=>{"use strict";var qr=require("util"),yt=At(),bt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Kr=e=>t=>e===!0?Number(t):String(t),De=e=>typeof e=="number"||typeof e=="string"&&e!=="",Re=e=>Number.isInteger(+e),Ue=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},Wr=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,jr=(e,t,r)=>{if(t>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?t-1:t,"0")}return r===!1?String(e):e},_t=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((i,o)=>io?1:0),e.positives.sort((i,o)=>io?1:0);let r=t.capture?"":"?:",n="",s="",a;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${r}${e.negatives.join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,t.wrap?`(${r}${a})`:a},Et=(e,t,r,n)=>{if(r)return yt(e,t,B({wrap:!1},n));let s=String.fromCharCode(e);if(e===t)return s;let a=String.fromCharCode(t);return`[${s}-${a}]`},xt=(e,t,r)=>{if(Array.isArray(e)){let n=r.wrap===!0,s=r.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return yt(e,t,r)},Ct=(...e)=>new RangeError("Invalid range arguments: "+qr.inspect(...e)),wt=(e,t,r)=>{if(r.strictRanges===!0)throw Ct([e,t]);return[]},Qr=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Xr=(e,t,r=1,n={})=>{let s=Number(e),a=Number(t);if(!Number.isInteger(s)||!Number.isInteger(a)){if(n.strictRanges===!0)throw Ct([e,t]);return[]}s===0&&(s=0),a===0&&(a=0);let i=s>a,o=String(e),h=String(t),g=String(r);r=Math.max(Math.abs(r),1);let f=Ue(o)||Ue(h)||Ue(g),A=f?Math.max(o.length,h.length,g.length):0,p=f===!1&&Wr(e,t,n)===!1,k=n.transform||Kr(p);if(n.toRegex&&r===1)return Et(_t(e,A),_t(t,A),!0,n);let y={negatives:[],positives:[]},R=T=>y[T<0?"negatives":"positives"].push(Math.abs(T)),_=[],x=0;for(;i?s>=a:s<=a;)n.toRegex===!0&&r>1?R(s):_.push(jr(k(s,x),A,p)),s=i?s-r:s+r,x++;return n.toRegex===!0?r>1?Fr(y,n):xt(_,null,B({wrap:!1},n)):_},Zr=(e,t,r=1,n={})=>{if(!Re(e)&&e.length>1||!Re(t)&&t.length>1)return wt(e,t,n);let s=n.transform||(p=>String.fromCharCode(p)),a=`${e}`.charCodeAt(0),i=`${t}`.charCodeAt(0),o=a>i,h=Math.min(a,i),g=Math.max(a,i);if(n.toRegex&&r===1)return Et(h,g,!1,n);let f=[],A=0;for(;o?a>=i:a<=i;)f.push(s(a,A)),a=o?a-r:a+r,A++;return n.toRegex===!0?xt(f,null,{wrap:!1,options:n}):f},Te=(e,t,r,n={})=>{if(t==null&&De(e))return[e];if(!De(e)||!De(t))return wt(e,t,n);if(typeof r=="function")return Te(e,t,1,{transform:r});if(bt(r))return Te(e,t,0,r);let s=B({},n);return s.capture===!0&&(s.wrap=!0),r=r||s.step||1,Re(r)?Re(e)&&Re(t)?Xr(e,t,r,s):Zr(e,t,Math.max(Math.abs(r),1),s):r!=null&&!bt(r)?Qr(r,s):Te(e,t,1,r)};Rt.exports=Te});var Ht=K((ls,St)=>{"use strict";var Yr=Ge(),vt=$e(),zr=(e,t={})=>{let r=(n,s={})=>{let a=vt.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o=a===!0||i===!0,h=t.escapeInvalid===!0?"\\":"",g="";if(n.isOpen===!0||n.isClose===!0)return h+n.value;if(n.type==="open")return o?h+n.value:"(";if(n.type==="close")return o?h+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":o?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let f=vt.reduce(n.nodes),A=Yr(...f,Q(B({},t),{wrap:!1,toRegex:!0}));if(A.length!==0)return f.length>1&&A.length>1?`(${A})`:A}if(n.nodes)for(let f of n.nodes)g+=r(f,n);return g};return r(e)};St.exports=zr});var Tt=K((ps,$t)=>{"use strict";var Vr=Ge(),kt=ke(),he=$e(),fe=(e="",t="",r=!1)=>{let n=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?he.flatten(t).map(s=>`{${s}}`):t;for(let s of e)if(Array.isArray(s))for(let a of s)n.push(fe(a,t,r));else for(let a of t)r===!0&&typeof a=="string"&&(a=`{${a}}`),n.push(Array.isArray(a)?fe(s,a,r):s+a);return he.flatten(n)},Jr=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,n=(s,a={})=>{s.queue=[];let i=a,o=a.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,o=i.queue;if(s.invalid||s.dollar){o.push(fe(o.pop(),kt(s,t)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){o.push(fe(o.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let A=he.reduce(s.nodes);if(he.exceedsLimit(...A,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=Vr(...A,t);p.length===0&&(p=kt(s,t)),o.push(fe(o.pop(),p)),s.nodes=[];return}let h=he.encloseBrace(s),g=s.queue,f=s;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,g=f.queue;for(let A=0;A{"use strict";Lt.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Pt=K((hs,Nt)=>{"use strict";var en=ke(),{MAX_LENGTH:It,CHAR_BACKSLASH:qe,CHAR_BACKTICK:tn,CHAR_COMMA:rn,CHAR_DOT:nn,CHAR_LEFT_PARENTHESES:sn,CHAR_RIGHT_PARENTHESES:an,CHAR_LEFT_CURLY_BRACE:on,CHAR_RIGHT_CURLY_BRACE:cn,CHAR_LEFT_SQUARE_BRACKET:Bt,CHAR_RIGHT_SQUARE_BRACKET:Mt,CHAR_DOUBLE_QUOTE:un,CHAR_SINGLE_QUOTE:ln,CHAR_NO_BREAK_SPACE:pn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:fn}=Ot(),hn=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},n=typeof r.maxLength=="number"?Math.min(It,r.maxLength):It;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},a=[s],i=s,o=s,h=0,g=e.length,f=0,A=0,p,k={},y=()=>e[f++],R=_=>{if(_.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&_.type==="text"){o.value+=_.value;return}return i.nodes.push(_),_.parent=i,_.prev=o,o=_,_};for(R({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let _=i.nodes.shift();i.nodes=[_,{type:"text",value:en(i)}]}R({type:"comma",value:p}),i.commas++;continue}if(p===nn&&A>0&&i.commas===0){let _=i.nodes;if(A===0||_.length===0){R({type:"text",value:p});continue}if(o.type==="dot"){if(i.range=[],o.value+=p,o.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,o.type="text";continue}i.ranges++,i.args=[];continue}if(o.type==="range"){_.pop();let x=_[_.length-1];x.value+=o.value+p,o=x,i.ranges--;continue}R({type:"dot",value:p});continue}R({type:"text",value:p})}do if(i=a.pop(),i.type!=="root"){i.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let _=a[a.length-1],x=_.nodes.indexOf(i);_.nodes.splice(x,1,...i.nodes)}while(a.length>0);return R({type:"eos"}),s};Nt.exports=hn});var Gt=K((ds,Dt)=>{"use strict";var Ut=ke(),dn=Ht(),gn=Tt(),mn=Pt(),V=(e,t={})=>{let r=[];if(Array.isArray(e))for(let n of e){let s=V.create(n,t);Array.isArray(s)?r.push(...s):r.push(s)}else r=[].concat(V.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};V.parse=(e,t={})=>mn(e,t);V.stringify=(e,t={})=>typeof e=="string"?Ut(V.parse(e,t),t):Ut(e,t);V.compile=(e,t={})=>(typeof e=="string"&&(e=V.parse(e,t)),dn(e,t));V.expand=(e,t={})=>{typeof e=="string"&&(e=V.parse(e,t));let r=gn(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};V.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?V.compile(e,t):V.expand(e,t);Dt.exports=V});var ye=K((gs,qt)=>{"use strict";var An=require("path"),ie="\\\\/",Kt=`[^${ie}]`,ce="\\.",Rn="\\+",yn="\\?",Le="\\/",bn="(?=.)",Wt="[^/]",Ke=`(?:${Le}|$)`,jt=`(?:^|${Le})`,We=`${ce}{1,2}${Ke}`,_n=`(?!${ce})`,En=`(?!${jt}${We})`,xn=`(?!${ce}{0,1}${Ke})`,Cn=`(?!${We})`,wn=`[^.${Le}]`,Sn=`${Wt}*?`,Ft={DOT_LITERAL:ce,PLUS_LITERAL:Rn,QMARK_LITERAL:yn,SLASH_LITERAL:Le,ONE_CHAR:bn,QMARK:Wt,END_ANCHOR:Ke,DOTS_SLASH:We,NO_DOT:_n,NO_DOTS:En,NO_DOT_SLASH:xn,NO_DOTS_SLASH:Cn,QMARK_NO_DOT:wn,STAR:Sn,START_ANCHOR:jt},vn=Q(B({},Ft),{SLASH_LITERAL:`[${ie}]`,QMARK:Kt,STAR:`${Kt}*?`,DOTS_SLASH:`${ce}{1,2}(?:[${ie}]|$)`,NO_DOT:`(?!${ce})`,NO_DOTS:`(?!(?:^|[${ie}])${ce}{1,2}(?:[${ie}]|$))`,NO_DOT_SLASH:`(?!${ce}{0,1}(?:[${ie}]|$))`,NO_DOTS_SLASH:`(?!${ce}{1,2}(?:[${ie}]|$))`,QMARK_NO_DOT:`[^.${ie}]`,START_ANCHOR:`(?:^|[${ie}])`,END_ANCHOR:`(?:[${ie}]|$)`}),Hn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Hn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:An.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?vn:Ft}}});var be=K(Z=>{"use strict";var $n=require("path"),kn=process.platform==="win32",{REGEX_BACKSLASH:Tn,REGEX_REMOVE_BACKSLASH:Ln,REGEX_SPECIAL_CHARS:On,REGEX_SPECIAL_CHARS_GLOBAL:Nn}=ye();Z.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Z.hasRegexChars=e=>On.test(e);Z.isRegexChar=e=>e.length===1&&Z.hasRegexChars(e);Z.escapeRegex=e=>e.replace(Nn,"\\$1");Z.toPosixSlashes=e=>e.replace(Tn,"/");Z.removeBackslashes=e=>e.replace(Ln,t=>t==="\\"?"":t);Z.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Z.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:kn===!0||$n.sep==="\\";Z.escapeLast=(e,t,r)=>{let n=e.lastIndexOf(t,r);return n===-1?e:e[n-1]==="\\"?Z.escapeLast(e,t,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Z.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};Z.wrapOutput=(e,t={},r={})=>{let n=r.contains?"":"^",s=r.contains?"":"$",a=`${n}(?:${e})${s}`;return t.negated===!0&&(a=`(?:^(?!${a}).*$)`),a}});var er=K((As,Qt)=>{"use strict";var Xt=be(),{CHAR_ASTERISK:je,CHAR_AT:In,CHAR_BACKWARD_SLASH:_e,CHAR_COMMA:Bn,CHAR_DOT:Fe,CHAR_EXCLAMATION_MARK:Qe,CHAR_FORWARD_SLASH:Zt,CHAR_LEFT_CURLY_BRACE:Xe,CHAR_LEFT_PARENTHESES:Ze,CHAR_LEFT_SQUARE_BRACKET:Mn,CHAR_PLUS:Pn,CHAR_QUESTION_MARK:Yt,CHAR_RIGHT_CURLY_BRACE:Dn,CHAR_RIGHT_PARENTHESES:zt,CHAR_RIGHT_SQUARE_BRACKET:Un}=ye(),Vt=e=>e===Zt||e===_e,Jt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Gn=(e,t)=>{let r=t||{},n=e.length-1,s=r.parts===!0||r.scanToEnd===!0,a=[],i=[],o=[],h=e,g=-1,f=0,A=0,p=!1,k=!1,y=!1,R=!1,_=!1,x=!1,T=!1,O=!1,W=!1,G=!1,ne=0,E,b,C={value:"",depth:0,isGlob:!1},M=()=>g>=n,l=()=>h.charCodeAt(g+1),H=()=>(E=b,h.charCodeAt(++g));for(;g0&&(j=h.slice(0,f),h=h.slice(f),A-=f),w&&y===!0&&A>0?(w=h.slice(0,A),c=h.slice(A)):y===!0?(w="",c=h):w=h,w&&w!==""&&w!=="/"&&w!==h&&Vt(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),r.unescape===!0&&(c&&(c=Xt.removeBackslashes(c)),w&&T===!0&&(w=Xt.removeBackslashes(w)));let u={prefix:j,input:e,start:f,base:w,glob:c,isBrace:p,isBracket:k,isGlob:y,isExtglob:R,isGlobstar:_,negated:O,negatedExtglob:W};if(r.tokens===!0&&(u.maxDepth=0,Vt(b)||i.push(C),u.tokens=i),r.parts===!0||r.tokens===!0){let I;for(let $=0;${"use strict";var Oe=ye(),J=be(),{MAX_LENGTH:Ne,POSIX_REGEX_SOURCE:qn,REGEX_NON_SPECIAL_CHARS:Kn,REGEX_SPECIAL_CHARS_BACKREF:Wn,REPLACEMENTS:rr}=Oe,jn=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch(n){return e.map(s=>J.escapeRegex(s)).join("..")}return r},de=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,nr=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=rr[e]||e;let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let a={type:"bos",value:"",output:r.prepend||""},i=[a],o=r.capture?"":"?:",h=J.isWindows(t),g=Oe.globChars(h),f=Oe.extglobChars(g),{DOT_LITERAL:A,PLUS_LITERAL:p,SLASH_LITERAL:k,ONE_CHAR:y,DOTS_SLASH:R,NO_DOT:_,NO_DOT_SLASH:x,NO_DOTS_SLASH:T,QMARK:O,QMARK_NO_DOT:W,STAR:G,START_ANCHOR:ne}=g,E=m=>`(${o}(?:(?!${ne}${m.dot?R:A}).)*?)`,b=r.dot?"":_,C=r.dot?O:W,M=r.bash===!0?E(r):G;r.capture&&(M=`(${M})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let l={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};e=J.removePrefix(e,l),s=e.length;let H=[],w=[],j=[],c=a,u,I=()=>l.index===s-1,$=l.peek=(m=1)=>e[l.index+m],ee=l.advance=()=>e[++l.index]||"",se=()=>e.slice(l.index+1),z=(m="",L=0)=>{l.consumed+=m,l.index+=L},Ce=m=>{l.output+=m.output!=null?m.output:m.value,z(m.value)},xr=()=>{let m=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)ee(),l.start++,m++;return m%2==0?!1:(l.negated=!0,l.start++,!0)},we=m=>{l[m]++,j.push(m)},ue=m=>{l[m]--,j.pop()},v=m=>{if(c.type==="globstar"){let L=l.braces>0&&(m.type==="comma"||m.type==="brace"),d=m.extglob===!0||H.length&&(m.type==="pipe"||m.type==="paren");m.type!=="slash"&&m.type!=="paren"&&!L&&!d&&(l.output=l.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=M,l.output+=c.output)}if(H.length&&m.type!=="paren"&&(H[H.length-1].inner+=m.value),(m.value||m.output)&&Ce(m),c&&c.type==="text"&&m.type==="text"){c.value+=m.value,c.output=(c.output||"")+m.value;return}m.prev=c,i.push(m),c=m},Se=(m,L)=>{let d=Q(B({},f[L]),{conditions:1,inner:""});d.prev=c,d.parens=l.parens,d.output=l.output;let S=(r.capture?"(":"")+d.open;we("parens"),v({type:m,value:L,output:l.output?"":y}),v({type:"paren",extglob:!0,value:ee(),output:S}),H.push(d)},Cr=m=>{let L=m.close+(r.capture?")":""),d;if(m.type==="negate"){let S=M;m.inner&&m.inner.length>1&&m.inner.includes("/")&&(S=E(r)),(S!==M||I()||/^\)+$/.test(se()))&&(L=m.close=`)$))${S}`),m.inner.includes("*")&&(d=se())&&/^\.[^\\/.]+$/.test(d)&&(L=m.close=`)${d})${S})`),m.prev.type==="bos"&&(l.negatedExtglob=!0)}v({type:"paren",extglob:!0,value:u,output:L}),ue("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let m=!1,L=e.replace(Wn,(d,S,P,F,q,Me)=>F==="\\"?(m=!0,d):F==="?"?S?S+F+(q?O.repeat(q.length):""):Me===0?C+(q?O.repeat(q.length):""):O.repeat(P.length):F==="."?A.repeat(P.length):F==="*"?S?S+F+(q?M:""):M:S?d:`\\${d}`);return m===!0&&(r.unescape===!0?L=L.replace(/\\/g,""):L=L.replace(/\\+/g,d=>d.length%2==0?"\\\\":d?"\\":"")),L===e&&r.contains===!0?(l.output=e,l):(l.output=J.wrapOutput(L,l,t),l)}for(;!I();){if(u=ee(),u==="\0")continue;if(u==="\\"){let d=$();if(d==="/"&&r.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",v({type:"text",value:u});continue}let S=/^\\+/.exec(se()),P=0;if(S&&S[0].length>2&&(P=S[0].length,l.index+=P,P%2!=0&&(u+="\\")),r.unescape===!0?u=ee():u+=ee(),l.brackets===0){v({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(r.posix!==!1&&u===":"){let d=c.value.slice(1);if(d.includes("[")&&(c.posix=!0,d.includes(":"))){let S=c.value.lastIndexOf("["),P=c.value.slice(0,S),F=c.value.slice(S+2),q=qn[F];if(q){c.value=P+q,l.backtrack=!0,ee(),!a.output&&i.indexOf(c)===1&&(a.output=y);continue}}}(u==="["&&$()!==":"||u==="-"&&$()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),r.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Ce({value:u});continue}if(l.quotes===1&&u!=='"'){u=J.escapeRegex(u),c.value+=u,Ce({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,r.keepQuotes===!0&&v({type:"text",value:u});continue}if(u==="("){we("parens"),v({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&r.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Cr(H.pop());continue}v({type:"paren",value:u,output:l.parens?")":"\\)"}),ue("parens");continue}if(u==="["){if(r.nobracket===!0||!se().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else we("brackets");v({type:"bracket",value:u});continue}if(u==="]"){if(r.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){v({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(de("opening","["));v({type:"text",value:u,output:`\\${u}`});continue}ue("brackets");let d=c.value.slice(1);if(c.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),c.value+=u,Ce({value:u}),r.literalBrackets===!1||J.hasRegexChars(d))continue;let S=J.escapeRegex(c.value);if(l.output=l.output.slice(0,-c.value.length),r.literalBrackets===!0){l.output+=S,c.value=S;continue}c.value=`(${o}${S}|${c.value})`,l.output+=c.value;continue}if(u==="{"&&r.nobrace!==!0){we("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),v(d);continue}if(u==="}"){let d=w[w.length-1];if(r.nobrace===!0||!d){v({type:"text",value:u,output:u});continue}let S=")";if(d.dots===!0){let P=i.slice(),F=[];for(let q=P.length-1;q>=0&&(i.pop(),P[q].type!=="brace");q--)P[q].type!=="dots"&&F.unshift(P[q].value);S=jn(F,r),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),F=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=S="\\}",l.output=P;for(let q of F)l.output+=q.output||q.value}v({type:"brace",value:u,output:S}),ue("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,v({type:"text",value:u});continue}if(u===","){let d=u,S=w[w.length-1];S&&j[j.length-1]==="braces"&&(S.comma=!0,d="|"),v({type:"comma",value:u,output:d});continue}if(u==="/"){if(c.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",i.pop(),c=a;continue}v({type:"slash",value:u,output:k});continue}if(u==="."){if(l.braces>0&&c.type==="dot"){c.value==="."&&(c.output=A);let d=w[w.length-1];c.type="dots",c.output+=u,c.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&c.type!=="bos"&&c.type!=="slash"){v({type:"text",value:u,output:A});continue}v({type:"dot",value:u,output:A});continue}if(u==="?"){if(!(c&&c.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("qmark",u);continue}if(c&&c.type==="paren"){let S=$(),P=u;if(S==="<"&&!J.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(S)||S==="<"&&!/<([!=]|\w+>)/.test(se()))&&(P=`\\${u}`),v({type:"text",value:u,output:P});continue}if(r.dot!==!0&&(c.type==="slash"||c.type==="bos")){v({type:"qmark",value:u,output:W});continue}v({type:"qmark",value:u,output:O});continue}if(u==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Se("negate",u);continue}if(r.nonegate!==!0&&l.index===0){xr();continue}}if(u==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("plus",u);continue}if(c&&c.value==="("||r.regex===!1){v({type:"plus",value:u,output:p});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||l.parens>0){v({type:"plus",value:u});continue}v({type:"plus",value:p});continue}if(u==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){v({type:"at",extglob:!0,value:u,output:""});continue}v({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Kn.exec(se());d&&(u+=d[0],l.index+=d[0].length),v({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=M,l.backtrack=!0,l.globstar=!0,z(u);continue}let m=se();if(r.noextglob!==!0&&/^\([^?]/.test(m)){Se("star",u);continue}if(c.type==="star"){if(r.noglobstar===!0){z(u);continue}let d=c.prev,S=d.prev,P=d.type==="slash"||d.type==="bos",F=S&&(S.type==="star"||S.type==="globstar");if(r.bash===!0&&(!P||m[0]&&m[0]!=="/")){v({type:"star",value:u,output:""});continue}let q=l.braces>0&&(d.type==="comma"||d.type==="brace"),Me=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!q&&!Me){v({type:"star",value:u,output:""});continue}for(;m.slice(0,3)==="/**";){let ve=e[l.index+4];if(ve&&ve!=="/")break;m=m.slice(3),z("/**",3)}if(d.type==="bos"&&I()){c.type="globstar",c.value+=u,c.output=E(r),l.output=c.output,l.globstar=!0,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!F&&I()){l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=E(r)+(r.strictSlashes?")":"|$)"),c.value+=u,l.globstar=!0,l.output+=d.output+c.output,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&m[0]==="/"){let ve=m[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=`${E(r)}${k}|${k}${ve})`,c.value+=u,l.output+=d.output+c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&m[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${k}|${E(r)}${k})`,l.output=c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-c.output.length),c.type="globstar",c.output=E(r),c.value+=u,l.output+=c.output,l.globstar=!0,z(u);continue}let L={type:"star",value:u,output:M};if(r.bash===!0){L.output=".*?",(c.type==="bos"||c.type==="slash")&&(L.output=b+L.output),v(L);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&r.regex===!0){L.output=u,v(L);continue}(l.index===l.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(l.output+=x,c.output+=x):r.dot===!0?(l.output+=T,c.output+=T):(l.output+=b,c.output+=b),$()!=="*"&&(l.output+=y,c.output+=y)),v(L)}for(;l.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=J.escapeLast(l.output,"["),ue("brackets")}for(;l.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=J.escapeLast(l.output,"("),ue("parens")}for(;l.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=J.escapeLast(l.output,"{"),ue("braces")}if(r.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&v({type:"maybe_slash",value:"",output:`${k}?`}),l.backtrack===!0){l.output="";for(let m of l.tokens)l.output+=m.output!=null?m.output:m.value,m.suffix&&(l.output+=m.suffix)}return l};nr.fastpaths=(e,t)=>{let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=rr[e]||e;let a=J.isWindows(t),{DOT_LITERAL:i,SLASH_LITERAL:o,ONE_CHAR:h,DOTS_SLASH:g,NO_DOT:f,NO_DOTS:A,NO_DOTS_SLASH:p,STAR:k,START_ANCHOR:y}=Oe.globChars(a),R=r.dot?A:f,_=r.dot?p:f,x=r.capture?"":"?:",T={negated:!1,prefix:""},O=r.bash===!0?".*?":k;r.capture&&(O=`(${O})`);let W=b=>b.noglobstar===!0?O:`(${x}(?:(?!${y}${b.dot?g:i}).)*?)`,G=b=>{switch(b){case"*":return`${R}${h}${O}`;case".*":return`${i}${h}${O}`;case"*.*":return`${R}${O}${i}${h}${O}`;case"*/*":return`${R}${O}${o}${h}${_}${O}`;case"**":return R+W(r);case"**/*":return`(?:${R}${W(r)}${o})?${_}${h}${O}`;case"**/*.*":return`(?:${R}${W(r)}${o})?${_}${O}${i}${h}${O}`;case"**/.*":return`(?:${R}${W(r)}${o})?${i}${h}${O}`;default:{let C=/^(.*?)\.(\w+)$/.exec(b);if(!C)return;let M=G(C[1]);return M?M+i+C[2]:void 0}}},ne=J.removePrefix(e,T),E=G(ne);return E&&r.strictSlashes!==!0&&(E+=`${o}?`),E};tr.exports=nr});var ir=K((ys,ar)=>{"use strict";var Fn=require("path"),Qn=er(),Ye=sr(),ze=be(),Xn=ye(),Zn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,t,r=!1)=>{if(Array.isArray(e)){let f=e.map(p=>D(p,t,r));return p=>{for(let k of f){let y=k(p);if(y)return y}return!1}}let n=Zn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=t||{},a=ze.isWindows(t),i=n?D.compileRe(e,t):D.makeRe(e,t,!1,!0),o=i.state;delete i.state;let h=()=>!1;if(s.ignore){let f=Q(B({},t),{ignore:null,onMatch:null,onResult:null});h=D(s.ignore,f,r)}let g=(f,A=!1)=>{let{isMatch:p,match:k,output:y}=D.test(f,i,t,{glob:e,posix:a}),R={glob:e,state:o,regex:i,posix:a,input:f,output:y,match:k,isMatch:p};return typeof s.onResult=="function"&&s.onResult(R),p===!1?(R.isMatch=!1,A?R:!1):h(f)?(typeof s.onIgnore=="function"&&s.onIgnore(R),R.isMatch=!1,A?R:!1):(typeof s.onMatch=="function"&&s.onMatch(R),A?R:!0)};return r&&(g.state=o),g};D.test=(e,t,r,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let a=r||{},i=a.format||(s?ze.toPosixSlashes:null),o=e===n,h=o&&i?i(e):e;return o===!1&&(h=i?i(e):e,o=h===n),(o===!1||a.capture===!0)&&(a.matchBase===!0||a.basename===!0?o=D.matchBase(e,t,r,s):o=t.exec(h)),{isMatch:Boolean(o),match:o,output:h}};D.matchBase=(e,t,r,n=ze.isWindows(r))=>(t instanceof RegExp?t:D.makeRe(t,r)).test(Fn.basename(e));D.isMatch=(e,t,r)=>D(t,r)(e);D.parse=(e,t)=>Array.isArray(e)?e.map(r=>D.parse(r,t)):Ye(e,Q(B({},t),{fastpaths:!1}));D.scan=(e,t)=>Qn(e,t);D.compileRe=(e,t,r=!1,n=!1)=>{if(r===!0)return e.output;let s=t||{},a=s.contains?"":"^",i=s.contains?"":"$",o=`${a}(?:${e.output})${i}`;e&&e.negated===!0&&(o=`^(?!${o}).*$`);let h=D.toRegex(o,t);return n===!0&&(h.state=e),h};D.makeRe=(e,t={},r=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ye.fastpaths(e,t)),s.output||(s=Ye(e,t)),D.compileRe(s,t,r,n)};D.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};D.constants=Xn;ar.exports=D});var cr=K((bs,or)=>{"use strict";or.exports=ir()});var hr=K((_s,ur)=>{"use strict";var lr=require("util"),pr=Gt(),oe=cr(),Ve=be(),fr=e=>e===""||e==="./",N=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let n=new Set,s=new Set,a=new Set,i=0,o=f=>{a.add(f.output),r&&r.onResult&&r.onResult(f)};for(let f=0;f!n.has(f));if(r&&g.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(f=>f.replace(/\\/g,"")):t}return g};N.match=N;N.matcher=(e,t)=>oe(e,t);N.isMatch=(e,t,r)=>oe(t,r)(e);N.any=N.isMatch;N.not=(e,t,r={})=>{t=[].concat(t).map(String);let n=new Set,s=[],a=o=>{r.onResult&&r.onResult(o),s.push(o.output)},i=N(e,t,Q(B({},r),{onResult:a}));for(let o of s)i.includes(o)||n.add(o);return[...n]};N.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);if(Array.isArray(t))return t.some(n=>N.contains(e,n,r));if(typeof t=="string"){if(fr(e)||fr(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return N.isMatch(e,t,Q(B({},r),{contains:!0}))};N.matchKeys=(e,t,r)=>{if(!Ve.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),t,r),s={};for(let a of n)s[a]=e[a];return s};N.some=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(n.some(i=>a(i)))return!0}return!1};N.every=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(!n.every(i=>a(i)))return!1}return!0};N.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);return[].concat(t).every(n=>oe(n,r)(e))};N.capture=(e,t,r)=>{let n=Ve.isWindows(r),a=oe.makeRe(String(e),Q(B({},r),{capture:!0})).exec(n?Ve.toPosixSlashes(t):t);if(a)return a.slice(1).map(i=>i===void 0?"":i)};N.makeRe=(...e)=>oe.makeRe(...e);N.scan=(...e)=>oe.scan(...e);N.parse=(e,t)=>{let r=[];for(let n of[].concat(e||[]))for(let s of pr(String(n),t))r.push(oe.parse(s,t));return r};N.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:pr(e,t)};N.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,Q(B({},t),{expand:!0}))};ur.exports=N});var gr=K((Es,dr)=>{"use strict";dr.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ar=K((xs,Je)=>{"use strict";var Yn=gr(),mr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,n=()=>{r--,t.length>0&&t.shift()()},s=(o,h,...g)=>{r++;let f=Yn(o,...g);h(f),f.then(n,n)},a=(o,h,...g)=>{rnew Promise(g=>a(o,g,...h));return Object.defineProperties(i,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),i};Je.exports=mr;Je.exports.default=mr});var Vn={};Or(Vn,{default:()=>es});var He=X(require("@yarnpkg/cli")),ae=X(require("@yarnpkg/core")),nt=X(require("@yarnpkg/core")),le=X(require("clipanion")),Ae=class extends He.BaseCommand{constructor(){super(...arguments);this.json=le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=le.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=le.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=le.Option.Rest()}async execute(){let t=await ae.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ae.Project.find(t,this.context.cwd),s=await ae.Cache.find(t);await r.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(r.workspaces);else if(this.workspaces.length===0){if(!n)throw new He.WorkspaceRequiredError(r.cwd,this.context.cwd);a=new Set([n])}else a=new Set(this.workspaces.map(o=>r.getWorkspaceByIdent(nt.structUtils.parseIdent(o))));for(let o of a)for(let h of this.production?["dependencies"]:ae.Manifest.hardDependencies)for(let g of o.manifest.getForScope(h).values()){let f=r.tryWorkspaceByDescriptor(g);f!==null&&a.add(f)}for(let o of r.workspaces)a.has(o)?this.production&&o.manifest.devDependencies.clear():(o.manifest.installConfig=o.manifest.installConfig||{},o.manifest.installConfig.selfReferences=!1,o.manifest.dependencies.clear(),o.manifest.devDependencies.clear(),o.manifest.peerDependencies.clear(),o.manifest.scripts.clear());return(await ae.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async o=>{await r.install({cache:s,report:o,persistProject:!1})})).exitCode()}};Ae.paths=[["workspaces","focus"]],Ae.usage=le.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var st=Ae;var Ie=X(require("@yarnpkg/cli")),ge=X(require("@yarnpkg/core")),Ee=X(require("@yarnpkg/core")),Y=X(require("@yarnpkg/core")),Rr=X(require("@yarnpkg/plugin-git")),U=X(require("clipanion")),Be=X(hr()),yr=X(require("os")),br=X(Ar()),re=X(require("typanion")),xe=class extends Ie.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:re.isOneOf([re.isEnum(["unlimited"]),re.applyCascade(re.isNumber(),[re.isInteger(),re.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!n)throw new Ie.WorkspaceRequiredError(r.cwd,this.context.cwd);await r.restoreInstallState();let s=this.cli.process([this.commandName,...this.args]),a=s.path.length===1&&s.path[0]==="run"&&typeof s.scriptName!="undefined"?s.scriptName:null;if(s.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let i=this.all?r.topLevelWorkspace:n,o=this.since?Array.from(await Rr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:r})):[i,...this.from.length>0?i.getRecursiveWorkspaceChildren():[]],h=E=>Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.from),g=this.from.length>0?o.filter(h):o,f=new Set([...g,...g.map(E=>[...this.recursive?this.since?E.getRecursiveWorkspaceDependents():E.getRecursiveWorkspaceDependencies():E.getRecursiveWorkspaceChildren()]).flat()]),A=[],p=!1;if(a==null?void 0:a.includes(":")){for(let E of r.workspaces)if(E.manifest.scripts.has(a)&&(p=!p,p===!1))break}for(let E of f)a&&!E.manifest.scripts.has(a)&&!p&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(E)).has(a)||a===process.env.npm_lifecycle_event&&E.cwd===n.cwd||this.include.length>0&&!Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.include)||this.exclude.length>0&&Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.exclude)||this.publicOnly&&E.manifest.private===!0||A.push(E);let k=this.parallel?this.jobs==="unlimited"?Infinity:Number(this.jobs)||Math.max(1,(0,yr.cpus)().length/2):1,y=k===1?!1:this.parallel,R=y?this.interlaced:!0,_=(0,br.default)(k),x=new Map,T=new Set,O=0,W=null,G=!1,ne=await Ee.StreamReport.start({configuration:t,stdout:this.context.stdout},async E=>{let b=async(C,{commandIndex:M})=>{if(G)return-1;!y&&this.verbose&&M>1&&E.reportSeparator();let l=zn(C,{configuration:t,verbose:this.verbose,commandIndex:M}),[H,w]=_r(E,{prefix:l,interlaced:R}),[j,c]=_r(E,{prefix:l,interlaced:R});try{this.verbose&&E.reportInfo(null,`${l} Process started`);let u=Date.now(),I=await this.cli.run([this.commandName,...this.args],{cwd:C.cwd,stdout:H,stderr:j})||0;H.end(),j.end(),await w,await c;let $=Date.now();if(this.verbose){let ee=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,$-u,Y.formatUtils.Type.DURATION)}`:"";E.reportInfo(null,`${l} Process exited (exit code ${I})${ee}`)}return I===130&&(G=!0,W=I),I}catch(u){throw H.end(),j.end(),await w,await c,u}};for(let C of A)x.set(C.anchoredLocator.locatorHash,C);for(;x.size>0&&!E.hasErrors();){let C=[];for(let[H,w]of x){if(T.has(w.anchoredDescriptor.descriptorHash))continue;let j=!0;if(this.topological||this.topologicalDev){let c=this.topologicalDev?new Map([...w.manifest.dependencies,...w.manifest.devDependencies]):w.manifest.dependencies;for(let u of c.values()){let I=r.tryWorkspaceByDescriptor(u);if(j=I===null||!x.has(I.anchoredLocator.locatorHash),!j)break}}if(!!j&&(T.add(w.anchoredDescriptor.descriptorHash),C.push(_(async()=>{let c=await b(w,{commandIndex:++O});return x.delete(H),T.delete(w.anchoredDescriptor.descriptorHash),c})),!y))break}if(C.length===0){let H=Array.from(x.values()).map(w=>Y.structUtils.prettyLocator(t,w.anchoredLocator)).join(", ");E.reportError(Ee.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${H})`);return}let l=(await Promise.all(C)).find(H=>H!==0);W===null&&(W=typeof l!="undefined"?1:W),(this.topological||this.topologicalDev)&&typeof l!="undefined"&&E.reportError(Ee.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return W!==null?W:ne.exitCode()}};xe.paths=[["workspaces","foreach"]],xe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});var Er=xe;function _r(e,{prefix:t,interlaced:r}){let n=e.createStreamReporter(t),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let a=new Promise(o=>{n.on("finish",()=>{o(s.active)})});if(r)return[s,a];let i=new Y.miscUtils.BufferStream;return i.pipe(s,{end:!1}),i.on("finish",()=>{s.end()}),[i,a]}function zn(e,{configuration:t,commandIndex:r,verbose:n}){if(!n)return null;let s=Y.structUtils.convertToIdent(e.locator),i=`[${Y.structUtils.stringifyIdent(s)}]:`,o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],h=o[r%o.length];return Y.formatUtils.pretty(t,i,h)}var Jn={commands:[st,Er]},es=Jn;return Vn;})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableTelemetry: false 2 | 3 | changesetBaseRefs: 4 | - main 5 | - origin/main 6 | - upstream/main 7 | 8 | changesetIgnorePatterns: 9 | - '**/*.test.{js,jsx,ts,tsx}' 10 | 11 | defaultSemverRangePrefix: '' 12 | 13 | enableGlobalCache: false 14 | 15 | nmMode: hardlinks-local 16 | 17 | logFilters: 18 | - code: YN0002 19 | level: discard 20 | - code: YN0060 21 | level: discard 22 | - code: YN0006 23 | level: discard 24 | - code: YN0076 25 | level: discard 26 | - code: YN0013 27 | level: discard 28 | 29 | nodeLinker: node-modules 30 | 31 | plugins: 32 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 33 | spec: '@yarnpkg/plugin-workspace-tools' 34 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 35 | spec: '@yarnpkg/plugin-version' 36 | - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs 37 | spec: '@yarnpkg/plugin-constraints' 38 | 39 | yarnPath: .yarn/releases/yarn-3.2.3.cjs 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🍔 TLDR 2 | - the aim of this project is to implement a sane DX and a CI/CD workflow using Github Actions and Expo EAS 3 | - we have 3 builds/apps: Development, Staging and Preview 4 | - developers can build features using a browser 5 | - the UI team can review UI components using Chromatic/Storybook 6 | - the whole team can view updates for each of these 3 apps along the development process 7 | 8 | ## ⚡️ Tech 9 | - Yarn 10 | - TypeScript 11 | - react-native 12 | - Expo 13 | - NextJS 14 | - Solito 15 | - Tamagui 16 | - Storybook 17 | - Jest, testing-library 18 | - Turbo 19 | ## 🔥 About & Motivation 20 | 21 | This is a monorepo for an Expo + NextJS app ⚡️ 22 | 23 | While our main objective is to develop a mobile app, now thanks to [Solito](https://solito.dev/) and [Tamagui](https://tamagui.dev/) we can target the web also. Just Tamagui alone brings great value due to its performance and [theme-ui](https://theme-ui.com/) styles, while Solito provides cross-platform navigation. 24 | 25 | react-native development is a bit tedious compared to web development, requiring testing on different platforms (iOS, Android), different OS versions, and different screen sizes. Even the processes of app building, submission and updating can get complicated without Expo. This is why this repo aims to easen the effort when it comes to native development, while implementing quality checks and CI/CD flows. 26 | 27 | In this regard, the main reason for choosing to have web compatibility is the Developer Experience. From my personal experience building react-native apps, working with just the Simulator to build UIs can get frustrating, like when debugging with an attached Chrome Dev Tools instance (can't use Inspect Element on DIVs to tinker with styles, for example) or using the in-app injected Debugger (sort of a dumbed down Dev Tools, not really productive). But with this stack, we can build our mobile app using the browser, as you would do with a normal web app. Sure, one might implement mobile-specific features which won't work on web, so in this case they have to be properly handled, but in our case we're more interested to use a browser for UI development especially. There are some [unsupported react-native APIs](https://necolas.github.io/react-native-web/docs/react-native-compatibility/) in react-native-web, which is used by NextJS and Storybook, but as long as we don't hit these limitations, we can continue using web. 28 | 29 | Another good reason is Tamagui itself. It has [the best performance](https://tamagui.dev/docs/intro/benchmarks) compared to any other react-native UI libs. 30 | 31 | Another reason for organizing everything in a monorepo is because we're targeting 2 different platforms (web & mobile), each having its own config. Overall, we're keeping each package clean of unwanted dependencies and with their own specific structure and configuration that are easy to follow. 32 | 33 | This monorepo is the result of: 34 | - initial scaffold using `npm create tamagui` (see the [docs](https://tamagui.dev/)) 35 | - getting some inspiration from `tamagui-kitchen-sync` for adding Storybook to it ([link](https://github.com/dohomi/tamagui-kitchen-sink)) 36 | - adding `turbo` as a build and task runner cache system ([link](https://turbo.build/repo)) 37 | - using `Yarn Workspaces` with plugins ([docs](https://classic.yarnpkg.com/lang/en/docs/workspaces/)) 38 | - tweaking `tsconfig.json` and `package.json` for each workspace package to glue them together nicely with TypeScript Project References and Path Aliases ([docs](https://www.typescriptlang.org/docs/handbook/project-references.html)). One useful trick was to define path aliases with the same name as the ones defied by the workspace packages; this way, the code editor will jump on click to source, instead of node_modules 39 | - setting up `build`, `format`, `lint`, `typecheck` for each package 40 | - setting up `husky` with `pre-commit`, `commitlint` and `lint-staged` hooks 41 | - setting up `semantic-release` for `apps/expo` 42 | - getting some inspiration for Github Actions cache from `nextjs-monorepo-example` ([link](https://github.com/belgattitude/nextjs-monorepo-example/blob/main/.github/actions/yarn-nm-install/action.yml)) 43 | - setting up a productive development workflow involving Github Actions + Chromatic/Storybook + Expo EAS ([chromatic docs](https://chromatic.com/), [EAS docs](https://docs.expo.dev/eas/)) 44 | - overall nitting and bolting everything together, occasionally with some help from the fantastic Discord communities of Tamagui, Turbo and Expo 45 | 46 | An easier alternative for setting up a monorepo would've been NX ([link](https://nx.dev/)), which I tried initially and didn't work out because: 47 | - you can leverage the full power of NX only in its `integrated` mode and not in `package` mode 48 | - integrated means you have only one root `package.json` + TS path aliases, and each app configuration is black-boxed behind NX custom config generators 49 | - you lose the flexibility of running custom `package.json` commands for your internal packages 50 | - if choosing package mode, you would be better off to ditch NX completely and manually configure Yarn + TypeScript + Turbo as I did 51 | - NX enforces that each custom environment variable has to be prefixed with `NX_` while packages like `tamagui` expect exactly `TAMAGUI_TARGET` or `APP_VARIANT` 52 | - overall, individual `package.json`s with `dotenv` do the trick 53 | 54 | ## 🗂 Directory structure 55 | 56 | - `apps` 57 | - `expo` 58 | - `next` 59 | - `storybook` 60 | - `tests` 61 | - `packages` shared packages across apps 62 | - `app` you'll be importing most files from `app/` 63 | - `features` (don't use a `screens` folder, instead organize by feature). We should organize our code with respect to Domain Driven Design [(blog article)](https://www.angulararchitects.io/en/aktuelles/tactical-domain-driven-design-with-monorepos/) 64 | - `provider` (all the providers that wrap the app, and some no-ops for Web.) 65 | - `navigation` Next.js has a `pages/` folder. React Native doesn't. This folder contains navigation-related code for RN. You may use it for any navigation code, such as custom links. 66 | - `eslint-config-custom` as a global ESLint config, used in packages as `{ extends: ["custom", ...] }` 67 | - `tsconfig` for specific TypeScript configuration for each package 68 | - `ui` for our dumb UI components 69 | - `backend` - not yet in the project, but will host our Supabase PostgreSQL [Prisma](https://supabase.com/docs/guides/integrations/prisma) schemas, Deno edge [functions](https://supabase.com/docs/guides/functions) + optionally [tRPC](https://dev.to/noahflk/supabase-with-typescript-using-trpc-and-prisma-to-achieve-end-to-end-typesafety-1021). One useful backend feature will be to trigger user notifications. Can get some inspiration from the [T3 stack](https://create.t3.gg/) 70 | 71 | The dependency graph looks like this: 72 | 73 | ![](/docs/dep-graph.png) 74 | 75 | ## 🛠️ Prerequisites for setting everything up 76 | - create an Expo account and a new Expo team 77 | - create a Turbo team 78 | - create a Chromatic account 79 | - search this project for "REPLACE-ME" and replace stuff accordingly 80 | - go over [Prerequisites for development]() as described below and make sure everything works: running Storybook, running the app in Next, simulators, on your device with Expo 81 | - go over [Setup Github Actions]() as described below 82 | - create your first builds with Expo as described in [App variants]() below 83 | - at this point you can safely start developing as described in [Workflow]() below: Github Actions, Chromatic updates and EAS Updates should work as expected 84 | 85 | ## 📱 App variants 86 | - see [eas.json](apps/expo/eas.json) 87 | - builds are real apps with different identifiers: `dev.com.REPLACE-ME`, `staging.com.REPLACE-ME`, `preview.com.REPLACE-ME`, see [app.config.ts](apps/expo/app.config.ts), [docs](https://docs.expo.dev/build/introduction/), [expo/package.json build commands](apps/expo/package.json) 88 | - for first time builds, use: 89 | - `yarn build:all:dev` 90 | - `yarn build:all:staging` 91 | - `yarn build:all:preview` 92 | - 2 platforms x 3 variants = 6 builds. They can be found [here](https://expo.dev/accounts/REPLACE-ME/projects/REPLACE-ME/builds) 93 | - all builds should have `1.0.0` version. They should only be rebuilt when the runtime policy of `sdkVersion` kicks in, meaning when we're forced to update if the Expo SDK version changes, see [app.json](apps/expo/app.json), [docs](https://docs.expo.dev/eas-update/runtime-versions/#sdkversion-runtime-version-policy). What changes automatically after each release is `buildNumber` (for iOS) and `versionCode` (for Android) 94 | - your team has to enable [Developer mode](https://docs.expo.dev/guides/ios-developer-mode/) to be able to run these apps 95 | - updates to these apps are delivered via EAS Update [(docs)](https://docs.expo.dev/eas-update/introduction/), instead of always building new app versions on each release (the Expo free tier offers only 20 builds / month and 1000 updates, so we prefer leveraging EAS Updates) 96 | - The Development app is useful only in tandem with `yarn start:devclient` to preview live changes from a developer's machine. Could be used for pair programming. 97 | - The Staging app is updated on each opened PR. Useful for code reviews (limitation: 1 PR to be opened at a time) 98 | - The Preview app is updated on each new git release (merges to main) and acts as an exact preview of the Production app (which hasn't been built yet) used for testing before an official release. Team demos use this app. 99 | 100 | ## 🎬 Github Actions setup 101 | - set the following secrets 102 | - `CHROMATIC_PROJECT_TOKEN` - you can get one after you create a Chromatic project 103 | - `EXPO_TOKEN` - get it from your Expo account 104 | - `EXTENDED_GITHUB_TOKEN` - this is a Github Personal Access Token, get one from your account; needs write access. Used by semantic-release to push new tags 105 | - `SLACK_WEBHOOK` - a Slack webhook configured to route messages to a channel, from Github Actions 106 | - `TURBO_TEAM` - follow Turbo docs how to create a team 107 | - `TURBO_TOKEN` - see Turbo docs 108 | - create the following environment variables: 109 | - for `staging` environment 110 | - `APP_VARIANT`=staging 111 | - `TAMAGUI_TARGET`=native 112 | - for `preview` environment 113 | - `APP_VARIANT`=preview 114 | - `TAMAGUI_TARGET`=native 115 | 116 | ## 👨🏻‍💻 Prerequisites for development 117 | - install [Android Studio Emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 118 | - install [iOS Simulator](https://docs.expo.dev/workflow/ios-simulator/) 119 | - install [Flipper](https://fbflipper.com/) (useful for debugging RN apps) 120 | - join the Expo project (invites were sent) 121 | - join the Chromatic project, [here](https://www.chromatic.com/builds?appId=63d3a6140eb623de94b96c97&inviteToken=25e1f92229924c418be9be4f3c64056d) 122 | - install the development, staging & preview builds on your personal device, taken from [here](https://expo.dev/accounts/REPLACE-ME/projects/REPLACE-ME/builds) 123 | ![](docs/apps-screenshot.jpg) 124 | - run `npm install -g eas-cli` . For more info check [EAS Build](https://docs.expo.dev/build/setup/) 125 | - read the docs for [Expo](https://docs.expo.dev/), [Tamagui](https://tamagui.dev/), [Turbo](https://turbo.build/repo) 126 | 127 | ## 🏁 Start the app 128 | - run `yarn` to install dependencies 129 | - run `yarn start:expo` to start Expo 130 | - run `yarn start:next` to start Next 131 | - run `yarn start:sb` to start Storybook 132 | 133 | ## 🏋️ Workflow 134 | 135 | ![](docs/ci-cd-flow.png) 136 | 137 | 1. The developer creates a new branch off `main` 138 | 2. Starts Storybook `yarn start:sb` and Expo `yarn start:expo` 139 | - mainly uses Chrome + Storybook for UI development in `packages/ui` 140 | - can also use Chrome + NextJS for app logic development in `packages/app` - run `yarn start:next` 141 | - checks his changes inside the iOS Simulator and Android Studio (press "i" / "a" in the console) 142 | - checks the app on his real mobile device using the consle QR Code 143 | - can share his changes live before opening a PR with other devs in 2 ways: 144 | - using Expo Go, by running `yarn start:tunnel` and sharing the QR code with others 145 | - using the Development Build Client (has to be installed by others), running `yarn start:devclient` and sharing the QR Code 146 | - the difference between these two is that the Development Build Client is more closer to the production app 147 | - checks the minified app running `start:ios:prod` and `start:android:prod`. Same as `yarn start:expo`, but it's a minified and closer to production build. Sometimes you can catch some 3rd party lib bugs this way. 148 | 149 | 3. Commits his changes 150 | - commit messages must respect the [conventional commit format](https://github.com/conventional-changelog/commitlint#what-is-commitlint), eg: `fix(login screen): adjusted font size` 151 | - on commit, husky runs `npx lint-staged --allow-empty && yarn g:run-all-checks`, where the checks are: `turbo run typecheck lint format:check test build --parallel --since=HEAD^1` 152 | 4. Pushes his changes and opens a new PR 153 | - Gihub Actions kick in: 154 | - `ci-build-test.yml`: runs Typecheck, Lint, Format check, Build, Unit tests for all configured packages 155 | - `ci-monorepo-integrity.yml`: Check for duplicate dependencies and yarn constraints 156 | - `cd-chromatic.yml`: Deploys the Storybook to Chromatic to be reviewed by the team ❗️ 157 | - `cd-eas-update-staging.yml`: Updates the Staging app to be reviewed by the team ❗️. Also a QR code is automaticallty generated in the PR comments and links to the Expo Go client. 158 | - requires at least 1 reviewer to accept the PR 159 | 5. On merging the PR to `main`, the same Github Actions run again, with the following additional actions: 160 | - `ci-semantic-release.yml`: Runs semantic-release which bumps the version of `apps/expo/package.json`, bumps `buildNumber` and `versionCode` for `apps/expo/app.json`, updates `apps/expo/CHANGELOG.md` by aggregating the conventional commit messages, creates a new git tag version and a new git release based on it. See [Releases](https://github.com/REPLACE-ME-io/REPLACE-ME/releases). Please join the `#REPLACE-ME-github` Slack channel for updates 📫 161 | - `cd-eas-update-preview.yml`: Triggered by a new git release, updates the Preview app 162 | 163 | ## 🏎️ Performance 164 | - we have yet to document the bundle size, but my memory recalls 1.9M for this barebone project 165 | - Tamagui by itself delivers great performance with respect to style updates and animations, leaving others in dust 166 | - Github Actions CI checks take around 10 minutes without Turbo cache, and around 2-3 minutes with Turbo cache 167 | - Manual Expo Builds take around 10 minutes using the Free Trial (we anyway trigger new builds very rarely because we leverage CI EAS hot Updates instead). Putting this in context, building in CI by yourself with fastlane takes around 1H from my experience (yeah very debatable, but still). 168 | 169 | ## 📐 Mobile app architecture and other considerations 170 | - which libs to use for state management and data fetching? [RTK](https://redux-toolkit.js.org/) handles both well in an integrated fashion. Jotai is another strong candidate, as it has [React-Query integration](https://jotai.org/docs/integrations/query) and [tRPC integration](https://jotai.org/docs/integrations/trpc) (we want to use tRPC on the backend) 171 | 172 | ## 🆕 Add new dependencies 173 | 174 | ### Pure JS dependencies 175 | 176 | If you're installing a JavaScript-only dependency that will be used across platforms, install it in `packages/app`: 177 | 178 | ```sh 179 | cd packages/app 180 | yarn add date-fns 181 | ``` 182 | 183 | ### Native dependencies 184 | 185 | If you're installing a library with any native code, you must install it in `apps/expo`: 186 | 187 | ```sh 188 | cd apps/expo 189 | yarn add react-native-reanimated 190 | ``` 191 | 192 | ## 📟 NPM Commands 193 | - go over each `package.json` to get accustomed with NPM commands specific to each package 194 | - the root `package.json` should contain only monorepo specific commands 195 | 196 | ## Notes 197 | - this repo serves as a good starting point for development with Expo EAS, and will be improved in terms of workflow and tooling as we progress and discover new stuff that we might need, for example: 198 | - for Storybook, we might swap Webpack in favor of Vite or Turbopack because of the slow build time 199 | - better testing architecture? Right now `apps/tests` is configured to run user interaction tests with `testing-library` ([docs](https://reactnative.dev/docs/testing-overview#testing-user-interactions)). We might want to add Snapshot testing and E2E Detox tests. Or picking between moving `jest` to each individual package VS root jest config with `projects: [...]`. Not a priority right now. 200 | - Tamagui is being actively developed and sometimes breaking changes occur. We have to keep an eye on it. 201 | - Running Turbo with `--since=HEAD^1` should be refactored to select the PR's first commit hash instead 202 | - no concrete UI components have been developed up to this point, and we should develop them along with a custom [theme](https://tamagui.dev/docs/core/theme). We don't want to repeat text sizes, colors and spacings everywhere. 203 | - missing from our workflow is the production [distribution of the app to stores](https://docs.expo.dev/submit/introduction/), and most probably it won't be integrated with Continous Deployment, because we want to control this sensible process manually. But we want to trigger Continous Deployment EAS Updates to the production app on pushing to a `production` branch, but hasn't been configured yet. There are some [Deployment patterns](https://docs.expo.dev/eas-update/deployment-patterns/) and we will pick one at the right time. Right now we're using `eas update --branch staging/preview` in our Github Actions which are linked to the `staging/preview` channels. These Expo branches shouldn't be confused with git branches: a build corresponds to a release channel, and these "branches" are pointed to channels. When using the `eas update` command, regardless of the git branch you're on, you can specify `--branch` so that your code can be routed to the specific channel. `--auto` sets the branch to the current git branch, but we're not using that. 204 | - some tweaking is needed around file globs used in Turbo and Github Actions, to prevent running unnecessary commands (eg: it doesn't make sense to trigger a typecheck when only the README.md file was updated) 205 | - we should be really careful when upgrading react, react-native, react-native-web, tamagui. Experience and public Github issues show us that things can break really easily for lots of people at once and often. Manual and automated tests are key. 206 | - before going public, we should scan and update our dependencies with [Snyk](https://snyk.io/), update this readme with respect to Release Management and check again all permissions regarding this process 207 | 208 | 209 | ## FAQ 210 | 211 | - if `yarn ios` or `npx pod-install` fails with: 212 | 213 | ``` 214 | Couldn't install Pods. Updating the Pods project and trying again... 215 | Command `pod install` failed. 216 | └─ Cause: Invalid `Podfile` file: 217 | [!] Invalid `RNGestureHandler.podspec` file: undefined method `exists?' for File:Class. 218 | ``` 219 | 220 | Check this https://github.com/facebook/react-native/issues/35807#issuecomment-1378831502 221 | 222 | - devs are encouraged to update this section any time they run into unexpected problems 223 | -------------------------------------------------------------------------------- /apps/expo/.env.development: -------------------------------------------------------------------------------- 1 | APP_VARIANT=development 2 | TAMAGUI_TARGET=native 3 | -------------------------------------------------------------------------------- /apps/expo/.env.preview: -------------------------------------------------------------------------------- 1 | APP_VARIANT=preview 2 | TAMAGUI_TARGET=native 3 | -------------------------------------------------------------------------------- /apps/expo/.env.production: -------------------------------------------------------------------------------- 1 | APP_VARIANT=production 2 | TAMAGUI_TARGET=native 3 | -------------------------------------------------------------------------------- /apps/expo/.env.staging: -------------------------------------------------------------------------------- 1 | APP_VARIANT=staging 2 | TAMAGUI_TARGET=native 3 | -------------------------------------------------------------------------------- /apps/expo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['universe/native', 'custom'], 4 | ignorePatterns: [ 5 | 'node_modules', 6 | '.expo', 7 | '.turbo', 8 | 'ios', 9 | 'assets', 10 | 'dist', 11 | '.eslintrc.js', 12 | 'babel.config.js', 13 | 'metro.config.js', 14 | ], 15 | parserOptions: { 16 | project: 'tsconfig.json', 17 | tsconfigRootDir: __dirname, 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 2018, 22 | sourceType: 'module', 23 | }, 24 | overrides: [ 25 | { 26 | files: ['*.ts', '*.tsx', '*.js', '*.jsx'], 27 | rules: {}, 28 | }, 29 | { 30 | files: ['*.ts', '*.tsx'], 31 | rules: {}, 32 | }, 33 | { 34 | files: ['*.js', '*.jsx'], 35 | rules: {}, 36 | }, 37 | ], 38 | rules: {}, 39 | } 40 | -------------------------------------------------------------------------------- /apps/expo/.gitignore: -------------------------------------------------------------------------------- 1 | .expo 2 | 3 | # OSX 4 | # 5 | .DS_Store 6 | 7 | # Xcode 8 | # 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | *.xccheckout 20 | *.moved-aside 21 | DerivedData 22 | *.hmap 23 | *.ipa 24 | *.xcuserstate 25 | project.xcworkspace 26 | 27 | # Android/IntelliJ 28 | # 29 | build/ 30 | .idea 31 | .gradle 32 | local.properties 33 | *.iml 34 | *.hprof 35 | 36 | # node.js 37 | # 38 | node_modules/ 39 | npm-debug.log 40 | yarn-error.log 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/ 53 | 54 | */fastlane/report.xml 55 | */fastlane/Preview.html 56 | */fastlane/screenshots 57 | 58 | # Bundle artifacts 59 | *.jsbundle 60 | 61 | # CocoaPods 62 | /ios/Pods/ 63 | 64 | # Expo 65 | .expo/* 66 | web-build/ 67 | 68 | 69 | ios/ 70 | android/ 71 | # @generated expo-cli sync-b25d41054229aa64f1468014f243adfae8268af2 72 | # The following patterns were generated by expo-cli 73 | 74 | # OSX 75 | # 76 | .DS_Store 77 | 78 | # Xcode 79 | # 80 | build/ 81 | *.pbxuser 82 | !default.pbxuser 83 | *.mode1v3 84 | !default.mode1v3 85 | *.mode2v3 86 | !default.mode2v3 87 | *.perspectivev3 88 | !default.perspectivev3 89 | xcuserdata 90 | *.xccheckout 91 | *.moved-aside 92 | DerivedData 93 | *.hmap 94 | *.ipa 95 | *.xcuserstate 96 | project.xcworkspace 97 | 98 | # Android/IntelliJ 99 | # 100 | build/ 101 | .idea 102 | .gradle 103 | local.properties 104 | *.iml 105 | *.hprof 106 | .cxx/ 107 | 108 | # node.js 109 | # 110 | node_modules/ 111 | npm-debug.log 112 | yarn-error.log 113 | 114 | # BUCK 115 | buck-out/ 116 | \.buckd/ 117 | *.keystore 118 | !debug.keystore 119 | 120 | # Bundle artifacts 121 | *.jsbundle 122 | 123 | # CocoaPods 124 | /ios/Pods/ 125 | 126 | # Expo 127 | .expo/ 128 | web-build/ 129 | dist/ 130 | 131 | # @end expo-cli 132 | 133 | tsconfig.tsbuildinfo 134 | -------------------------------------------------------------------------------- /apps/expo/.versionrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bumpFiles: [ 3 | { 4 | filename: 'package.json', 5 | }, 6 | { 7 | filename: 'app.json', 8 | updater: require.resolve('standard-version-expo'), 9 | }, 10 | { 11 | filename: 'app.json', 12 | updater: require.resolve('standard-version-expo/android'), 13 | }, 14 | { 15 | filename: 'app.json', 16 | updater: require.resolve('standard-version-expo/ios'), 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /apps/expo/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/expo/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/CHANGELOG.md -------------------------------------------------------------------------------- /apps/expo/app.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable turbo/no-undeclared-env-vars */ 2 | import { ConfigContext, ExpoConfig } from '@expo/config' 3 | import dotenv from 'dotenv' 4 | 5 | dotenv.config({ 6 | path: `.env.${process.env.APP_VARIANT} ?? 'development'`, 7 | }) 8 | 9 | const { APP_VARIANT } = process.env 10 | 11 | const getVariantConfig = (config: ConfigContext['config']) => ({ 12 | development: { 13 | ...config, 14 | name: `[DEV] ${config.name}`, 15 | slug: `${config.slug}`, 16 | icon: './assets/icon-dev.png', 17 | android: { 18 | ...config.android, 19 | package: 'dev.com.REPLACE-ME', 20 | }, 21 | ios: { 22 | ...config.ios, 23 | bundleIdentifier: 'dev.com.REPLACE-ME', 24 | }, 25 | }, 26 | staging: { 27 | ...config, 28 | name: `[STAGING] ${config.name}`, 29 | slug: `${config.slug}`, 30 | icon: './assets/icon-dev.png', 31 | android: { 32 | ...config.android, 33 | package: 'staging.com.REPLACE-ME', 34 | }, 35 | ios: { 36 | ...config.ios, 37 | bundleIdentifier: 'staging.com.REPLACE-ME', 38 | }, 39 | }, 40 | preview: { 41 | ...config, 42 | name: `[PREVIEW] ${config.name}`, 43 | slug: `${config.slug}`, 44 | icon: './assets/icon.png', 45 | android: { 46 | ...config.android, 47 | package: 'preview.com.REPLACE-ME', 48 | }, 49 | ios: { 50 | ...config.ios, 51 | bundleIdentifier: 'preview.com.REPLACE-ME', 52 | }, 53 | }, 54 | production: { 55 | ...config, 56 | name: `${config.name}`, 57 | slug: `${config.slug}`, 58 | icon: './assets/icon.png', 59 | android: { 60 | ...config.android, 61 | package: 'com.REPLACE-ME', 62 | }, 63 | ios: { 64 | ...config.ios, 65 | bundleIdentifier: 'com.REPLACE-ME', 66 | }, 67 | }, 68 | }) 69 | 70 | export default ({ config }: ConfigContext): ExpoConfig => ({ 71 | ...getVariantConfig(config)[APP_VARIANT], 72 | }) 73 | -------------------------------------------------------------------------------- /apps/expo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "owner": "REPLACE-ME", 4 | "name": "REPLACE-ME", 5 | "slug": "REPLACE-ME", 6 | "version": "1.0.0", 7 | "scheme": "REPLACE-ME", 8 | "orientation": "portrait", 9 | "icon": "./assets/icon.png", 10 | "userInterfaceStyle": "light", 11 | "sdkVersion": "47.0.0", 12 | "splash": { 13 | "image": "./assets/splash.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "assetBundlePatterns": ["**/*"], 18 | "ios": { 19 | "supportsTablet": true, 20 | "buildNumber": "1.0.0" 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | }, 27 | "versionCode": 470010000 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png", 31 | "bundler": "metro" 32 | }, 33 | "extra": { 34 | "eas": { 35 | "projectId": "REPLACE-ME" 36 | } 37 | }, 38 | "updates": { 39 | "url": "https://u.expo.dev/REPLACE-ME" 40 | }, 41 | "runtimeVersion": { 42 | "policy": "sdkVersion" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/expo/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'app/src/provider' 2 | import { SplashScreen, Stack } from 'expo-router' 3 | import { useFonts } from 'expo-font' 4 | 5 | export default function Root() { 6 | const [loaded] = useFonts({ 7 | Inter: require('@tamagui/font-inter/otf/Inter-Medium.otf'), 8 | InterBold: require('@tamagui/font-inter/otf/Inter-Bold.otf'), 9 | }) 10 | 11 | if (!loaded) { 12 | return 13 | } 14 | 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/expo/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { HomeScreen } from 'app/src/features/home/HomeScreen' 2 | 3 | export default function Home() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /apps/expo/app/user/[id].tsx: -------------------------------------------------------------------------------- 1 | import { UserDetailScreen } from 'app/src/features/user/detail-screen' 2 | 3 | export default function UserDetail() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /apps/expo/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/assets/adaptive-icon.png -------------------------------------------------------------------------------- /apps/expo/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/assets/favicon.png -------------------------------------------------------------------------------- /apps/expo/assets/icon-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/assets/icon-dev.png -------------------------------------------------------------------------------- /apps/expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/assets/icon.png -------------------------------------------------------------------------------- /apps/expo/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silthus/pax-react-native-starter/c3fc1eb670d3904820eb99325218893d5b2b6b63/apps/expo/assets/splash.png -------------------------------------------------------------------------------- /apps/expo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: [['babel-preset-expo', { jsxRuntime: 'automatic' }]], 5 | plugins: [ 6 | // NOTE: `expo-router/babel` is a temporary extension to `babel-preset-expo`. 7 | require.resolve('expo-router/babel'), 8 | 'inline-dotenv', 9 | [ 10 | require.resolve('babel-plugin-module-resolver'), 11 | { 12 | root: ['../..'], 13 | alias: { 14 | // define aliases to shorten the import paths 15 | app: '../../packages/app', 16 | ui: '../../packages/ui', 17 | }, 18 | extensions: ['.js', '.jsx', '.tsx', '.ios.js', '.android.js'], 19 | }, 20 | ], 21 | // if you want reanimated support 22 | // 'react-native-reanimated/plugin', 23 | // fix android 24 | ...(process.env.EAS_BUILD_PLATFORM === 'android' 25 | ? [] 26 | : [ 27 | [ 28 | '@tamagui/babel-plugin', 29 | { 30 | components: ['ui', 'tamagui'], 31 | config: './tamagui.config.ts', 32 | }, 33 | ], 34 | ]), 35 | [ 36 | 'transform-inline-environment-variables', 37 | { 38 | include: 'TAMAGUI_TARGET', 39 | }, 40 | ], 41 | ], 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/expo/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "development": { 4 | "distribution": "internal", 5 | "channel": "development", 6 | "developmentClient": true, 7 | "android": { 8 | "buildType": "apk" 9 | }, 10 | "ios": { 11 | "image": "latest", 12 | "resourceClass": "m1-medium" 13 | }, 14 | "env": { 15 | "TAMAGUI_TARGET": "native", 16 | "APP_VARIANT": "development" 17 | } 18 | }, 19 | "staging": { 20 | "distribution": "internal", 21 | "channel": "staging", 22 | "developmentClient": false, 23 | "android": { 24 | "buildType": "apk" 25 | }, 26 | "ios": { 27 | "image": "latest", 28 | "resourceClass": "m1-medium" 29 | }, 30 | "env": { 31 | "TAMAGUI_TARGET": "native", 32 | "APP_VARIANT": "staging" 33 | } 34 | }, 35 | "preview": { 36 | "distribution": "internal", 37 | "channel": "preview", 38 | "developmentClient": false, 39 | "ios": { 40 | "resourceClass": "m1-medium" 41 | }, 42 | "env": { 43 | "TAMAGUI_TARGET": "native", 44 | "APP_VARIANT": "preview" 45 | } 46 | }, 47 | "production": { 48 | "distribution": "store", 49 | "ios": { 50 | "resourceClass": "m1-medium" 51 | }, 52 | "android": { 53 | "buildType": "app-bundle" 54 | }, 55 | "env": { 56 | "TAMAGUI_TARGET": "native", 57 | "APP_VARIANT": "production" 58 | }, 59 | "channel": "production" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/expo/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | type AppVariant = 'development' | 'staging' | 'preview' | 'production' 3 | export interface ProcessEnv { 4 | APP_VARIANT: AppVariant 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/expo/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo' 2 | import { ExpoRoot } from 'expo-router' 3 | 4 | // Must be exported or Fast Refresh won't update the context 5 | export function App() { 6 | const ctx = require.context('./app') 7 | return 8 | } 9 | 10 | registerRootComponent(App) 11 | -------------------------------------------------------------------------------- /apps/expo/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | /** 3 | * @type {import('expo/metro-config')} 4 | */ 5 | const { getDefaultConfig } = require('@expo/metro-config') 6 | const path = require('path') 7 | 8 | const projectRoot = __dirname 9 | const workspaceRoot = path.resolve(__dirname, '..', '..') 10 | 11 | const config = getDefaultConfig(projectRoot) 12 | 13 | config.watchFolders = [workspaceRoot] 14 | config.resolver.nodeModulesPaths = [ 15 | path.resolve(projectRoot, 'node_modules'), 16 | path.resolve(workspaceRoot, 'node_modules'), 17 | ] 18 | 19 | config.transformer = config.transformer || {} 20 | config.transformer.minifierPath = require.resolve('metro-minify-terser') 21 | 22 | module.exports = config 23 | -------------------------------------------------------------------------------- /apps/expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-app", 3 | "version": "1.0.25", 4 | "dependencies": { 5 | "@expo-google-fonts/inter": "^0.2.3", 6 | "@expo/config": "~8.0.2", 7 | "@expo/config-plugins": "~6.0.0", 8 | "@expo/metro-config": "^0.7.1", 9 | "@react-navigation/native": "^6.1.6", 10 | "@react-navigation/native-stack": "^6.9.12", 11 | "@tamagui/font-inter": "^1.8.1", 12 | "app": "workspace:^", 13 | "dotenv": "^16.0.3", 14 | "expo": "^48.0.7", 15 | "expo-constants": "~14.2.1", 16 | "expo-dev-client": "~2.1.5", 17 | "expo-font": "~11.1.1", 18 | "expo-image": "1.0.0", 19 | "expo-linear-gradient": "~12.1.2", 20 | "expo-linking": "~4.0.1", 21 | "expo-router": "1.2.2", 22 | "expo-screen-orientation": "~5.1.1", 23 | "expo-splash-screen": "~0.18.1", 24 | "expo-status-bar": "~1.4.4", 25 | "expo-updates": "~0.16.3", 26 | "global": "4.4.0", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-native": "0.71.4", 30 | "react-native-gesture-handler": "~2.9.0", 31 | "react-native-safe-area-context": "4.5.0", 32 | "react-native-screens": "~3.20.0", 33 | "react-native-svg": "13.4.0", 34 | "react-native-web": "~0.18.12", 35 | "ui": "workspace:^" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.19.6", 39 | "@babel/runtime": "^7.18.9", 40 | "@dmeents/semantic-release-yarn": "^1.1.2", 41 | "@google/semantic-release-replace-plugin": "1.2.0", 42 | "@semantic-release/changelog": "^6.0.2", 43 | "@semantic-release/commit-analyzer": "^9.0.2", 44 | "@semantic-release/git": "^10.0.1", 45 | "@semantic-release/github": "^8.0.7", 46 | "@semantic-release/npm": "^9.0.2", 47 | "@semantic-release/release-notes-generator": "^10.0.3", 48 | "@tamagui/babel-plugin": "1.8.1", 49 | "@types/react": "~18.0.28", 50 | "babel-plugin-inline-dotenv": "^1.7.0", 51 | "babel-plugin-module-resolver": "^5.0.0", 52 | "babel-plugin-transform-inline-environment-variables": "^0.4.4", 53 | "eslint": "^8.21.0", 54 | "metro-minify-terser": "^0.74.1", 55 | "prettier": "latest", 56 | "semantic-release": "^20.1.3", 57 | "semantic-release-monorepo": "^7.0.5", 58 | "standard-version": "next", 59 | "standard-version-expo": "^1.0.3", 60 | "typescript": "5.0.2" 61 | }, 62 | "main": "index.js", 63 | "private": true, 64 | "scripts": { 65 | "build": "expo export", 66 | "build:all:dev": "npx dotenv -c development -- eas build --profile development --platform all", 67 | "build:all:preview": "npx dotenv -c preview -- eas build --profile preview --platform all", 68 | "build:all:staging": "npx dotenv -c staging -- eas build --profile staging --platform all", 69 | "build:android:dev": "npx dotenv -c development -- eas build --profile development --platform android", 70 | "build:android:preview": "npx dotenv -c preview -- eas build --profile preview --platform android", 71 | "build:android:staging": "npx dotenv -c staging -- eas build --profile staging --platform android", 72 | "build:ios:dev": "npx dotenv -c development -- eas build --profile development --platform ios", 73 | "build:ios:preview": "npx dotenv -c preview -- eas build --profile preview --platform ios", 74 | "build:ios:staging": "npx dotenv -c staging -- eas build --profile staging --platform ios", 75 | "fix:deps": "expo-cli doctor --fix-dependencies", 76 | "format": "prettier --write \"**/*.{ts,tsx}\" --config ../../.prettierrc", 77 | "format:check": "prettier -c \"**/*.{ts,tsx}\"", 78 | "lint": "eslint .", 79 | "lint:fix": "eslint . --fix", 80 | "semantic-release": "semantic-release -e semantic-release-monorepo", 81 | "start": "npx expo start", 82 | "start:android": "npx dotenv -c development -- npx expo run:android", 83 | "start:android:prod": "npx dotenv -c production -- npx expo run:android --variant release", 84 | "start:devclient": "npx dotenv -c development -- npx expo start --tunnel --dev-client --scheme dev.com.REPLACE-ME", 85 | "start:ios": "npx dotenv -c development -- npx expo run:ios", 86 | "start:ios:prod": "npx dotenv -c production -- npx expo run:ios --configuration Release", 87 | "start:prod": "npx dotenv -c production -- npx expo start --no-dev --minify", 88 | "start:tunnel": "npx dotenv -c development -- npx expo start --tunnel", 89 | "typecheck": "tsc -b" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /apps/expo/release.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | module.exports = { 3 | branches: [{ name: 'main' }], 4 | analyzeCommits: [ 5 | [ 6 | '@semantic-release/commit-analyzer', 7 | { 8 | preset: 'angular', 9 | releaseRules: [ 10 | { type: 'feat!', release: 'major' }, 11 | { type: 'feat', release: 'minor' }, 12 | { type: 'fix!', release: 'major' }, 13 | { type: 'fix', release: 'patch' }, 14 | { type: 'chore', release: 'patch' }, 15 | { type: 'ci', release: false }, 16 | { type: 'docs', release: false }, 17 | ], 18 | }, 19 | ], 20 | ], 21 | verifyConditions: ['@semantic-release/changelog', '@semantic-release/git'], 22 | generateNotes: [ 23 | [ 24 | '@semantic-release/release-notes-generator', 25 | { 26 | preset: 'conventionalcommits', 27 | presetConfig: { 28 | types: [ 29 | { 30 | type: 'feat!', 31 | section: ':boom: BREAKING CHANGE', 32 | hidden: false, 33 | }, 34 | { 35 | type: 'fix!', 36 | section: ':boom: BREAKING CHANGE', 37 | hidden: false, 38 | }, 39 | { 40 | type: 'feat', 41 | section: ':sparkles: Features', 42 | hidden: false, 43 | }, 44 | { 45 | type: 'fix', 46 | section: ':bug: Fixes', 47 | hidden: false, 48 | }, 49 | { 50 | type: 'docs', 51 | section: ':memo: Documentation', 52 | hidden: false, 53 | }, 54 | { 55 | type: 'ci', 56 | section: ':repeat: CI/CD', 57 | hidden: false, 58 | }, 59 | { 60 | type: 'chore', 61 | section: ':broom: Chore', 62 | hidden: false, 63 | }, 64 | ], 65 | }, 66 | }, 67 | ], 68 | ], 69 | prepare: [ 70 | [ 71 | '@google/semantic-release-replace-plugin', 72 | { 73 | replacements: [ 74 | { 75 | files: ['package.json'], 76 | from: "\"version\": \".*\"", // eslint-disable-line 77 | to: "\"version\": \"${nextRelease.version}\"", // eslint-disable-line 78 | }, 79 | { 80 | files: ['app.json'], 81 | from: "\"buildNumber\": \".*\"", // eslint-disable-line 82 | to: "\"buildNumber\": \"${nextRelease.version}\"", // eslint-disable-line 83 | }, 84 | { 85 | files: ['app.json'], 86 | from: `"versionCode": [^\n]*`, // eslint-disable-line 87 | to: (match) => `"versionCode": ${parseInt(match.split(':')[1].trim()) + 1}`, // eslint-disable-line 88 | }, 89 | ], 90 | }, 91 | ], 92 | '@semantic-release/changelog', 93 | [ 94 | '@dmeents/semantic-release-yarn', 95 | { 96 | npmPublish: false, 97 | changeVersion: true, 98 | tarballDir: 'dist', 99 | }, 100 | ], 101 | { 102 | path: '@semantic-release/git', 103 | assets: ['CHANGELOG.md', 'package.json', 'yarn.lock', 'app.json'], 104 | message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 105 | }, 106 | ], 107 | publish: [['@semantic-release/github']], 108 | success: false, 109 | fail: false, 110 | } 111 | -------------------------------------------------------------------------------- /apps/expo/tamagui.config.ts: -------------------------------------------------------------------------------- 1 | import config from 'app/src/tamagui.config' 2 | 3 | export default config 4 | -------------------------------------------------------------------------------- /apps/expo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/expo.json", 3 | "include": ["*.js", "*.ts", "*.tsx", "*.json"], 4 | "exclude": ["node_modules"], 5 | "references": [ 6 | { 7 | "path": "../../packages/ui" 8 | }, 9 | { 10 | "path": "../../packages/app" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/next/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['next', 'custom'], 4 | ignorePatterns: ['node_modules', '.next', '.turbo', '.tamagui', 'next.config.js', '.eslintrc.js'], 5 | parserOptions: { 6 | project: 'tsconfig.json', 7 | tsconfigRootDir: __dirname, 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | ecmaVersion: 2018, 12 | sourceType: 'module', 13 | }, 14 | overrides: [ 15 | { 16 | files: ['*.ts', '*.tsx', '*.js', '*.jsx'], 17 | rules: {}, 18 | }, 19 | { 20 | files: ['*.ts', '*.tsx'], 21 | rules: {}, 22 | }, 23 | { 24 | files: ['*.js', '*.jsx'], 25 | rules: {}, 26 | }, 27 | ], 28 | rules: {}, 29 | } 30 | -------------------------------------------------------------------------------- /apps/next/.gitignore: -------------------------------------------------------------------------------- 1 | .tamagui 2 | tsconfig.tsbuildinfo 3 | -------------------------------------------------------------------------------- /apps/next/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 12 | 13 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 14 | 15 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /apps/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited 4 | // see https://nextjs.org/docs/basic-features/typescript for more information. 5 | -------------------------------------------------------------------------------- /apps/next/next.config.js: -------------------------------------------------------------------------------- 1 | const { withTamagui } = require('@tamagui/next-plugin') 2 | const { join } = require('path') 3 | const withImages = require('next-images') 4 | const withTM = require('next-transpile-modules') // pass the modules you would like to see transpiled 5 | 6 | process.env.IGNORE_TS_CONFIG_PATHS = 'true' 7 | process.env.TAMAGUI_TARGET = 'web' 8 | process.env.TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD = '1' 9 | 10 | const boolVals = { 11 | true: true, 12 | false: false, 13 | } 14 | 15 | const disableExtraction = boolVals[process.env.DISABLE_EXTRACTION] ?? process.env.NODE_ENV === 'development' 16 | 17 | if (disableExtraction) { 18 | console.log('Disabling static extraction in development mode for better HMR') 19 | } 20 | 21 | const transpilePackages = [ 22 | 'solito', 23 | 'react-native-web', 24 | 'react-native', 25 | 'expo-linking', 26 | 'expo-constants', 27 | 'expo-modules-core', 28 | // 'expo-document-picker', 29 | // 'expo-av', 30 | // 'ui' 31 | // '@expo/vector-icons', 32 | ] 33 | 34 | const plugins = [ 35 | withTM(transpilePackages), 36 | withImages, 37 | withTamagui({ 38 | config: './tamagui.config.ts', 39 | components: ['ui', 'tamagui'], 40 | importsWhitelist: ['constants.js', 'colors.js'], 41 | logTimings: true, 42 | disableExtraction, 43 | shouldExtract: (path) => { 44 | if (path.includes(join('packages', 'app'))) { 45 | return true 46 | } 47 | }, 48 | useReactNativeWebLite: false, // if enabled dont need excludeReactNativeWebExports 49 | excludeReactNativeWebExports: ['Switch', 'ProgressBar', 'Picker', 'CheckBox', 'Touchable'], 50 | }), 51 | ] 52 | 53 | module.exports = function () { 54 | /** @type {import('next').NextConfig} */ 55 | let config = { 56 | typescript: { 57 | ignoreBuildErrors: true, 58 | }, 59 | images: { 60 | disableStaticImages: true, 61 | }, 62 | modularizeImports: { 63 | '@tamagui/lucide-icons': { 64 | transform: `@tamagui/lucide-icons/dist/esm/icons/{{kebabCase member}}`, 65 | skipDefaultConversion: true, 66 | }, 67 | 'tamagui-phosphor-icons': { 68 | transform: 'tamagui-phosphor-icons/dist/jsx/icons/icons/{{member}}', 69 | skipDefaultConversion: true, 70 | }, 71 | }, 72 | // transpilePackages: transpilePackages, 73 | experimental: { 74 | scrollRestoration: true, 75 | legacyBrowsers: false, 76 | }, 77 | } 78 | for (const plugin of plugins) { 79 | config = { 80 | ...config, 81 | ...plugin(config), 82 | } 83 | } 84 | return config 85 | } 86 | -------------------------------------------------------------------------------- /apps/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@fontsource/inter": "^4.5.15", 6 | "@tamagui/core": "^1.8.1", 7 | "@tamagui/next-theme": "1.8.1", 8 | "app": "workspace:^", 9 | "next": "13.2.4", 10 | "raf": "^3.4.1", 11 | "react": "^18.2.0", 12 | "react-native": "^0.71.4", 13 | "react-native-web": "^0.18.12", 14 | "react-native-web-linear-gradient": "^1.1.2", 15 | "react-native-web-lite": "^1.8.1", 16 | "ui": "workspace:^" 17 | }, 18 | "devDependencies": { 19 | "@tamagui/next-plugin": "^1.8.1", 20 | "@types/node": "^18.6.5", 21 | "critters": "^0.0.16", 22 | "eslint": "^8.21.0", 23 | "eslint-config-next": "^13.2.4", 24 | "file-loader": "^6.2.0", 25 | "html-webpack-plugin": "^5.5.0", 26 | "next-images": "^1.8.4", 27 | "next-transpile-modules": "^10.0.0", 28 | "prettier": "latest", 29 | "typescript": "^5.0.2", 30 | "url-loader": "^4.1.1" 31 | }, 32 | "private": true, 33 | "scripts": { 34 | "build": "next build", 35 | "format": "prettier --write \"**/*.{ts,tsx}\" --config ../../.prettierrc", 36 | "format:check": "prettier -c \"**/*.{ts,tsx}\"", 37 | "lint": "eslint .", 38 | "lint:fix": "eslint . --fix", 39 | "serve": "NODE_ENV=production next start --port 8151", 40 | "start": "next dev", 41 | "typecheck": "tsc -b" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@tamagui/core/reset.css' 2 | // import '@tamagui/font-inter/css/400.css' 3 | // import '@tamagui/font-inter/css/700.css' 4 | import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme' 5 | import { Provider } from 'app/src/provider' 6 | import { useThemeState } from 'app/src/state/themeState' 7 | import Head from 'next/head' 8 | import React, { useEffect, useMemo } from 'react' 9 | import type { SolitoAppProps } from 'solito' 10 | import '@fontsource/inter/400.css' 11 | import '@fontsource/inter/700.css' 12 | 13 | import 'raf/polyfill' 14 | 15 | function MyApp({ Component, pageProps }: SolitoAppProps) { 16 | const [theme, setTheme] = useRootTheme() 17 | const { name } = useThemeState() 18 | useEffect(() => { 19 | if (name) { 20 | setTheme(name) 21 | } 22 | }, [name, setTheme]) 23 | const contents = useMemo(() => , [pageProps, Component]) 24 | 25 | return ( 26 | <> 27 | 28 | Tamagui Example App 29 | 30 | 31 | 32 | 33 | 34 | {contents} 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | export default MyApp 42 | -------------------------------------------------------------------------------- /apps/next/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import NextDocument, { Head, Html, Main, NextScript } from 'next/document' 2 | import { Children } from 'react' 3 | import { AppRegistry } from 'react-native' 4 | 5 | import Tamagui from '../tamagui.config' 6 | 7 | export default class Document extends NextDocument { 8 | static async getInitialProps({ renderPage }: any) { 9 | AppRegistry.registerComponent('Main', () => Main) 10 | const page = await renderPage() 11 | 12 | // @ts-ignore 13 | const { getStyleElement } = AppRegistry.getApplication('Main') 14 | const styles = [ 15 | getStyleElement(), 16 |