Commit 4e8a4bbd authored by Merzough Münker's avatar Merzough Münker
Browse files

feat(schematics): add init schematic

parent 1579bfd3
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -4,6 +4,11 @@
      "description": "Setup the package @rxap/firebase for the workspace.",
      "factory": "./src/schematics/ng-add/index",
      "schema": "./src/schematics/ng-add/schema.json"
    },
    "init": {
      "description": "Init the select project with required angular fire imports and providers",
      "factory": "./src/schematics/init/index",
      "schema": "./src/schematics/init/schema.json"
    }
  },
  "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json"
+662 −0
Original line number Diff line number Diff line
import {
  Rule,
  Tree,
  chain,
  noop,
  SchematicsException,
  externalSchematic
} from '@angular-devkit/schematics';
import { InitSchema } from './schema';
import {
  AddPackageJsonDependency,
  GetProjectSourceRoot,
  CoerceFile,
  UpdateJsonFile,
  UpdateAngularProject,
  InstallNodePackages,
  AddPackageJsonScript
} from '@rxap/schematics-utilities';
import {
  Project,
  IndentationText
} from 'ts-morph';
import {
  ApplyTsMorphProject,
  AddNgModuleImport,
  AddNgModuleProvider,
  AddToArray
} from '@rxap/schematics-ts-morph';
import { join } from 'path';

export interface UpdateIgnoreFileOptions {
  filePath?: string
}

export function UpdateIgnoreFile(ignore: string[], options: UpdateIgnoreFileOptions = {}): Rule {
  return tree => {
    const filePath = options.filePath ?? '.gitignore';
    CoerceFile(tree, filePath, '');
    let content = tree.read(filePath)!.toString('utf-8');
    for (const entry of ignore) {
      if (!content.match(new RegExp(`^${entry}$`))) {
        content += `\n${entry}`;
      }
    }
    tree.overwrite(filePath, content);
  };
}

