├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── README.md ├── docs ├── firebase-versions.md ├── nx-firebase-applications.md ├── nx-firebase-databases.md ├── nx-firebase-emulators.md ├── nx-firebase-functions-environment.md ├── nx-firebase-functions.md ├── nx-firebase-hosting.md ├── nx-firebase-migrations.md ├── nx-firebase-project-structure.md ├── nx-firebase-projects.md ├── nx-firebase-sync.md ├── nx-libraries.md ├── nx-migration.md ├── nx-plugin-commands.md ├── nx-setup-mac.md ├── nx-workspace-layout.md └── user-guide.md ├── e2e ├── compat │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── config.ts │ │ │ ├── setup.ts │ │ │ ├── test.ts │ │ │ ├── utils │ │ │ │ ├── cache.ts │ │ │ │ ├── cwd.ts │ │ │ │ ├── exec.ts │ │ │ │ ├── jest-ish.ts │ │ │ │ ├── log.ts │ │ │ │ └── utils.ts │ │ │ ├── versions.ts │ │ │ └── workspace.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── nx-firebase-e2e │ ├── jest.config.js │ ├── project.json │ ├── test-utils │ ├── index.ts │ ├── test-utils-apps.ts │ ├── test-utils-commands.ts │ ├── test-utils-functions.ts │ ├── test-utils-helpers.ts │ ├── test-utils-imports.ts │ ├── test-utils-logger.ts │ └── test-utils-project-data.ts │ ├── tests │ ├── nx-firebase.spec.ts │ ├── test-application.ts │ ├── test-bundler.ts │ ├── test-function.ts │ ├── test-libraries.ts │ ├── test-migrate.ts │ ├── test-sync.ts │ ├── test-targets.ts │ └── test-workspace.ts │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.ts ├── jest.preset.js ├── migrations.json ├── nx.json ├── package.json ├── packages └── nx-firebase │ ├── .eslintrc.json │ ├── README.md │ ├── executors.json │ ├── generators.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── __generated__ │ │ └── nx-firebase-versions.ts │ ├── executors │ │ └── serve │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ ├── serve.spec.ts │ │ │ └── serve.ts │ ├── generators │ │ ├── application │ │ │ ├── application.spec.ts │ │ │ ├── application.ts │ │ │ ├── files │ │ │ │ ├── database.rules.json │ │ │ │ ├── environment │ │ │ │ │ ├── .env │ │ │ │ │ ├── .env.local │ │ │ │ │ └── .secret.local │ │ │ │ ├── firestore.indexes.json │ │ │ │ ├── firestore.rules │ │ │ │ ├── public │ │ │ │ │ ├── 404.html │ │ │ │ │ └── index.html │ │ │ │ ├── readme.md__tmpl__ │ │ │ │ └── storage.rules │ │ │ ├── files_firebase │ │ │ │ └── firebase.json__tmpl__ │ │ │ ├── files_firebaserc │ │ │ │ └── .firebaserc__tmpl__ │ │ │ ├── files_workspace │ │ │ │ └── firebase.__projectName__.json__tmpl__ │ │ │ ├── lib │ │ │ │ ├── create-files.ts │ │ │ │ └── index.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── function │ │ │ ├── files │ │ │ │ ├── package.json__tmpl__ │ │ │ │ ├── readme.md__tmpl__ │ │ │ │ └── src │ │ │ │ │ └── main.ts__tmpl__ │ │ │ ├── function.spec.ts │ │ │ ├── function.ts │ │ │ ├── lib │ │ │ │ ├── add-function-config.ts │ │ │ │ ├── create-files.ts │ │ │ │ ├── delete-files.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-project.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── init │ │ │ ├── init.spec.ts │ │ │ ├── init.ts │ │ │ ├── lib │ │ │ │ ├── add-dependencies.ts │ │ │ │ ├── add-git-ignore-entry.ts │ │ │ │ └── index.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── migrate │ │ │ ├── migrate.spec.ts │ │ │ ├── migrate.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ └── sync │ │ │ ├── lib │ │ │ ├── firebase-changes.ts │ │ │ ├── firebase-configs.ts │ │ │ ├── firebase-projects.ts │ │ │ ├── firebase-workspace.ts │ │ │ ├── index.ts │ │ │ ├── tags.ts │ │ │ └── update-targets.ts │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ ├── sync.spec.ts │ │ │ └── sync.ts │ ├── index.ts │ ├── types │ │ ├── index.ts │ │ └── lib │ │ │ ├── firebase-config.ts │ │ │ ├── firebase-function.ts │ │ │ └── firebase-workspace.ts │ └── utils │ │ ├── debug.ts │ │ ├── firebase-config.ts │ │ ├── index.ts │ │ ├── project-name.ts │ │ ├── update-tsconfig.ts │ │ └── workspace.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── pnpm-lock.yaml ├── tools ├── generate-package-versions.js └── tsconfig.tools.json └── tsconfig.base.json /.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 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx", "prettier", "unused-imports"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": { 28 | "@typescript-eslint/ban-types": "warn", 29 | "@typescript-eslint/no-var-requires": "warn", 30 | "@typescript-eslint/no-inferrable-types": "warn", 31 | "@typescript-eslint/no-empty-interface": "warn", 32 | "@typescript-eslint/no-empty-function": "warn", 33 | "no-prototype-builtins": "warn", 34 | "no-useless-escape": "warn", 35 | "no-inner-declarations": "off", 36 | "no-fallthrough": "warn", 37 | "no-irregular-whitespace": "warn", 38 | "no-constant-condition": "warn", 39 | "@typescript-eslint/require-await": "warn", 40 | "@typescript-eslint/no-floating-promises": "error", 41 | "@typescript-eslint/await-thenable": "error", 42 | "@typescript-eslint/no-namespace": "warn", 43 | "@typescript-eslint/no-this-alias": "warn", 44 | "@typescript-eslint/no-duplicate-imports": "error", 45 | "@typescript-eslint/no-unused-vars": "off", 46 | "unused-imports/no-unused-imports": "warn", 47 | "unused-imports/no-unused-vars": [ 48 | "warn", 49 | { 50 | "vars": "all", 51 | "varsIgnorePattern": "^_", 52 | "args": "after-used", 53 | "argsIgnorePattern": "^_" 54 | } 55 | ] 56 | } 57 | }, 58 | { 59 | "files": ["*.ts", "*.tsx"], 60 | "extends": ["plugin:prettier/recommended"], 61 | "rules": { 62 | "prettier/prettier": "warn" 63 | } 64 | }, 65 | { 66 | "files": ["*.js", "*.jsx"], 67 | "extends": ["plugin:@nx/javascript"], 68 | "rules": {} 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: simondotm 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | ignore: 13 | # Ignore updates to these packages: 14 | - dependency-name: "@nx*" 15 | - dependency-name: "@swc*" 16 | - dependency-name: "@types/*" 17 | - dependency-name: "@typescript*" 18 | - dependency-name: "eslint*" 19 | - dependency-name: "jest*" 20 | - dependency-name: "nx*" 21 | - dependency-name: "prettier*" 22 | - dependency-name: "ts-jest" 23 | - dependency-name: "ts-node" 24 | - dependency-name: "tslib" 25 | - dependency-name: "typescript" 26 | # For all packages, ignore all patch updates 27 | # EDIT: Actually, we'll take all updates, but this is a good example 28 | # - dependency-name: "*" 29 | # update-types: ["version-update:semver-patch"] 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Build, Test, Lint & e2e pushes. 2 | name: CI 3 | 4 | on: 5 | # support manual trigger of this workflow 6 | workflow_dispatch: 7 | 8 | # run this build action for all pushes to main 9 | push: 10 | branches: ['main'] 11 | 12 | # also run for pull requests that target main 13 | pull_request: 14 | branches: 15 | - main 16 | 17 | jobs: 18 | setup: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v3 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | cache: 'pnpm' 27 | - run: pnpm install 28 | 29 | build: 30 | needs: [setup] 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: pnpm/action-setup@v3 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version-file: '.nvmrc' 38 | cache: 'pnpm' 39 | - run: pnpm install 40 | - run: pnpm run build 41 | 42 | lint: 43 | needs: [setup] 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: pnpm/action-setup@v3 48 | - uses: actions/setup-node@v4 49 | with: 50 | node-version-file: '.nvmrc' 51 | cache: 'pnpm' 52 | - run: pnpm install 53 | - run: pnpm run lint 54 | 55 | test: 56 | needs: [setup] 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: pnpm/action-setup@v3 61 | - uses: actions/setup-node@v4 62 | with: 63 | node-version-file: '.nvmrc' 64 | cache: 'pnpm' 65 | - run: pnpm install 66 | - run: pnpm run test 67 | 68 | # e2e might be too much work todo for every push, lets see. 69 | e2e: 70 | needs: [setup] 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: pnpm/action-setup@v3 75 | - uses: actions/setup-node@v4 76 | with: 77 | node-version-file: '.nvmrc' 78 | cache: 'pnpm' 79 | - run: pnpm install 80 | - run: pnpm run e2e 81 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Publish package to npm when a Github release is published 2 | name: Publish 3 | 4 | on: 5 | release: 6 | types: [released] 7 | 8 | jobs: 9 | verify_tag: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - run: | 14 | TAG_VERSION=${GITHUB_REF#refs/tags/v} 15 | PACKAGE_VERSION=$(node -p "require('./packages/nx-firebase/package.json').version") 16 | echo "Tag version: $TAG_VERSION" 17 | echo "Package version: $PACKAGE_VERSION" 18 | if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then 19 | echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)" 20 | exit 1 21 | fi 22 | shell: bash 23 | 24 | publish_github: 25 | needs: [verify_tag] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: pnpm/action-setup@v3 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version-file: '.nvmrc' 33 | cache: 'pnpm' 34 | registry-url: 'https://npm.pkg.github.com/simondotm' 35 | #scope: '@simondotm' 36 | #- run: npm run addscope 37 | - run: pnpm install 38 | 39 | - name: Build plugin 40 | run: npx nx build nx-firebase 41 | 42 | - name: Create package 43 | run: pnpm pack 44 | working-directory: ./dist/packages/nx-firebase 45 | 46 | - name: Publish to GitHub Packages 47 | run: pnpm publish --no-git-checks 48 | working-directory: ./dist/packages/nx-firebase 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | publish_npm: 53 | needs: [verify_tag] 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: pnpm/action-setup@v3 58 | - uses: actions/setup-node@v4 59 | with: 60 | node-version-file: '.nvmrc' 61 | cache: 'pnpm' 62 | registry-url: 'https://registry.npmjs.org' 63 | - run: pnpm install 64 | 65 | - name: Build plugin 66 | run: npx nx build nx-firebase 67 | 68 | - name: Publish to NPM 69 | run: pnpm publish --no-git-checks --access=public 70 | working-directory: ./dist/packages/nx-firebase 71 | env: 72 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | 42 | e2e.log 43 | 44 | .nx/cache 45 | 46 | .nx-firebase 47 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.13.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "printWidth": 80, 7 | "bracketSpacing": true, 8 | "endOfLine": "auto", 9 | "useTabs": false, 10 | "quoteProps": "as-needed", 11 | "tabWidth": 2 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["json"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build -- nx-firebase", 7 | "group": "build", 8 | "problemMatcher": [], 9 | "label": "Build Nx firebase plugin", 10 | "detail": "nx build nx-firebase" 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "test -- nx-firebase", 15 | "group": "test", 16 | "problemMatcher": [], 17 | "label": "Test Nx firebase plugin", 18 | "detail": "nx test nx-firebase" 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "e2e -- nx-firebase-e2e", 23 | "group": "test", 24 | "problemMatcher": [], 25 | "label": "Test e2e Nx firebase plugin", 26 | "detail": "nx e2e nx-firebase-e2e" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @simondotm/nx-firebase ![actions](https://github.com/simondotm/nx-firebase/actions/workflows/ci.yml/badge.svg) ![nx](https://img.shields.io/badge/Nx-v16.8.1-blue) ![npm](https://img.shields.io/npm/v/@simondotm/nx-firebase) ![downloads](https://img.shields.io/npm/dw/@simondotm/nx-firebase.svg) 2 | 3 | A plugin for [Nx](https://nx.dev) v16.8.1+ that integrates Firebase workflows in an Nx monorepo workspace. 4 | 5 | * Easily generate Firebase applications and functions 6 | * Uses `esbuild` for fast Firebase function builds so you can easily create & import shared Nx libraries with the benefits of tree-shaking 7 | * Supports function environment variables and secrets 8 | * Supports single or multiple firebase projects/apps within an Nx workspace 9 | * Full support for the Firebase Emulator suite for local development, with watch mode for functions 10 | * Keeps your `firebase.json` configurations in sync when renaming or deleting Firebase apps & functions 11 | * Only very lightly opinionated about your Firebase configurations and workspace layouts; you can use Nx or the Firebase CLI 12 | 13 | See [CHANGELOG](https://github.com/simondotm/nx-firebase/blob/main/CHANGELOG.md) for release notes. 14 | 15 | ## Install Plugin 16 | 17 | **`npm install @simondotm/nx-firebase --save-dev`** 18 | 19 | - Installs this plugin into your Nx workspace 20 | - This will also install `@nx/node` and firebase SDK's to your root workspace `package.json` if they are not already installed 21 | 22 | ## Generate Firebase Application 23 | 24 | **`nx g @simondotm/nx-firebase:app my-new-firebase-app [--directory=dir] [--project=proj]`** 25 | 26 | - Generates a new Nx Firebase application project in the workspace 27 | - The app generator will also create a Firebase configuration file in the root of your workspace (along with a default `.firebaserc` and `firebase.json` if they don't already exist) 28 | - For the first firebase application you create, the project firebase configuration will be `firebase.json` 29 | - If you create additional firebase applications, the project firebase configuration will be `firebase..json` 30 | - Use `--project` to link your Firebase App to a Firebase project name in your `.firebaserc` file 31 | 32 | ## Generate Firebase Function 33 | 34 | **`nx g @simondotm/nx-firebase:function my-new-firebase-function --app=my-new-firebase-app [--directory=dir]`** 35 | 36 | - Generates a new Nx Firebase function application project in the workspace 37 | - Firebase Function projects must be linked to a Firebase application project with the `--app` option 38 | - Firebase Function projects can contain one or more firebase functions 39 | - You can generate as many Firebase Function projects as you need for your application 40 | 41 | ## Build 42 | 43 | **`nx build my-new-firebase-app`** 44 | 45 | - Compiles & builds all Firebase function applications linked to the Nx Firebase application or an individual function 46 | 47 | **`nx build my-new-firebase-function`** 48 | 49 | - Compiles & builds an individual function 50 | 51 | 52 | ## Serve 53 | 54 | **`nx serve my-new-firebase-app`** 55 | 56 | - Builds & Watches all Firebase functions apps linked to the Firebase application 57 | - Starts the Firebase emulators 58 | 59 | ## Deploy 60 | 61 | ### Firebase Application 62 | 63 | **`nx deploy my-new-firebase-app [--only ...]`** 64 | 65 | - By default, deploys ALL of your cloud resources associated with your Firebase application (eg. sites, functions, database rules etc.) 66 | - Use the `--only` option to selectively deploy (same as Firebase CLI) 67 | 68 | For initial deployment: 69 | 70 | - **`firebase login`** if not already authenticated 71 | - **`firebase use --add`** to add your Firebase Project(s) to the `.firebaserc` file in your workspace. This step must be completed before you can deploy anything to Firebase. 72 | 73 | Note that you can also use the firebase CLI directly if you prefer: 74 | 75 | - **`firebase deploy --config=firebase..json --only functions`** 76 | 77 | ### Firebase Function 78 | 79 | **`nx deploy my-new-firebase-function`** 80 | 81 | - Deploys only a specific Firebase function 82 | 83 | 84 | 85 | ## Test 86 | 87 | **`nx test my-new-firebase-app`** 88 | 89 | - Runs unit tests for all Firebase functions apps linked to the Firebase application 90 | 91 | **`nx test my-new-firebase-function`** 92 | 93 | - Runs unit tests for an individual function 94 | 95 | 96 | ## Lint 97 | 98 | **`nx lint my-new-firebase-app`** 99 | 100 | - Runs linter for all Firebase functions apps linked to the Firebase application or an individual function 101 | 102 | **`nx lint my-new-firebase-function`** 103 | 104 | - Runs linter for an individual function 105 | 106 | ## Sync Workspace 107 | 108 | **`nx g @simondotm/nx-firebase:sync`** 109 | 110 | - Ensures that your `firebase.json` configurations are kept up to date with your workspace 111 | - If you rename or move firebase application or firebase function projects 112 | - If you delete firebase function projects 113 | 114 | ## Further Information 115 | 116 | See the full plugin [User Guide](https://github.com/simondotm/nx-firebase/blob/main/docs/user-guide.md) for more details. -------------------------------------------------------------------------------- /docs/nx-firebase-applications.md: -------------------------------------------------------------------------------- 1 | # Nx Firebase Applications 2 | 3 | An Nx Firebase application project is a top-level container for the various configurations for Firebase features you might wish to use such as _functions_, _storage, firestore, and real time database_. 4 | 5 | You don't have to use all of these features, but the Nx-Firebase plugin ensures they are all there if/when you do. 6 | 7 | Generate a new Firebase application using: 8 | 9 | **`nx g @simondotm/nx-firebase:application`** 10 | 11 | OR 12 | 13 | **`nx g @simondotm/nx-firebase:app`** 14 | 15 | | Options | Type | Description | 16 | | ----------------- | -------- | ------------------------------------------------------------------ | 17 | | `name` | required | the project name for your firebase app | 18 | | `--directory=dir` | optional | the directory this app will be located in | 19 | | `--project=proj` | optional | the `--project` option that will be used for firebase CLI commands | 20 | | `--projectNameAndRootFormat` | optional | `derived` or `as-provided` see [projectNameAndRootFormat](https://nx.dev/nx-api/node/generators/application#projectnameandrootformat) | 21 | 22 | ## About Firebase Apps 23 | 24 | Firebase app projects are a customised Nx project. 25 | 26 | When a new Nx Firebase application project is generated in the workspace it will create: 27 | 28 | **Within the application folder:** 29 | 30 | - Default `firestore.indexes` for Firestore database indexes 31 | - Default `firestore.rules` for Firestore database rules 32 | - Default `database.rules.json` for Firebase realtime database 33 | - Default `storage.rules` for Firebase storage rules 34 | - Default `public/index.html` for Firebase hosting - _you can delete this if your firebase configuration for hosting points elsewhere_. 35 | - Default `public/404.html` for Firebase hosting - _you can delete this if your firebase configuration for hosting points elsewhere_. 36 | - Default [environment variables](./nx-firebase-functions-environment.md) for your firebase functions 37 | 38 | **And in the workspace root:** 39 | 40 | - A `firebase.json` configuration file for the Firebase application 41 | - This is preset with references to the various configuration files in the application folder 42 | - The plugin supports multiple firebase projects in one workspace, so if other firebase applications already exist in the workspace, the firebase configuration file will be named `firebase..json` 43 | 44 | **It will also generate:** 45 | 46 | - A default/empty `.firebaserc` in the root of the workspace (if it doesn't already exist) 47 | 48 | You should use `npx firebase --add` to register your [projects & aliases](nx-firebase-projects.md) in the `.firebaserc`. 49 | 50 | ## Nx-Firebase Application Project Targets 51 | 52 | These targets will be generated in `project.json` for your new Firebase application: 53 | 54 | - `build` - Build all Firebase function applications linked to this Firebase application (if any) 55 | - `serve` - Build all functions in `watch` mode and start the Firebase Emulators 56 | - `deploy` - Run the Firebase CLI `deploy` command with the application's Firebase configuration. This target accepts forwarded command line options. 57 | - `lint` - Lint all Firebase function applications linked to this Firebase application 58 | - `test` - Run Jest unit tests for all Firebase function applications linked to this Firebase application 59 | - `getconfig` - Fetch the firebase remote config 60 | - `firebase` - Run the firebase CLI with the appropriate firebase `--config` and firebase `--project` parameters automatically provided 61 | -------------------------------------------------------------------------------- /docs/nx-firebase-databases.md: -------------------------------------------------------------------------------- 1 | # Firebase Databases 2 | 3 | `nx-firebase` assumes you may wish to use either realtime and/or firestore database. 4 | 5 | ## Rules & Indexes 6 | 7 | To keep the root of your Nx workspace tidy, the Nx-Firebase plugin puts default Firebase rules and index files inside the nx-firebase appliocation folder, and the `firebase.json` or `firebase..json` configuration file simply points to them there. 8 | 9 | Again, this works just fine with the usual Firebase CLI command, eg: 10 | 11 | **`firebase deploy --only firestore:rules --config firebase.appname.json`** 12 | 13 | Or 14 | 15 | **`nx deploy appname --only firestore:rules`** 16 | 17 | This is also useful for cleaner separation if you have multiple Firebase projects in your Nx workspace. 18 | 19 | Again, you are free to modify these locations or remove these cloud features if you wish by simply changing the Firebase configuration files; the `nx-firebase` plugin does not use these configuration files in any way. 20 | -------------------------------------------------------------------------------- /docs/nx-firebase-emulators.md: -------------------------------------------------------------------------------- 1 | # Using Firebase Emulators 2 | 3 | The Firebase emulators work well within Nx and `nx-firebase`. 4 | 5 | To locally develop with the Firebase emulator suite: 6 | 7 | - **`nx serve `** 8 | 9 | This will launch the Firebase emulators using the app's firebase configuration 10 | 11 | It will also build all functions attached to this app in watch mode, so that any changes you make to your function code will be automatically be rebuilt and reloaded by the emulator. 12 | 13 | ## Nx Issue with Firebase Emulator 14 | 15 | Due to a bug in Nx, which does not correctly pass process termination signals to child processes, CTRL+C after `serve` does not shutdown the emulator properly. 16 | 17 | For this reason, `serve` uses the `kill-port` npm package to ensure the emulator is properly shutdown before re-running `serve`. 18 | 19 | Unfortunately this means the emulator will not properly export data on exit either. 20 | 21 | The Nx issue is discussed [here](https://github.com/simondotm/nx-firebase/issues/40). 22 | 23 | Hopefully Nx will address this issue in future releases. 24 | 25 | ## Emulator workaround 26 | 27 | Until Nx fix this problem, we are using an experimental executor in the plugin as a interim workaround: 28 | 29 | The `serve` target in Firebase app `project.json` configurations is using: 30 | 31 | - `@simondotm/nx-firebase:serve` 32 | 33 | instead of 34 | 35 | - `nx:run-commands` 36 | 37 | This custom executor handles CTRL+C in a way that ensures the Firebase Emulator shuts down cleanly. 38 | 39 | With this executor you can pass extra CLI parameters for example: 40 | 41 | - `nx serve myfirebaseapp --only functions` - only serve functions in the Emulator 42 | -------------------------------------------------------------------------------- /docs/nx-firebase-functions-environment.md: -------------------------------------------------------------------------------- 1 | # Firebase Function Environment Variables 2 | 3 | ## Overview 4 | 5 | Firebase functions can make use of environment variables in their runtime. 6 | 7 | The Nx-firebase [application generator](./nx-firebase-applications.md) will automatically create default function environment files for you to use: 8 | 9 | - `environment/.env` 10 | - `environment/.env.local` 11 | - `environment/.secret.local` 12 | 13 | If you run the `getconfig` target, it will place `.runtimeconfig.json` file in the `environment` folder also. 14 | 15 | These environment files are considered common to all of your functions by nx-firebase, and are copied from your firebase app's `environment` folder to your function's `dist` folder when the function is built. 16 | 17 | This ensures they are available for deployment and emulation. 18 | 19 | All functions share the same environment variable files. 20 | 21 | ## Environment file types 22 | 23 | | File | Description | Git Ignored | Deployed | 24 | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | 25 | | `.env` | [General environment variables for functions](https://firebase.google.com/docs/functions/config-env?gen=2nd#env-variables) | - | Yes | 26 | | `.env.local` | [Environment variable overrides for function emulation](https://firebase.google.com/docs/functions/config-env?gen=2nd#emulator_support) | - | - | 27 | | `.env.` | [Environment variable overrides for specific deployment targets (eg. dev/prod)](https://firebase.google.com/docs/functions/config-env?gen=2nd#deploying_multiple_sets_of_environment_variables) | - | Yes | 28 | | `.secret.local` | [Secrets only for function emulation](https://firebase.google.com/docs/functions/config-env?gen=2nd#secrets_and_credentials_in_the_emulator) | Yes | - | 29 | | `.runtimeconfig.json` | [Function configurations](https://firebase.google.com/docs/cli#functions-commands) | Yes | - | 30 | 31 | > _Note that the Firebase team appear to be deprecating the use of `.runtimeconfig.json` function configs and recommending migration to dotenv environment variables._ 32 | 33 | ## Deployed Files 34 | 35 | The firebase CLI will deploy `.env` and/or `.env.` files along with function code, and they can be version controlled. 36 | 37 | ## Non-deployed files 38 | 39 | `.env.local` and `.secret.local` files are excluded from deployment by using an `functions.ignore` rule in `firebase.json`. 40 | 41 | ## Local Files 42 | 43 | `.secret.local` and `.env.local` are only used by the Firebase emulator suite. 44 | 45 | `.secret.local` and `.runtimeconfig.json` should not be version controlled, and are both git ignored, but included in Nx dependency graph using a `.nxignore` override. 46 | -------------------------------------------------------------------------------- /docs/nx-firebase-hosting.md: -------------------------------------------------------------------------------- 1 | # Firebase Hosting 2 | 3 | If you have one or more other web apps (Angular/React/HTML) that are deployed to a hosting site on your Firebase project, simply add them to your workspace as usual using the standard `nx g` app generators. 4 | 5 | Then just update your `firebase.json` or `firebase.appname.json` [hosting configuration](https://firebase.google.com/docs/hosting/full-config) to point to the `dist/apps/` where your web app build output is. 6 | 7 | You can then run the Firebase CLI as usual to deploy the site: 8 | 9 | **`firebase deploy --only hosting --config firebase.appname.json`** 10 | 11 | Or 12 | 13 | **`nx deploy appname --only hosting`** 14 | 15 | ## Static Sites 16 | 17 | If you deploy static websites to Firebase Hosting (that do not need to get built by Nx), just create a folder in your `apps` directory (rather than generate an Nx web app) and put your content in that folder. Then update the `hosting` section of your `firebase.json` or `firebase.appname.json` to simply point directly to this folder. 18 | 19 | The firebase CLI hosting deploy command above will just upload the static content as required. 20 | 21 | An application generated by Nx-Firebase is by default configured to host content in the `apps/appname/public` directory. 22 | -------------------------------------------------------------------------------- /docs/nx-firebase-migrations.md: -------------------------------------------------------------------------------- 1 | # Nx-Firebase Plugin Migrations 2 | 3 | Newer versions of the plugin occasionally need to update the workspace configurations, and this page documents the strategies available across these versions. 4 | 5 | Please note that these migrations are provided on a 'best effort' basis, due to the fact that workspaces are quite complex and often customised. 6 | 7 | - [Nx-Firebase Plugin Migrations](#nx-firebase-plugin-migrations) 8 | - [Migrating to plugin v2.1 from v2.0](#migrating-to-plugin-v21-from-v20) 9 | - [Migration to plugin v2.x from v1.x](#migration-to-plugin-v2x-from-v1x) 10 | - [1. Workspace Migration](#1-workspace-migration) 11 | - [2. Firebase Project Migration](#2-firebase-project-migration) 12 | - [3. Firebase Application Migration](#3-firebase-application-migration) 13 | - [4. Firebase Functions Migration](#4-firebase-functions-migration) 14 | - [5. Library updates](#5-library-updates) 15 | - [6. Check Migration](#6-check-migration) 16 | 17 | ## Migrating from plugin v2.0.0 to v2.1.0 18 | 19 | Plugin version 2.1 [added some new features](../CHANGELOG.md#v210) that required changes to the project configurations. 20 | 21 | To help with this & future updates, an automatic migration generator has been added: 22 | 23 | - Update to the latest plugin using `npm i @simondotm/nx-firebase@latest --save-dev` 24 | - Run **npx nx g @simondotm/nx-firebase:migrate** 25 | 26 | This tool will run checks on your workspace firebase apps, functions and configurations and try to ensure they are correctly configured for compatibility with the plugin version you are using. 27 | 28 | > Please note, this generator is not the same as Nx's own migration tool, so always review the changes it makes to ensure they are appropriate for your workspace. 29 | 30 | ## Migration from plugin v1.x to v2.0.0 31 | 32 | Version 2.x of this plugin has been completely rewritten, and uses a completely new approach to building & deploying, so migrating an existing Nx workspace using V1 of the plugin to use V2 requires some manual migration procedures. 33 | 34 | ### 1. Workspace Migration 35 | 36 | - First of all, your workspace will need to be migrated to at least Nx 16.1.1. 37 | 38 | - Next, update the `@simondotm/nx-firebase` plugin package to the latest v2.x version. 39 | 40 | ### 2. Firebase Project Migration 41 | 42 | Run the following steps 3-5 in order, for each separate Firebase application project in your workspace. 43 | 44 | ### 3. Firebase Application Migration 45 | 46 | - Next, you can either [generate a new firebase v2 application](./nx-firebase-applications.md) project and copy across your Firebase rules, indexes, storage rules etc. to the new firebase application project folder 47 | 48 | OR 49 | 50 | - you can manually modify your existing Firebase `project.json` to be structured [as shown here](./nx-firebase-project-structure.md#firebase-applications). 51 | 52 | ### 4. Firebase Functions Migration 53 | 54 | If you are using Firebase functions in your project, the migration process is as follows: 55 | 56 | - Update the `"functions"` section in the `firebase.json` or `firebase.project-name.json` config file for your firebase application project to be set to `"functions": []` 57 | 58 | - Generate a new [Firebase functions application](./nx-firebase-functions.md) in your workspace, using the `--app` parameter set to the name of your Firebase application above 59 | 60 | - Move the source code from your old Nx Firebase application `functions/src` folder to the `src` folder in the newly created Firebase function application 61 | 62 | - You will need to rename your entry point source file from `index.ts` to `main.ts` 63 | - If you migrated your Firebase application in place (as described above), you will need to manually delete the following from your Firebase application folder: 64 | 65 | - `src` folder 66 | - All `tsconfig.*.json` files 67 | - `package.json` file 68 | 69 | - OR, if you migrated by creating a new firebase application, you can now simply delete the old v1 Firebase application project 70 | 71 | Check the [Nx-Firebase project schemas](./nx-firebase-project-structure.md) document for more information about the v2 plugin generators project layouts. 72 | 73 | ### 5. Library updates 74 | 75 | The previous version of the plugin required that all Nx libraries imported by firebase functions were buildable. 76 | 77 | With v2 of the plugin, this is no longer the case and Nx libraries can be buildable or non-buildable, since `esbuild` builds from Typescript source files, not compiled JS. 78 | 79 | Nx Typescript libraries can be converted to non-buildable by simply removing the `build` target from their `project.json` files. 80 | 81 | ### 6. Check Migration 82 | 83 | Run `nx build your-firebase-project-name` to compile & bundle your functions. 84 | 85 | Run `nx deploy your-firebase-project-name` to deploy your project. 86 | -------------------------------------------------------------------------------- /docs/nx-firebase-projects.md: -------------------------------------------------------------------------------- 1 | # Firebase CLI Projects 2 | 3 | - [Firebase CLI Projects](#firebase-cli-projects) 4 | - [Introduction](#introduction) 5 | - [Nx Workspaces With Single Firebase Projects](#nx-workspaces-with-single-firebase-projects) 6 | - [Nx Workspaces With Multiple Firebase Projects](#nx-workspaces-with-multiple-firebase-projects) 7 | - [Updating Firebase Configurations](#updating-firebase-configurations) 8 | - [Binding Nx projects to Firebase projects](#binding-nx-projects-to-firebase-projects) 9 | - [Deployment Environments](#deployment-environments) 10 | 11 | ## Introduction 12 | 13 | [Firebase projects](https://firebase.google.com/docs/projects/learn-more) are created in the firebase web console, and then specified in your local workspace configurations as deployment targets in your `.firebaserc` file. 14 | 15 | `nx-firebase` assumes a mapping of one `@simondotm/nx-firebase:app` to one firebase CLI project and one [Firebase configuration file](./nx-firebase-project-structure.md#firebase-function-configs). 16 | 17 | ## Nx Workspaces With Single Firebase Projects 18 | 19 | The first time you run `nx g @simondotm/nx-firebase:app` in an Nx workspace, it will generate a firebase configuration file called `firebase.json` in the workspace root. If you only use a single Firebase project in your Nx workspace, this will be all you need. 20 | 21 | The Firebase CLI will use this configuration file by default, and in this scenario there's no need to pass the additional `--config` CLI option. 22 | 23 | ## Nx Workspaces With Multiple Firebase Projects 24 | 25 | This plugin supports multiple Firebase Applications/Projects inside one Nx workspace. 26 | 27 | If you run `nx g @simondotm/nx-firebase:app` in an Nx workspace that already has a `firebase.json` configuration file, it will generate a file called `firebase..json` configuration in the Nx workspace root which can then be used with any Firebase CLI command by using the `--config ` [CLI option](https://firebase.google.com/docs/cli#initialize_a_firebase_project). 28 | 29 | ## Updating Firebase Configurations 30 | 31 | Once your Nx Firebase application has been initially generated you are free to change the firebase configurations however you like. 32 | 33 | The Firebase CLI usually warns you anyway if you try to deploy a feature that isn't yet enabled on your Firebase Project console. 34 | 35 | ## Binding Nx projects to Firebase projects 36 | 37 | When using multiple Firebase projects in a workspace, remember that there is only one `.firebaserc` file to contain aliases for all of your deployment targets. 38 | 39 | You can add projects using `firebase use --add` as normal. 40 | 41 | It's fine to add multiple Firebase projects to your workspace `.firebaserc` file, but it is important to remember to correctly switch between them using `firebase use ` before any deployments! 42 | 43 | You can ensure this is always the case for commands like `nx deploy app` etc. by adding `--project ` to any firebase commands in your nx-firebase application's targets. 44 | 45 | See also: [Changing Firebase CLI Project](./nx-firebase-sync.md#changing-firebase-cli-project) 46 | 47 | ## Deployment Environments 48 | 49 | A common practice with Firebase is to generate different Firebase projects for each deployment environments (such as dev / staging / production etc.) 50 | 51 | This can be manually achieved with `nx-firebase` by adding `production` configurations to the `firebase` target in your Nx firebase application `project.json` file eg.: 52 | 53 | ``` 54 | firebase: { 55 | executor: 'nx:run-commands', 56 | options: { 57 | command: `firebase --config firebase.json --project your-dev-firebase-project`, 58 | }, 59 | configurations: { 60 | production: { 61 | command: `firebase --config firebase.json --project your-prod-firebase-project`, 62 | }, 63 | }, 64 | }, 65 | ``` 66 | 67 | You can now run: 68 | 69 | - `nx deploy my-firebase-app` for dev deployment 70 | - `nx deploy my-firebase-app --prod` for production deployment 71 | 72 | Note that `your-dev-firebase-project` and `your-prod-firebase-project` must exist as [aliases](https://firebase.google.com/docs/cli#project_aliases) in your `.firebaserc` file for this to work. 73 | -------------------------------------------------------------------------------- /docs/nx-libraries.md: -------------------------------------------------------------------------------- 1 | # Using Nx Libraries with Firebase Functions 2 | 3 | Nx-Firebase supports use of Nx Libraries within functions code. 4 | 5 | ## Creating a library 6 | 7 | To use a shared library with an Nx Firebase Function Application, simply create a Typescript Nx node library in your workspace: 8 | 9 | **`nx g @nx/js:lib mylib --importPath="@myorg/mylib [--bundler=]`** 10 | 11 | > _Note: The `--importPath` option is highly recommended to ensure the correct typescript aliases and npm package configurations for your library._ 12 | 13 | 14 | ## Importing a library 15 | 16 | You can now: 17 | 18 | `import { stuff } from '@myorg/mynodelib'` 19 | 20 | in your Firebase functions code as you'd normally expect. 21 | 22 | ## Building with libraries 23 | 24 | You can then build your Firebase application or function with: 25 | 26 | **`nx build `** 27 | 28 | OR 29 | 30 | **`nx build `** 31 | 32 | ## Nx Library Notes 33 | 34 | [Firebase functions](./nx-firebase-functions.md) use `esbuild` to bundle function code, we gain a few benefits: 35 | 36 | - Nx takes care of ensuring all necessary dependencies will be also built. 37 | - It does not matter if libraries are buildable or non-buildable, as `esbuild` builds from Typescript sources, however buildable libraries may be preferred since `esbuild` does not do type checking of imported libraries. 38 | - We do not have to worry about how we structure libraries anymore for optimal function runtime. 39 | - For instance, we can use barrel imports freely, since `esbuild` will treeshake unused code and inline imports into the bundled output `main.js` 40 | - We can create as many libraries as we wish for our functions to use, and organise them in whatever makes most sense for the workspace 41 | - For cleaner code sharing, Firebase function applications can simply import a library module containing the firebase function export/implementation 42 | -------------------------------------------------------------------------------- /docs/nx-migration.md: -------------------------------------------------------------------------------- 1 | # Migrating an existing Firebase project to Nx 2 | 3 | To bring an existing Firebase project into an Nx workspace, simply [generate the Nx Firebase application(s)](./nx-firebase-applications.md) first, and then just overwrite the generated Firebase configuration & rules/indexes with your existing `firebase.json`, rules & index files etc.. 4 | 5 | See also [Nx Versions](nx-versions.md) for further information on specific Nx version migrations. 6 | 7 | For your Firebase functions, [generate a function application](./nx-firebase-functions.md) and copy your existing source code to this new project `src` directory. 8 | 9 | From here, you can simply `nx build` and `nx deploy` your firebase application & functions. 10 | 11 | ## Firebase SDK versions 12 | 13 | Which version of the Firebase CLI + SDK's you use will depend on your particular project requirements, and although generally the latest SDK's are widely compatible, you may need to experiment to find specific compatible versions: 14 | 15 | - If you are using Angular/AngularFire libraries, depending on your version of Angular 16 | - If you are using older runtimes than Node 16 17 | - If you are using ES modules or commonjs. 18 | - Use of at least Node 16 functions runtime is required for this plugin 19 | 20 | The `nx-firebase` plugin will install Firebase dependencies in the workspace if they are not already present, but it does not require, enforce or change a specific version beyond that initial setup, so you are free to `npm install` whichever versions of the firebase SDK packages you need. 21 | -------------------------------------------------------------------------------- /docs/nx-plugin-commands.md: -------------------------------------------------------------------------------- 1 | # Nx Plugin Development 2 | 3 | Notes mainly for my own benefit/reminder here. 4 | 5 | ## To create the plugin workspace 6 | 7 | - `npx create-nx-plugin simondotm --pluginName nx-firebase` 8 | 9 | ## To build the plugin 10 | 11 | - `nx run nx-firebase:build` 12 | 13 | ## To test the plugin 14 | 15 | - `nx run nx-firebase:test` 16 | 17 | ## To run end-to-end tests for the plugin 18 | 19 | - `nx run nx-firebase-e2e:e2e` 20 | - This creates a temporary workspace in `/tmp/nx-e2e/...` 21 | - After the e2e test has completed, the plugin can be further manually tested in this temporary workspace. 22 | 23 | ## To reformat the project 24 | 25 | For example, after changing `.prettierrc` settings 26 | 27 | - `nx format:write --all` 28 | 29 | **Unit Tests** 30 | 31 | I've not implemented a full set of unit tests yet, but the e2e tests do perform a few standard tests. 32 | 33 | > **Note**: I created this plugin to primarily serve my own needs, so feature requests may take a bit of time to consider, but feedback and collaboration is very welcome for this project, since the Nrwl Nx team have an extremely rapid release cadence and it's sometimes hard to keep up with all the changes! 34 | -------------------------------------------------------------------------------- /docs/nx-setup-mac.md: -------------------------------------------------------------------------------- 1 | # Nx Development Setup Guide (Mac) 2 | 3 | Clean installation steps for the whole setup of node/npm/nvm/nx, assuming mac with `zsh` shell. 4 | 5 | Recommendations: 6 | 7 | - Remove any pre-existing installations of node/npm/nvm/nx for a clean setup 8 | - This guide recommends use of `nvm` to enable management of node versions which allows much easier switching between different node versions if you work across different projects with different dependencies 9 | 10 | ## 1. Install `antigen` 11 | 12 | - Install [antigen](https://github.com/zsh-users/antigen) via [brew](https://formulae.brew.sh/formula/antigen) - `brew install antigen` 13 | - Add `antigen` to your `~/.zshrc` file: 14 | 15 | ``` 16 | source $(brew --prefix)/share/antigen/antigen.zsh 17 | ``` 18 | 19 | - Restart your shell/terminal 20 | - If you [get 'Antigen: Another process in running.' on shell startup](https://github.com/zsh-users/antigen/issues/543) just try closing & restarting your terminal/shell 21 | 22 | ## 2. Install `nvm` using the `zsh-nvm` plugin 23 | 24 | - Use the excellent [zsh-nvm plugin](https://github.com/lukechilds/zsh-nvm) for nvm to make life easier (do not install `nvm` using brew) 25 | - Add the following to your `~/.zshrc` file: 26 | 27 | ``` 28 | export NVM_DIR="$HOME/.nvm" 29 | antigen bundle lukechilds/zsh-nvm 30 | antigen apply 31 | ``` 32 | 33 | - Add any extra `zsh-nvm` options to your `~/.zshrc` file, such as: 34 | 35 | ``` 36 | export NVM_LAZY_LOAD=true 37 | export NVM_AUTO_USE=true 38 | ``` 39 | 40 | - The finished `~/.zshrc` file looks something like this: 41 | 42 | ``` 43 | source $(brew --prefix)/share/antigen/antigen.zsh 44 | export NVM_DIR="$HOME/.nvm" 45 | export NVM_LAZY_LOAD=true 46 | export NVM_AUTO_USE=true 47 | antigen bundle lukechilds/zsh-nvm 48 | antigen apply 49 | ``` 50 | 51 | - Restart your shell/terminal and verify installation using the `nvm --version` 52 | - Upgrade `nvm` using `nvm upgrade` 53 | 54 | ## 3. Install `node` 55 | 56 | - `nvm install ` eg. `nvm install 19.1.0` 57 | - This will install node and a compatible version of `npm`/`npx` to `~/.nvm/versions/node/19.1.0/...` 58 | - `echo $path` should now show `~/.nvm/versions/node/19.1.0/bin` 59 | - `node`, `npm` and `npx` should now be accessible from the terminal/shell prompt 60 | 61 | ## 4. Installing multiple `node` versions (optional) 62 | 63 | - Multiple node versions can be installed with `nvm install x.y.z` 64 | - `nvm install node` will install the latest version 65 | - Switch between node versions using `nvm use ` 66 | - If your project requires a specific node version, add a `.nvmrc` file to the root of your project containing the text version required 67 | - Remember that `npm` packages installed with `-g` are installed to the currently selected `node` version only, if you switch version, you may have to reinstall a global package for that version 68 | - `nvm list` will show all installed versions 69 | - `nvm current` will show the currently selected version 70 | - `nvm default ` will select a version that you like to be the default when you run `nvm use default` 71 | 72 | ## 5. Install `nx` 73 | 74 | - Select the node version you want to install `nx` to: `nvm use ` 75 | - `npm i -g nx` 76 | - The previous command ensures that `nx` command line can be run without `npx` 77 | - Not sure when this became a thing, nor sure if the nx cli is still required as a global install, but it works. 78 | - `nx` should now be accessible from the terminal/shell prompt 79 | 80 | ## 6. Visual Studio Code 81 | 82 | Recommended extensions for Nx development: 83 | 84 | - [VS Code ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 85 | - [Prettier Formatter for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 86 | - [Nx Console](https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console) 87 | - [vscode-jest-runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) 88 | -------------------------------------------------------------------------------- /docs/nx-workspace-layout.md: -------------------------------------------------------------------------------- 1 | # Nx-Firebase Workspace Layout 2 | 3 | Firebase applications and functions can be generated in whichever directories you like. 4 | 5 | While there are plenty of ways to organise your workspace layout, one suggestion is: 6 | 7 | ``` 8 | /apps 9 | /project1 10 | /firebase 11 | /functions 12 | /function1 13 | /function2 14 | /web 15 | /site1-app 16 | /site2-app 17 | /mobile 18 | /app 19 | /project2 20 | /firebase 21 | /functions 22 | /web 23 | ... 24 | firebase.rc 25 | firebase.json 26 | firebase.project2.json 27 | ... 28 | ``` 29 | 30 | ## Organising functions 31 | 32 | Since [firebase function projects](./nx-firebase-functions.md) are apps, and nx-firebase supports multiple functions for each firebase app, there is plenty of flexibility for how you wish to develop your cloud functions. 33 | 34 | 1. Just have one function Nx app project that exports all of the cloud functions you need 35 | 2. Have multiple Nx function app projects which group and export multiple cloud functions by common functionality 36 | 3. Have one Nx function app project per cloud function 37 | 38 | Option 1 is the simplest approach. 39 | 40 | However, over time as your project grows you may find that options 2 or 3 bring organisational benefits, (and also potentially code start time optimisations as separated function app projects will only import the code they need to compile). 41 | 42 | Firebase CLI also has capability to check for unchanged function code when deploying, so having more codebases can also reduce the deployment time for your functions. 43 | -------------------------------------------------------------------------------- /docs/user-guide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | **Nx Firebase Generators** 4 | 5 | - [Firebase Applications](./nx-firebase-applications.md) 6 | - [Firebase Functions](./nx-firebase-functions.md) 7 | - [Firebase Functions - Environment Variables](./nx-firebase-functions-environment.md) 8 | 9 | **Nx Firebase** 10 | 11 | - [Firebase Hosting](./nx-firebase-hosting.md) 12 | - [Firebase Emulators](./nx-firebase-emulators.md) 13 | - [Firebase Databases](./nx-firebase-databases.md) 14 | - [Firebase Projects](./nx-firebase-projects.md) 15 | 16 | **Nx Firebase Workspace Management** 17 | 18 | - [Nx-Firebase Sync](./nx-firebase-sync.md) 19 | - [Nx-Firebase Project Schemas](./nx-firebase-project-structure.md) 20 | - [Migrating to new plugin versions](./nx-firebase-migrations.md) 21 | 22 | **Nx Workspace** 23 | 24 | - [Nx Workspace Layout Ideas](./nx-workspace-layout.md) 25 | - [Using Nx Libraries with Firebase Functions](./nx-libraries.md) 26 | - [Migrating an existing Firebase project to Nx](./nx-migration.md) 27 | 28 | **Version information** 29 | 30 | - [Firebase Versions](./firebase-versions.md) 31 | 32 | _Note: Some of these may not always be upto date - it's hard work keeping track of external releases and compatibilities!_ 33 | 34 | **Notes** 35 | 36 | - [Plugin Development Notes](./nx-plugin-commands.md) 37 | - [Nx Development Setup for Mac](./nx-setup-mac.md) -------------------------------------------------------------------------------- /e2e/compat/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "parserOptions": { 8 | "project": ["e2e/compat/tsconfig.*?.json"] 9 | }, 10 | "rules": {} 11 | }, 12 | { 13 | "files": ["*.ts", "*.tsx"], 14 | "rules": {} 15 | }, 16 | { 17 | "files": ["*.js", "*.jsx"], 18 | "rules": {} 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /e2e/compat/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'compat', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]s$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'js', 'html'], 16 | } 17 | -------------------------------------------------------------------------------- /e2e/compat/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compat", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "e2e/compat/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/e2e/compat", 12 | "main": "e2e/compat/src/main.ts", 13 | "tsConfig": "e2e/compat/tsconfig.app.json", 14 | "assets": ["e2e/compat/src/assets"], 15 | "target": "node", 16 | "compiler": "tsc" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "optimization": true, 21 | "extractLicenses": true, 22 | "inspect": false, 23 | "fileReplacements": [ 24 | { 25 | "replace": "e2e/compat/src/environments/environment.ts", 26 | "with": "e2e/compat/src/environments/environment.prod.ts" 27 | } 28 | ] 29 | } 30 | } 31 | }, 32 | "serve": { 33 | "executor": "@nx/js:node", 34 | "options": { 35 | "buildTarget": "compat:build" 36 | } 37 | }, 38 | "lint": { 39 | "executor": "@nx/linter:eslint", 40 | "outputs": ["{options.outputFile}"], 41 | "options": { 42 | "lintFilePatterns": ["e2e/compat/**/*.ts"] 43 | } 44 | }, 45 | "test": { 46 | "executor": "@nx/jest:jest", 47 | "outputs": ["{workspaceRoot}/coverage/e2e/compat"], 48 | "options": { 49 | "jestConfig": "e2e/compat/jest.config.ts", 50 | "passWithNoTests": true 51 | } 52 | } 53 | }, 54 | "tags": [] 55 | } 56 | -------------------------------------------------------------------------------- /e2e/compat/src/app/config.ts: -------------------------------------------------------------------------------- 1 | import { defaultCwd } from './utils/cwd' 2 | 3 | // this is the package manager that will be used for the test Nx workspace 4 | export type PackageManager = 'npm' | 'yarn' | 'pnpm' 5 | export const PACKAGE_MANAGER: PackageManager = 'pnpm' 6 | 7 | // const CACHE_DIR = `${defaultCwd}/node_modules/.cache/nx-firebase` 8 | export const CACHE_DIR = `${defaultCwd}/.nx-firebase` 9 | // const CACHE_DIR = `${defaultCwd}/../.nx-firebase` 10 | -------------------------------------------------------------------------------- /e2e/compat/src/app/setup.ts: -------------------------------------------------------------------------------- 1 | // setup re-usable workspaces for e2e testbed 2 | import { customExec } from './utils/exec' 3 | import { green, info, log, red } from './utils/log' 4 | import { 5 | deleteDir, 6 | deleteFile, 7 | ensureDir, 8 | fileExists, 9 | setCwd, 10 | } from './utils/utils' 11 | import { createTestDir, createWorkspace } from './workspace' 12 | import { Cache } from './utils/cache' 13 | 14 | /** 15 | * Generate an NxWorkspace with the given versions and gzip it 16 | * unless the gzip archive of a version already exists 17 | * @param nxVersion - target nx version eg. '13.10.6' 18 | * @param pluginVersion - target nx-firebase version eg. '0.3.4' 19 | * @param force - always recreate the workspace 20 | */ 21 | export async function setupNxWorkspace(cache: Cache, force = false) { 22 | try { 23 | // setup the target Nx workspace 24 | const archiveExists = fileExists(cache.archiveFile) && !force 25 | 26 | info( 27 | `SETUP NX VERSION '${cache.nxVersion}' WITH PLUGIN VERSION '${ 28 | cache.pluginVersion 29 | }' ${archiveExists ? '[CACHED]' : 'INSTALLING'}\n`, 30 | ) 31 | 32 | ensureDir(cache.rootDir) 33 | 34 | info( 35 | `Creating new Nx workspace version ${cache.nxVersion} in directory '${cache.testDir}'`, 36 | ) 37 | 38 | // create workspace & archive if it doesn't already exist 39 | if (!archiveExists) { 40 | deleteDir(cache.testDir) 41 | createTestDir(cache.testDir) 42 | await createWorkspace(cache) 43 | 44 | // delete any existing archive file so we do not accidentally append to archive 45 | if (fileExists(cache.archiveFile)) { 46 | deleteFile(cache.archiveFile) 47 | } 48 | 49 | // cwd is workspaceDir 50 | setCwd(cache.rootDir) 51 | // archive the workspace 52 | await customExec(`tar -zcf ${cache.archiveFile} ./${cache.nxVersion}`) // add -v for verbose 53 | deleteDir(cache.testDir) 54 | } else { 55 | log( 56 | `Workspace archive '${cache.archiveFile}' already exists for '${cache.workspaceDir}', no setup required`, 57 | ) 58 | } 59 | info(green(`SETUP VERSION '${cache.nxVersion}' SUCCEEDED\n`)) 60 | } catch (err) { 61 | info(err.message) 62 | info(red(`SETUP VERSION '${cache.nxVersion}' FAILED\n`)) 63 | // escalate, this is a show stopper 64 | throw err 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_DIR } from '../config' 2 | import { defaultCwd } from './cwd' 3 | import { info } from './log' 4 | import { satisfies } from 'semver' 5 | 6 | export const localPluginVersion = 'local' 7 | 8 | export type Cache = { 9 | nxVersion: string 10 | pluginVersion: string 11 | rootDir: string 12 | testDir: string 13 | workspaceDir: string 14 | archiveFile: string 15 | pluginWorkspace: string 16 | disableDaemon: boolean 17 | isLocalPlugin: boolean 18 | deferPluginInstall: boolean // defer plugin installs during each test suite rather than in the workspace setup 19 | nodeVersion: number // major node version 20 | } 21 | 22 | /** 23 | * compat tests are run in these directories 24 | * @param nxVersion 25 | * @param pluginVersion 26 | * @returns 27 | */ 28 | export function getCache(nxVersion: string, pluginVersion: string): Cache { 29 | info( 30 | `getting Cache for nxVersion=${nxVersion} pluginVersion=${pluginVersion}, using cache dir '${CACHE_DIR}'`, 31 | ) 32 | const rootDir = `${CACHE_DIR}/${pluginVersion}` 33 | const testDir = `${rootDir}/${nxVersion}` 34 | const archiveFile = `${rootDir}/${nxVersion}.tar.gz` 35 | const workspaceDir = `${testDir}/myorg` 36 | const pluginWorkspace = defaultCwd 37 | const isLocalPlugin = pluginVersion === localPluginVersion 38 | return { 39 | nxVersion, 40 | pluginVersion, 41 | rootDir, 42 | testDir, 43 | workspaceDir, 44 | archiveFile, 45 | pluginWorkspace, 46 | isLocalPlugin, 47 | deferPluginInstall: true, // dont think this is needed after all, was introduced because we had an issue from not installing @nx/js plugin 48 | disableDaemon: false, 49 | // disableDaemon: isLocalPlugin, 50 | // deferPluginInstall: isLocalPlugin, // for local plugin tests, install them for tests so that code changes are present in tests 51 | nodeVersion: parseInt(process.versions.node.split('.')[0]), 52 | } 53 | } 54 | 55 | export function isNxVersionSince(cache: Cache, nxVersion: string) { 56 | console.log( 57 | `checking isNxVersionSince satisfies ${cache.nxVersion} >= ${nxVersion}`, 58 | ) 59 | const isOk = satisfies(cache.nxVersion, `>=${nxVersion}`) 60 | console.log('isNxVersionSince check returned ', isOk) 61 | return isOk 62 | } 63 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/cwd.ts: -------------------------------------------------------------------------------- 1 | export const defaultCwd = process.cwd() 2 | console.log(`cwd=${defaultCwd}`) 3 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_MANAGER } from '../config' 2 | import { info, log } from './log' 3 | import { exec } from 'child_process' 4 | 5 | /** 6 | * Promisify node `exec`, with stdout & stderr piped to console 7 | * @param command 8 | * @param dir - defaults to cwd if not specified 9 | * @returns 10 | */ 11 | export async function customExec( 12 | command: string, 13 | dir?: string, 14 | ): Promise<{ stdout: string; stderr: string }> { 15 | const cwd = dir ? dir : process.cwd() 16 | return new Promise((resolve, reject) => { 17 | info(`Executing command '${command}' in '${cwd}'`) 18 | const process = exec( 19 | command, 20 | { cwd: cwd }, 21 | // { cwd: cwd, env: { NX_DAEMON: 'false' } }, // force CI type environment so Nx Daemon doesn't act up with multiple instances 22 | // { cwd: cwd, env: { CI: 'true' } }, // force CI type environment so Nx Daemon doesn't act up with multiple instances 23 | (error, stdout, stderr) => { 24 | if (error) { 25 | console.warn(error.message) 26 | reject(error) 27 | } 28 | resolve({ stdout, stderr }) 29 | }, 30 | ) 31 | 32 | process.stdout.on('data', (data) => { 33 | log(data.toString()) 34 | }) 35 | 36 | process.stderr.on('data', (data) => { 37 | log(data.toString()) 38 | }) 39 | 40 | process.on('exit', (code) => { 41 | if (code) { 42 | log('child process exited with code ' + code.toString()) 43 | } 44 | }) 45 | }) 46 | } 47 | 48 | export async function runNxCommandAsync(command: string, dir?: string) { 49 | const exec = PACKAGE_MANAGER === 'npm' ? 'npx' : 'pnpm exec' 50 | const cmd = `${exec} nx ${command} --verbose` 51 | const result = await customExec(cmd, dir) 52 | return result 53 | } 54 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/jest-ish.ts: -------------------------------------------------------------------------------- 1 | import { info } from 'console' 2 | import { log, red } from './log' 3 | 4 | /** 5 | * Test helper function approximating the Jest style of expect().toContain() 6 | * @param content 7 | * @param expected 8 | * @returns true if content contains expected string 9 | */ 10 | function etc(content: string, expected: string) { 11 | const pass = content.includes(expected) 12 | return pass 13 | } 14 | 15 | function expectToContainInner(content: string, expected: string | string[]) { 16 | if (Array.isArray(expected)) { 17 | for (const e of expected) { 18 | if (!etc(content, e)) { 19 | return false 20 | } 21 | } 22 | return true 23 | } else { 24 | return etc(content, expected) 25 | } 26 | } 27 | 28 | export function expectToContain(content: string, expected: string | string[]) { 29 | // log(`- expectToContain`) 30 | // log(`- content='${content}'`) 31 | // log(`- expected='${expected}'`) 32 | 33 | const pass = expectToContainInner(content, expected) 34 | if (!pass) { 35 | throw new Error( 36 | `TEST FAILED: expected '${expected}', received '${content}'`, 37 | ) 38 | } 39 | return pass 40 | } 41 | 42 | export function expectToNotContain( 43 | content: string, 44 | expected: string | string[], 45 | ) { 46 | // log(`- expectToNotContain`) 47 | // log(`- content='${content}'`) 48 | // log(`- not expected='${expected}'`) 49 | 50 | const pass = !expectToContainInner(content, expected) 51 | if (!pass) { 52 | throw new Error( 53 | `TEST FAILED: not expected '${expected}', received '${content}'`, 54 | ) 55 | } 56 | return pass 57 | } 58 | 59 | // hacky jest-like tester 60 | export async function it(testName: string, testFunc: () => Promise) { 61 | info(` - it ${testName}`) 62 | log(` - it ${testName}`) 63 | try { 64 | await testFunc() 65 | } catch (err) { 66 | info(red(err)) 67 | throw err 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/log.ts: -------------------------------------------------------------------------------- 1 | const ENABLE_LOG = false 2 | const DEFAULT_LOG_FILE = `${process.cwd()}/e2e.log` 3 | 4 | import * as fs from 'fs' 5 | import { ensureDir } from './utils' 6 | import { dirname } from 'path' 7 | 8 | let LOG_FILE: string | undefined 9 | 10 | function writeLog(msg: string) { 11 | ensureDir(dirname(LOG_FILE)) 12 | fs.appendFileSync(LOG_FILE, `${msg}\n`) 13 | } 14 | 15 | export function setLogFile(path?: string) { 16 | LOG_FILE = path || DEFAULT_LOG_FILE 17 | console.log(`Logging to '${LOG_FILE}'`) 18 | ensureDir(dirname(LOG_FILE)) 19 | fs.writeFileSync(LOG_FILE, '') // reset log file 20 | } 21 | 22 | setLogFile() 23 | 24 | export function log(msg: string) { 25 | if (ENABLE_LOG) { 26 | console.log(msg) 27 | } 28 | writeLog(msg) 29 | } 30 | 31 | export function info(msg: string) { 32 | console.log(msg) 33 | writeLog(msg) 34 | // fs.appendFileSync(LOG_FILE, `${msg}\n`) 35 | } 36 | 37 | export function time(ms: number) { 38 | return `${(ms / 1000.0).toFixed(1)}s` 39 | } 40 | 41 | // https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color 42 | const GREEN_FG = '\x1b[32m' 43 | const RED_FG = '\x1b[31m' 44 | const RESET_FG = '\x1b[0m' 45 | 46 | export function green(text: string) { 47 | return `${GREEN_FG}${text}${RESET_FG}` 48 | } 49 | export function red(text: string) { 50 | return `${RED_FG}${text}${RESET_FG}` 51 | } 52 | -------------------------------------------------------------------------------- /e2e/compat/src/app/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | // import { readJsonFile, writeJsonFile } from '@nx/devkit' 3 | // import { exit } from 'process' 4 | import { log } from './log' 5 | 6 | /** 7 | * Set current working directory 8 | * @param dir 9 | */ 10 | export function setCwd(dir: string) { 11 | log(`Switching cwd to '${dir}'`) 12 | 13 | process.chdir(dir) 14 | 15 | log(`Switched cwd to '${process.cwd()}'`) 16 | } 17 | 18 | /** 19 | * Ensure given directory path exists, create if it doesn't 20 | * @param dir - directory path 21 | * @returns true if path already exists 22 | */ 23 | export function ensureDir(dir: string) { 24 | const pathExists = fs.existsSync(dir) 25 | if (!pathExists) { 26 | console.log(` - Creating dir '${dir}'...`) 27 | fs.mkdirSync(dir, { recursive: true }) 28 | } 29 | return pathExists 30 | } 31 | 32 | export function fileExists(path: string) { 33 | return fs.existsSync(path) 34 | } 35 | 36 | export function deleteFile(path: string) { 37 | log(`deleting file '${path}'`) 38 | fs.rmSync(path) 39 | } 40 | 41 | export function deleteDir(path: string) { 42 | log(`deleting dir '${path}'`) 43 | fs.rmSync(path, { recursive: true, force: true }) 44 | } 45 | 46 | /** 47 | * Replace content in file `path` that matches `match` with `addition` 48 | * @param path - path to the target text file 49 | * @param match - string to match in the index.ts 50 | * @param addition - string to add after the matched line in the index.ts 51 | */ 52 | export function addContentToTextFile( 53 | path: string, 54 | match: string, 55 | addition: string, 56 | ) { 57 | const content = fs.readFileSync(path, 'utf8') 58 | if (!content.includes(match)) { 59 | throw Error( 60 | `ERROR: addContentToTextFile: Could not find '${match}' in '${path}'`, 61 | ) 62 | } 63 | const replaced = content.replace(match, `${match}\n${addition}`) 64 | fs.writeFileSync(path, replaced) 65 | } 66 | 67 | export function getFileSize(path: string) { 68 | const stats = fs.statSync(path) 69 | return stats.size 70 | } 71 | -------------------------------------------------------------------------------- /e2e/compat/src/app/versions.ts: -------------------------------------------------------------------------------- 1 | export const testVersions = { 2 | pluginVersions: [ 3 | 'local', 4 | // '2.1.2', 5 | // '0.3.4' 6 | ], 7 | nxReleases: { 8 | '16': { 9 | '1': [4], 10 | '2': [2], 11 | '3': [2], 12 | '4': [3], 13 | '5': [5], 14 | '6': [0], 15 | '7': [4], 16 | '8': [1], 17 | '9': [1], 18 | '10': [0], 19 | }, 20 | '17': { 21 | '0': [3], 22 | '1': [3], 23 | '2': [8], 24 | // --nxCloud option for create-nx-workspace changed in 17.3.2, options are yes, github, circleci, skip 25 | '3': [2], 26 | }, 27 | '18': { 28 | '0': [7], 29 | '1': [2], 30 | }, 31 | 32 | //-------------------------------- 33 | // LEGACY 34 | //-------------------------------- 35 | 36 | // '15': { 37 | // '4': [0, 1, 2, 3, 4, 5], 38 | // '3': [0, 2, 3], 39 | // '2': [0, 1, 2, 3, 4], 40 | // '1': [0, 1], 41 | // '0': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 42 | // }, 43 | // '14': { 44 | // '8': [0, 1, 2, 3, 4, 5, 6], 45 | // '7': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], 46 | // '6': [0, 1, 2, 3, 4, 5], 47 | // '5': [0, 1, 2, 3, 4, 5, 6, 8, 10], 48 | // '4': [0, 1, 2, 3], 49 | // '3': [0, 1, 2, 3, 4, 5, 6], 50 | // '2': [1, 2, 4], 51 | // '1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 52 | // '0': [0, 1, 2, 3, 4, 5], 53 | // }, 54 | // '13': { 55 | // '10': [6], 56 | // }, 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /e2e/compat/src/app/workspace.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { Cache, isNxVersionSince } from './utils/cache' 3 | import { customExec, runNxCommandAsync } from './utils/exec' 4 | import { info, log } from './utils/log' 5 | import { deleteDir, ensureDir, setCwd } from './utils/utils' 6 | import { PACKAGE_MANAGER } from './config' 7 | 8 | export function createTestDir(testDir: string) { 9 | ensureDir(testDir) 10 | setCwd(testDir) 11 | } 12 | 13 | export function workspaceExists(workspaceDir: string) { 14 | return fs.existsSync(workspaceDir) 15 | } 16 | 17 | export function cleanWorkspace(dir: string) { 18 | if (workspaceExists(dir)) { 19 | log(`Cleaning workspace '${dir}'...`) 20 | deleteDir(dir) 21 | } 22 | } 23 | 24 | export async function installPlugin(cache: Cache) { 25 | // this version of plugin has peerdeps that only work with node 14 / npm 6 26 | const requireLegacyPeerDeps = 27 | cache.nodeVersion >= 16 && cache.pluginVersion === '0.3.4' 28 | const legacyPeerDeps = requireLegacyPeerDeps ? '--legacy-peer-deps' : '' 29 | if (cache.isLocalPlugin) { 30 | // install the plugin from the nx-firebase workspace as a local file dependency 31 | // await customExec( 32 | // `npm i ${cache.pluginWorkspace}/dist/packages/nx-firebase --save-dev ${legacyPeerDeps}`, 33 | // ) 34 | 35 | // const pluginVersion = `${cache.pluginVersion}` 36 | const pluginVersion = `2.2.0` 37 | 38 | // log( 39 | // `Packing plugin '${cache.pluginWorkspace}/dist/packages/nx-firebase}'...`, 40 | // ) 41 | // await customExec( 42 | // `npm pack`, 43 | // `${cache.pluginWorkspace}/dist/packages/nx-firebase`, 44 | // ) 45 | 46 | const pluginFileSrc = `${cache.pluginWorkspace}/dist/packages/nx-firebase/simondotm-nx-firebase-${pluginVersion}.tgz` 47 | const pluginFileDst = `${cache.workspaceDir}/simondotm-nx-firebase-${pluginVersion}.tgz` 48 | log(`Copying plugin '${pluginFileSrc}' to '${pluginFileDst}'...`) 49 | await customExec(`cp -rf ${pluginFileSrc} ${pluginFileDst}`) 50 | 51 | log(`Installing plugin '${pluginFileDst}'...`) 52 | await customExec( 53 | `${PACKAGE_MANAGER} i ${pluginFileDst} --save-dev ${legacyPeerDeps}`, 54 | ) 55 | } else { 56 | await customExec( 57 | `${PACKAGE_MANAGER} i @simondotm/nx-firebase@${cache.pluginVersion} --save-dev ${legacyPeerDeps}`, 58 | ) 59 | } 60 | } 61 | 62 | export async function createWorkspace(cache: Cache) { 63 | cleanWorkspace(cache.workspaceDir) 64 | const nxCloudOption = isNxVersionSince(cache, '17.3.2') ? 'skip' : 'false' 65 | 66 | await customExec( 67 | `npx create-nx-workspace@${cache.nxVersion} --preset=apps --interactive=false --name=myorg --nxCloud=${nxCloudOption} --packageManager=${PACKAGE_MANAGER}`, 68 | ) 69 | setCwd(cache.workspaceDir) 70 | 71 | // we have issues with the daemon when running the workspace with local plugin 72 | // so we turn it off for these workspaces 73 | if (cache.disableDaemon) { 74 | log(`Disabling daemon in workspace...`) 75 | const nxJsonFile = `${cache.workspaceDir}/nx.json` 76 | const content = fs.readFileSync(nxJsonFile, 'utf8') 77 | const nxJson = JSON.parse(content) 78 | nxJson.tasksRunnerOptions.default.options.useDaemonProcess = false 79 | fs.writeFileSync(nxJsonFile, JSON.stringify(nxJson)) 80 | 81 | // stop nx daemon before we run plugin - why? 82 | log(`Stopping nx daemon...`) 83 | await runNxCommandAsync(`reset`) 84 | } 85 | 86 | // we meed this plugin for test suite libs 87 | // update: @nx/node plugin brings in this dependency 88 | // https://github.com/nrwl/nx/blob/fb90767af87c77955f8b8b7cace7cd0b5e3be27d/packages/node/package.json#L32 89 | // so we dont need to install it here as long as we run @simondotm/nx-firebase:init first 90 | // await customExec(`npm i @nx/js@${cache.nxVersion} --save-dev`) 91 | 92 | if (!cache.deferPluginInstall) { 93 | info(`Installing plugin in workspace...`) 94 | // // these should be installed with the plugin I guess? 95 | // // if we dont install them here, they'll be found in the parent workspace node_modules 96 | // await customExec(`npm i @nx/js@${cache.nxVersion} --save-dev`) 97 | // await customExec(`npm i @nx/devkit@${cache.nxVersion} --save-dev`) 98 | // await customExec(`npm i @nx/jest@${cache.nxVersion} --save-dev`) 99 | // install the target plugin we want to test 100 | await installPlugin(cache) 101 | 102 | // run the plugin initialiser to ensure we have the correct dependencies installed 103 | info(`Initialising plugin in workspace...`) 104 | await runNxCommandAsync(`g @simondotm/nx-firebase:init`) 105 | } 106 | 107 | // if (cache.disableDaemon) { 108 | // cleanup - stop nx daemon post setup to prevent connection in use errors 109 | // log(`Stopping nx daemon...`) 110 | // await runNxCommandAsync('reset') 111 | // } 112 | } 113 | -------------------------------------------------------------------------------- /e2e/compat/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondotm/nx-firebase/9d810fe7dec86d1c810acfd3fff6337ef3b64713/e2e/compat/src/assets/.gitkeep -------------------------------------------------------------------------------- /e2e/compat/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | } 4 | -------------------------------------------------------------------------------- /e2e/compat/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | } 4 | -------------------------------------------------------------------------------- /e2e/compat/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom e2e compatibility test suite for @simondotm/nx-firebase Nx plugin 3 | * The plugin e2e test suite can be unreliable and has limitations in Jest 4 | * This script allows us to run full matrix e2e and regression tests of the plugin across: 5 | * - Node versions 14,16,18 6 | * - Nx versions against plugin versions 7 | * - Check firebase deployments in CI environment 8 | * - We only do light functional tests, this test matrix is for ensuring wide compatibility of plugin generator & executor 9 | */ 10 | 11 | import { green, info, red, setLogFile, time } from './app/utils/log' 12 | import { setupNxWorkspace } from './app/setup' 13 | import { testVersions } from './app/versions' 14 | import { clean, testNxVersion } from './app/test' 15 | import { getCache } from './app/utils/cache' 16 | import { customExec } from './app/utils/exec' 17 | import { defaultCwd } from './app/utils/cwd' 18 | 19 | // Force CI environment if necessary 20 | // process.env.CI = 'true' 21 | // process.env.NX_DAEMON = 'false' 22 | 23 | type CmdOptions = { 24 | onlySetup: boolean 25 | force: boolean 26 | clean: boolean 27 | } 28 | 29 | async function main(options: CmdOptions) { 30 | const t = Date.now() 31 | 32 | const pluginVersions = testVersions.pluginVersions 33 | 34 | // gather all nx versions in the test matrix 35 | const nxReleases: string[] = [] 36 | for (const maj in testVersions.nxReleases) { 37 | const majVersions = testVersions.nxReleases[maj] 38 | for (const min in majVersions) { 39 | const patchVersions = majVersions[min] 40 | const latestVersion = patchVersions[patchVersions.length - 1] 41 | const version = `${maj}.${min}.${latestVersion}` 42 | nxReleases.push(version) 43 | } 44 | } 45 | 46 | info(`Packing plugin '${defaultCwd}/dist/packages/nx-firebase'...`) 47 | await customExec(`npm pack`, `${defaultCwd}/dist/packages/nx-firebase`) 48 | 49 | //----------------------------------------------------------------------- 50 | // setup phase - generates workspaces for each Nx minor release 51 | //----------------------------------------------------------------------- 52 | // gzip's and caches them for re-use 53 | // splitting the setup phase from the test phase allows us to cache 54 | // node_modules in CI github actions for this compat test 55 | const testMatrixSize = nxReleases.length * pluginVersions.length * 2 // 2 = setup+test 56 | 57 | //----------------------------------------------------------------------- 58 | // test phase - tests each Nx minor release 59 | //----------------------------------------------------------------------- 60 | let testCounter = 0 61 | const errors: string[] = [] 62 | for (let i = 0; i < nxReleases.length; ++i) { 63 | for (const pluginVersion of pluginVersions) { 64 | const release = nxReleases[i] 65 | 66 | //----------------------------------------------------------------------- 67 | // setup phase - generates workspaces for each Nx minor release 68 | //----------------------------------------------------------------------- 69 | // gzip's and caches them for re-use 70 | // splitting the setup phase from the test phase allows us to cache 71 | // node_modules in CI github actions for this compat test 72 | 73 | const cache = getCache(release, pluginVersion) 74 | 75 | setLogFile(`${cache.rootDir}/${cache.nxVersion}.log.txt`) 76 | 77 | info( 78 | `-- ${ 79 | testCounter + 1 80 | }/${testMatrixSize} --------------------------------------------------------------------------\n`, 81 | ) 82 | 83 | await setupNxWorkspace(cache, options.force) 84 | ++testCounter 85 | 86 | //----------------------------------------------------------------------- 87 | // test phase - tests each Nx minor release 88 | //----------------------------------------------------------------------- 89 | info( 90 | `-- ${ 91 | testCounter + 1 92 | }/${testMatrixSize} --------------------------------------------------------------------------\n`, 93 | ) 94 | 95 | if (!options.onlySetup) { 96 | const result = await testNxVersion(cache) 97 | if (result) { 98 | errors.push(result) 99 | } 100 | } 101 | ++testCounter 102 | } 103 | } 104 | 105 | // report error summary 106 | if (errors.length) { 107 | info(red('TEST ERRORS:`n')) 108 | for (const error of errors) { 109 | info(red(error)) 110 | } 111 | } else { 112 | info(green('ALL TESTS SUCCEEDED')) 113 | } 114 | 115 | //----------------------------------------------------------------------- 116 | // Complete 117 | //----------------------------------------------------------------------- 118 | const dt = Date.now() - t 119 | info(`Total time ${time(dt)}`) 120 | } 121 | 122 | // entry 123 | const options: CmdOptions = { onlySetup: false, force: false, clean: false } 124 | if (process.argv.length > 2) { 125 | if (process.argv[2] === '--setup') { 126 | options.onlySetup = true 127 | } else if (process.argv[2] === '--clean') { 128 | options.clean = true 129 | } else if (process.argv[2] === '--force') { 130 | options.force = true 131 | } 132 | } 133 | 134 | if (options.clean) { 135 | clean() 136 | } else { 137 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 138 | main(options) 139 | } 140 | -------------------------------------------------------------------------------- /e2e/compat/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"] 7 | }, 8 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 9 | "include": ["**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /e2e/compat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /e2e/compat/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'nx-firebase-e2e', 3 | preset: '../../jest.preset.js', 4 | globals: {}, 5 | transform: { 6 | '^.+\\.[tj]s$': [ 7 | 'ts-jest', 8 | { 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | ], 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | } 15 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-firebase-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "e2e/nx-firebase-e2e/src", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nx/jest:jest", 9 | "options": { 10 | "jestConfig": "e2e/nx-firebase-e2e/jest.config.js", 11 | "runInBand": true 12 | }, 13 | "dependsOn": ["nx-firebase:build"] 14 | } 15 | }, 16 | "tags": [], 17 | "implicitDependencies": ["nx-firebase"] 18 | } 19 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test-utils-apps' 2 | export * from './test-utils-commands' 3 | export * from './test-utils-functions' 4 | export * from './test-utils-helpers' 5 | export * from './test-utils-imports' 6 | export * from './test-utils-logger' 7 | export * from './test-utils-project-data' 8 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-apps.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectData } from './test-utils-project-data' 2 | import { readJson } from '@nx/plugin/testing' 3 | 4 | export function expectedAppProjectTargets(appProject: ProjectData) { 5 | return { 6 | build: { 7 | executor: 'nx:run-commands', 8 | options: { 9 | command: `echo Build succeeded.`, 10 | }, 11 | }, 12 | watch: { 13 | executor: 'nx:run-commands', 14 | options: { 15 | command: `nx run-many --targets=build --projects=tag:firebase:dep:${appProject.projectName} --parallel=100 --watch`, 16 | }, 17 | }, 18 | lint: { 19 | executor: 'nx:run-commands', 20 | options: { 21 | command: `nx run-many --targets=lint --projects=tag:firebase:dep:${appProject.projectName} --parallel=100`, 22 | }, 23 | }, 24 | test: { 25 | executor: 'nx:run-commands', 26 | options: { 27 | command: `nx run-many --targets=test --projects=tag:firebase:dep:${appProject.projectName} --parallel=100`, 28 | }, 29 | }, 30 | firebase: { 31 | executor: 'nx:run-commands', 32 | options: { 33 | command: `firebase --config=firebase.json`, 34 | }, 35 | configurations: { 36 | production: { 37 | command: `firebase --config=firebase.json`, 38 | }, 39 | }, 40 | }, 41 | killports: { 42 | executor: 'nx:run-commands', 43 | options: { 44 | command: `kill-port --port 9099,5001,8080,9000,5000,8085,9199,9299,4000,4400,4500`, 45 | }, 46 | }, 47 | getconfig: { 48 | executor: 'nx:run-commands', 49 | options: { 50 | command: `nx run ${appProject.projectName}:firebase functions:config:get > ${appProject.projectDir}/environment/.runtimeconfig.json`, 51 | }, 52 | }, 53 | emulate: { 54 | executor: 'nx:run-commands', 55 | options: { 56 | commands: [ 57 | `nx run ${appProject.projectName}:killports`, 58 | `nx run ${appProject.projectName}:firebase emulators:start --import=${appProject.projectDir}/.emulators --export-on-exit`, 59 | ], 60 | parallel: false, 61 | }, 62 | }, 63 | serve: { 64 | executor: '@simondotm/nx-firebase:serve', 65 | options: { 66 | commands: [ 67 | `nx run ${appProject.projectName}:watch`, 68 | `nx run ${appProject.projectName}:emulate`, 69 | ], 70 | }, 71 | }, 72 | deploy: { 73 | executor: 'nx:run-commands', 74 | dependsOn: ['build'], 75 | options: { 76 | command: `nx run ${appProject.projectName}:firebase deploy`, 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | export function validateProjectConfig(appProject: ProjectData) { 83 | const project = readJson(`${appProject.projectDir}/project.json`) 84 | // expect(project.root).toEqual(`apps/${projectName}`) 85 | expect(project.targets).toEqual( 86 | expect.objectContaining(expectedAppProjectTargets(appProject)), 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-functions.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectData } from './test-utils-project-data' 2 | import { readJson } from '@nx/plugin/testing' 3 | 4 | export function expectedFunctionProjectTargets( 5 | functionProject: ProjectData, 6 | appProject: ProjectData, 7 | ) { 8 | return { 9 | build: { 10 | executor: '@nx/esbuild:esbuild', 11 | outputs: ['{options.outputPath}'], 12 | options: { 13 | platform: 'node', 14 | outputPath: `dist/${functionProject.projectDir}`, 15 | main: `${functionProject.projectDir}/src/main.ts`, 16 | tsConfig: `${functionProject.projectDir}/tsconfig.app.json`, 17 | assets: [ 18 | `${functionProject.projectDir}/src/assets`, 19 | { 20 | glob: '**/*', 21 | input: `${appProject.projectDir}/environment`, 22 | output: '.', 23 | }, 24 | ], 25 | generatePackageJson: true, 26 | bundle: true, 27 | dependenciesFieldType: 'dependencies', 28 | format: ['esm'], 29 | thirdParty: false, 30 | target: 'node16', 31 | esbuildOptions: { 32 | logLevel: 'info', 33 | }, 34 | }, 35 | }, 36 | deploy: { 37 | executor: 'nx:run-commands', 38 | options: { 39 | command: `nx run ${appProject.projectName}:deploy --only functions:${functionProject.projectName}`, 40 | }, 41 | dependsOn: ['build'], 42 | }, 43 | lint: { 44 | executor: '@nx/linter:eslint', 45 | outputs: ['{options.outputFile}'], 46 | options: { 47 | lintFilePatterns: [`${functionProject.projectDir}/**/*.ts`], 48 | }, 49 | }, 50 | test: { 51 | executor: '@nx/jest:jest', 52 | outputs: [`{workspaceRoot}/coverage/{projectRoot}`], 53 | options: { 54 | jestConfig: `${functionProject.projectDir}/jest.config.ts`, 55 | passWithNoTests: true, 56 | }, 57 | configurations: { 58 | ci: { 59 | ci: true, 60 | codeCoverage: true, 61 | }, 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | export function validateFunctionConfig( 68 | functionProject: ProjectData, 69 | appProject: ProjectData, 70 | ) { 71 | const project = readJson(`${functionProject.projectDir}/project.json`) 72 | // expect(project.root).toEqual(`apps/${projectName}`) 73 | expect(project.targets).toEqual( 74 | expect.objectContaining( 75 | expectedFunctionProjectTargets(functionProject, appProject), 76 | ), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-helpers.ts: -------------------------------------------------------------------------------- 1 | export function expectStrings(input: string, contains: string[]) { 2 | contains.forEach((item) => { 3 | expect(input).toContain(item) 4 | }) 5 | } 6 | 7 | export function expectNoStrings(input: string, contains: string[]) { 8 | contains.forEach((item) => { 9 | expect(input).not.toContain(item) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-imports.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectData } from './test-utils-project-data' 2 | 3 | const IMPORT_MATCH = `import * as logger from "firebase-functions/logger";` 4 | 5 | export function getMainTs() { 6 | return ` 7 | /** 8 | * Import function triggers from their respective submodules: 9 | * 10 | * import {onCall} from "firebase-functions/v2/https"; 11 | * import {onDocumentWritten} from "firebase-functions/v2/firestore"; 12 | * 13 | * See a full list of supported triggers at https://firebase.google.com/docs/functions 14 | */ 15 | 16 | import {onRequest} from "firebase-functions/v2/https"; 17 | ${IMPORT_MATCH} 18 | 19 | // Start writing functions 20 | // https://firebase.google.com/docs/functions/typescript 21 | 22 | export const helloWorld = onRequest((request, response) => { 23 | logger.info("Hello logs!", {structuredData: true}); 24 | response.send("Hello from Firebase!"); 25 | }); 26 | ` 27 | } 28 | 29 | /** 30 | * return the import function for a generated library 31 | */ 32 | export function getLibImport(projectData: ProjectData) { 33 | // convert kebab-case project name to camelCase library import 34 | const libName = projectData.projectName 35 | .split('-') 36 | .map((part, index) => 37 | index > 0 ? part[0].toUpperCase() + part.substring(1) : part, 38 | ) 39 | .join('') 40 | return libName 41 | } 42 | 43 | export function addImport(mainTs: string, addition: string) { 44 | const replaced = mainTs.replace(IMPORT_MATCH, `${IMPORT_MATCH}\n${addition}`) 45 | return replaced 46 | } 47 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-logger.ts: -------------------------------------------------------------------------------- 1 | const ENABLE_TEST_DEBUG_INFO = true 2 | 3 | // https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color 4 | const GREEN_FG = '\x1b[32m' 5 | const RED_FG = '\x1b[31m' 6 | const RESET_FG = '\x1b[0m' 7 | 8 | export function green(text: string) { 9 | return `${GREEN_FG}${text}${RESET_FG}` 10 | } 11 | export function red(text: string) { 12 | return `${RED_FG}${text}${RESET_FG}` 13 | } 14 | 15 | export function testDebug(info: string) { 16 | if (ENABLE_TEST_DEBUG_INFO) { 17 | console.debug(info) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/test-utils/test-utils-project-data.ts: -------------------------------------------------------------------------------- 1 | import { joinPathFragments, names } from '@nx/devkit' 2 | 3 | const NPM_SCOPE = '@proj' 4 | 5 | export interface ProjectData { 6 | name: string 7 | directory: string // from Nx 16.8.0 this is the apps/libs prefix 8 | projectName: string 9 | projectDir: string 10 | srcDir: string 11 | distDir: string 12 | mainTsPath: string 13 | npmScope: string 14 | configName: string 15 | } 16 | 17 | /** 18 | * Generate test project data 19 | * Note: call this function AFTER initial app firebase.json has been created in order to have a 20 | * correct configName 21 | * @param name - project name (cannot be camel case) 22 | * @param dir - project dir 23 | * @returns - asset locations for this project 24 | */ 25 | export function getProjectData( 26 | type: 'libs' | 'apps', 27 | name: string, 28 | options?: { dir?: string; customConfig?: boolean }, 29 | ): ProjectData { 30 | // Nx16.8.0+ no longer adds the 'apps' or 'libs' prefix in the project name 31 | // --directory=${projectData.projectDir} is used instead 32 | // see https://nx.dev/deprecated/as-provided-vs-derived 33 | 34 | // we want to maintain the kebab-case name for the project/dir 35 | // but we need to ensure the 'apps' or 'libs' prefix is added to the --directory 36 | 37 | // EDIT: Actually this isn't true, it defaults to derived, for now, but the e2e project 38 | // setup doesnt seem to set a layout, or a derived default, so thats why we have to consider 39 | // that here, because without apps/libs layout we get different project names in the e2e tests. 40 | // See projectNameAndRootFormat option 41 | 42 | // const dir = options?.dir ? `${names(options.dir).fileName}` : '' 43 | const d = options?.dir ? `${names(options.dir).fileName}` : '' 44 | const dir = joinPathFragments(type, d) 45 | 46 | // project name is kebab case dir + name 47 | const n = names(name).fileName 48 | const prefix = options?.dir ? `${d}-` : '' 49 | const projectName = `${prefix}${n}` 50 | 51 | const projectDir = joinPathFragments(dir, n) 52 | 53 | const srcDir = joinPathFragments(projectDir, 'src') 54 | const mainTsPath = joinPathFragments(srcDir, 'main.ts') 55 | const distDir = joinPathFragments('dist', projectDir) 56 | 57 | return { 58 | name, // name passed to generator 59 | directory: dir, // --directory option required for generators 60 | projectName, // project name 61 | projectDir, 62 | srcDir, 63 | distDir, 64 | mainTsPath, 65 | npmScope: `${NPM_SCOPE}/${projectName}`, 66 | configName: options?.customConfig 67 | ? `firebase.${projectName}.json` 68 | : 'firebase.json', 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tests/nx-firebase.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ensureNxProject, 3 | readJson, 4 | runNxCommandAsync, 5 | updateFile, 6 | } from '@nx/plugin/testing' 7 | 8 | import { testWorkspace } from './test-workspace' 9 | import { testLibraries } from './test-libraries' 10 | import { testFunction } from './test-function' 11 | import { testApplication } from './test-application' 12 | import { testBundler } from './test-bundler' 13 | import { testSync } from './test-sync' 14 | import { testTargets } from './test-targets' 15 | import { testMigrate } from './test-migrate' 16 | 17 | /** 18 | * Nx 16.8.1 is giving me a massive headache with the daemon running during e2e tests 19 | * We get missing projects and LOCK_FILE_CHANGED errors 20 | * So we force CI environment variable to be true to ensure it is disabled 21 | * At least this way we know what runs locally will also match in actual CI environments 22 | * https://nx.dev/concepts/more-concepts/nx-daemon#turning-it-off 23 | */ 24 | process.env['CI'] = 'true' 25 | 26 | const JEST_TIMEOUT = 190000 27 | jest.setTimeout(JEST_TIMEOUT) 28 | 29 | // NOTE: If one e2e test fails, cleanup fails, so all subsequent tests will fail. 30 | 31 | // DONE 32 | // not gonna test watch, serve, emulate, killports, getconfig 33 | // check that deploy runs the application deploy 34 | // check that lint works for functions & apps 35 | // check that test works for functions & apps 36 | // check that serve works for apps 37 | // remove all tests related to old plugin 38 | // dont check anything that the generator tests already test, this is just e2e 39 | // check all options 40 | // check dependent packages are installed 41 | // check that functions are added 42 | // check that functions build 43 | // check all build artefacts are correct 44 | // check that libraries can be buildable and non-buildable 45 | // check that build includes building of dependent functions 46 | 47 | const pluginName = '@simondotm/nx-firebase' 48 | const pluginPath = 'dist/packages/nx-firebase' 49 | const workspaceLayout = { 50 | appsDir: 'apps', 51 | libsDir: 'libs', 52 | projectNameAndRootFormat: 'derived', 53 | } 54 | 55 | describe('nx-firebase e2e', () => { 56 | // Setting up individual workspaces per 57 | // test can cause e2e runs to take a long time. 58 | // For this reason, we recommend each suite only 59 | // consumes 1 workspace. The tests should each operate 60 | // on a unique project in the workspace, such that they 61 | // are not dependant on one another. 62 | beforeAll(async () => { 63 | ensureNxProject(pluginName, pluginPath) 64 | 65 | // Nx 16.8.1 defaults to as-provided, lets override this for my own sanity 66 | updateFile('nx.json', (text) => { 67 | const json = JSON.parse(text) 68 | console.debug(json) 69 | json.workspaceLayout = workspaceLayout 70 | // Disabling daemon for e2e tests as well, even though CI is enabled 71 | json.tasksRunnerOptions.default.useDaemonProcess = false 72 | return JSON.stringify(json, null, 2) 73 | }) 74 | // ensure daemon is off for e2e test 75 | runNxCommandAsync('reset') 76 | }, JEST_TIMEOUT) 77 | 78 | afterAll(() => { 79 | // `nx reset` kills the daemon, and performs 80 | // some work which can help clean up e2e leftovers 81 | runNxCommandAsync('reset') 82 | }) 83 | 84 | // test to ensure workspace setup is working 85 | describe('ensureNxProject workspace', () => { 86 | it('should successfuly configure workspace layout', () => { 87 | const nxJson = readJson('nx.json') 88 | expect(nxJson.workspaceLayout).toMatchObject(workspaceLayout) 89 | expect(nxJson.tasksRunnerOptions.default.useDaemonProcess).toBe(false) 90 | }) 91 | }) 92 | 93 | // run test suites 94 | testWorkspace() 95 | testLibraries() 96 | testApplication() 97 | testFunction() 98 | testBundler() 99 | testSync() 100 | testTargets() 101 | testMigrate() 102 | }) 103 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tests/test-application.ts: -------------------------------------------------------------------------------- 1 | import { 2 | readJson, 3 | runNxCommandAsync, 4 | uniq, 5 | checkFilesExist, 6 | readFile, 7 | } from '@nx/plugin/testing' 8 | 9 | import { 10 | ProjectData, 11 | appGeneratorAsync, 12 | cleanAppAsync, 13 | getProjectData, 14 | testDebug, 15 | validateProjectConfig, 16 | } from '../test-utils' 17 | function expectedAppFiles(projectData: ProjectData) { 18 | const projectPath = projectData.projectDir 19 | return [ 20 | `${projectPath}/public/index.html`, 21 | `${projectPath}/public/404.html`, 22 | `${projectPath}/database.rules.json`, 23 | `${projectPath}/firestore.indexes.json`, 24 | `${projectPath}/firestore.rules`, 25 | `${projectPath}/project.json`, 26 | `${projectPath}/readme.md`, 27 | `${projectPath}/storage.rules`, 28 | `${projectData.configName}`, 29 | `.firebaserc`, 30 | ] 31 | } 32 | 33 | //-------------------------------------------------------------------------------------------------- 34 | // Application generator e2e tests 35 | //-------------------------------------------------------------------------------------------------- 36 | export function testApplication() { 37 | describe('nx-firebase application', () => { 38 | it('should create nx-firebase app', async () => { 39 | const appData = getProjectData('apps', uniq('firebaseSetupApp')) 40 | await appGeneratorAsync(appData) 41 | // test generator output 42 | expect(() => checkFilesExist(...expectedAppFiles(appData))).not.toThrow() 43 | 44 | validateProjectConfig(appData) 45 | 46 | // check that the firestore.rules file has had the IN_30_DAYS placeholder replaced 47 | const firestoreRules = readFile(`${appData.projectDir}/firestore.rules`) 48 | testDebug(firestoreRules) 49 | expect(firestoreRules).not.toContain('IN_30_DAYS') 50 | 51 | 52 | // cleanup - app 53 | await cleanAppAsync(appData) 54 | }) 55 | 56 | it('should build nx-firebase app', async () => { 57 | const appData = getProjectData('apps', uniq('firebaseSetupApp')) 58 | await appGeneratorAsync(appData) 59 | 60 | // test app builder 61 | // at this point there are no functions so it does nothing 62 | const result = await runNxCommandAsync(`build ${appData.projectName}`) 63 | expect(result.stdout).toContain('Build succeeded.') 64 | 65 | // cleanup - app 66 | await cleanAppAsync(appData) 67 | }) 68 | 69 | describe('--directory', () => { 70 | it('should create nx-firebase app in the specified directory', async () => { 71 | const appData = getProjectData('apps', uniq('firebaseSetupApp'), { 72 | dir: 'subdir', 73 | }) 74 | await appGeneratorAsync(appData) 75 | expect(() => 76 | checkFilesExist(...expectedAppFiles(appData)), 77 | ).not.toThrow() 78 | 79 | const project = readJson(`${appData.projectDir}/project.json`) 80 | expect(project.name).toEqual(`${appData.projectName}`) 81 | 82 | validateProjectConfig(appData) 83 | 84 | // cleanup - app 85 | await cleanAppAsync(appData) 86 | }) 87 | }) 88 | 89 | describe('--tags', () => { 90 | it('should add tags to the project', async () => { 91 | const appData = getProjectData('apps', uniq('firebaseSetupApp')) 92 | await appGeneratorAsync(appData, `--tags e2etag,e2ePackage`) 93 | const project = readJson(`${appData.projectDir}/project.json`) 94 | expect(project.tags).toEqual([ 95 | 'firebase:app', 96 | `firebase:name:${appData.projectName}`, 97 | 'e2etag', 98 | 'e2ePackage', 99 | ]) 100 | 101 | // cleanup - app 102 | await cleanAppAsync(appData) 103 | }) 104 | }) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tests/test-libraries.ts: -------------------------------------------------------------------------------- 1 | import { readJson, checkFilesExist } from '@nx/plugin/testing' 2 | 3 | import { 4 | safeRunNxCommandAsync, 5 | libGeneratorAsync, 6 | getProjectData, 7 | } from '../test-utils' 8 | 9 | // libraries persist across all e2e tests 10 | export const buildableLibData = getProjectData('libs', 'buildablelib') 11 | export const nonbuildableLibData = getProjectData('libs', 'nonbuildablelib') 12 | export const subDirBuildableLibData = getProjectData('libs', 'buildablelib', { 13 | dir: 'subdir', 14 | }) 15 | export const subDirNonbuildableLibData = getProjectData( 16 | 'libs', 17 | 'nonbuildablelib', 18 | { dir: 'subdir' }, 19 | ) 20 | 21 | const compileComplete = 'Done compiling TypeScript files for project' 22 | const buildSuccess = 'Successfully ran target build for project' 23 | 24 | //-------------------------------------------------------------------------------------------------- 25 | // Create Libraries for e2e function generator tests 26 | //-------------------------------------------------------------------------------------------------- 27 | export function testLibraries() { 28 | describe('setup libraries', () => { 29 | it('should create buildable typescript library', async () => { 30 | await libGeneratorAsync( 31 | buildableLibData, 32 | `--bundler=tsc --importPath="${buildableLibData.npmScope}"`, 33 | ) 34 | 35 | // no need to test the js library generator, only that it ran ok 36 | expect(() => 37 | checkFilesExist(`${buildableLibData.projectDir}/package.json`), 38 | ).not.toThrow() 39 | 40 | const result = await safeRunNxCommandAsync( 41 | `build ${buildableLibData.projectName}`, 42 | ) 43 | expect(result.stdout).toContain(compileComplete) 44 | expect(result.stdout).toContain( 45 | `${buildSuccess} ${buildableLibData.projectName}`, 46 | ) 47 | }) 48 | 49 | it('should create buildable typescript library in subdir', async () => { 50 | await libGeneratorAsync( 51 | subDirBuildableLibData, 52 | `--bundler=tsc --importPath="${subDirBuildableLibData.npmScope}"`, 53 | ) 54 | 55 | // no need to test the js library generator, only that it ran ok 56 | expect(() => 57 | checkFilesExist(`${subDirBuildableLibData.projectDir}/package.json`), 58 | ).not.toThrow() 59 | 60 | const result = await safeRunNxCommandAsync( 61 | `build ${subDirBuildableLibData.projectName}`, 62 | ) 63 | expect(result.stdout).toContain(compileComplete) 64 | expect(result.stdout).toContain( 65 | `${buildSuccess} ${subDirBuildableLibData.projectName}`, 66 | ) 67 | }) 68 | 69 | it('should create non-buildable typescript library', async () => { 70 | await libGeneratorAsync( 71 | nonbuildableLibData, 72 | `--bundler=none --importPath="${nonbuildableLibData.npmScope}"`, 73 | ) 74 | 75 | expect(() => 76 | checkFilesExist(`${nonbuildableLibData.projectDir}/package.json`), 77 | ).toThrow() 78 | 79 | const project = readJson(`${nonbuildableLibData.projectDir}/project.json`) 80 | expect(project.targets.build).not.toBeDefined() 81 | }) 82 | 83 | it('should create non-buildable typescript library in subdir', async () => { 84 | // const projectData = getProjectData('libs', 'nonbuildablelib', { dir: 'subdir' }) 85 | await libGeneratorAsync( 86 | subDirNonbuildableLibData, 87 | `--bundler=none --importPath="${subDirNonbuildableLibData.npmScope}"`, 88 | ) 89 | 90 | expect(() => 91 | checkFilesExist(`${subDirNonbuildableLibData.projectDir}/package.json`), 92 | ).toThrow() 93 | 94 | const project = readJson( 95 | `${subDirNonbuildableLibData.projectDir}/project.json`, 96 | ) 97 | expect(project.targets.build).not.toBeDefined() 98 | }) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tests/test-migrate.ts: -------------------------------------------------------------------------------- 1 | import { readJson, uniq, updateFile, renameFile } from '@nx/plugin/testing' 2 | 3 | import { 4 | appGeneratorAsync, 5 | cleanAppAsync, 6 | cleanFunctionAsync, 7 | functionGeneratorAsync, 8 | getProjectData, 9 | validateProjectConfig, 10 | validateFunctionConfig, 11 | migrateGeneratorAsync, 12 | expectStrings, 13 | expectNoStrings, 14 | } from '../test-utils' 15 | import { ProjectConfiguration, joinPathFragments } from '@nx/devkit' 16 | 17 | //-------------------------------------------------------------------------------------------------- 18 | // Test migrations 19 | //-------------------------------------------------------------------------------------------------- 20 | export function testMigrate() { 21 | describe('nx-firebase migrate', () => { 22 | it('should successfuly migrate for legacy app', async () => { 23 | const appData = getProjectData('apps', uniq('firebaseMigrateApp')) 24 | const functionData = getProjectData( 25 | 'apps', 26 | uniq('firebaseMigrateFunction'), 27 | ) 28 | await appGeneratorAsync(appData) 29 | await functionGeneratorAsync(functionData, `--app ${appData.projectName}`) 30 | 31 | const result = await migrateGeneratorAsync() 32 | // testDebug(result.stdout) 33 | expectStrings(result.stdout, [`Running plugin migrations for workspace`]) 34 | 35 | // modify firebase app to be v2 schema 36 | const projectFile = `${appData.projectDir}/project.json` 37 | const projectJson = readJson(projectFile) 38 | projectJson.targets['serve'].executor = 'nx:run-commands' 39 | projectJson.targets[ 40 | 'getconfig' 41 | ].options.command = `nx run ${appData.projectName}:firebase functions:config:get > ${appData.projectDir}/.runtimeconfig.json` 42 | updateFile(projectFile, JSON.stringify(projectJson, null, 3)) 43 | 44 | // remove environment folder from app 45 | // cant delete in e2e, so lets just rename environment dir for now 46 | renameFile( 47 | joinPathFragments(appData.projectDir, 'environment'), 48 | joinPathFragments(appData.projectDir, uniq('environment')), 49 | ) 50 | 51 | // modify firebase.json to be v2 schema 52 | const configFile = `firebase.json` 53 | const configJson = readJson(configFile) 54 | delete configJson.functions[0].ignore 55 | updateFile(configFile, JSON.stringify(configJson, null, 3)) 56 | 57 | // remove globs from function project 58 | const functionFile = `${functionData.projectDir}/project.json` 59 | const functionJson = readJson(functionFile) 60 | const options = functionJson.targets['build'].options 61 | const assets = options.assets as string[] 62 | options.assets = [assets.shift()] 63 | updateFile(functionFile, JSON.stringify(functionJson, null, 3)) 64 | 65 | // run migrate script 66 | const result2 = await migrateGeneratorAsync() 67 | // testDebug(result2.stdout) 68 | expectStrings(result2.stdout, [ 69 | `MIGRATE Added default environment file 'environment/.env' for firebase app '${appData.projectName}'`, 70 | `MIGRATE Added default environment file 'environment/.env.local' for firebase app '${appData.projectName}'`, 71 | `MIGRATE Added default environment file 'environment/.secret.local' for firebase app '${appData.projectName}'`, 72 | `MIGRATE Updated getconfig target to use ignore environment directory for firebase app '${appData.projectName}'`, 73 | `MIGRATE Updated serve target for firebase app '${appData.projectName}'`, 74 | `MIGRATE Added assets glob for firebase function app '${functionData.projectName}'`, 75 | `UPDATE firebase.json`, 76 | `CREATE ${appData.projectDir}/environment/.env`, 77 | `CREATE ${appData.projectDir}/environment/.env.local`, 78 | `CREATE ${appData.projectDir}/environment/.secret.local`, 79 | `UPDATE ${appData.projectDir}/project.json`, 80 | ]) 81 | 82 | validateProjectConfig(appData) 83 | 84 | //todo: validateFunctionConfig - IMPORTANT since we missed some errors in last release due to this missing test 85 | // where assets glob was malformed 86 | validateFunctionConfig(functionData, appData) 87 | 88 | // run it again 89 | const result3 = await migrateGeneratorAsync() 90 | // testDebug(result3.stdout) 91 | expectStrings(result.stdout, [`Running plugin migrations for workspace`]) 92 | expectNoStrings(result3.stdout, [ 93 | `MIGRATE Added default environment file 'environment/.env' for firebase app '${appData.projectName}'`, 94 | `MIGRATE Added default environment file 'environment/.env.local' for firebase app '${appData.projectName}'`, 95 | `MIGRATE Added default environment file 'environment/.secret.local' for firebase app '${appData.projectName}'`, 96 | `MIGRATE Updated getconfig target to use ignore environment directory for firebase app '${appData.projectName}'`, 97 | `MIGRATE Updated serve target for firebase app '${appData.projectName}'`, 98 | `MIGRATE Added assets glob for firebase function app '${functionData.projectName}'`, 99 | `UPDATE firebase.json`, 100 | `CREATE ${appData.projectDir}/environment/.env`, 101 | `CREATE ${appData.projectDir}/environment/.env.local`, 102 | `CREATE ${appData.projectDir}/environment/.secret.local`, 103 | `UPDATE ${appData.projectDir}/project.json`, 104 | ]) 105 | 106 | // expect(true).toBeFalsy() 107 | 108 | // cleanup 109 | await cleanFunctionAsync(functionData) 110 | await cleanAppAsync(appData) 111 | }) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tests/test-workspace.ts: -------------------------------------------------------------------------------- 1 | import { readJson } from '@nx/plugin/testing' 2 | 3 | import { safeRunNxCommandAsync } from '../test-utils' 4 | import { detectPackageManager } from '@nx/devkit' 5 | 6 | //-------------------------------------------------------------------------------------------------- 7 | // Test the workspace setup & init generator 8 | //-------------------------------------------------------------------------------------------------- 9 | export function testWorkspace() { 10 | describe('workspace setup', () => { 11 | it('should create workspace without firebase dependencies', async () => { 12 | // test that generator adds dependencies to workspace package.json 13 | // should not be initially set 14 | const packageJson = readJson(`package.json`) 15 | expect(packageJson.dependencies['firebase']).toBeUndefined() 16 | expect(packageJson.dependencies['firebase-admin']).toBeUndefined() 17 | expect(packageJson.dependencies['firebase-functions']).toBeUndefined() 18 | expect( 19 | packageJson.devDependencies['firebase-functions-test'], 20 | ).toBeUndefined() 21 | expect(packageJson.devDependencies['firebase-tools']).toBeUndefined() 22 | }) 23 | 24 | it('should create workspace without nx dependencies', async () => { 25 | // test that generator adds dependencies to workspace package.json 26 | // should not be initially set 27 | const packageJson = readJson(`package.json`) 28 | expect(packageJson.devDependencies['@nx/node']).toBeUndefined() 29 | }) 30 | 31 | it('should run nx-firebase init', async () => { 32 | await safeRunNxCommandAsync(`generate @simondotm/nx-firebase:init`) 33 | // test that generator adds dependencies to workspace package.json 34 | const packageJson = readJson(`package.json`) 35 | expect(packageJson.dependencies['firebase']).toBeDefined() 36 | expect(packageJson.dependencies['firebase-admin']).toBeDefined() 37 | expect(packageJson.dependencies['firebase-functions']).toBeDefined() 38 | expect( 39 | packageJson.devDependencies['firebase-functions-test'], 40 | ).toBeDefined() 41 | 42 | // check that plugin init generator adds @google-cloud/functions-framework if pnpm is being used 43 | if (detectPackageManager() === 'pnpm') { 44 | expect( 45 | packageJson.dependencies['@google-cloud/functions-framework'], 46 | ).toBeDefined() 47 | } else { 48 | expect( 49 | packageJson.dependencies['@google-cloud/functions-framework'], 50 | ).not.toBeDefined() 51 | } 52 | 53 | // test that generator adds dev dependencies to workspace package.json 54 | expect(packageJson.devDependencies['firebase-tools']).toBeDefined() 55 | //SM: Mar'24: our plugin init generator now only add @nx/node 56 | expect(packageJson.devDependencies['@nx/node']).toBeDefined() 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.spec.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /e2e/nx-firebase-e2e/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nx/jest') 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | } 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=e2e --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | } 16 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "16.8.0-beta.3", 6 | "description": "Escape $ in env variables", 7 | "implementation": "./src/migrations/update-16-8-0/escape-dollar-sign-env-variables", 8 | "package": "nx", 9 | "name": "16.8.0-escape-dollar-sign-env" 10 | }, 11 | { 12 | "version": "16.8.0", 13 | "description": "update-16-8-0-add-ignored-files", 14 | "implementation": "./src/migrations/update-16-8-0-add-ignored-files/update-16-8-0-add-ignored-files", 15 | "package": "@nx/linter", 16 | "name": "update-16-8-0-add-ignored-files" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "simondotm", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "nx/tasks-runners/default", 9 | "options": { 10 | "cacheableOperations": ["build", "lint", "test", "e2e"] 11 | } 12 | } 13 | }, 14 | "workspaceLayout": { 15 | "appsDir": "e2e", 16 | "libsDir": "packages" 17 | }, 18 | "defaultProject": "nx-firebase", 19 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 20 | "targetDefaults": { 21 | "build": { 22 | "dependsOn": ["^build"], 23 | "inputs": ["production", "^production"] 24 | }, 25 | "lint": { 26 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 27 | }, 28 | "test": { 29 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 30 | } 31 | }, 32 | "namedInputs": { 33 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 34 | "sharedGlobals": [], 35 | "production": [ 36 | "default", 37 | "!{projectRoot}/.eslintrc.json", 38 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 39 | "!{projectRoot}/tsconfig.spec.json", 40 | "!{projectRoot}/jest.config.[jt]s", 41 | "!{projectRoot}/src/test-setup.[jt]s" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simondotm", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "packageManager": "pnpm@8.15.5", 6 | "scripts": { 7 | "preinstall": "npx only-allow pnpm", 8 | "postinstall": "node ./tools/generate-package-versions.js", 9 | "start": "nx serve", 10 | "build": "nx run nx-firebase:build", 11 | "test": "nx run nx-firebase:test", 12 | "lint": "nx run nx-firebase:lint", 13 | "e2e": "nx run nx-firebase-e2e:e2e --silent=false --bail=true", 14 | "release": "cd packages/nx-firebase && npm version", 15 | "release-help": "echo `npm run version -- v1.2.3` to set package version & commit tag", 16 | "compat:test": "npm run build && nx run compat:build && node dist/e2e/compat/main.js", 17 | "compat:clean": "nx run compat:build && node dist/e2e/compat/main.js --clean", 18 | "compat:setup": "npm run build && nx run compat:build && node dist/e2e/compat/main.js --setup" 19 | }, 20 | "private": true, 21 | "devDependencies": { 22 | "@google-cloud/functions-framework": "^3.3.0", 23 | "@nx/devkit": "16.8.1", 24 | "@nx/eslint-plugin": "16.8.1", 25 | "@nx/jest": "16.8.1", 26 | "@nx/js": "16.8.1", 27 | "@nx/linter": "16.8.1", 28 | "@nx/node": "16.8.1", 29 | "@nx/plugin": "16.8.1", 30 | "@nx/webpack": "16.8.1", 31 | "@nx/workspace": "16.8.1", 32 | "@swc-node/register": "~1.4.2", 33 | "@swc/core": "~1.3.51", 34 | "@types/jest": "29.4.4", 35 | "@types/node": "18.7.1", 36 | "@types/semver": "^7.3.13", 37 | "@typescript-eslint/eslint-plugin": "5.62.0", 38 | "@typescript-eslint/parser": "5.62.0", 39 | "eslint": "8.46.0", 40 | "eslint-config-prettier": "8.1.0", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "eslint-plugin-unused-imports": "^2.0.0", 43 | "firebase": "10.10.0", 44 | "firebase-admin": "11.11.1", 45 | "firebase-functions": "4.8.2", 46 | "firebase-functions-test": "3.1.1", 47 | "firebase-tools": "12.9.1", 48 | "jest": "29.4.3", 49 | "jest-environment-jsdom": "28.1.3", 50 | "kill-port": "2.0.1", 51 | "nx": "16.8.1", 52 | "only-allow": "^1.2.1", 53 | "prettier": "2.8.8", 54 | "semver": "^7.6.0", 55 | "ts-jest": "29.1.0", 56 | "ts-node": "10.9.1", 57 | "tslib": "^2.0.0", 58 | "typescript": "4.8.4" 59 | }, 60 | "dependencies": { 61 | "tslib": "^2.0.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/nx-firebase/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "parserOptions": { 8 | "project": ["packages/nx-firebase/tsconfig.*?.json"] 9 | }, 10 | "rules": {} 11 | }, 12 | { 13 | "files": ["*.ts", "*.tsx"], 14 | "rules": {} 15 | }, 16 | { 17 | "files": ["*.js", "*.jsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["./package.json", "./generators.json", "./executors.json"], 22 | "parser": "jsonc-eslint-parser", 23 | "rules": { 24 | "@nx/nx-plugin-checks": "error" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/nx-firebase/README.md: -------------------------------------------------------------------------------- 1 | # @simondotm/nx-firebase ![actions](https://github.com/simondotm/nx-firebase/actions/workflows/ci.yml/badge.svg) ![nx](https://img.shields.io/badge/Nx-v16.8.1-blue) ![npm](https://img.shields.io/npm/v/@simondotm/nx-firebase) ![downloads](https://img.shields.io/npm/dw/@simondotm/nx-firebase.svg) 2 | 3 | A plugin for [Nx](https://nx.dev) v16.8.1+ that integrates Firebase workflows in an Nx monorepo workspace. 4 | 5 | * Easily generate Firebase applications and functions 6 | * Uses `esbuild` for fast Firebase function builds so you can easily create & import shared Nx libraries with the benefits of tree-shaking 7 | * Supports function environment variables and secrets 8 | * Supports single or multiple firebase projects/apps within an Nx workspace 9 | * Full support for the Firebase Emulator suite for local development, with watch mode for functions 10 | * Keeps your `firebase.json` configurations in sync when renaming or deleting Firebase apps & functions 11 | * Only very lightly opinionated about your Firebase configurations and workspace layouts; you can use Nx or the Firebase CLI 12 | 13 | See [CHANGELOG](https://github.com/simondotm/nx-firebase/blob/main/CHANGELOG.md) for release notes. 14 | 15 | ## Install Plugin 16 | 17 | **`npm install @simondotm/nx-firebase --save-dev`** 18 | 19 | - Installs this plugin into your Nx workspace 20 | - This will also install `@nx/node` and firebase SDK's to your root workspace `package.json` if they are not already installed 21 | 22 | ## Generate Firebase Application 23 | 24 | **`nx g @simondotm/nx-firebase:app my-new-firebase-app [--directory=dir] [--project=proj]`** 25 | 26 | - Generates a new Nx Firebase application project in the workspace 27 | - The app generator will also create a Firebase configuration file in the root of your workspace (along with a default `.firebaserc` and `firebase.json` if they don't already exist) 28 | - For the first firebase application you create, the project firebase configuration will be `firebase.json` 29 | - If you create additional firebase applications, the project firebase configuration will be `firebase..json` 30 | - Use `--project` to link your Firebase App to a Firebase project name in your `.firebaserc` file 31 | 32 | ## Generate Firebase Function 33 | 34 | **`nx g @simondotm/nx-firebase:function my-new-firebase-function --app=my-new-firebase-app [--directory=dir]`** 35 | 36 | - Generates a new Nx Firebase function application project in the workspace 37 | - Firebase Function projects must be linked to a Firebase application project with the `--app` option 38 | - Firebase Function projects can contain one or more firebase functions 39 | - You can generate as many Firebase Function projects as you need for your application 40 | 41 | ## Build 42 | 43 | **`nx build my-new-firebase-app`** 44 | 45 | - Compiles & builds all Firebase function applications linked to the Nx Firebase application or an individual function 46 | 47 | **`nx build my-new-firebase-function`** 48 | 49 | - Compiles & builds an individual function 50 | 51 | 52 | ## Serve 53 | 54 | **`nx serve my-new-firebase-app`** 55 | 56 | - Builds & Watches all Firebase functions apps linked to the Firebase application 57 | - Starts the Firebase emulators 58 | 59 | ## Deploy 60 | 61 | ### Firebase Application 62 | 63 | **`nx deploy my-new-firebase-app [--only ...]`** 64 | 65 | - By default, deploys ALL of your cloud resources associated with your Firebase application (eg. sites, functions, database rules etc.) 66 | - Use the `--only` option to selectively deploy (same as Firebase CLI) 67 | 68 | For initial deployment: 69 | 70 | - **`firebase login`** if not already authenticated 71 | - **`firebase use --add`** to add your Firebase Project(s) to the `.firebaserc` file in your workspace. This step must be completed before you can deploy anything to Firebase. 72 | 73 | Note that you can also use the firebase CLI directly if you prefer: 74 | 75 | - **`firebase deploy --config=firebase..json --only functions`** 76 | 77 | ### Firebase Function 78 | 79 | **`nx deploy my-new-firebase-function`** 80 | 81 | - Deploys only a specific Firebase function 82 | 83 | 84 | 85 | ## Test 86 | 87 | **`nx test my-new-firebase-app`** 88 | 89 | - Runs unit tests for all Firebase functions apps linked to the Firebase application 90 | 91 | **`nx test my-new-firebase-function`** 92 | 93 | - Runs unit tests for an individual function 94 | 95 | 96 | ## Lint 97 | 98 | **`nx lint my-new-firebase-app`** 99 | 100 | - Runs linter for all Firebase functions apps linked to the Firebase application or an individual function 101 | 102 | **`nx lint my-new-firebase-function`** 103 | 104 | - Runs linter for an individual function 105 | 106 | ## Sync Workspace 107 | 108 | **`nx g @simondotm/nx-firebase:sync`** 109 | 110 | - Ensures that your `firebase.json` configurations are kept up to date with your workspace 111 | - If you rename or move firebase application or firebase function projects 112 | - If you delete firebase function projects 113 | 114 | ## Further Information 115 | 116 | See the full plugin [User Guide](https://github.com/simondotm/nx-firebase/blob/main/docs/user-guide.md) for more details. -------------------------------------------------------------------------------- /packages/nx-firebase/executors.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "executors": { 4 | "serve": { 5 | "implementation": "./src/executors/serve/serve", 6 | "schema": "./src/executors/serve/schema.json", 7 | "description": "firebase cli emulation serve executor" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/nx-firebase/generators.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "name": "nx-firebase", 4 | "version": "0.0.1", 5 | "generators": { 6 | "init": { 7 | "factory": "./src/generators/init/init", 8 | "schema": "./src/generators/init/schema.json", 9 | "description": "nx-firebase initializer", 10 | "hidden": true 11 | }, 12 | "application": { 13 | "factory": "./src/generators/application/application", 14 | "schema": "./src/generators/application/schema.json", 15 | "description": "nx-firebase application generator", 16 | "aliases": ["app"] 17 | }, 18 | "function": { 19 | "factory": "./src/generators/function/function", 20 | "schema": "./src/generators/function/schema.json", 21 | "description": "nx-firebase function generator", 22 | "aliases": ["func"] 23 | }, 24 | "sync": { 25 | "factory": "./src/generators/sync/sync", 26 | "schema": "./src/generators/sync/schema.json", 27 | "description": "nx-firebase project sync tool" 28 | }, 29 | "migrate": { 30 | "factory": "./src/generators/migrate/migrate", 31 | "schema": "./src/generators/migrate/schema.json", 32 | "description": "nx-firebase project migration tool" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/nx-firebase/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'nx-firebase', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | transform: { 7 | '^.+\\.[tj]s$': [ 8 | 'ts-jest', 9 | { 10 | tsconfig: '/tsconfig.spec.json', 11 | }, 12 | ], 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'html'], 15 | } 16 | -------------------------------------------------------------------------------- /packages/nx-firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simondotm/nx-firebase", 3 | "version": "2.3.0", 4 | "description": "A Firebase plugin for Nx monorepo workspaces.", 5 | "author": "Simon Morris", 6 | "license": "MIT", 7 | "homepage": "https://github.com/simondotm/nx-firebase", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:simondotm/nx-firebase.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/simondotm/nx-firebase/issues" 14 | }, 15 | "keywords": [ 16 | "Firebase", 17 | "Nx", 18 | "Monorepo" 19 | ], 20 | "main": "src/index.js", 21 | "generators": "./generators.json", 22 | "executors": "./executors.json", 23 | "peerDependencies": { 24 | "@nx/devkit": ">= 16.8.1", 25 | "@nx/workspace": ">= 16.8.1", 26 | "nx": ">= 16.8.1" 27 | }, 28 | "dependencies": {} 29 | } 30 | -------------------------------------------------------------------------------- /packages/nx-firebase/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-firebase", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/nx-firebase/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/packages/nx-firebase", 11 | "main": "packages/nx-firebase/src/index.ts", 12 | "tsConfig": "packages/nx-firebase/tsconfig.lib.json", 13 | "assets": [ 14 | "packages/nx-firebase/*.md", 15 | { 16 | "input": "./packages/nx-firebase/src", 17 | "glob": "**/!(*.ts)", 18 | "output": "./src" 19 | }, 20 | { 21 | "input": "./packages/nx-firebase/src", 22 | "glob": "**/*.d.ts", 23 | "output": "./src" 24 | }, 25 | { 26 | "input": "./packages/nx-firebase", 27 | "glob": "generators.json", 28 | "output": "." 29 | }, 30 | { 31 | "input": "./packages/nx-firebase", 32 | "glob": "executors.json", 33 | "output": "." 34 | } 35 | ] 36 | } 37 | }, 38 | "lint": { 39 | "executor": "@nx/linter:eslint", 40 | "outputs": ["{options.outputFile}"], 41 | "options": { 42 | "lintFilePatterns": ["packages/nx-firebase/**/*.ts"] 43 | } 44 | }, 45 | "test": { 46 | "executor": "@nx/jest:jest", 47 | "outputs": ["{workspaceRoot}/coverage/packages/nx-firebase"], 48 | "options": { 49 | "jestConfig": "packages/nx-firebase/jest.config.ts", 50 | "passWithNoTests": true 51 | } 52 | } 53 | }, 54 | "tags": [] 55 | } 56 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/__generated__/nx-firebase-versions.ts: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // This file is automatically generated by tools/generate-package-versions.js 3 | // Do not edit this file manually 4 | //------------------------------------------------------------------------------ 5 | export const packageVersions = { 6 | nx: '16.8.1', 7 | firebase: '10.10.0', 8 | firebaseAdmin: '11.11.1', 9 | firebaseFunctions: '4.8.2', 10 | firebaseFunctionsTest: '3.1.1', 11 | firebaseTools: '12.9.1', 12 | killPort: '2.0.1', 13 | nodeEngine: '16', 14 | googleCloudFunctionsFramework: '3.3.0', 15 | } 16 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/executors/serve/schema.d.ts: -------------------------------------------------------------------------------- 1 | // maintains the existing executor format as `nx:run-commands` for compatibility 2 | export interface FirebaseServeExecutorSchema { 3 | commands: string[] 4 | __unparsed__: string[] 5 | } 6 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/executors/serve/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "title": "Firebase serve", 5 | "description": "Runs the Firebase CLI emulator for the target project", 6 | "type": "object", 7 | "properties": { 8 | "commands": { 9 | "description": "The serve commands - watch & emulate.", 10 | "type": "array", 11 | "items": { 12 | "type": "string" 13 | } 14 | }, 15 | "__unparsed__": { 16 | "hidden": true, 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "$default": { 22 | "$source": "unparsed" 23 | }, 24 | "x-priority": "internal" 25 | } 26 | }, 27 | "required": [] 28 | } 29 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/executors/serve/serve.spec.ts: -------------------------------------------------------------------------------- 1 | // import { FirebaseServeExecutorSchema } from './schema' 2 | // import runFirebaseServeExecutor from './firebase' 3 | 4 | // const options: FirebaseServeExecutorSchema = {} 5 | 6 | describe('Firebase Serve Executor', () => { 7 | it('can run', async () => { 8 | // const output = await runFirebaseServeExecutor([]) 9 | // expect(output.success).toBe(true) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/executors/serve/serve.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutorContext, 3 | logger, 4 | TargetConfiguration, 5 | ProjectConfiguration, 6 | } from '@nx/devkit' 7 | import { FirebaseServeExecutorSchema } from './schema' 8 | import { spawn } from 'child_process' 9 | 10 | type NxRunCommandsTargetConfiguration = TargetConfiguration<{ 11 | command?: string 12 | commands?: string[] 13 | }> 14 | 15 | type ValidTarget = 'firebase' | 'watch' | 'emulate' 16 | 17 | function getCommandFromTarget( 18 | project: ProjectConfiguration, 19 | targetName: ValidTarget, 20 | commandGrep: string, 21 | ) { 22 | const target = project.targets[targetName] as NxRunCommandsTargetConfiguration 23 | if (!target) { 24 | throw new Error( 25 | `Could not find target '${targetName}' in project '${project.name}'`, 26 | ) 27 | } 28 | const commands: string[] = [ 29 | ...(target.options.command ? [target.options.command] : []), 30 | ...(target.options.commands ? target.options.commands : []), 31 | ].filter((cmd) => cmd.includes(commandGrep)) 32 | 33 | if (commands.length === 0) { 34 | throw new Error( 35 | `Could not find a command in target '${targetName}' that matches '${commandGrep}'`, 36 | ) 37 | } 38 | 39 | if (commands.length !== 1) { 40 | logger.warn( 41 | `Found multiple commands in target '${targetName}' that match '${commandGrep}', using first match`, 42 | ) 43 | } 44 | return commands[0] 45 | } 46 | 47 | export default async function runFirebaseServeExecutor( 48 | options: FirebaseServeExecutorSchema, 49 | context: ExecutorContext, 50 | ) { 51 | return new Promise<{ success: boolean }>((resolve) => { 52 | const projectName = context.projectName 53 | const projects = context.projectsConfigurations 54 | const project = projects.projects[projectName] 55 | 56 | // Determine the watch target command 57 | const watchCommand = getCommandFromTarget( 58 | project, 59 | 'watch', 60 | 'nx run-many --targets=build', 61 | ) 62 | 63 | // Determine the firebase target command, so we get --config & --project 64 | const firebaseCommand = getCommandFromTarget( 65 | project, 66 | 'firebase', 67 | 'firebase', 68 | ) 69 | 70 | // Determine the emulator target command 71 | const emulateCommand = getCommandFromTarget( 72 | project, 73 | 'emulate', 74 | 'emulators:', 75 | ).replace(`nx run ${projectName}:firebase`, '') 76 | 77 | // Run the watch process 78 | // eslint-disable-next-line 79 | const watchProcess = spawn(watchCommand, [], { 80 | shell: true, 81 | stdio: 'inherit', 82 | detached: false, 83 | }).on('exit', (code) => { 84 | if (!code) { 85 | logger.warn(`serve: watch process finished successfully`) 86 | } else { 87 | logger.error(`serve: watch process finished with error '${code}'`) 88 | } 89 | }) 90 | 91 | // determine any extra commands passed on the command line 92 | const extraArgs = 93 | options.__unparsed__.length > 0 94 | ? ' ' + options.__unparsed__.join(' ') 95 | : '' 96 | 97 | // Run the firebase emulator process 98 | const emulatorProcess = spawn( 99 | `${firebaseCommand} ${emulateCommand} ${extraArgs}`, 100 | [], 101 | { 102 | shell: true, 103 | stdio: 'inherit', 104 | detached: false, 105 | }, 106 | ) 107 | emulatorProcess.on('exit', (code) => { 108 | if (!code) { 109 | logger.warn(`serve: Firebase emulator finished successfully`) 110 | resolve({ success: true }) // not sure what difference this makes 111 | } else { 112 | logger.error(`serve: Firebase emulator finished with error '${code}'`) 113 | resolve({ success: false }) 114 | } 115 | }) 116 | 117 | // Handle signals for serve executor process 118 | const processExitListener = (signal) => { 119 | // logger.error(`\nserve: executor received signal '${signal}'`) 120 | if (signal === 'SIGINT') { 121 | logger.warn(`\nserve: terminating`) 122 | // no need for these it seems 123 | // emulatorProcess.kill() 124 | // watchProcess.kill() 125 | } 126 | if (signal === 0) { 127 | logger.warn(`serve: finished successfully`) 128 | } 129 | if (signal === 1) { 130 | logger.error(`serve: finished with errors`) 131 | } 132 | } 133 | process.on('exit', processExitListener) 134 | process.on('SIGTERM', processExitListener) 135 | process.on('SIGINT', processExitListener) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/environment/.env: -------------------------------------------------------------------------------- 1 | # Firebase function environment variables 2 | # https://firebase.google.com/docs/functions/config-env?gen=2nd#env-variables 3 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/environment/.env.local: -------------------------------------------------------------------------------- 1 | # Firebase function emulator environment variables 2 | # https://firebase.google.com/docs/functions/config-env?gen=2nd#emulator_support 3 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/environment/.secret.local: -------------------------------------------------------------------------------- 1 | # Firebase function emulator secret environment variables 2 | # https://firebase.google.com/docs/functions/config-env?gen=2nd#secrets_and_credentials_in_the_emulator 3 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | // Example: 3 | // 4 | // "indexes": [ 5 | // { 6 | // "collectionGroup": "widgets", 7 | // "queryScope": "COLLECTION", 8 | // "fields": [ 9 | // { "fieldPath": "foo", "arrayConfig": "CONTAINS" }, 10 | // { "fieldPath": "bar", "mode": "DESCENDING" } 11 | // ] 12 | // }, 13 | // 14 | // "fieldOverrides": [ 15 | // { 16 | // "collectionGroup": "widgets", 17 | // "fieldPath": "baz", 18 | // "indexes": [ 19 | // { "order": "ASCENDING", "queryScope": "COLLECTION" } 20 | // ] 21 | // }, 22 | // ] 23 | // ] 24 | "indexes": [], 25 | "fieldOverrides": [] 26 | } -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | // This rule allows anyone with your database reference to view, edit, 5 | // and delete all data in your database. It is useful for getting 6 | // started, but it is configured to expire after 30 days because it 7 | // leaves your app open to attackers. At that time, all client 8 | // requests to your database will be denied. 9 | // 10 | // Make sure to write security rules for your app before that time, or 11 | // else all client requests to your database will be denied until you 12 | // update your rules. 13 | allow read, write: if request.time < timestamp.date(<%= IN_30_DAYS %>); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 40 | 41 | 42 |
43 |

Welcome

44 |

Firebase Hosting Setup Complete

45 |

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

46 | Open Hosting Documentation 47 |
48 |

Firebase SDK Loading…

49 | 50 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/readme.md__tmpl__: -------------------------------------------------------------------------------- 1 | # <%= projectName %> 2 | 3 | This Nx Firebase application was generated by [@simondotm/nx-firebase](https://github.com/simondotm/nx-firebase). 4 | 5 | ## Generated Application Files 6 | 7 | * `database.rules.json` - Default Firebase Realtime Database Rules 8 | * `firestore.indexes.json` - Default Firebase Firestore Database Rules 9 | * `storage.rules` - Default Firebase Storage Rules 10 | * `public/index.ts` - Default Firebase hosting site 11 | 12 | ## Generated Workspace Root Files 13 | 14 | * `<%= firebaseAppConfig %>` - Firebase CLI Configuration for this project 15 | * `.firebaserc` - Default Firebase CLI Deployment Targets Configuration 16 | 17 | ## Generated dependencies 18 | 19 | Nx-Firebase will add `firebase-tools`, `firebase-admin` and `firebase-functions` to your workspace if they do not already exist. 20 | 21 | ## Next Steps 22 | 23 | * Read about the [Firebase CLI here](https://firebase.google.com/docs/cli) 24 | * `firebase login` - Authenticate the Firebase CLI 25 | * `firebase use --add` - Add your Firebase Project as a target to `.firebaserc` 26 | * `nx g @simondotm/nx-firebase:function my-function --firebaseApp <%= projectName %>` - Add a firebase function to this project 27 | 28 | See the plugin [README](https://github.com/simondotm/nx-firebase/blob/main/README.md) for more information. 29 | 30 | ## Commands 31 | 32 | * `nx run <%= projectName %>:deploy` - Deploy this app to firebase 33 | * `nx run <%= projectName %>:serve` - Serve this app using the firebase emulator 34 | * `nx run <%= projectName %>:build` - Build all functions associated with this app 35 | * `nx run <%= projectName %>:test` - Test all functions associated with this app 36 | * `nx run <%= projectName %>:lint` - Lint all functions associated with this app 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | // Craft rules based on data in your Firestore database 4 | // allow write: if firestore.get( 5 | // /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; 6 | service firebase.storage { 7 | match /b/{bucket}/o { 8 | match /{allPaths=**} { 9 | allow read, write: if false; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files_firebase/firebase.json__tmpl__: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "<%= projectRoot %>/database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "<%= projectRoot %>/firestore.rules", 7 | "indexes": "<%= projectRoot %>/firestore.indexes.json" 8 | }, 9 | "hosting": { 10 | "public": "<%= projectRoot %>/public", 11 | "ignore": [ 12 | "firebase.json", 13 | "**/.*", 14 | "**/node_modules/**" 15 | ], 16 | "rewrites": [ 17 | { 18 | "source": "**", 19 | "destination": "/index.html" 20 | } 21 | ] 22 | }, 23 | "storage": { 24 | "rules": "<%= projectRoot %>/storage.rules" 25 | }, 26 | "functions": [], 27 | "emulators": { 28 | "auth": { 29 | "port": 9099 30 | }, 31 | "functions": { 32 | "port": 5001 33 | }, 34 | "firestore": { 35 | "port": 8080 36 | }, 37 | "database": { 38 | "port": 9000 39 | }, 40 | "hosting": { 41 | "port": 5000 42 | }, 43 | "pubsub": { 44 | "port": 8085 45 | }, 46 | "storage": { 47 | "port": 9199 48 | }, 49 | "eventarc": { 50 | "port": 9299 51 | }, 52 | "ui": { 53 | "enabled": true 54 | }, 55 | "singleProjectMode": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files_firebaserc/.firebaserc__tmpl__: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | }, 4 | "projects": { 5 | } 6 | } -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/files_workspace/firebase.__projectName__.json__tmpl__: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "<%= projectRoot %>/database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "<%= projectRoot %>/firestore.rules", 7 | "indexes": "<%= projectRoot %>/firestore.indexes.json" 8 | }, 9 | "hosting": { 10 | "public": "<%= projectRoot %>/public", 11 | "ignore": [ 12 | "firebase.json", 13 | "**/.*", 14 | "**/node_modules/**" 15 | ], 16 | "rewrites": [ 17 | { 18 | "source": "**", 19 | "destination": "/index.html" 20 | } 21 | ] 22 | }, 23 | "storage": { 24 | "rules": "<%= projectRoot %>/storage.rules" 25 | }, 26 | "functions": [], 27 | "emulators": { 28 | "auth": { 29 | "port": 9099 30 | }, 31 | "functions": { 32 | "port": 5001 33 | }, 34 | "firestore": { 35 | "port": 8080 36 | }, 37 | "database": { 38 | "port": 9000 39 | }, 40 | "hosting": { 41 | "port": 5000 42 | }, 43 | "pubsub": { 44 | "port": 8085 45 | }, 46 | "storage": { 47 | "port": 9199 48 | }, 49 | "eventarc": { 50 | "port": 9299 51 | }, 52 | "ui": { 53 | "enabled": true 54 | }, 55 | "singleProjectMode": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/lib/create-files.ts: -------------------------------------------------------------------------------- 1 | import { generateFiles, joinPathFragments, Tree } from '@nx/devkit' 2 | 3 | import type { NormalizedSchema } from '../schema' 4 | 5 | /** 6 | * Generate the firebase app specific files 7 | * 8 | * @param tree 9 | * @param options 10 | */ 11 | export function createFiles(tree: Tree, options: NormalizedSchema): void { 12 | const firebaseAppConfig = options.firebaseConfigName 13 | 14 | // Firebase SDK firestore.rules template has a placeholder for the date 30 days from now 15 | // so we add that substitution here at the point of app generation 16 | const date = new Date() 17 | date.setDate(date.getDate() + 30) 18 | const dateString = date 19 | .toISOString() 20 | .split('T')[0] 21 | .split('-') 22 | .map((v) => parseInt(v).toString()) 23 | .join(', ') 24 | 25 | const substitutions = { 26 | tmpl: '', 27 | projectName: options.projectName, 28 | projectRoot: options.projectRoot, 29 | firebaseAppConfig, 30 | IN_30_DAYS: dateString, 31 | } 32 | 33 | // The default functions package.json & templated typescript source files are added here 34 | // to match the `firebase init` cli setup 35 | // if the user isn't using functions, they can just delete and update their firebase config accordingly 36 | // 37 | // Rules and index files also get generated in the application folder; 38 | // 1. so that they dont clutter up the root workspace 39 | // 2. so that they are located within the nx firebase application project they relate to 40 | generateFiles( 41 | tree, 42 | joinPathFragments(__dirname, '..', 'files'), 43 | options.projectRoot, 44 | substitutions, 45 | ) 46 | 47 | // The first firebase app project in a workspace will always use `firebase.json` as its config file 48 | // Subsequent firebase app projects will be assigned a config file based on the project name, so `firebase..json` 49 | if (firebaseAppConfig === 'firebase.json') { 50 | // if (!tree.exists('firebase.json')) { 51 | generateFiles( 52 | tree, 53 | joinPathFragments(__dirname, '..', 'files_firebase'), 54 | '', 55 | substitutions, 56 | ) 57 | } else { 58 | generateFiles( 59 | tree, 60 | joinPathFragments(__dirname, '..', 'files_workspace'), 61 | '', // SM: this is a tree path, not a file system path 62 | substitutions, 63 | ) 64 | } 65 | 66 | // For a fresh workspace, the firebase CLI needs at least a firebase.json and an empty .firebaserc 67 | // in order to use commands like 'firebase use --add' 68 | if (!tree.exists('.firebaserc')) { 69 | generateFiles( 70 | tree, 71 | joinPathFragments(__dirname, '..', 'files_firebaserc'), 72 | '', 73 | substitutions, 74 | ) 75 | } 76 | // else { 77 | // logger.log('✓ .firebaserc already exists in this workspace') 78 | // } 79 | } 80 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-files' 2 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils' 2 | 3 | export interface Schema { 4 | // standard @nx project generator options 5 | name: string 6 | directory?: string 7 | tags?: string 8 | projectNameAndRootFormat?: ProjectNameAndRootFormat 9 | rootProject?: boolean 10 | // extra options for @simondotm/nx-firebase:app generator 11 | project?: string 12 | // firebaseProject?: string 13 | // firebaseConfig?: string 14 | } 15 | 16 | export interface NormalizedSchema extends Schema { 17 | projectName: string 18 | projectRoot: string 19 | parsedTags: string[] 20 | firebaseConfigName: string 21 | } 22 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/application/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "NxFirebaseApplicationGenerator", 4 | "title": "Nx Firebase Application Options Schema", 5 | "description": "Nx Firebase Application Options Schema.", 6 | "type": "object", 7 | "properties": { 8 | "name": { 9 | "description": "The name of the firebase application.", 10 | "type": "string", 11 | "$default": { 12 | "$source": "argv", 13 | "index": 0 14 | }, 15 | "x-prompt": "What name would you like to use for the firebase application?" 16 | }, 17 | "directory": { 18 | "description": "A directory where the application is placed.", 19 | "type": "string", 20 | "alias": "d" 21 | }, 22 | "tags": { 23 | "type": "string", 24 | "description": "Add tags to the project (used for linting)", 25 | "alias": "t" 26 | }, 27 | "projectNameAndRootFormat": { 28 | "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", 29 | "type": "string", 30 | "enum": ["as-provided", "derived"] 31 | }, 32 | "project": { 33 | "type": "string", 34 | "description": "The firebase CLI project that should be associated with this application", 35 | "default": "" 36 | } 37 | }, 38 | "additionalProperties": false, 39 | "required": ["name"] 40 | } 41 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/files/package.json__tmpl__: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= projectName %>", 3 | "description": "Firebase Function, auto generated by @simondotm/nx-firebase", 4 | "scripts": { 5 | "test": "nx test <%= projectName %>", 6 | "lint": "nx lint <%= projectName %>", 7 | "build": "nx build <%= projectName %>", 8 | "deploy": "nx deploy <%= projectName %>" 9 | }, 10 | "type": "<%= moduleType %>", 11 | "engines": { 12 | "node": "<%= firebaseNodeEngine %>" 13 | }, 14 | "dependencies": { 15 | }, 16 | "devDependencies": { 17 | }, 18 | "private": true 19 | } 20 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/files/readme.md__tmpl__: -------------------------------------------------------------------------------- 1 | # <%= projectName %> 2 | 3 | This Nx Firebase function was generated by [@simondotm/nx-firebase](https://github.com/simondotm/nx-firebase). 4 | 5 | ## Generated Application Files 6 | 7 | * `src/main.ts` - Default Firebase Functions entry point 8 | 9 | ## Generated Workspace Root Files 10 | 11 | * `<%= firebaseAppConfig %>` - Firebase CLI Configuration for this project 12 | * `.firebaserc` - Default Firebase CLI Deployment Targets Configuration 13 | * `firebase.json` - Intentionally Empty Firebase CLI Configuration (only needed to allow Firebase CLI to run in your workspace) 14 | 15 | ## Generated modules 16 | 17 | Nx-Firebase will add `firebase-admin` and `firebase-functions` to your workspace `package.json` at the `'latest'` version. You may wish to set these to a specific version. 18 | 19 | ## Next Steps 20 | 21 | * `npm install -g firebase-tools` - Install the [Firebase CLI](https://firebase.google.com/docs/cli) 22 | * `firebase login` - Authenticate the Firebase CLI 23 | * `firebase use --add` - Add your Firebase Project as a target to `.firebaserc` 24 | * You do not need to `npm install` in the app project directory, but can still add and run custom npm scripts to the app `package.json` if you wish 25 | 26 | 27 | 28 | See the plugin [README](https://github.com/simondotm/nx-firebase/blob/main/README.md) for more information. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/files/src/main.ts__tmpl__: -------------------------------------------------------------------------------- 1 | /** 2 | * Import function triggers from their respective submodules: 3 | * 4 | * import {onCall} from "firebase-functions/v2/https"; 5 | * import {onDocumentWritten} from "firebase-functions/v2/firestore"; 6 | * 7 | * See a full list of supported triggers at https://firebase.google.com/docs/functions 8 | */ 9 | 10 | import {onRequest} from "firebase-functions/v2/https"; 11 | import * as logger from "firebase-functions/logger"; 12 | 13 | // Start writing functions 14 | // https://firebase.google.com/docs/functions/typescript 15 | 16 | // export const helloWorld = onRequest((request, response) => { 17 | // logger.info("Hello logs!", {structuredData: true}); 18 | // response.send("Hello from Firebase!"); 19 | // }); 20 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/function.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GeneratorCallback, 3 | readProjectConfiguration, 4 | Tree, 5 | convertNxGenerator, 6 | formatFiles, 7 | runTasksInSerial, 8 | names, 9 | getProjects, 10 | } from '@nx/devkit' 11 | import { applicationGenerator as nodeApplicationGenerator } from '@nx/node' 12 | 13 | import { initGenerator } from '../init/init' 14 | import { getFirebaseConfigFromProject, updateTsConfig } from '../../utils' 15 | 16 | import { addFunctionConfig, createFiles, updateProject } from './lib' 17 | import type { Schema, NormalizedSchema } from './schema' 18 | import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils' 19 | import { packageVersions } from '../../__generated__/nx-firebase-versions' 20 | 21 | export async function normalizeOptions( 22 | host: Tree, 23 | options: Schema, 24 | callingGenerator = '@simondotm/nx-firebase:function', 25 | ): Promise { 26 | const { 27 | projectName: appProjectName, 28 | projectRoot, 29 | projectNameAndRootFormat, 30 | } = await determineProjectNameAndRootOptions(host, { 31 | name: options.name, 32 | projectType: 'application', 33 | directory: options.directory, 34 | projectNameAndRootFormat: options.projectNameAndRootFormat, 35 | rootProject: options.rootProject, 36 | callingGenerator, 37 | }) 38 | 39 | options.rootProject = projectRoot === '.' 40 | options.projectNameAndRootFormat = projectNameAndRootFormat 41 | 42 | const parsedTags = options.tags 43 | ? options.tags.split(',').map((s) => s.trim()) 44 | : [] 45 | 46 | // const { projectName, projectRoot } = getProjectName( 47 | // host, 48 | // options.name, 49 | // options.directory, 50 | // ) 51 | 52 | // get & validate the firebase app project this function will be attached to 53 | const firebaseApp = names(options.app).fileName 54 | const projects = getProjects(host) 55 | if (!projects.has(firebaseApp)) { 56 | throw new Error( 57 | `A firebase application project called '${firebaseApp}' was not found in this workspace.`, 58 | ) 59 | } 60 | 61 | const firebaseAppProject = readProjectConfiguration(host, firebaseApp) 62 | 63 | // read the firebase config used by the parent app project 64 | const firebaseConfigName = getFirebaseConfigFromProject( 65 | host, 66 | firebaseAppProject, 67 | ) 68 | 69 | return { 70 | ...options, 71 | name: names(options.name).fileName, 72 | projectName: appProjectName, 73 | projectRoot, 74 | parsedTags, 75 | firebaseConfigName, 76 | firebaseAppProject, 77 | } 78 | 79 | // return { 80 | // ...options, 81 | // runTime: options.runTime || '16', 82 | // format: options.format || 'esm', 83 | // projectRoot, 84 | // projectName, 85 | // firebaseConfigName, 86 | // firebaseAppProject, 87 | // } 88 | } 89 | 90 | /** 91 | * Firebase 'functions' application generator 92 | * Uses the `@nx/node` application generator as a base implementation 93 | * 94 | * @param host 95 | * @param schema 96 | * @returns 97 | */ 98 | export async function functionGenerator( 99 | host: Tree, 100 | schema: Schema, 101 | ): Promise { 102 | const tasks: GeneratorCallback[] = [] 103 | 104 | const options = await normalizeOptions(host, { 105 | // set default options 106 | projectNameAndRootFormat: 'derived', 107 | runTime: packageVersions.nodeEngine as typeof schema.runTime, // we can be sure that our firebaseNodeEngine value satisfies the type 108 | // apply overrides from user 109 | ...schema, 110 | }) 111 | 112 | if (!options.runTime) { 113 | throw new Error('No runtime specified for the function app') 114 | } 115 | 116 | // const options = normalizeOptions(host, schema) 117 | 118 | // initialise plugin 119 | const initTask = await initGenerator(host, {}) 120 | tasks.push(initTask) 121 | 122 | // We use @nx/node:app to scaffold our function application, then modify as required 123 | // `nx g @nx/node:app function-name --directory functions/dir --e2eTestRunner=none --framework=none --unitTestRunner=jest --bundler=esbuild --tags=firebase:firebase-app` 124 | 125 | // Function apps are tagged so that they can built/watched with run-many 126 | const tags = 127 | `firebase:function,firebase:name:${options.projectName},firebase:dep:${options.firebaseAppProject.name}` + 128 | (options.tags ? `,${options.tags}` : '') 129 | 130 | const nodeApplicationTask = await nodeApplicationGenerator(host, { 131 | name: options.name, 132 | directory: options.directory, 133 | projectNameAndRootFormat: options.projectNameAndRootFormat, 134 | tags, 135 | setParserOptionsProject: options.setParserOptionsProject, 136 | skipFormat: options.skipFormat, 137 | e2eTestRunner: 'none', 138 | bundler: 'esbuild', 139 | framework: 'none', 140 | unitTestRunner: 'jest', 141 | }) 142 | tasks.push(nodeApplicationTask) 143 | 144 | // generate function app specific files 145 | createFiles(host, options) 146 | 147 | // update TS config for esm or cjs 148 | updateTsConfig(host, options.projectRoot, options.runTime, options.format) 149 | 150 | // reconfigure the @nx/node:app to suit firebase functions 151 | updateProject(host, options) 152 | 153 | // update firebase functions config 154 | addFunctionConfig(host, options) 155 | 156 | // ensures newly added files are formatted to match workspace style 157 | if (!options.skipFormat) { 158 | await formatFiles(host) 159 | } 160 | 161 | return runTasksInSerial(...tasks) 162 | } 163 | 164 | export default functionGenerator 165 | export const functionSchematic = convertNxGenerator(functionGenerator) 166 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/lib/add-function-config.ts: -------------------------------------------------------------------------------- 1 | import { Tree, joinPathFragments, updateJson } from '@nx/devkit' 2 | import type { FirebaseConfig, FirebaseFunction } from '../../../types' 3 | import type { NormalizedSchema } from '../schema' 4 | 5 | export function addFunctionConfig(host: Tree, options: NormalizedSchema) { 6 | updateJson(host, options.firebaseConfigName, (json: FirebaseConfig) => { 7 | const functionConfig = { 8 | codebase: options.projectName, 9 | source: joinPathFragments('dist', options.projectRoot), 10 | runtime: `nodejs${options.runTime}`, 11 | ignore: ['*.local'], 12 | } 13 | 14 | // console.log(`json.functions=${JSON.stringify(json.functions)}`) 15 | if (!Array.isArray(json.functions)) { 16 | const existingFunction = Object.assign({}, json.functions) 17 | json.functions = [] 18 | if (Object.keys(existingFunction).length) { 19 | json.functions.push(existingFunction) 20 | } 21 | } 22 | json.functions.push(functionConfig) 23 | // sort the codebases to be neat & tidy 24 | json.functions.sort((a: FirebaseFunction, b: FirebaseFunction) => { 25 | return a.codebase < b.codebase ? -1 : a.codebase > b.codebase ? 1 : 0 26 | }) 27 | return json 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/lib/create-files.ts: -------------------------------------------------------------------------------- 1 | import { 2 | detectPackageManager, 3 | offsetFromRoot, 4 | Tree, 5 | updateJson, 6 | generateFiles, 7 | joinPathFragments, 8 | } from '@nx/devkit' 9 | import type { NormalizedSchema } from '../schema' 10 | import { packageVersions } from '../../../__generated__/nx-firebase-versions' 11 | 12 | /** 13 | * Generate the firebase app specific files 14 | * 15 | * @param host 16 | * @param options 17 | */ 18 | export function createFiles(host: Tree, options: NormalizedSchema): void { 19 | const firebaseAppConfig = options.firebaseConfigName 20 | const firebaseAppConfigPath = joinPathFragments( 21 | offsetFromRoot(options.projectRoot), 22 | firebaseAppConfig, 23 | ) 24 | 25 | const substitutions = { 26 | tmpl: '', 27 | projectName: options.projectName, 28 | projectRoot: options.projectRoot, 29 | 30 | firebaseAppName: options.app, 31 | firebaseAppConfig, 32 | firebaseAppConfigPath, 33 | 34 | firebaseNodeEngine: options.runTime, 35 | 36 | // firebaseNodeRuntime, 37 | // firebaseNodeEngine, 38 | 39 | moduleType: options.format === 'esm' ? 'module' : 'commonjs', 40 | } 41 | 42 | // The default functions package.json & templated typescript source files are added here 43 | // to match the `firebase init` cli setup 44 | // if the user isn't using functions, they can just delete and update their firebase config accordingly 45 | // 46 | // Rules and index files also get generated in the application folder; 47 | // 1. so that they dont clutter up the root workspace 48 | // 2. so that they are located within the nx firebase application project they relate to 49 | generateFiles( 50 | host, 51 | joinPathFragments(__dirname, '..', 'files'), 52 | options.projectRoot, 53 | substitutions, 54 | ) 55 | 56 | // set dependencies for the firebase function 57 | const firebasePackageDependencies = {} 58 | if (detectPackageManager() === 'pnpm') { 59 | firebasePackageDependencies[ 60 | '@google-cloud/functions-framework' 61 | ] = `^${packageVersions.googleCloudFunctionsFramework}` 62 | } 63 | 64 | if (Object.keys(firebasePackageDependencies).length > 0) { 65 | updateJson( 66 | host, 67 | joinPathFragments(options.projectRoot, 'package.json'), 68 | (json) => { 69 | json.dependencies = firebasePackageDependencies 70 | return json 71 | }, 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/lib/delete-files.ts: -------------------------------------------------------------------------------- 1 | import type { Tree } from '@nx/devkit' 2 | import { joinPathFragments } from '@nx/devkit' 3 | import type { NormalizedSchema } from '../schema' 4 | 5 | /** 6 | * Delete unwanted files created by the node generator 7 | * @param host 8 | * @param options 9 | */ 10 | export function deleteFiles(host: Tree, options: NormalizedSchema): void { 11 | const nodeFilesToDelete = [ 12 | joinPathFragments(options.projectRoot, 'src', 'main.ts'), 13 | joinPathFragments(options.projectRoot, 'src', 'app', '.gitkeep'), 14 | joinPathFragments(options.projectRoot, 'src', 'assets', '.gitkeep'), 15 | joinPathFragments( 16 | options.projectRoot, 17 | 'src', 18 | 'environments', 19 | 'environment.prod.ts', 20 | ), 21 | joinPathFragments( 22 | options.projectRoot, 23 | 'src', 24 | 'environments', 25 | 'environment.ts', 26 | ), 27 | ] 28 | 29 | for (const path of nodeFilesToDelete) { 30 | host.delete(path) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-function-config' 2 | export * from './create-files' 3 | export * from './update-project' 4 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/lib/update-project.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tree, 3 | readProjectConfiguration, 4 | updateProjectConfiguration, 5 | } from '@nx/devkit' 6 | import type { NormalizedSchema } from '../schema' 7 | import type { FunctionAssetsEntry, FunctionAssetsGlob } from '../../../types' 8 | 9 | export function updateProject(host: Tree, options: NormalizedSchema): void { 10 | const project = readProjectConfiguration(host, options.projectName) 11 | 12 | const firebaseAppProject = options.firebaseAppProject 13 | 14 | // replace the default node build target with a simplified version 15 | // we dont need dev/production build configurations for firebase functions since its a confined secure environment 16 | project.targets.build = { 17 | executor: '@nx/esbuild:esbuild', 18 | outputs: ['{options.outputPath}'], 19 | options: { 20 | outputPath: project.targets.build.options.outputPath, 21 | main: project.targets.build.options.main, 22 | tsConfig: project.targets.build.options.tsConfig, 23 | assets: project.targets.build.options.assets, 24 | generatePackageJson: true, 25 | // these are the defaults for esbuild, but let's set them anyway 26 | platform: 'node', 27 | bundle: true, 28 | thirdParty: false, 29 | dependenciesFieldType: 'dependencies', 30 | target: 'node16', 31 | format: [options.format || 'esm'], // default for esbuild is esm 32 | esbuildOptions: { 33 | logLevel: 'info', 34 | }, 35 | }, 36 | } 37 | 38 | // add reference to firebase app environment assets 39 | const firebaseAppRoot = firebaseAppProject.root 40 | const assets: FunctionAssetsEntry[] = project.targets.build.options.assets 41 | const glob: FunctionAssetsGlob = { 42 | glob: '**/*', 43 | input: `${firebaseAppRoot}/environment`, 44 | output: '.', 45 | } 46 | assets.push(glob) 47 | 48 | // add deploy target 49 | project.targets.deploy = { 50 | executor: 'nx:run-commands', 51 | options: { 52 | // command: `firebase deploy${firebaseProject} --config=${firebaseConfig}`, 53 | // use the firebase app to deploy, this way the function does not need to know the project or config 54 | command: `nx run ${firebaseAppProject.name}:deploy --only functions:${options.projectName}`, 55 | }, 56 | dependsOn: ['build'], 57 | } 58 | 59 | // Remove default node app serve target 60 | // No serve target for functions, since we may have multiple functions in a firebase project 61 | // Instead we serve at the firebase app project 62 | delete project.targets.serve 63 | 64 | updateProjectConfiguration(host, options.projectName, project) 65 | 66 | // Add function project as implicit dep of firebase app project 67 | firebaseAppProject.implicitDependencies ||= [] 68 | firebaseAppProject.implicitDependencies.push(options.projectName) 69 | firebaseAppProject.implicitDependencies.sort() 70 | updateProjectConfiguration(host, options.app, firebaseAppProject) 71 | } 72 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { ProjectConfiguration } from '@nx/devkit' 2 | import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils' 3 | 4 | // export interface Schema { 5 | // name: string; 6 | // skipFormat?: boolean; 7 | // skipPackageJson?: boolean; 8 | // directory?: string; 9 | // unitTestRunner?: 'jest' | 'none'; 10 | // e2eTestRunner?: 'jest' | 'none'; 11 | // linter?: Linter; 12 | // tags?: string; 13 | // frontendProject?: string; 14 | // babelJest?: boolean; 15 | // js?: boolean; 16 | // pascalCaseFiles?: boolean; 17 | // setParserOptionsProject?: boolean; 18 | // standaloneConfig?: boolean; 19 | // bundler?: 'esbuild' | 'webpack'; 20 | // framework?: NodeJsFrameWorks; 21 | // port?: number; 22 | // rootProject?: boolean; 23 | // docker?: boolean; 24 | // isNest?: boolean; 25 | // } 26 | 27 | export interface Schema { 28 | // standard nx generator options 29 | name: string 30 | directory?: string 31 | tags?: string 32 | projectNameAndRootFormat?: ProjectNameAndRootFormat 33 | rootProject?: boolean 34 | 35 | // subset of @nx/node:application options that we forward to node app generator 36 | setParserOptionsProject?: boolean 37 | skipFormat?: boolean 38 | // unitTestRunner is always jest 39 | // bundler is always esbuild 40 | // linter is always eslint 41 | 42 | // nx-firebase:function generator specific options 43 | app: string 44 | runTime?: '16' | '18' | '20' 45 | format?: 'esm' | 'cjs' 46 | } 47 | 48 | interface NormalizedSchema extends Schema { 49 | projectName: string 50 | projectRoot: string 51 | parsedTags: string[] 52 | 53 | firebaseConfigName: string 54 | firebaseAppProject: ProjectConfiguration 55 | } 56 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/function/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "NxFirebaseFunctionGenerator", 4 | "title": "Nx Firebase Function Options Schema", 5 | "description": "Nx Firebase Function Options Schema.", 6 | "cli": "nx", 7 | "type": "object", 8 | "properties": { 9 | "name": { 10 | "description": "The name of the firebase function project.", 11 | "type": "string", 12 | "$default": { 13 | "$source": "argv", 14 | "index": 0 15 | }, 16 | "x-prompt": "What name would you like to use for the firebase function project?" 17 | }, 18 | "directory": { 19 | "description": "A directory where the function is placed.", 20 | "type": "string", 21 | "alias": "d" 22 | }, 23 | "tags": { 24 | "type": "string", 25 | "description": "Add tags to the project (used for linting)", 26 | "alias": "t" 27 | }, 28 | "projectNameAndRootFormat": { 29 | "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", 30 | "type": "string", 31 | "enum": ["as-provided", "derived"] 32 | }, 33 | "setParserOptionsProject": { 34 | "type": "boolean", 35 | "description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.", 36 | "default": false 37 | }, 38 | "skipFormat": { 39 | "description": "Skip formatting files.", 40 | "type": "boolean", 41 | "default": false 42 | }, 43 | "format": { 44 | "description": "The module format for this function (esm or cjs).", 45 | "type": "string", 46 | "enum": ["esm", "cjs"], 47 | "default": "esm" 48 | }, 49 | "app": { 50 | "description": "The name of the parent Nx firebase application project this function will be added to.", 51 | "type": "string", 52 | "x-prompt": "Which firebase Nx application project will this function be created for?" 53 | }, 54 | "runTime": { 55 | "description": "The node runtime target for this function.", 56 | "type": "string", 57 | "enum": ["16", "18", "20"], 58 | "default": "16" 59 | } 60 | }, 61 | "additionalProperties": false, 62 | "required": ["name", "app"] 63 | } 64 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/init.ts: -------------------------------------------------------------------------------- 1 | import { 2 | runTasksInSerial, 3 | type GeneratorCallback, 4 | type Tree, 5 | formatFiles, 6 | } from '@nx/devkit' 7 | import { addDependencies } from './lib' 8 | import { addGitIgnore, addNxIgnore } from './lib/add-git-ignore-entry' 9 | import type { InitGeneratorOptions } from './schema' 10 | 11 | /** 12 | * `nx g @simondotm/nx-firebase:init` 13 | * 14 | * Ensures the necessary firebase packages are installed in the nx workspace 15 | * It also adds `@nx/node` if it is not already installed 16 | * 17 | */ 18 | export async function initGenerator( 19 | host: Tree, 20 | schema: InitGeneratorOptions, 21 | ): Promise { 22 | const tasks: GeneratorCallback[] = [] 23 | addGitIgnore(host) 24 | addNxIgnore(host) 25 | if (!schema.skipFormat) { 26 | await formatFiles(host) 27 | } 28 | tasks.push(addDependencies(host)) 29 | return runTasksInSerial(...tasks) 30 | } 31 | 32 | export default initGenerator 33 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/lib/add-dependencies.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GeneratorCallback, 3 | readJson, 4 | Tree, 5 | addDependenciesToPackageJson, 6 | detectPackageManager, 7 | logger, 8 | } from '@nx/devkit' 9 | import { workspaceNxVersion } from '../../../utils' 10 | import { packageVersions } from '../../../__generated__/nx-firebase-versions' 11 | 12 | export function addDependencies(tree: Tree): GeneratorCallback { 13 | const dependencies: Record = {} 14 | const devDependencies: Record = {} 15 | 16 | // SM: only add firebase related dependencies if they do not already exist 17 | // This is atypical for Nx plugins that usually migrate versions automatically 18 | // however the nx-firebase plugin is not (currently) opinionated about which version is needed, 19 | // so this ensures workspaces retain control over their firebase versions. 20 | const packageJson = readJson(tree, 'package.json') 21 | 22 | function addDependencyIfNotPresent( 23 | packageName: string, 24 | packageVersion: string, 25 | ): boolean { 26 | if (!packageJson.dependencies || !packageJson.dependencies[packageName]) { 27 | dependencies[packageName] = packageVersion 28 | return true 29 | } 30 | return false 31 | } 32 | function addDevDependencyIfNotPresent( 33 | packageName: string, 34 | packageVersion: string, 35 | ) { 36 | if ( 37 | !packageJson.devDependencies || 38 | !packageJson.devDependencies[packageName] 39 | ) { 40 | devDependencies[packageName] = packageVersion 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | // Firebase packages are not managed by the plugin 47 | // but they are added if they do not exist already in the workspace 48 | addDependencyIfNotPresent('firebase', `^${packageVersions.firebase}`) 49 | addDependencyIfNotPresent( 50 | 'firebase-admin', 51 | `^${packageVersions.firebaseAdmin}`, 52 | ) 53 | addDependencyIfNotPresent( 54 | 'firebase-functions', 55 | `^${packageVersions.firebaseFunctions}`, 56 | ) 57 | 58 | // if the workspace uses pnpm, we need to add the @google-cloud/functions-framework package 59 | if (detectPackageManager() === 'pnpm') { 60 | if ( 61 | addDependencyIfNotPresent( 62 | '@google-cloud/functions-framework', 63 | `^${packageVersions.googleCloudFunctionsFramework}`, 64 | ) 65 | ) { 66 | logger.info( 67 | `This workspace is using pnpm, adding '@google-cloud/functions-framework' dependency for firebase functions compatibility\nSee https://github.com/firebase/firebase-tools/issues/5911#issuecomment-1730263400\n\n`, 68 | ) 69 | } 70 | } 71 | 72 | // firebase dev dependencies 73 | addDevDependencyIfNotPresent( 74 | 'firebase-tools', 75 | `^${packageVersions.firebaseTools}`, 76 | ) 77 | addDevDependencyIfNotPresent( 78 | 'firebase-functions-test', 79 | `^${packageVersions.firebaseFunctionsTest}`, 80 | ) 81 | 82 | // kill-port is used by the emulate target to ensure clean emulator startup 83 | // since Nx doesn't kill processes cleanly atm 84 | addDevDependencyIfNotPresent('kill-port', `^${packageVersions.killPort}`) 85 | 86 | // @nx/node is used by the plugin function generator as a proxy for creating a typescript app 87 | // since users have to create a firebase app before they generate a function, we can be sure 88 | // this plugin init will have been run before the function generator that requires @nx/node is used 89 | // we defer to @nx/node to install its own plugins such as @nx/eslint, @nx/jest, @nx/js, @nx/esbuild, @nx/webpack etc. 90 | addDevDependencyIfNotPresent('@nx/node', workspaceNxVersion.version) 91 | return addDependenciesToPackageJson(tree, dependencies, devDependencies) 92 | } 93 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/lib/add-git-ignore-entry.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@nx/devkit' 2 | 3 | export const gitIgnoreRules = [ 4 | '# Nx-Firebase', 5 | '.runtimeconfig.json', 6 | '**/.emulators/*', 7 | '**/.firebase/*', 8 | 'database-debug.log', 9 | 'firebase-debug.log', 10 | 'firestore-debug.log', 11 | 'pubsub-debug.log', 12 | 'ui-debug.log', 13 | 'firebase-export*', 14 | '.secret.local', 15 | ] 16 | 17 | // these rules tell nx to override .gitignore and consider these files as dependencies 18 | export const nxIgnoreRules = [ 19 | '# Nx-Firebase', 20 | '!.secret.local', 21 | '!.runtimeconfig.json', 22 | ] 23 | 24 | function addIgnoreRules(host: Tree, ignoreRules: string[], ignoreFile: string) { 25 | if (!host.exists(ignoreFile)) { 26 | host.write(ignoreFile, `${ignoreRules.join('\n')}\n`) 27 | return 28 | } 29 | 30 | let content = host.read(ignoreFile)?.toString('utf-8') 31 | let updated = false 32 | for (const entry of ignoreRules) { 33 | if (!content.includes(entry)) { 34 | content += `${entry}\n` 35 | updated = true 36 | } 37 | } 38 | if (updated) { 39 | host.write(ignoreFile, content) 40 | } 41 | } 42 | 43 | export function addGitIgnore(host: Tree) { 44 | addIgnoreRules(host, gitIgnoreRules, '.gitignore') 45 | } 46 | 47 | export function addNxIgnore(host: Tree) { 48 | addIgnoreRules(host, nxIgnoreRules, '.nxignore') 49 | } 50 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-dependencies' 2 | export * from './add-git-ignore-entry' 3 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/schema.d.ts: -------------------------------------------------------------------------------- 1 | export type InitGeneratorOptions = { 2 | skipFormat?: boolean 3 | } 4 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/init/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "NxFirebaseInitGenerator", 4 | "title": "Init Firebase Plugin", 5 | "description": "Init Firebase Plugin.", 6 | "type": "object", 7 | "properties": { 8 | "skipFormat": { 9 | "description": "Skip formatting files.", 10 | "type": "boolean", 11 | "default": false 12 | } 13 | }, 14 | "required": [] 15 | } 16 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/migrate/migrate.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing' 2 | import { Tree } from '@nx/devkit' 3 | 4 | import generator from './migrate' 5 | // import { MigrateGeneratorSchema } from './schema' 6 | 7 | // migrate is tested in e2e. 8 | 9 | describe('migrate generator', () => { 10 | let tree: Tree 11 | // const options: MigrateGeneratorSchema = {} 12 | 13 | beforeEach(() => { 14 | tree = createTreeWithEmptyWorkspace() 15 | }) 16 | 17 | it('should run successfully', async () => { 18 | await generator(tree, {}) 19 | // const config = readProjectConfiguration(tree, 'test') 20 | // expect(config).toBeDefined() 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/migrate/schema.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 2 | export interface MigrateGeneratorSchema {} 3 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/migrate/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "Migrate", 4 | "title": "", 5 | "type": "object", 6 | "properties": {}, 7 | "required": [] 8 | } 9 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/firebase-configs.ts: -------------------------------------------------------------------------------- 1 | import { Tree, readJson } from '@nx/devkit' 2 | 3 | import { 4 | getFirebaseConfigFromCommand, 5 | getFirebaseConfigFromProject, 6 | debugInfo, 7 | mapEntries, 8 | mapKeys, 9 | } from '../../../utils' 10 | import { 11 | CONFIG_NO_APP, 12 | FirebaseConfigs, 13 | FirebaseProjects, 14 | FirebaseConfig, 15 | } from '../../../types' 16 | 17 | const FIREBASE_CONFIG_FILE_MATCHER = /(firebase)(\S*)(.json)/ 18 | 19 | export function getFirebaseConfigs( 20 | tree: Tree, 21 | projects: FirebaseProjects, 22 | ): FirebaseConfigs { 23 | const firebaseConfigs = new Map() 24 | const firebaseAppConfigs = new Map() 25 | const firebaseConfigProjects = new Map() 26 | 27 | debugInfo(`- firebaseAppProjects=${mapKeys(projects.firebaseAppProjects)}`) 28 | debugInfo( 29 | `- firebaseFunctionProjects=${mapKeys(projects.firebaseFunctionProjects)}`, 30 | ) 31 | 32 | // collect all firebase[.*].json config files in workspace 33 | const rootFiles = tree.children('') 34 | rootFiles.map((child) => { 35 | if (tree.isFile(child)) { 36 | if (child.match(FIREBASE_CONFIG_FILE_MATCHER)) { 37 | firebaseConfigs.set(child, readJson(tree, child)) 38 | // set an firebaseConfigProjects as null for now, later we will add the project 39 | firebaseConfigProjects.set(child, CONFIG_NO_APP) 40 | } 41 | } 42 | }) 43 | debugInfo(`- firebaseConfigs=${mapKeys(firebaseConfigs)}`) 44 | 45 | // map firebase configs to their apps 46 | // we do this by reading the --config setting directly from each firebase app's firebase target 47 | // this is more robust & flexible than using filename assumptions for configs 48 | // it also means users can freely rename their firebase configs and the plugin will work 49 | projects.firebaseAppProjects.forEach((project, name) => { 50 | const firebaseTarget = project.targets.firebase 51 | if (!firebaseTarget) { 52 | throw new Error( 53 | `Firebase app project ${name} does not have a 'firebase' target. Sync will no longer work.`, 54 | ) 55 | } 56 | const firebaseConfigName = getFirebaseConfigFromProject(tree, project) 57 | firebaseAppConfigs.set(name, firebaseConfigName) 58 | firebaseConfigProjects.set(firebaseConfigName, name) 59 | 60 | // bit opinionated, but we need to sanity check that 61 | // production configurations on this target use the same config file. 62 | // since we cant rename configs safely with this scenario 63 | // different build configs should only differ in --project 64 | const configurations = firebaseTarget.configurations 65 | for (const configuration in configurations) { 66 | const additionalConfigName = getFirebaseConfigFromCommand( 67 | tree, 68 | project, 69 | configurations[configuration].command, 70 | ) 71 | if (additionalConfigName !== firebaseConfigName) { 72 | throw new Error( 73 | `Firebase app project ${name} target firebase.configurations.${configuration} has a different --config setting which is unsupported by this plugin.`, 74 | ) 75 | } 76 | } 77 | }) 78 | 79 | debugInfo(`- firebaseAppConfigs=${mapEntries(firebaseAppConfigs)}`) 80 | debugInfo( 81 | `- firebaseConfigProjects=${mapEntries(firebaseConfigProjects)}, 82 | )}`, 83 | ) 84 | 85 | return { 86 | firebaseConfigs, 87 | firebaseAppConfigs, 88 | firebaseConfigProjects, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/firebase-projects.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProjectConfiguration, 3 | TargetConfiguration, 4 | Tree, 5 | getProjects, 6 | logger, 7 | } from '@nx/devkit' 8 | 9 | import { SyncGeneratorSchema } from '../schema' 10 | import { debugInfo } from '../../../utils/debug' 11 | import { FirebaseProjects } from '../../../types' 12 | 13 | const FIREBASE_PROJECT_MATCHER = /(--project[ =])([^\s]+)/ 14 | const FIREBASE_COMMAND_MATCHER = /(firebase)/ 15 | 16 | export function isFirebaseApp(project: ProjectConfiguration) { 17 | return project.tags?.includes('firebase:app') 18 | } 19 | 20 | export function isFirebaseFunction(project: ProjectConfiguration) { 21 | return project.tags?.includes('firebase:function') 22 | } 23 | 24 | /** 25 | * Scan the workspace, identifying firebase projects managed by this plugin by their tags: 26 | * - firebase:app 27 | * - firebase:function 28 | * @param tree - host workspace 29 | * @returns FirebaseProjects object 30 | */ 31 | export function getFirebaseProjects(tree: Tree): FirebaseProjects { 32 | const projects = getProjects(tree) 33 | const firebaseAppProjects = new Map() 34 | const firebaseFunctionProjects = new Map() 35 | 36 | debugInfo('- building list of firebase apps & functions') 37 | projects.forEach((project, projectName) => { 38 | if (isFirebaseApp(project)) { 39 | firebaseAppProjects.set(projectName, project) 40 | } 41 | if (isFirebaseFunction(project)) { 42 | firebaseFunctionProjects.set(projectName, project) 43 | } 44 | }) 45 | 46 | return { 47 | firebaseAppProjects, 48 | firebaseFunctionProjects, 49 | } 50 | } 51 | 52 | /** 53 | * Rewrite the firebase deploy command for the given function deploy target 54 | * to either add or update the --project cli parameter 55 | * @param target - deploy target in the function 56 | * @param options - 57 | */ 58 | export function updateFirebaseAppDeployProject( 59 | target: TargetConfiguration, 60 | options: SyncGeneratorSchema, 61 | ) { 62 | const command: string = target.options.command 63 | if (command.includes('firebase')) { 64 | debugInfo('- found firebase target command') 65 | debugInfo(`- command=${command}`) 66 | if (command.includes('--project')) { 67 | debugInfo('- found --project in firebase target command') 68 | // already set, so update 69 | target.options.command = command.replace( 70 | FIREBASE_PROJECT_MATCHER, 71 | '$1' + options.project, 72 | ) 73 | debugInfo(`- new command: ${target.options.command}`) 74 | 75 | logger.info( 76 | `CHANGE updating firebase target --project for '${options.app}' to '--project=${options.project}'`, 77 | ) 78 | return true 79 | } else { 80 | debugInfo('- did not find --project in deploy command') 81 | 82 | // no set, so add 83 | target.options.command = command.replace( 84 | FIREBASE_COMMAND_MATCHER, 85 | '$1' + ' ' + `--project=${options.project}`, 86 | ) 87 | debugInfo(`- new command: ${target.options.command}`) 88 | logger.info( 89 | `CHANGE setting firebase target --project for '${options.app}' to '--project=${options.project}'`, 90 | ) 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/firebase-workspace.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@nx/devkit' 2 | import { getFirebaseChanges } from './firebase-changes' 3 | import { getFirebaseProjects } from './firebase-projects' 4 | 5 | import { getFirebaseConfigs } from './firebase-configs' 6 | import { FirebaseWorkspace } from '../../../types' 7 | 8 | export function getFirebaseWorkspace(tree: Tree): FirebaseWorkspace { 9 | // build list of firebase apps and functions in the workspace 10 | const projects = getFirebaseProjects(tree) 11 | const configs = getFirebaseConfigs(tree, projects) 12 | const changes = getFirebaseChanges(tree, projects, configs) 13 | 14 | return { 15 | ...projects, 16 | ...changes, 17 | ...configs, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../../utils/debug' 2 | export * from './firebase-changes' 3 | export * from './firebase-configs' 4 | export * from './firebase-projects' 5 | export * from './firebase-workspace' 6 | export * from './tags' 7 | export * from './update-targets' 8 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/tags.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProjectConfiguration, 3 | Tree, 4 | updateProjectConfiguration, 5 | } from '@nx/devkit' 6 | import { debugInfo } from '../../../utils/debug' 7 | 8 | /** 9 | * Parse a firebase tag value for the given scope 10 | * Expected to find tag of format: `firebase:name:` or `firebase:dep:` 11 | * @param project - Project to be checked 12 | * @param scope - `firebase:name` or `firebase:dep` 13 | * @returns value part of tag 14 | * @throws if requested tag is missing, or malformed 15 | */ 16 | export function getFirebaseScopeFromTag( 17 | project: ProjectConfiguration, 18 | scope: 'firebase:name' | 'firebase:dep', 19 | ) { 20 | const tags = project.tags 21 | if (tags) { 22 | for (let i = 0; i < tags.length; ++i) { 23 | const tag = tags[i] 24 | debugInfo(`- checking tag '${tag}' for scope '${scope}'`) 25 | if (tag.includes(scope)) { 26 | debugInfo(`- matched tag '${tag}' for scope '${scope}'`) 27 | const scopes = tag.split(':') 28 | if (scopes.length === 3) { 29 | debugInfo(`- returning tagValue '${scopes[2]}' tagIndex '${i}'`) 30 | return { tagValue: scopes[2], tagIndex: i } 31 | } else { 32 | throw new Error( 33 | `Malformed '${scope}' tag in project '${project.name}', expected '${scope}:', found '${tag}'`, 34 | ) 35 | } 36 | } 37 | } 38 | } 39 | throw new Error( 40 | `Project '${project.name}' has a missing '${scope}' tag in project. Ensure this is set correctly.`, 41 | ) 42 | } 43 | 44 | export function updateFirebaseProjectNameTag( 45 | tree: Tree, 46 | project: ProjectConfiguration, 47 | ) { 48 | const appNameTag = getFirebaseScopeFromTag(project, 'firebase:name') 49 | const newAppNameTag = `firebase:name:${project.name}` 50 | project.tags[appNameTag.tagIndex] = newAppNameTag 51 | updateProjectConfiguration(tree, project.name, project) 52 | } 53 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/lib/update-targets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProjectConfiguration, 3 | TargetConfiguration, 4 | Tree, 5 | updateProjectConfiguration, 6 | } from '@nx/devkit' 7 | 8 | type NxRunCommandsTargetConfiguration = TargetConfiguration<{ 9 | command?: string 10 | commands?: string[] 11 | }> 12 | 13 | type ValidTarget = 14 | | 'firebase' 15 | | 'watch' 16 | | 'emulate' 17 | | 'lint' 18 | | 'test' 19 | | 'serve' 20 | | 'deploy' 21 | | 'getconfig' 22 | | 'killports' 23 | 24 | export function renameCommandForTarget( 25 | tree: Tree, 26 | project: ProjectConfiguration, 27 | targetName: ValidTarget, 28 | search: string, 29 | replace: string, 30 | ) { 31 | const target = project.targets[targetName] as NxRunCommandsTargetConfiguration 32 | if (!target) { 33 | throw new Error( 34 | `Could not find target '${targetName}' in project '${project.name}'`, 35 | ) 36 | } 37 | if (target.options.commands) { 38 | target.options.commands = target.options.commands.map((cmd) => { 39 | return cmd.replace(search, replace) 40 | }) 41 | } 42 | if (target.options.command) { 43 | target.options.command = target.options.command.replace(search, replace) 44 | } 45 | updateProjectConfiguration(tree, project.name, project) 46 | } 47 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface SyncGeneratorSchema { 2 | app?: string 3 | project?: string 4 | // functions?: boolean 5 | } 6 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "Sync", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "app": { 8 | "type": "string", 9 | "description": "The Nx firebase application you would like to sync", 10 | "default": "" 11 | }, 12 | "project": { 13 | "type": "string", 14 | "description": "The firebase project that should be associated with this application", 15 | "default": "" 16 | } 17 | }, 18 | "required": [] 19 | } 20 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/generators/sync/sync.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing' 2 | import { Tree } from '@nx/devkit' 3 | 4 | import generator from './sync' 5 | // import { SyncGeneratorSchema } from './schema' 6 | 7 | // sync is tested in e2e. 8 | 9 | describe('sync generator', () => { 10 | let tree: Tree 11 | 12 | // const options: SyncGeneratorSchema = { app: 'test' } 13 | 14 | beforeEach(() => { 15 | tree = createTreeWithEmptyWorkspace() 16 | }) 17 | 18 | // check we handle: 19 | // sync per project & sync all projects 20 | // - function renamed 21 | // - app renamed 22 | // - function deleted 23 | // - app deleted 24 | // - function & app renamed 25 | // --project option 26 | 27 | it('should run successfully', async () => { 28 | await generator(tree, {}) 29 | // const config = readProjectConfiguration(tree, 'test') 30 | // expect(config).toBeDefined() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondotm/nx-firebase/9d810fe7dec86d1c810acfd3fff6337ef3b64713/packages/nx-firebase/src/index.ts -------------------------------------------------------------------------------- /packages/nx-firebase/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/firebase-config' 2 | export * from './lib/firebase-function' 3 | export * from './lib/firebase-workspace' 4 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/types/lib/firebase-config.ts: -------------------------------------------------------------------------------- 1 | export interface FirebaseFunction { 2 | predeploy?: string[] 3 | source: string 4 | codebase: string 5 | runtime: string // 'nodejs16' | 'nodejs18' | 'nodejs20' 6 | ignore?: string[] 7 | } 8 | 9 | export interface FirebaseHosting { 10 | target?: string 11 | public: string 12 | ignore?: string[] 13 | redirects?: { 14 | source: string 15 | destination: string 16 | type: number 17 | }[] 18 | rewrites?: { 19 | source: string 20 | destination: string 21 | dynamicLinks?: boolean 22 | function?: { 23 | functionId: string 24 | region?: string 25 | pinTag?: boolean 26 | } 27 | run?: { 28 | serviceId: string 29 | region: string 30 | } 31 | } 32 | cleanUrls?: boolean 33 | trailingSlash?: boolean 34 | appAssociation?: 'AUTO' 35 | headers?: { 36 | source: string 37 | headers: { 38 | key: string 39 | value: string 40 | }[] 41 | }[] 42 | } 43 | 44 | export interface FirebaseConfig { 45 | database: { 46 | rules: string 47 | } 48 | firestore: { 49 | rules: string 50 | indexes: string 51 | } 52 | hosting: FirebaseHosting | FirebaseHosting[] 53 | storage: { 54 | rules: string 55 | } 56 | functions: FirebaseFunction | FirebaseFunction[] 57 | emulators: { 58 | functions: { 59 | port: number 60 | } 61 | firestore: { 62 | port: number 63 | } 64 | hosting: { 65 | port: number 66 | } 67 | auth: { 68 | port: number 69 | } 70 | pubsub: { 71 | port: number 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/types/lib/firebase-function.ts: -------------------------------------------------------------------------------- 1 | export type FunctionAssetsGlob = { 2 | glob: string 3 | input: string 4 | output: string 5 | } 6 | export type FunctionAssetsEntry = string | FunctionAssetsGlob 7 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/types/lib/firebase-workspace.ts: -------------------------------------------------------------------------------- 1 | import { ProjectConfiguration } from '@nx/devkit' 2 | 3 | import { FirebaseConfig } from './firebase-config' 4 | 5 | export const CONFIG_NO_APP = 'CONFIG_NO_APP' 6 | 7 | export interface FirebaseProjects { 8 | /** 9 | * app projectName -> ProjectConfig 10 | */ 11 | firebaseAppProjects: Map 12 | /** 13 | * function projectName -> ProjectConfig 14 | */ 15 | firebaseFunctionProjects: Map 16 | } 17 | 18 | export interface FirebaseConfigs { 19 | /** 20 | * configFilename -> FirebaseConfig 21 | */ 22 | firebaseConfigs: Map 23 | /** 24 | * projectName -> configFileName 25 | */ 26 | firebaseAppConfigs: Map 27 | /** 28 | * configFilename -> Project Name (or MISSING_CONFIG if config is not referenced by an app) 29 | */ 30 | firebaseConfigProjects: Map 31 | } 32 | 33 | export interface FirebaseChanges { 34 | /** 35 | * map of app project name -> deletion status 36 | */ 37 | deletedApps: Map 38 | /** 39 | * map of function project name -> deletion status 40 | */ 41 | deletedFunctions: Map 42 | /** 43 | * map of previous app name -> new app project 44 | */ 45 | renamedApps: Map 46 | 47 | /** 48 | * map of previous function name -> new function project 49 | */ 50 | renamedFunctions: Map 51 | } 52 | 53 | export type FirebaseWorkspace = FirebaseProjects & 54 | FirebaseChanges & 55 | FirebaseConfigs 56 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@nx/devkit' 2 | 3 | // debug info just for plugin 4 | const ENABLE_DEBUG_INFO = false 5 | 6 | export function debugInfo(info: string) { 7 | if (ENABLE_DEBUG_INFO) { 8 | logger.info(info) 9 | } 10 | } 11 | 12 | export function mapKeys(map: Map) { 13 | return JSON.stringify([...map.keys()], null, 3) 14 | } 15 | 16 | export function mapValues(map: Map) { 17 | return JSON.stringify([...map.values()], null, 3) 18 | } 19 | 20 | export function mapEntries(map: Map) { 21 | return JSON.stringify([...map.entries()], null, 3) 22 | } 23 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/firebase-config.ts: -------------------------------------------------------------------------------- 1 | import { Tree, ProjectConfiguration } from '@nx/devkit' 2 | 3 | const FIREBASE_TARGET_CONFIG_MATCHER = /(--config[ =])([^\s]+)/ 4 | 5 | /** 6 | * Return the config file from the provided firebase target command 7 | * This can be used to parse commands in additional configurations 8 | * @param project 9 | * @param command 10 | * @returns 11 | */ 12 | export function getFirebaseConfigFromCommand( 13 | tree: Tree, 14 | project: ProjectConfiguration, 15 | command: string, 16 | ) { 17 | const match = command.match(FIREBASE_TARGET_CONFIG_MATCHER) 18 | if (match && match.length === 3) { 19 | const configName = match[2] 20 | // check the config we've parsed actually resolves to a firebase config file in the workspace 21 | if (!tree.exists(configName)) { 22 | throw new Error( 23 | `Firebase app project ${project.name} is using a firebase config file ${configName} that does not exist in the workspace.`, 24 | ) 25 | } 26 | return configName 27 | } 28 | throw new Error( 29 | `Firebase app project ${project.name} does not have --config set in its 'firebase' target.`, 30 | ) 31 | } 32 | 33 | /** 34 | * Return the config file used by the `firebase` target command of the provided firebase app project 35 | * @param command 36 | * @param project 37 | * @param firebaseConfigs 38 | * @returns 39 | */ 40 | export function getFirebaseConfigFromProject( 41 | tree: Tree, 42 | project: ProjectConfiguration, 43 | ) { 44 | return getFirebaseConfigFromCommand( 45 | tree, 46 | project, 47 | project.targets.firebase.options.command, 48 | ) 49 | } 50 | 51 | /** 52 | * Modify the config file used by the `firebase` target command of the provided firebase app project 53 | * @param project 54 | * @param configFileName 55 | */ 56 | export function setFirebaseConfigFromCommand( 57 | project: ProjectConfiguration, 58 | configFileName: string, 59 | ) { 60 | // we've already checked that firebase target exists when setting up workspace 61 | const firebaseTarget = project.targets.firebase 62 | firebaseTarget.options.command = firebaseTarget.options.command.replace( 63 | FIREBASE_TARGET_CONFIG_MATCHER, 64 | '$1' + configFileName, 65 | ) 66 | // do this for all other configurations on this target too 67 | const configurations = firebaseTarget.configurations 68 | for (const configuration in configurations) { 69 | configurations[configuration].command = configurations[ 70 | configuration 71 | ].command.replace(FIREBASE_TARGET_CONFIG_MATCHER, '$1' + configFileName) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './workspace' 2 | export * from './update-tsconfig' 3 | export * from './project-name' 4 | export * from './firebase-config' 5 | export * from './debug' 6 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/project-name.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getWorkspaceLayout, 3 | joinPathFragments, 4 | names, 5 | Tree, 6 | extractLayoutDirectory, 7 | } from '@nx/devkit' 8 | 9 | export function getProjectName(tree: Tree, name: string, directory?: string) { 10 | // console.log(`name ${name}`) 11 | // console.log(`directory ${directory}`) 12 | 13 | const { layoutDirectory, projectDirectory } = 14 | extractLayoutDirectory(directory) 15 | 16 | // console.log(`layoutDirectory ${layoutDirectory}`) 17 | // console.log(`projectDirectory ${projectDirectory}`) 18 | 19 | const appsDir = layoutDirectory ?? getWorkspaceLayout(tree).appsDir 20 | 21 | // console.log(`appsDir ${appsDir}`) 22 | 23 | const appDirectory = projectDirectory 24 | ? `${names(projectDirectory).fileName}/${names(name).fileName}` 25 | : names(name).fileName 26 | 27 | // console.log(`appDirectory ${appDirectory}`) 28 | 29 | const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-') 30 | 31 | // console.log(`appProjectName ${appProjectName}`) 32 | 33 | const appProjectRoot = joinPathFragments(appsDir, appDirectory) 34 | 35 | // console.log(`appProjectRoot ${appProjectRoot}`) 36 | 37 | // // see https://github.com/nrwl/nx/blob/84cbcb7e105cd2b3bf5b3d84a519e5c52951e0f3/packages/js/src/generators/library/library.ts#L332 38 | // // for how the project name is derived from options.name and --directory 39 | // const fileName = names(name).fileName 40 | // const projectDirectory = directory 41 | // ? `${names(directory).fileName}/${fileName}` 42 | // : name 43 | 44 | // const projectRoot = joinPathFragments( 45 | // getWorkspaceLayout(tree).appsDir, 46 | // projectDirectory, 47 | // ) 48 | 49 | // const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-') 50 | return { 51 | projectRoot: appProjectRoot, 52 | projectName: appProjectName, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/update-tsconfig.ts: -------------------------------------------------------------------------------- 1 | import type { Tree } from '@nx/devkit' 2 | import { joinPathFragments, updateJson } from '@nx/devkit' 3 | import { packageVersions } from '../__generated__/nx-firebase-versions' 4 | 5 | // https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12 6 | 7 | export const nodeEsVersion: Record = { 8 | '12': 'es2019', 9 | '14': 'es2020', 10 | '16': 'es2020', // es2020 seems more preferred with node 16 than es2021 11 | '18': 'es2022', 12 | '20': 'es2022', 13 | } 14 | 15 | /** 16 | * With firebase cli > 10.0.1 now compatible with node versions >=14 we can use es modules rather than commonjs 17 | * 18 | * @param tree 19 | * @param options 20 | */ 21 | export function updateTsConfig( 22 | tree: Tree, 23 | projectRoot: string, 24 | runTime: string, 25 | format: 'esm' | 'cjs', 26 | ): void { 27 | const tsConfigTarget = 28 | nodeEsVersion[runTime] ?? nodeEsVersion[packageVersions.nodeEngine] 29 | updateJson( 30 | tree, 31 | joinPathFragments(projectRoot, 'tsconfig.app.json'), 32 | (json) => { 33 | json.compilerOptions.target = tsConfigTarget 34 | json.compilerOptions.module = format === 'esm' ? 'es2020' : 'commonjs' 35 | return json 36 | }, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/nx-firebase/src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | import { logger, readJsonFile, workspaceRoot } from '@nx/devkit' 2 | import { packageVersions } from '../__generated__/nx-firebase-versions' 3 | 4 | type PackageJson = { 5 | dependencies: { 6 | [packageName: string]: string 7 | } 8 | devDependencies: { 9 | [packageName: string]: string 10 | } 11 | } 12 | 13 | // we dont care about patch versions, also they might be alpha or beta type strings 14 | export type WorkspaceVersion = 15 | | { 16 | version: string 17 | versionCode: number // major*10000 + minor*100 + patch eg. 151001 18 | major: number 19 | minor: number 20 | } 21 | | undefined 22 | 23 | function readNxWorkspaceVersion(): WorkspaceVersion { 24 | // Check the runtime Nx version being used by the current workspace 25 | const semVerRegEx = /[~^]?([\dvx*]+(?:[-.](?:[\dx*]+|alpha|beta))*)/g 26 | const workspacePackage = readJsonFile( 27 | `${workspaceRoot}/package.json`, 28 | ) 29 | const workspaceNxPackageVersion = workspacePackage.devDependencies['nx'] 30 | if (workspaceNxPackageVersion) { 31 | const workspaceNxVersion = workspaceNxPackageVersion.match(semVerRegEx) 32 | if (workspaceNxVersion.length) { 33 | const semver = workspaceNxVersion[0].split('.') 34 | const major = parseInt(semver[0]) 35 | const minor = parseInt(semver[1]) 36 | const patch = parseInt(semver[2]) 37 | return { 38 | version: workspaceNxVersion[0], 39 | versionCode: major * 10000 + minor * 100 + patch, 40 | major, 41 | minor, 42 | } 43 | } 44 | } 45 | return undefined 46 | } 47 | 48 | // determine the Nx version being used by the host workspace 49 | export const workspaceNxVersion = readNxWorkspaceVersion() 50 | 51 | export function checkNxVersion() { 52 | // Declare target version of Nx that the plugin is currently compatible with 53 | const pluginNxVersionMajor = parseInt(packageVersions.nx.split('.')[0]) 54 | if (workspaceNxVersion) { 55 | if (workspaceNxVersion.major !== pluginNxVersionMajor) { 56 | logger.warn( 57 | `WARNING: This version of @simondotm/nx-firebase plugin was built for Nx version ^${packageVersions.nx}, and may not be compatible with your version of Nx (${workspaceNxVersion.version})`, 58 | ) 59 | } 60 | } else { 61 | logger.warn( 62 | `@simondotm/nx-firebase plugin could not determine your version of Nx. It may not be compatible.`, 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/nx-firebase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/nx-firebase/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": [ 9 | "**/*.ts", 10 | "../archived-nx-firebase/src/generators/init/generator.spec.ts", 11 | "src/generators/application/files/src/index.ts__template__" 12 | ], 13 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/nx-firebase/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.d.ts", 13 | "../archived-nx-firebase/src/generators/init/generator.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tools/generate-package-versions.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | const packageJson = require('../package.json') 5 | const nxVersion = packageJson.devDependencies['nx'] 6 | const nxMajorVersion = parseInt(nxVersion.split('.')[0]) 7 | 8 | // // read node version from .nvmrc 9 | // const nvmVersion = fs.readFileSync(path.join(__dirname, '..', '.nvmrc'), 'utf8') 10 | // const nodeVersion = nvmVersion.trim().split('.')[0] 11 | 12 | // default firebase node version is to be derived from Nx version for now 13 | // Nx 17+ offically suports Node 18, so may as well use that pattern 14 | const nodeVersion = nxMajorVersion >= 17 ? '18' : '16' 15 | 16 | function ensureDirectoryExistence(filePath) { 17 | const dirname = path.dirname(filePath) 18 | if (fs.existsSync(dirname)) { 19 | return true 20 | } 21 | ensureDirectoryExistence(dirname) 22 | fs.mkdirSync(dirname) 23 | } 24 | 25 | 26 | function shouldUpdateFile(file, data) { 27 | if (!fs.existsSync(file)) { 28 | return true 29 | } 30 | const existingData = fs.readFileSync(file, 'utf8') 31 | return existingData !== data 32 | } 33 | 34 | function maybeUpdateFile(file, data) { 35 | if (shouldUpdateFile(file, data)) { 36 | console.log(`Updating '${file}'`) 37 | fs.writeFileSync(file, data) 38 | } 39 | } 40 | 41 | function updateVersions() { 42 | const generatedFile = path.join( 43 | __dirname, 44 | '..', 45 | 'packages', 46 | 'nx-firebase', 47 | 'src', 48 | '__generated__', 49 | 'nx-firebase-versions.ts', 50 | ) 51 | 52 | // strip out the ^ and ~ from the versions 53 | for (const [key, value] of Object.entries(packageJson.devDependencies)) { 54 | packageJson.devDependencies[key] = value.replace('^', '').replace('~', '') 55 | } 56 | 57 | const data = `//------------------------------------------------------------------------------ 58 | // This file is automatically generated by tools/generate-package-versions.js 59 | // Do not edit this file manually 60 | //------------------------------------------------------------------------------ 61 | export const packageVersions = { 62 | nx: '${nxVersion}', 63 | firebase: '${packageJson.devDependencies['firebase']}', 64 | firebaseAdmin: '${packageJson.devDependencies['firebase-admin']}', 65 | firebaseFunctions: '${packageJson.devDependencies['firebase-functions']}', 66 | firebaseFunctionsTest: '${packageJson.devDependencies['firebase-functions-test']}', 67 | firebaseTools: '${packageJson.devDependencies['firebase-tools']}', 68 | killPort: '${packageJson.devDependencies['kill-port']}', 69 | nodeEngine: '${nodeVersion}', 70 | googleCloudFunctionsFramework: '${packageJson.devDependencies['@google-cloud/functions-framework']}', 71 | } 72 | ` 73 | ensureDirectoryExistence(generatedFile) 74 | maybeUpdateFile(generatedFile, data) 75 | } 76 | 77 | function updateTemplates() { 78 | const pluginSrcDir = path.join(__dirname, '..', 'packages', 'nx-firebase', 'src', 'generators') 79 | const firebaseTemplatesDir = path.join(__dirname, '..', 'node_modules', 'firebase-tools', 'templates', 'init' ) 80 | 81 | const templates = [ 82 | [ path.join(firebaseTemplatesDir, 'hosting', 'index.html'), path.join(pluginSrcDir, 'application', 'files', 'public', 'index.html')], 83 | [ path.join(firebaseTemplatesDir, 'hosting', '404.html'), path.join(pluginSrcDir, 'application', 'files', 'public', '404.html')], 84 | [ path.join(firebaseTemplatesDir, 'firestore', 'firestore.indexes.json'), path.join(pluginSrcDir, 'application', 'files', 'firestore.indexes.json')], 85 | [ path.join(firebaseTemplatesDir, 'firestore', 'firestore.rules'), path.join(pluginSrcDir, 'application', 'files', 'firestore.rules')], 86 | [ path.join(firebaseTemplatesDir, 'storage', 'storage.rules'), path.join(pluginSrcDir, 'application', 'files', 'storage.rules')], 87 | [ path.join(firebaseTemplatesDir, 'functions', 'typescript', 'index.ts'), path.join(pluginSrcDir, 'function', 'files', 'src', 'main.ts__tmpl__')], 88 | ] 89 | 90 | for (const [src, dest] of templates) { 91 | let data = fs.readFileSync(src, 'utf8') 92 | if (src.includes('index.html')){ 93 | data = data.replaceAll('{{VERSION}}', packageJson.devDependencies['firebase']) 94 | } 95 | if (src.includes('firestore.rules')){ 96 | data = data.replaceAll('{{IN_30_DAYS}}', '<%= IN_30_DAYS %>') 97 | } 98 | maybeUpdateFile(dest, data) 99 | } 100 | } 101 | 102 | updateVersions() 103 | updateTemplates() 104 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@simondotm/nx-firebase": ["packages/nx-firebase/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | --------------------------------------------------------------------------------