export default function(options: InitSchema): Rule {

  return async (host: Tree) => {

    const project = new Project({
      manipulationSettings:  {
        indentationText: IndentationText.TwoSpaces
      },
      useInMemoryFileSystem: true
    });

    const projectSourceRoot = GetProjectSourceRoot(host, options.project);

    const appModuleSourceFile = project.createSourceFile(
      'app/app.module.ts',
      host.read(join(projectSourceRoot, 'app/app.module.ts')
      )!.toString('utf-8')
    );

    const mainSourceFile = project.createSourceFile(
      'main.ts',
      host.read(join(projectSourceRoot, 'main.ts')
      )!.toString('utf-8')
    );

    AddToArray(
      mainSourceFile,
      'configSideLoad',
      `ConfigService
      .SideLoad('/config.firebase.options.json', 'firebase.options', true)
      .catch(async () => {
        if (!ConfigService.Config.firebase || !ConfigService.Config.firebase.options || !ConfigService.Config.firebase.options.apiKey) {
          try {
            const response = await fetch('/__/firebase/init.json');
            SetObjectValue(ConfigService.Config, 'firebase.options', await response.json());
          } catch (e) {
            throw new Error(\`Could not load the firebase: \${e.message}\`);
          }
        }
      }),`,
      'Promise<any>[]'
    );
    mainSourceFile.addImportDeclaration({
      namedImports:    [ 'SetObjectValue' ],
      moduleSpecifier: '@rxap/utilities'
    });

    if (options.functions) {
      AddNgModuleImport(
        appModuleSourceFile,
        'AngularFireFunctionsModule',
        '@angular/fire/functions'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_FUNCTIONS_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_FUNCTIONS_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    if (options.analytics) {
      AddNgModuleImport(
        appModuleSourceFile,
        'AngularFireAnalyticsModule',
        '@angular/fire/analytics'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_ANALYTICS_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_ANALYTICS_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'ScreenTrackingService',
        [
          {
            namedImports:    [ 'ScreenTrackingService' ],
            moduleSpecifier: '@angular/fire/analytics'
          }
        ]
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'UserTrackingService',
        [
          {
            namedImports:    [ 'UserTrackingService' ],
            moduleSpecifier: '@angular/fire/analytics'
          }
        ]
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        {
          provide:  'ANALYTICS_APP_VERSION',
          useValue: 'environment.release'
        },
        [
          {
            namedImports:    [
              {
                name:  'APP_VERSION',
                alias: 'ANALYTICS_APP_VERSION'
              }
            ],
            moduleSpecifier: '@angular/fire/analytics'
          },
          {
            namedImports:    [ 'environment' ],
            moduleSpecifier: '../environments/environment'
          }
        ]
      );
    }

    if (options.performance) {
      AddNgModuleImport(
        appModuleSourceFile,
        'AngularFirePerformanceModule',
        '@angular/fire/performance'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'PerformanceMonitoringService',
        [
          {
            namedImports:    [ 'PerformanceMonitoringService' ],
            moduleSpecifier: '@angular/fire/performance'
          }
        ]
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_PERFORMANCE_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_PERFORMANCE_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    if (options.storage) {
      AddNgModuleImport(
        appModuleSourceFile,
        'RxapAngularFireStorageModule',
        '@rxap/firebase'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_STORAGE_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_STORAGE_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    if (options.appCheck) {
      AddNgModuleImport(
        appModuleSourceFile,
        'RxapAngularFireAppCheckModule',
        '@rxap/firebase'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_APP_CHECK_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_APP_CHECK_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    if (options.firestore) {
      AddNgModuleImport(
        appModuleSourceFile,
        'AngularFirestoreModule',
        '@angular/fire/firestore'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_FIRESTORE_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_FIRESTORE_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    if (options.auth) {
      AddNgModuleImport(
        appModuleSourceFile,
        'AngularFireAuthModule',
        '@angular/fire/auth'
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        {
          provide:  'RxapAuthenticationService',
          useClass: 'IdentityPlatformService'
        },
        [
          {
            namedImports:    [ 'IdentityPlatformService' ],
            moduleSpecifier: '@rxap/firebase/auth'
          },
          {
            namedImports:    [ 'RxapAuthenticationService' ],
            moduleSpecifier: '@rxap/authentication'
          }
        ]
      );
      AddNgModuleProvider(
        appModuleSourceFile,
        'FIREBASE_AUTH_PROVIDERS',
        [
          {
            namedImports:    [ 'FIREBASE_AUTH_PROVIDERS' ],
            moduleSpecifier: '@rxap/firebase'
          }
        ]
      );
    }

    return chain([
      ApplyTsMorphProject(project, projectSourceRoot),
      AddPackageJsonDependency('first-input-delay'),
      AddPackageJsonScript('emulators:start', 'firebase emulators:start --export-on-exit=\".firebase-emulator\" --import=\".firebase-emulator\"'),
      tree => CoerceFile(
        tree,
        join(projectSourceRoot, 'config.firebase.options.json'),
        JSON.stringify(
          {},
          undefined,
          2
        )
      ),
      options.auth ? AddPackageJsonDependency('@rxap/authentication') : noop(),
      options.functions ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.functions.json'),
          JSON.stringify(
            {
              region: 'europe-west3'
            },
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.functions.json',
            propertyPath: 'firebase.functions',
            required:     true
          }
        )
      ]) : noop(),
      options.analytics ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.analytics.json'),
          JSON.stringify(
            {
              enabled: false
            },
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.analytics.json',
            propertyPath: 'firebase.analytics'
          }
        )
      ]) : noop(),
      options.performance ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.performance.json'),
          JSON.stringify(
            {
              instrumentationEnabled: false,
              dataCollectionEnabled:  false
            },
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.performance.json',
            propertyPath: 'firebase.performance'
          }
        )
      ]) : noop(),
      options.storage ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.storage.json'),
          JSON.stringify(
            {},
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.storage.json',
            propertyPath: 'firebase.storage'
          }
        )
      ]) : noop(),
      options.appCheck ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.appcheck.json'),
          JSON.stringify(
            {
              siteKey: '',
              enabled: false
            },
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.appcheck.json',
            propertyPath: 'firebase.appCheck',
            required:     true
          }
        )
      ]) : noop(),
      options.firestore ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.firestore.json'),
          JSON.stringify(
            {},
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.firestore.json',
            propertyPath: 'firebase.firestore'
          }
        )
      ]) : noop(),
      options.auth ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.auth.json'),
          JSON.stringify(
            {},
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.auth.json',
            propertyPath: 'firebase.auth'
          }
        )
      ]) : noop(),
      options.useEmulator ? chain([
        tree => CoerceFile(
          tree,
          join(projectSourceRoot, 'config.firebase.emulator.json'),
          JSON.stringify(
            Object.assign(
              {},
              options.functions ? {
                auth: [ 'localhost', 5501 ]
              } : {},
              options.storage ? {
                auth: [ 'localhost', 9199 ]
              } : {},
              options.firestore ? {
                auth: [ 'localhost', 8888 ]
              } : {},
              options.auth ? {
                auth: [ 'localhost', 9099 ]
              } : {}
            ),
            undefined,
            2
          )
        ),
        externalSchematic(
          '@rxap/config',
          'side-load',
          {
            project:      options.project,
            url:          '/config.firebase.emulator.json',
            propertyPath: 'firebase.emulator'
          }
        )
      ]) : noop(),
      tree => {
        let content = tree.read(join(projectSourceRoot, 'polyfills.ts'))!.toString('utf-8');
        if (options.appCheck) {
          if (!content.includes(`import 'firebase/app-check';`)) {
            content = `import 'firebase/app-check';\n\n` + content;
          }
        }
        if (options.performance) {
          if (!content.includes(`import 'first-input-delay';`)) {
            content = `import 'first-input-delay';\n\n` + content;
          }
        }
        tree.overwrite(join(projectSourceRoot, 'polyfills.ts'), content);
      },
      UpdateAngularProject(angularProject => {
        if (!angularProject.targets.has('build')) {
          throw new SchematicsException('The project does not have a build target');
        }
        const buildTarget = angularProject.targets.get('build')!;
        if (!buildTarget.options.assets) {
          buildTarget.options.assets = [];
        }
        const assets: string[] = [];
        if (options.functions) {
          assets.push(join(projectSourceRoot, 'config.firebase.functions.json'));
        }
        if (options.analytics) {
          assets.push(join(projectSourceRoot, 'config.firebase.analytics.json'));
        }
        if (options.performance) {
          assets.push(join(projectSourceRoot, 'config.firebase.performance.json'));
        }
        if (options.storage) {
          assets.push(join(projectSourceRoot, 'config.firebase.storage.json'));
        }
        if (options.appCheck) {
          assets.push(join(projectSourceRoot, 'config.firebase.appcheck.json'));
        }
        if (options.firestore) {
          assets.push(join(projectSourceRoot, 'config.firebase.firestore.json'));
        }
        if (options.auth) {
          assets.push(join(projectSourceRoot, 'config.firebase.auth.json'));
        }
        if (options.useEmulator) {
          assets.push(join(projectSourceRoot, 'config.firebase.emulator.json'));
        }
        for (const asset of assets) {
          if (!buildTarget.options.assets.includes(asset)) {
            buildTarget.options.assets.push(asset);
          }
        }
      }, { projectName: options.project }),
      UpdateJsonFile(jsonFile => {
        if (options.useEmulator) {
          if (!jsonFile.emulators) {
            jsonFile.emulators = {};
          }
          if (options.functions) {
            jsonFile.emulators.functions = { port: 5501 };
          }
          if (options.storage) {
            jsonFile.emulators.storage = { port: 9199 };
          }
          if (options.firestore) {
            jsonFile.emulators.firestore = { port: 8888 };
          }
          if (options.auth) {
            jsonFile.emulators.auth = { port: 9099 };
          }
          jsonFile.emulators.hosting = { port: 5000 };
          jsonFile.emulators.ui      = { enabled: true };
        }
        if (options.storage) {
          if (!jsonFile.storage) {
            jsonFile.storage = {};
          }
          jsonFile.storage.rules = 'storage.rules';
        }
        if (options.firestore) {
          if (!jsonFile.firestore) {
            jsonFile.firestore = {};
          }
          jsonFile.firestore.rules   = 'firestore.rules';
          jsonFile.firestore.indexes = 'firestore.indexes.json';
        }
        if (options.hostingSite) {
          jsonFile.hosting = {
            site:     options.hostingSite,
            public:   'dist/apps/pwa',
            ignore:   [
              '**/.*'
            ],
            headers:  [
              {
                'source':  '*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)',
                'headers': [
                  {
                    'key':   'Cache-Control',
                    'value': 'public,max-age=31536000,immutable'
                  }
                ]
              },
              {
                'source':  '**/*.@(json)',
                'headers': [
                  {
                    'key':   'Cache-Control',
                    'value': 'public,max-age=60'
                  }
                ]
              }
            ],
            rewrites: [
              {
                'source':      '**',
                'destination': '/index.html'
              }
            ]
          };
        }
        if (options.functions) {
          jsonFile.functions = {
            source: 'dist/apps/functions'
          };
        }
      }, 'firebase.json', { create: true }),
      options.storage ? tree => CoerceFile(
        tree,
        'storage.rules',
        `rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if false;
    }
  }
}`
      ) : noop(),
      options.firestore ? tree => CoerceFile(
        tree,
        'firestore.indexes.json',
        JSON.stringify({
          indexes:        [],
          fieldOverrides: []
        }, undefined, 2)
      ) : noop(),
      options.firestore ? tree => CoerceFile(
        tree,
        'firestore.rules',
        `rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}`
      ) : noop(),
      UpdateIgnoreFile([
        '.firebase-emulator',
        '.runtimeconfig.json'
      ]),
      UpdateIgnoreFile([
        'config.*.json'
      ], { filePath: join(projectSourceRoot, '.gitignore') }),
      InstallNodePackages()
    ]);

  };

}
+12 −0
Original line number Diff line number Diff line
export interface InitSchema {
  project: string;
  functions: boolean;
  analytics: boolean;
  performance: boolean;
  storage: boolean;
  appCheck: boolean;
  firestore: boolean;
  auth: boolean;
  useEmulator: boolean;
  hostingSite?: string;
}
+72 −0
Original line number Diff line number Diff line
{
  "$schema": "http://json-schema.org/schema",
  "$id": "firebase-init",
  "title": "Init",
  "type": "object",
  "properties": {
    "project": {
      "type": "string",
      "description": "The project name where firebase should be init",
      "$default": {
        "$source": "projectName"
      },
      "x-prompt": "Which project should be initialized?"
    },
    "functions": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase functions",
      "x-prompt": "Should the project use firebase functions?"
    },
    "analytics": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase analytics",
      "x-prompt": "Should the project use firebase analytics?"
    },
    "performance": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase performance",
      "x-prompt": "Should the project use firebase performance?"
    },
    "storage": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase storage",
      "x-prompt": "Should the project use firebase storage?"
    },
    "appCheck": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase app check",
      "x-prompt": "Should the project use firebase app check?"
    },
    "firestore": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase firestore",
      "x-prompt": "Should the project use firebase firestore?"
    },
    "auth": {
      "type": "boolean",
      "default": false,
      "description": "Whether the project should support firebase auth",
      "x-prompt": "Should the project use firebase auth?"
    },
    "useEmulator": {
      "type": "boolean",
      "default": false,
      "description": "Whether the firebase emulator should be setup?",
      "x-prompt": "Should the firebase emulator be setup?"
    },
    "hostingSite": {
      "type": "string",
      "description": "The name of the firebase hosting site",
      "x-prompt": "Enter the name of the firebase hosting site: (optional)"
    }
  },
  "required": [
    "project"
  ]
}
+11 −3
Original line number Diff line number Diff line
import { Rule } from '@angular-devkit/schematics';
import {
  Rule,
  chain,
  schematic
} from '@angular-devkit/schematics';
import { InstallPeerDependencies } from '@rxap/schematics-utilities';
import { NgAddSchema } from './schema';

export default function(): Rule {
  return InstallPeerDependencies();
export default function(options: NgAddSchema): Rule {
  return chain([
    InstallPeerDependencies(),
    schematic('init', options)
  ]);
}
Loading