From a623e99845de417e2734b030770bb142a2c60cf8 Mon Sep 17 00:00:00 2001
From: timdeschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 29 Jun 2020 13:36:55 +0200
Subject: [PATCH 1/3] feat(store): add ng-add and ng-update schematics

---
 modules/component-store/migrations/BUILD      |  31 +
 .../component-store/migrations/migration.json |   4 +
 modules/component-store/package.json          |  15 +-
 .../component-store/schematics-core/index.ts  |  92 +++
 .../schematics-core/utility/angular-utils.ts  |  50 ++
 .../schematics-core/utility/ast-utils.ts      | 745 ++++++++++++++++++
 .../schematics-core/utility/change.ts         | 187 +++++
 .../schematics-core/utility/config.ts         | 150 ++++
 .../schematics-core/utility/find-module.ts    | 140 ++++
 .../schematics-core/utility/json-utilts.ts    |  16 +
 .../schematics-core/utility/libs-version.ts   |   1 +
 .../schematics-core/utility/ngrx-utils.ts     | 270 +++++++
 .../schematics-core/utility/package.ts        |  27 +
 .../schematics-core/utility/parse-name.ts     |  16 +
 .../schematics-core/utility/project.ts        |  52 ++
 .../schematics-core/utility/strings.ts        | 147 ++++
 .../schematics-core/utility/update.ts         |  43 +
 .../schematics-core/utility/visitors.ts       | 227 ++++++
 modules/component-store/schematics/BUILD      |  36 +
 .../schematics/collection.json                |  10 +
 .../schematics/ng-add/index.spec.ts           |  41 +
 .../schematics/ng-add/index.ts                |  34 +
 .../schematics/ng-add/schema.json             |  14 +
 .../schematics/ng-add/schema.ts               |   3 +
 modules/data/package.json                     |   3 +-
 modules/effects/package.json                  |   3 +-
 modules/entity/package.json                   |   3 +-
 modules/router-store/package.json             |   3 +-
 modules/schematics/package.json               |   3 +-
 modules/store-devtools/package.json           |   3 +-
 modules/store/package.json                    |   3 +-
 tsconfig.json                                 |   3 +
 32 files changed, 2367 insertions(+), 8 deletions(-)
 create mode 100644 modules/component-store/migrations/BUILD
 create mode 100644 modules/component-store/migrations/migration.json
 create mode 100644 modules/component-store/schematics-core/index.ts
 create mode 100644 modules/component-store/schematics-core/utility/angular-utils.ts
 create mode 100644 modules/component-store/schematics-core/utility/ast-utils.ts
 create mode 100644 modules/component-store/schematics-core/utility/change.ts
 create mode 100644 modules/component-store/schematics-core/utility/config.ts
 create mode 100644 modules/component-store/schematics-core/utility/find-module.ts
 create mode 100644 modules/component-store/schematics-core/utility/json-utilts.ts
 create mode 100644 modules/component-store/schematics-core/utility/libs-version.ts
 create mode 100644 modules/component-store/schematics-core/utility/ngrx-utils.ts
 create mode 100644 modules/component-store/schematics-core/utility/package.ts
 create mode 100644 modules/component-store/schematics-core/utility/parse-name.ts
 create mode 100644 modules/component-store/schematics-core/utility/project.ts
 create mode 100644 modules/component-store/schematics-core/utility/strings.ts
 create mode 100644 modules/component-store/schematics-core/utility/update.ts
 create mode 100644 modules/component-store/schematics-core/utility/visitors.ts
 create mode 100644 modules/component-store/schematics/BUILD
 create mode 100644 modules/component-store/schematics/collection.json
 create mode 100644 modules/component-store/schematics/ng-add/index.spec.ts
 create mode 100644 modules/component-store/schematics/ng-add/index.ts
 create mode 100644 modules/component-store/schematics/ng-add/schema.json
 create mode 100644 modules/component-store/schematics/ng-add/schema.ts

diff --git a/modules/component-store/migrations/BUILD b/modules/component-store/migrations/BUILD
new file mode 100644
index 0000000000..e7d3d63a72
--- /dev/null
+++ b/modules/component-store/migrations/BUILD
@@ -0,0 +1,31 @@
+load("//tools:defaults.bzl", "pkg_npm", "ts_library")
+
+package(default_visibility = ["//visibility:public"])
+
+ts_library(
+    name = "migrations",
+    srcs = glob(
+        [
+            "**/*.ts",
+        ],
+        exclude = [
+            "**/testing/*.ts",
+            "**/*.spec.ts",
+        ],
+    ),
+    module_name = "@ngrx/component-store/migrations",
+    deps = [
+        "//modules/component-store/schematics-core",
+        "@npm//@angular-devkit/schematics",
+    ],
+)
+
+pkg_npm(
+    name = "npm_package",
+    srcs = [
+        ":migration.json",
+    ],
+    deps = [
+        ":migrations",
+    ],
+)
diff --git a/modules/component-store/migrations/migration.json b/modules/component-store/migrations/migration.json
new file mode 100644
index 0000000000..00b46c7ac6
--- /dev/null
+++ b/modules/component-store/migrations/migration.json
@@ -0,0 +1,4 @@
+{
+  "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
+  "schematics": {}
+}
diff --git a/modules/component-store/package.json b/modules/component-store/package.json
index dc3029e731..7cbf6acbe6 100644
--- a/modules/component-store/package.json
+++ b/modules/component-store/package.json
@@ -25,5 +25,18 @@
     "@angular/core": "^10.0.0",
     "rxjs": "^6.5.3"
   },
-  "sideEffects": false
+  "sideEffects": false,
+  "ng-update": {
+    "packageGroup": [
+      "@ngrx/store",
+      "@ngrx/effects",
+      "@ngrx/entity",
+      "@ngrx/router-store",
+      "@ngrx/data",
+      "@ngrx/schematics",
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
+    ],
+    "migrations": "./migrations/migration.json"
+  }
 }
diff --git a/modules/component-store/schematics-core/index.ts b/modules/component-store/schematics-core/index.ts
new file mode 100644
index 0000000000..327bb6460d
--- /dev/null
+++ b/modules/component-store/schematics-core/index.ts
@@ -0,0 +1,92 @@
+import {
+  dasherize,
+  decamelize,
+  camelize,
+  classify,
+  underscore,
+  group,
+  capitalize,
+  featurePath,
+  pluralize,
+} from './utility/strings';
+
+export { isIvyEnabled } from './utility/angular-utils';
+
+export {
+  findNodes,
+  getSourceNodes,
+  getDecoratorMetadata,
+  getContentOfKeyLiteral,
+  insertAfterLastOccurrence,
+  insertImport,
+  addBootstrapToModule,
+  addDeclarationToModule,
+  addExportToModule,
+  addImportToModule,
+  addProviderToModule,
+  replaceImport,
+  containsProperty,
+} from './utility/ast-utils';
+
+export {
+  Host,
+  Change,
+  NoopChange,
+  InsertChange,
+  RemoveChange,
+  ReplaceChange,
+  createReplaceChange,
+  createChangeRecorder,
+  commitChanges,
+} from './utility/change';
+
+export { AppConfig, getWorkspace, getWorkspacePath } from './utility/config';
+
+export {
+  findModule,
+  findModuleFromOptions,
+  buildRelativePath,
+  ModuleOptions,
+} from './utility/find-module';
+
+export { findPropertyInAstObject } from './utility/json-utilts';
+
+export {
+  addReducerToState,
+  addReducerToStateInterface,
+  addReducerImportToNgModule,
+  addReducerToActionReducerMap,
+  omit,
+} from './utility/ngrx-utils';
+
+export { getProjectPath, getProject, isLib } from './utility/project';
+
+export const stringUtils = {
+  dasherize,
+  decamelize,
+  camelize,
+  classify,
+  underscore,
+  group,
+  capitalize,
+  featurePath,
+  pluralize,
+};
+
+export { updatePackage } from './utility/update';
+
+export { parseName } from './utility/parse-name';
+
+export { addPackageToPackageJson } from './utility/package';
+
+export { platformVersion } from './utility/libs-version';
+
+export {
+  visitTSSourceFiles,
+  visitNgModuleImports,
+  visitNgModuleExports,
+  visitComponents,
+  visitDecorator,
+  visitNgModules,
+  visitTemplates,
+} from './utility/visitors';
diff --git a/modules/component-store/schematics-core/utility/angular-utils.ts b/modules/component-store/schematics-core/utility/angular-utils.ts
new file mode 100644
index 0000000000..448eb24784
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/angular-utils.ts
@@ -0,0 +1,50 @@
+import {
+  JsonParseMode,
+  dirname,
+  normalize,
+  parseJsonAst,
+  resolve,
+} from '@angular-devkit/core';
+import { Tree } from '@angular-devkit/schematics';
+import { findPropertyInAstObject } from './json-utilts';
+
+// https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/migrations/update-9/utils.ts
+export function isIvyEnabled(tree: Tree, tsConfigPath: string): boolean {
+  // In version 9, Ivy is turned on by default
+  // Ivy is opted out only when 'enableIvy' is set to false.
+
+  const buffer = tree.read(tsConfigPath);
+  if (!buffer) {
+    return true;
+  }
+
+  const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
+
+  if (tsCfgAst.kind !== 'object') {
+    return true;
+  }
+
+  const ngCompilerOptions = findPropertyInAstObject(
+    tsCfgAst,
+    'angularCompilerOptions'
+  );
+  if (ngCompilerOptions && ngCompilerOptions.kind === 'object') {
+    const enableIvy = findPropertyInAstObject(ngCompilerOptions, 'enableIvy');
+
+    if (enableIvy) {
+      return !!enableIvy.value;
+    }
+  }
+
+  const configExtends = findPropertyInAstObject(tsCfgAst, 'extends');
+  if (configExtends && configExtends.kind === 'string') {
+    const extendedTsConfigPath = resolve(
+      dirname(normalize(tsConfigPath)),
+      normalize(configExtends.value)
+    );
+
+    return isIvyEnabled(tree, extendedTsConfigPath);
+  }
+
+  return true;
+}
diff --git a/modules/component-store/schematics-core/utility/ast-utils.ts b/modules/component-store/schematics-core/utility/ast-utils.ts
new file mode 100644
index 0000000000..f3f805dbe6
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/ast-utils.ts
@@ -0,0 +1,745 @@
+/* istanbul ignore file */
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import * as ts from 'typescript';
+import {
+  Change,
+  InsertChange,
+  NoopChange,
+  createReplaceChange,
+  ReplaceChange,
+  RemoveChange,
+  createRemoveChange,
+} from './change';
+import { Path } from '@angular-devkit/core';
+
+/**
+ * Find all nodes from the AST in the subtree of node of SyntaxKind kind.
+ * @param node
+ * @param kind
+ * @param max The maximum number of items to return.
+ * @return all nodes of kind, or [] if none is found
+ */
+export function findNodes(
+  node: ts.Node,
+  kind: ts.SyntaxKind,
+  max = Infinity
+): ts.Node[] {
+  if (!node || max == 0) {
+    return [];
+  }
+
+  const arr: ts.Node[] = [];
+  if (node.kind === kind) {
+    arr.push(node);
+    max--;
+  }
+  if (max > 0) {
+    for (const child of node.getChildren()) {
+      findNodes(child, kind, max).forEach((node) => {
+        if (max > 0) {
+          arr.push(node);
+        }
+        max--;
+      });
+
+      if (max <= 0) {
+        break;
+      }
+    }
+  }
+
+  return arr;
+}
+
+/**
+ * Get all the nodes from a source.
+ * @param sourceFile The source file object.
+ * @returns {Observable<ts.Node>} An observable of all the nodes in the source.
+ */
+export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
+  const nodes: ts.Node[] = [sourceFile];
+  const result = [];
+
+  while (nodes.length > 0) {
+    const node = nodes.shift();
+
+    if (node) {
+      result.push(node);
+      if (node.getChildCount(sourceFile) >= 0) {
+        nodes.unshift(...node.getChildren());
+      }
+    }
+  }
+
+  return result;
+}
+
+/**
+ * Helper for sorting nodes.
+ * @return function to sort nodes in increasing order of position in sourceFile
+ */
+function nodesByPosition(first: ts.Node, second: ts.Node): number {
+  return first.pos - second.pos;
+}
+
+/**
+ * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
+ * or after the last of occurence of `syntaxKind` if the last occurence is a sub child
+ * of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
+ *
+ * @param nodes insert after the last occurence of nodes
+ * @param toInsert string to insert
+ * @param file file to insert changes into
+ * @param fallbackPos position to insert if toInsert happens to be the first occurence
+ * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
+ * @return Change instance
+ * @throw Error if toInsert is first occurence but fall back is not set
+ */
+export function insertAfterLastOccurrence(
+  nodes: ts.Node[],
+  toInsert: string,
+  file: string,
+  fallbackPos: number,
+  syntaxKind?: ts.SyntaxKind
+): Change {
+  let lastItem = nodes.sort(nodesByPosition).pop();
+  if (!lastItem) {
+    throw new Error();
+  }
+  if (syntaxKind) {
+    lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
+  }
+  if (!lastItem && fallbackPos == undefined) {
+    throw new Error(
+      `tried to insert ${toInsert} as first occurence with no fallback position`
+    );
+  }
+  const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
+
+  return new InsertChange(file, lastItemPosition, toInsert);
+}
+
+export function getContentOfKeyLiteral(
+  _source: ts.SourceFile,
+  node: ts.Node
+): string | null {
+  if (node.kind == ts.SyntaxKind.Identifier) {
+    return (node as ts.Identifier).text;
+  } else if (node.kind == ts.SyntaxKind.StringLiteral) {
+    return (node as ts.StringLiteral).text;
+  } else {
+    return null;
+  }
+}
+
+function _angularImportsFromNode(
+  node: ts.ImportDeclaration,
+  _sourceFile: ts.SourceFile
+): { [name: string]: string } {
+  const ms = node.moduleSpecifier;
+  let modulePath: string;
+  switch (ms.kind) {
+    case ts.SyntaxKind.StringLiteral:
+      modulePath = (ms as ts.StringLiteral).text;
+      break;
+    default:
+      return {};
+  }
+
+  if (!modulePath.startsWith('@angular/')) {
+    return {};
+  }
+
+  if (node.importClause) {
+    if (node.importClause.name) {
+      // This is of the form `import Name from 'path'`. Ignore.
+      return {};
+    } else if (node.importClause.namedBindings) {
+      const nb = node.importClause.namedBindings;
+      if (nb.kind == ts.SyntaxKind.NamespaceImport) {
+        // This is of the form `import * as name from 'path'`. Return `name.`.
+        return {
+          [(nb as ts.NamespaceImport).name.text + '.']: modulePath,
+        };
+      } else {
+        // This is of the form `import {a,b,c} from 'path'`
+        const namedImports = nb as ts.NamedImports;
+
+        return namedImports.elements
+          .map((is: ts.ImportSpecifier) =>
+            is.propertyName ? is.propertyName.text : is.name.text
+          )
+          .reduce((acc: { [name: string]: string }, curr: string) => {
+            acc[curr] = modulePath;
+
+            return acc;
+          }, {});
+      }
+    }
+
+    return {};
+  } else {
+    // This is of the form `import 'path';`. Nothing to do.
+    return {};
+  }
+}
+
+export function getDecoratorMetadata(
+  source: ts.SourceFile,
+  identifier: string,
+  module: string
+): ts.Node[] {
+  const angularImports: { [name: string]: string } = findNodes(
+    source,
+    ts.SyntaxKind.ImportDeclaration
+  )
+    .map((node) =>
+      _angularImportsFromNode(node as ts.ImportDeclaration, source)
+    )
+    .reduce(
+      (
+        acc: { [name: string]: string },
+        current: { [name: string]: string }
+      ) => {
+        for (const key of Object.keys(current)) {
+          acc[key] = current[key];
+        }
+
+        return acc;
+      },
+      {}
+    );
+
+  return getSourceNodes(source)
+    .filter((node) => {
+      return (
+        node.kind == ts.SyntaxKind.Decorator &&
+        (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression
+      );
+    })
+    .map((node) => (node as ts.Decorator).expression as ts.CallExpression)
+    .filter((expr) => {
+      if (expr.expression.kind == ts.SyntaxKind.Identifier) {
+        const id = expr.expression as ts.Identifier;
+
+        return (
+          id.getFullText(source) == identifier &&
+          angularImports[id.getFullText(source)] === module
+        );
+      } else if (
+        expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression
+      ) {
+        // This covers foo.NgModule when importing * as foo.
+        const paExpr = expr.expression as ts.PropertyAccessExpression;
+        // If the left expression is not an identifier, just give up at that point.
+        if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
+          return false;
+        }
+
+        const id = paExpr.name.text;
+        const moduleId = (paExpr.expression as ts.Identifier).getText(source);
+
+        return id === identifier && angularImports[moduleId + '.'] === module;
+      }
+
+      return false;
+    })
+    .filter(
+      (expr) =>
+        expr.arguments[0] &&
+        expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression
+    )
+    .map((expr) => expr.arguments[0] as ts.ObjectLiteralExpression);
+}
+
+function _addSymbolToNgModuleMetadata(
+  source: ts.SourceFile,
+  ngModulePath: string,
+  metadataField: string,
+  symbolName: string,
+  importPath: string
+): Change[] {
+  const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
+  let node: any = nodes[0]; // tslint:disable-line:no-any
+
+  // Find the decorator declaration.
+  if (!node) {
+    return [];
+  }
+
+  // Get all the children property assignment of object literals.
+  const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties
+    .filter((prop) => prop.kind == ts.SyntaxKind.PropertyAssignment)
+    // Filter out every fields that's not "metadataField". Also handles string literals
+    // (but not expressions).
+    .filter((prop: any) => {
+      const name = prop.name;
+      switch (name.kind) {
+        case ts.SyntaxKind.Identifier:
+          return (name as ts.Identifier).getText(source) == metadataField;
+        case ts.SyntaxKind.StringLiteral:
+          return (name as ts.StringLiteral).text == metadataField;
+      }
+
+      return false;
+    });
+
+  // Get the last node of the array literal.
+  if (!matchingProperties) {
+    return [];
+  }
+  if (matchingProperties.length == 0) {
+    // We haven't found the field in the metadata declaration. Insert a new field.
+    const expr = node as ts.ObjectLiteralExpression;
+    let position: number;
+    let toInsert: string;
+    if (expr.properties.length == 0) {
+      position = expr.getEnd() - 1;
+      toInsert = `  ${metadataField}: [${symbolName}]\n`;
+    } else {
+      node = expr.properties[expr.properties.length - 1];
+      position = node.getEnd();
+      // Get the indentation of the last element, if any.
+      const text = node.getFullText(source);
+      const matches = text.match(/^\r?\n\s*/);
+      if (matches.length > 0) {
+        toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
+      } else {
+        toInsert = `, ${metadataField}: [${symbolName}]`;
+      }
+    }
+    const newMetadataProperty = new InsertChange(
+      ngModulePath,
+      position,
+      toInsert
+    );
+    const newMetadataImport = insertImport(
+      source,
+      ngModulePath,
+      symbolName.replace(/\..*$/, ''),
+      importPath
+    );
+
+    return [newMetadataProperty, newMetadataImport];
+  }
+
+  const assignment = matchingProperties[0] as ts.PropertyAssignment;
+
+  // If it's not an array, nothing we can do really.
+  if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
+    return [];
+  }
+
+  const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
+  if (arrLiteral.elements.length == 0) {
+    // Forward the property.
+    node = arrLiteral;
+  } else {
+    node = arrLiteral.elements;
+  }
+
+  if (!node) {
+    console.log(
+      'No app module found. Please add your new class to your component.'
+    );
+
+    return [];
+  }
+
+  if (Array.isArray(node)) {
+    const nodeArray = (node as {}) as Array<ts.Node>;
+    const symbolsArray = nodeArray.map((node) => node.getText());
+    if (symbolsArray.includes(symbolName)) {
+      return [];
+    }
+
+    node = node[node.length - 1];
+
+    const effectsModule = nodeArray.find(
+      (node) =>
+        (node.getText().includes('EffectsModule.forRoot') &&
+          symbolName.includes('EffectsModule.forRoot')) ||
+        (node.getText().includes('EffectsModule.forFeature') &&
+          symbolName.includes('EffectsModule.forFeature'))
+    );
+
+    if (effectsModule && symbolName.includes('EffectsModule')) {
+      const effectsArgs = (effectsModule as any).arguments.shift();
+
+      if (
+        effectsArgs &&
+        effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression
+      ) {
+        const effectsElements = (effectsArgs as ts.ArrayLiteralExpression)
+          .elements;
+        const [, effectsSymbol] = (<any>symbolName).match(/\[(.*)\]/);
+
+        let epos;
+        if (effectsElements.length === 0) {
+          epos = effectsArgs.getStart() + 1;
+          return [new InsertChange(ngModulePath, epos, effectsSymbol)];
+        } else {
+          const lastEffect = effectsElements[
+            effectsElements.length - 1
+          ] as ts.Expression;
+          epos = lastEffect.getEnd();
+          // Get the indentation of the last element, if any.
+          const text: any = lastEffect.getFullText(source);
+
+          let effectInsert: string;
+          if (text.match('^\r?\r?\n')) {
+            effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`;
+          } else {
+            effectInsert = `, ${effectsSymbol}`;
+          }
+
+          return [new InsertChange(ngModulePath, epos, effectInsert)];
+        }
+      } else {
+        return [];
+      }
+    }
+  }
+
+  let toInsert: string;
+  let position = node.getEnd();
+  if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
+    // We haven't found the field in the metadata declaration. Insert a new
+    // field.
+    const expr = node as ts.ObjectLiteralExpression;
+    if (expr.properties.length == 0) {
+      position = expr.getEnd() - 1;
+      toInsert = `  ${metadataField}: [${symbolName}]\n`;
+    } else {
+      node = expr.properties[expr.properties.length - 1];
+      position = node.getEnd();
+      // Get the indentation of the last element, if any.
+      const text = node.getFullText(source);
+      if (text.match('^\r?\r?\n')) {
+        toInsert = `,${
+          text.match(/^\r?\n\s+/)[0]
+        }${metadataField}: [${symbolName}]`;
+      } else {
+        toInsert = `, ${metadataField}: [${symbolName}]`;
+      }
+    }
+  } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
+    // We found the field but it's empty. Insert it just before the `]`.
+    position--;
+    toInsert = `${symbolName}`;
+  } else {
+    // Get the indentation of the last element, if any.
+    const text = node.getFullText(source);
+    if (text.match(/^\r?\n/)) {
+      toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`;
+    } else {
+      toInsert = `, ${symbolName}`;
+    }
+  }
+  const insert = new InsertChange(ngModulePath, position, toInsert);
+  const importInsert: Change = insertImport(
+    source,
+    ngModulePath,
+    symbolName.replace(/\..*$/, ''),
+    importPath
+  );
+
+  return [insert, importInsert];
+}
+
+/**
+ * Custom function to insert a declaration (component, pipe, directive)
+ * into NgModule declarations. It also imports the component.
+ */
+export function addDeclarationToModule(
+  source: ts.SourceFile,
+  modulePath: string,
+  classifiedName: string,
+  importPath: string
+): Change[] {
+  return _addSymbolToNgModuleMetadata(
+    source,
+    modulePath,
+    'declarations',
+    classifiedName,
+    importPath
+  );
+}
+
+/**
+ * Custom function to insert a declaration (component, pipe, directive)
+ * into NgModule declarations. It also imports the component.
+ */
+export function addImportToModule(
+  source: ts.SourceFile,
+  modulePath: string,
+  classifiedName: string,
+  importPath: string
+): Change[] {
+  return _addSymbolToNgModuleMetadata(
+    source,
+    modulePath,
+    'imports',
+    classifiedName,
+    importPath
+  );
+}
+
+/**
+ * Custom function to insert a provider into NgModule. It also imports it.
+ */
+export function addProviderToModule(
+  source: ts.SourceFile,
+  modulePath: string,
+  classifiedName: string,
+  importPath: string
+): Change[] {
+  return _addSymbolToNgModuleMetadata(
+    source,
+    modulePath,
+    'providers',
+    classifiedName,
+    importPath
+  );
+}
+
+/**
+ * Custom function to insert an export into NgModule. It also imports it.
+ */
+export function addExportToModule(
+  source: ts.SourceFile,
+  modulePath: string,
+  classifiedName: string,
+  importPath: string
+): Change[] {
+  return _addSymbolToNgModuleMetadata(
+    source,
+    modulePath,
+    'exports',
+    classifiedName,
+    importPath
+  );
+}
+
+/**
+ * Custom function to insert an export into NgModule. It also imports it.
+ */
+export function addBootstrapToModule(
+  source: ts.SourceFile,
+  modulePath: string,
+  classifiedName: string,
+  importPath: string
+): Change[] {
+  return _addSymbolToNgModuleMetadata(
+    source,
+    modulePath,
+    'bootstrap',
+    classifiedName,
+    importPath
+  );
+}
+
+/**
+ * Add Import `import { symbolName } from fileName` if the import doesn't exit
+ * already. Assumes fileToEdit can be resolved and accessed.
+ * @param fileToEdit (file we want to add import to)
+ * @param symbolName (item to import)
+ * @param fileName (path to the file)
+ * @param isDefault (if true, import follows style for importing default exports)
+ * @return Change
+ */
+
+export function insertImport(
+  source: ts.SourceFile,
+  fileToEdit: string,
+  symbolName: string,
+  fileName: string,
+  isDefault = false
+): Change {
+  const rootNode = source;
+  const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
+
+  // get nodes that map to import statements from the file fileName
+  const relevantImports = allImports.filter((node) => {
+    // StringLiteral of the ImportDeclaration is the import file (fileName in this case).
+    const importFiles = node
+      .getChildren()
+      .filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
+      .map((n) => (n as ts.StringLiteral).text);
+
+    return importFiles.filter((file) => file === fileName).length === 1;
+  });
+
+  if (relevantImports.length > 0) {
+    let importsAsterisk = false;
+    // imports from import file
+    const imports: ts.Node[] = [];
+    relevantImports.forEach((n) => {
+      Array.prototype.push.apply(
+        imports,
+        findNodes(n, ts.SyntaxKind.Identifier)
+      );
+      if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
+        importsAsterisk = true;
+      }
+    });
+
+    // if imports * from fileName, don't add symbolName
+    if (importsAsterisk) {
+      return new NoopChange();
+    }
+
+    const importTextNodes = imports.filter(
+      (n) => (n as ts.Identifier).text === symbolName
+    );
+
+    // insert import if it's not there
+    if (importTextNodes.length === 0) {
+      const fallbackPos =
+        findNodes(
+          relevantImports[0],
+          ts.SyntaxKind.CloseBraceToken
+        )[0].getStart() ||
+        findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
+
+      return insertAfterLastOccurrence(
+        imports,
+        `, ${symbolName}`,
+        fileToEdit,
+        fallbackPos
+      );
+    }
+
+    return new NoopChange();
+  }
+
+  // no such import declaration exists
+  const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
+    (n) => n.getText() === 'use strict'
+  );
+  let fallbackPos = 0;
+  if (useStrict.length > 0) {
+    fallbackPos = useStrict[0].end;
+  }
+  const open = isDefault ? '' : '{ ';
+  const close = isDefault ? '' : ' }';
+  // if there are no imports or 'use strict' statement, insert import at beginning of file
+  const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
+  const separator = insertAtBeginning ? '' : ';\n';
+  const toInsert =
+    `${separator}import ${open}${symbolName}${close}` +
+    ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
+
+  return insertAfterLastOccurrence(
+    allImports,
+    toInsert,
+    fileToEdit,
+    fallbackPos,
+    ts.SyntaxKind.StringLiteral
+  );
+}
+
+export function replaceImport(
+  sourceFile: ts.SourceFile,
+  path: Path,
+  importFrom: string,
+  importAsIs: string,
+  importToBe: string
+): (ReplaceChange | RemoveChange)[] {
+  const imports = sourceFile.statements
+    .filter(ts.isImportDeclaration)
+    .filter(
+      ({ moduleSpecifier }) =>
+        moduleSpecifier.getText(sourceFile) === `'${importFrom}'` ||
+        moduleSpecifier.getText(sourceFile) === `"${importFrom}"`
+    );
+
+  if (imports.length === 0) {
+    return [];
+  }
+
+  const importText = (specifier: ts.ImportSpecifier) => {
+    if (specifier.name.text) {
+      return specifier.name.text;
+    }
+
+    // if import is renamed
+    if (specifier.propertyName && specifier.propertyName.text) {
+      return specifier.propertyName.text;
+    }
+
+    return '';
+  };
+
+  const changes = imports.map((p) => {
+    const importSpecifiers = (p.importClause!.namedBindings! as ts.NamedImports)
+      .elements;
+
+    const isAlreadyImported = importSpecifiers
+      .map(importText)
+      .includes(importToBe);
+
+    const importChanges = importSpecifiers.map((specifier, index) => {
+      const text = importText(specifier);
+
+      // import is not the one we're looking for, can be skipped
+      if (text !== importAsIs) {
+        return undefined;
+      }
+
+      // identifier has not been imported, simply replace the old text with the new text
+      if (!isAlreadyImported) {
+        return createReplaceChange(
+          sourceFile,
+          specifier!,
+          importAsIs,
+          importToBe
+        );
+      }
+
+      const nextIdentifier = importSpecifiers[index + 1];
+      // identifer is not the last, also clean up the comma
+      if (nextIdentifier) {
+        return createRemoveChange(
+          sourceFile,
+          specifier,
+          specifier.getStart(sourceFile),
+          nextIdentifier.getStart(sourceFile)
+        );
+      }
+
+      // there are no imports following, just remove it
+      return createRemoveChange(
+        sourceFile,
+        specifier,
+        specifier.getStart(sourceFile),
+        specifier.getEnd()
+      );
+    });
+
+    return importChanges.filter(Boolean) as (ReplaceChange | RemoveChange)[];
+  });
+
+  return changes.reduce((imports, curr) => imports.concat(curr), []);
+}
+
+export function containsProperty(
+  objectLiteral: ts.ObjectLiteralExpression,
+  propertyName: string
+) {
+  return (
+    objectLiteral &&
+    objectLiteral.properties.some(
+      (prop) =>
+        ts.isPropertyAssignment(prop) &&
+        ts.isIdentifier(prop.name) &&
+        prop.name.text === propertyName
+    )
+  );
+}
diff --git a/modules/component-store/schematics-core/utility/change.ts b/modules/component-store/schematics-core/utility/change.ts
new file mode 100644
index 0000000000..3b8ed0cd2e
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/change.ts
@@ -0,0 +1,187 @@
+import * as ts from 'typescript';
+import { Tree, UpdateRecorder } from '@angular-devkit/schematics';
+import { Path } from '@angular-devkit/core';
+
+/* istanbul ignore file */
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+export interface Host {
+  write(path: string, content: string): Promise<void>;
+  read(path: string): Promise<string>;
+}
+
+export interface Change {
+  apply(host: Host): Promise<void>;
+
+  // The file this change should be applied to. Some changes might not apply to
+  // a file (maybe the config).
+  readonly path: string | null;
+
+  // The order this change should be applied. Normally the position inside the file.
+  // Changes are applied from the bottom of a file to the top.
+  readonly order: number;
+
+  // The description of this change. This will be outputted in a dry or verbose run.
+  readonly description: string;
+}
+
+/**
+ * An operation that does nothing.
+ */
+export class NoopChange implements Change {
+  description = 'No operation.';
+  order = Infinity;
+  path = null;
+  apply() {
+    return Promise.resolve();
+  }
+}
+
+/**
+ * Will add text to the source code.
+ */
+export class InsertChange implements Change {
+  order: number;
+  description: string;
+
+  constructor(public path: string, public pos: number, public toAdd: string) {
+    if (pos < 0) {
+      throw new Error('Negative positions are invalid');
+    }
+    this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
+    this.order = pos;
+  }
+
+  /**
+   * This method does not insert spaces if there is none in the original string.
+   */
+  apply(host: Host) {
+    return host.read(this.path).then((content) => {
+      const prefix = content.substring(0, this.pos);
+      const suffix = content.substring(this.pos);
+
+      return host.write(this.path, `${prefix}${this.toAdd}${suffix}`);
+    });
+  }
+}
+
+/**
+ * Will remove text from the source code.
+ */
+export class RemoveChange implements Change {
+  order: number;
+  description: string;
+
+  constructor(public path: string, public pos: number, public end: number) {
+    if (pos < 0 || end < 0) {
+      throw new Error('Negative positions are invalid');
+    }
+    this.description = `Removed text in position ${pos} to ${end} of ${path}`;
+    this.order = pos;
+  }
+
+  apply(host: Host): Promise<void> {
+    return host.read(this.path).then((content) => {
+      const prefix = content.substring(0, this.pos);
+      const suffix = content.substring(this.end);
+
+      // TODO: throw error if toRemove doesn't match removed string.
+      return host.write(this.path, `${prefix}${suffix}`);
+    });
+  }
+}
+
+/**
+ * Will replace text from the source code.
+ */
+export class ReplaceChange implements Change {
+  order: number;
+  description: string;
+
+  constructor(
+    public path: string,
+    public pos: number,
+    public oldText: string,
+    public newText: string
+  ) {
+    if (pos < 0) {
+      throw new Error('Negative positions are invalid');
+    }
+    this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
+    this.order = pos;
+  }
+
+  apply(host: Host): Promise<void> {
+    return host.read(this.path).then((content) => {
+      const prefix = content.substring(0, this.pos);
+      const suffix = content.substring(this.pos + this.oldText.length);
+      const text = content.substring(this.pos, this.pos + this.oldText.length);
+
+      if (text !== this.oldText) {
+        return Promise.reject(
+          new Error(`Invalid replace: "${text}" != "${this.oldText}".`)
+        );
+      }
+
+      // TODO: throw error if oldText doesn't match removed string.
+      return host.write(this.path, `${prefix}${this.newText}${suffix}`);
+    });
+  }
+}
+
+export function createReplaceChange(
+  sourceFile: ts.SourceFile,
+  node: ts.Node,
+  oldText: string,
+  newText: string
+): ReplaceChange {
+  return new ReplaceChange(
+    sourceFile.fileName,
+    node.getStart(sourceFile),
+    oldText,
+    newText
+  );
+}
+
+export function createRemoveChange(
+  sourceFile: ts.SourceFile,
+  node: ts.Node,
+  from = node.getStart(sourceFile),
+  to = node.getEnd()
+): RemoveChange {
+  return new RemoveChange(sourceFile.fileName, from, to);
+}
+
+export function createChangeRecorder(
+  tree: Tree,
+  path: string,
+  changes: Change[]
+): UpdateRecorder {
+  const recorder = tree.beginUpdate(path);
+  for (const change of changes) {
+    if (change instanceof InsertChange) {
+      recorder.insertLeft(change.pos, change.toAdd);
+    } else if (change instanceof RemoveChange) {
+      recorder.remove(change.pos, change.end - change.pos);
+    } else if (change instanceof ReplaceChange) {
+      recorder.remove(change.pos, change.oldText.length);
+      recorder.insertLeft(change.pos, change.newText);
+    }
+  }
+  return recorder;
+}
+
+export function commitChanges(tree: Tree, path: string, changes: Change[]) {
+  if (changes.length === 0) {
+    return false;
+  }
+
+  const recorder = createChangeRecorder(tree, path, changes);
+  tree.commitUpdate(recorder);
+  return true;
+}
diff --git a/modules/component-store/schematics-core/utility/config.ts b/modules/component-store/schematics-core/utility/config.ts
new file mode 100644
index 0000000000..38826d4aea
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/config.ts
@@ -0,0 +1,150 @@
+import { SchematicsException, Tree } from '@angular-devkit/schematics';
+import { experimental } from '@angular-devkit/core';
+
+// The interfaces below are generated from the Angular CLI configuration schema
+// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json
+export interface AppConfig {
+  /**
+   * Name of the app.
+   */
+  name?: string;
+  /**
+   * Directory where app files are placed.
+   */
+  appRoot?: string;
+  /**
+   * The root directory of the app.
+   */
+  root?: string;
+  /**
+   * The output directory for build results.
+   */
+  outDir?: string;
+  /**
+   * List of application assets.
+   */
+  assets?: (
+    | string
+    | {
+        /**
+         * The pattern to match.
+         */
+        glob?: string;
+        /**
+         * The dir to search within.
+         */
+        input?: string;
+        /**
+         * The output path (relative to the outDir).
+         */
+        output?: string;
+      }
+  )[];
+  /**
+   * URL where files will be deployed.
+   */
+  deployUrl?: string;
+  /**
+   * Base url for the application being built.
+   */
+  baseHref?: string;
+  /**
+   * The runtime platform of the app.
+   */
+  platform?: 'browser' | 'server';
+  /**
+   * The name of the start HTML file.
+   */
+  index?: string;
+  /**
+   * The name of the main entry-point file.
+   */
+  main?: string;
+  /**
+   * The name of the polyfills file.
+   */
+  polyfills?: string;
+  /**
+   * The name of the test entry-point file.
+   */
+  test?: string;
+  /**
+   * The name of the TypeScript configuration file.
+   */
+  tsconfig?: string;
+  /**
+   * The name of the TypeScript configuration file for unit tests.
+   */
+  testTsconfig?: string;
+  /**
+   * The prefix to apply to generated selectors.
+   */
+  prefix?: string;
+  /**
+   * Experimental support for a service worker from @angular/service-worker.
+   */
+  serviceWorker?: boolean;
+  /**
+   * Global styles to be included in the build.
+   */
+  styles?: (
+    | string
+    | {
+        input?: string;
+        [name: string]: any; // tslint:disable-line:no-any
+      }
+  )[];
+  /**
+   * Options to pass to style preprocessors
+   */
+  stylePreprocessorOptions?: {
+    /**
+     * Paths to include. Paths will be resolved to project root.
+     */
+    includePaths?: string[];
+  };
+  /**
+   * Global scripts to be included in the build.
+   */
+  scripts?: (
+    | string
+    | {
+        input: string;
+        [name: string]: any; // tslint:disable-line:no-any
+      }
+  )[];
+  /**
+   * Source file for environment config.
+   */
+  environmentSource?: string;
+  /**
+   * Name and corresponding file for environment config.
+   */
+  environments?: {
+    [name: string]: any; // tslint:disable-line:no-any
+  };
+  appShell?: {
+    app: string;
+    route: string;
+  };
+}
+
+export type WorkspaceSchema = experimental.workspace.WorkspaceSchema;
+
+export function getWorkspacePath(host: Tree): string {
+  const possibleFiles = ['/angular.json', '/.angular.json'];
+  const path = possibleFiles.filter((path) => host.exists(path))[0];
+
+  return path;
+}
+
+export function getWorkspace(host: Tree): WorkspaceSchema {
+  const path = getWorkspacePath(host);
+  const configBuffer = host.read(path);
+  if (configBuffer === null) {
+    throw new SchematicsException(`Could not find (${path})`);
+  }
+  const config = configBuffer.toString();
+
+  return JSON.parse(config);
+}
diff --git a/modules/component-store/schematics-core/utility/find-module.ts b/modules/component-store/schematics-core/utility/find-module.ts
new file mode 100644
index 0000000000..134a59f0e6
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/find-module.ts
@@ -0,0 +1,140 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+  Path,
+  join,
+  normalize,
+  relative,
+  strings,
+  basename,
+  extname,
+  dirname,
+} from '@angular-devkit/core';
+import { DirEntry, Tree } from '@angular-devkit/schematics';
+
+export interface ModuleOptions {
+  module?: string;
+  name: string;
+  flat?: boolean;
+  path?: string;
+  skipImport?: boolean;
+}
+
+/**
+ * Find the module referred by a set of options passed to the schematics.
+ */
+export function findModuleFromOptions(
+  host: Tree,
+  options: ModuleOptions
+): Path | undefined {
+  if (options.hasOwnProperty('skipImport') && options.skipImport) {
+    return undefined;
+  }
+
+  if (!options.module) {
+    const pathToCheck =
+      (options.path || '') +
+      (options.flat ? '' : '/' + strings.dasherize(options.name));
+
+    return normalize(findModule(host, pathToCheck));
+  } else {
+    const modulePath = normalize('/' + options.path + '/' + options.module);
+    const moduleBaseName = normalize(modulePath).split('/').pop();
+
+    if (host.exists(modulePath)) {
+      return normalize(modulePath);
+    } else if (host.exists(modulePath + '.ts')) {
+      return normalize(modulePath + '.ts');
+    } else if (host.exists(modulePath + '.module.ts')) {
+      return normalize(modulePath + '.module.ts');
+    } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) {
+      return normalize(modulePath + '/' + moduleBaseName + '.module.ts');
+    } else {
+      throw new Error(`Specified module path ${modulePath} does not exist`);
+    }
+  }
+}
+
+/**
+ * Function to find the "closest" module to a generated file's path.
+ */
+export function findModule(host: Tree, generateDir: string): Path {
+  let dir: DirEntry | null = host.getDir('/' + generateDir);
+
+  const moduleRe = /\.module\.ts$/;
+  const routingModuleRe = /-routing\.module\.ts/;
+
+  while (dir) {
+    const matches = dir.subfiles.filter(
+      (p) => moduleRe.test(p) && !routingModuleRe.test(p)
+    );
+
+    if (matches.length == 1) {
+      return join(dir.path, matches[0]);
+    } else if (matches.length > 1) {
+      throw new Error(
+        'More than one module matches. Use skip-import option to skip importing ' +
+          'the component into the closest module.'
+      );
+    }
+
+    dir = dir.parent;
+  }
+
+  throw new Error(
+    'Could not find an NgModule. Use the skip-import ' +
+      'option to skip importing in NgModule.'
+  );
+}
+
+/**
+ * Build a relative path from one file path to another file path.
+ */
+export function buildRelativePath(from: string, to: string): string {
+  const {
+    path: fromPath,
+    filename: fromFileName,
+    directory: fromDirectory,
+  } = parsePath(from);
+  const {
+    path: toPath,
+    filename: toFileName,
+    directory: toDirectory,
+  } = parsePath(to);
+  const relativePath = relative(fromDirectory, toDirectory);
+  const fixedRelativePath = relativePath.startsWith('.')
+    ? relativePath
+    : `./${relativePath}`;
+
+  return !toFileName || toFileName === 'index.ts'
+    ? fixedRelativePath
+    : `${
+        fixedRelativePath.endsWith('/')
+          ? fixedRelativePath
+          : fixedRelativePath + '/'
+      }${convertToTypeScriptFileName(toFileName)}`;
+}
+
+function parsePath(path: string) {
+  const pathNormalized = normalize(path) as Path;
+  const filename = extname(pathNormalized) ? basename(pathNormalized) : '';
+  const directory = filename ? dirname(pathNormalized) : pathNormalized;
+  return {
+    path: pathNormalized,
+    filename,
+    directory,
+  };
+}
+/**
+ * Strips the typescript extension and clears index filenames
+ * foo.ts -> foo
+ * index.ts -> empty
+ */
+function convertToTypeScriptFileName(filename: string | undefined) {
+  return filename ? filename.replace(/(\.ts)|(index\.ts)$/, '') : '';
+}
diff --git a/modules/component-store/schematics-core/utility/json-utilts.ts b/modules/component-store/schematics-core/utility/json-utilts.ts
new file mode 100644
index 0000000000..4edbbc9edc
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/json-utilts.ts
@@ -0,0 +1,16 @@
+import { JsonAstNode, JsonAstObject } from '@angular-devkit/core';
+
+// https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/json-utils.ts
+export function findPropertyInAstObject(
+  node: JsonAstObject,
+  propertyName: string
+): JsonAstNode | null {
+  let maybeNode: JsonAstNode | null = null;
+  for (const property of node.properties) {
+    if (property.key.value == propertyName) {
+      maybeNode = property.value;
+    }
+  }
+
+  return maybeNode;
+}
diff --git a/modules/component-store/schematics-core/utility/libs-version.ts b/modules/component-store/schematics-core/utility/libs-version.ts
new file mode 100644
index 0000000000..85a636749c
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/libs-version.ts
@@ -0,0 +1 @@
+export const platformVersion = '^10.0.0-beta.0';
diff --git a/modules/component-store/schematics-core/utility/ngrx-utils.ts b/modules/component-store/schematics-core/utility/ngrx-utils.ts
new file mode 100644
index 0000000000..9c96e5a862
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/ngrx-utils.ts
@@ -0,0 +1,270 @@
+import * as ts from 'typescript';
+import * as stringUtils from './strings';
+import { InsertChange, Change, NoopChange } from './change';
+import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics';
+import { normalize } from '@angular-devkit/core';
+import { buildRelativePath } from './find-module';
+import { addImportToModule, insertImport } from './ast-utils';
+
+export function addReducerToState(options: any): Rule {
+  return (host: Tree) => {
+    if (!options.reducers) {
+      return host;
+    }
+
+    const reducersPath = normalize(`/${options.path}/${options.reducers}`);
+
+    if (!host.exists(reducersPath)) {
+      throw new Error(`Specified reducers path ${reducersPath} does not exist`);
+    }
+
+    const text = host.read(reducersPath);
+    if (text === null) {
+      throw new SchematicsException(`File ${reducersPath} does not exist.`);
+    }
+
+    const sourceText = text.toString('utf-8');
+
+    const source = ts.createSourceFile(
+      reducersPath,
+      sourceText,
+      ts.ScriptTarget.Latest,
+      true
+    );
+
+    const reducerPath =
+      `/${options.path}/` +
+      (options.flat ? '' : stringUtils.dasherize(options.name) + '/') +
+      (options.group ? 'reducers/' : '') +
+      stringUtils.dasherize(options.name) +
+      '.reducer';
+
+    const relativePath = buildRelativePath(reducersPath, reducerPath);
+    const reducerImport = insertImport(
+      source,
+      reducersPath,
+      `* as from${stringUtils.classify(options.name)}`,
+      relativePath,
+      true
+    );
+
+    const stateInterfaceInsert = addReducerToStateInterface(
+      source,
+      reducersPath,
+      options
+    );
+    const reducerMapInsert = addReducerToActionReducerMap(
+      source,
+      reducersPath,
+      options
+    );
+
+    const changes = [reducerImport, stateInterfaceInsert, reducerMapInsert];
+    const recorder = host.beginUpdate(reducersPath);
+    for (const change of changes) {
+      if (change instanceof InsertChange) {
+        recorder.insertLeft(change.pos, change.toAdd);
+      }
+    }
+    host.commitUpdate(recorder);
+
+    return host;
+  };
+}
+
+/**
+ * Insert the reducer into the first defined top level interface
+ */
+export function addReducerToStateInterface(
+  source: ts.SourceFile,
+  reducersPath: string,
+  options: { name: string; plural: boolean }
+): Change {
+  const stateInterface = source.statements.find(
+    (stm) => stm.kind === ts.SyntaxKind.InterfaceDeclaration
+  );
+  let node = stateInterface as ts.Statement;
+
+  if (!node) {
+    return new NoopChange();
+  }
+
+  const state = options.plural
+    ? stringUtils.pluralize(options.name)
+    : stringUtils.camelize(options.name);
+
+  const keyInsert = `[from${stringUtils.classify(
+    options.name
+  )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify(
+    options.name
+  )}.State;`;
+  const expr = node as any;
+  let position;
+  let toInsert;
+
+  if (expr.members.length === 0) {
+    position = expr.getEnd() - 1;
+    toInsert = `  ${keyInsert}\n`;
+  } else {
+    node = expr.members[expr.members.length - 1];
+    position = node.getEnd() + 1;
+    // Get the indentation of the last element, if any.
+    const text = node.getFullText(source);
+    const matches = text.match(/^\r?\n+(\s*)/);
+
+    if (matches!.length > 0) {
+      toInsert = `${matches![1]}${keyInsert}\n`;
+    } else {
+      toInsert = `\n${keyInsert}`;
+    }
+  }
+
+  return new InsertChange(reducersPath, position, toInsert);
+}
+
+/**
+ * Insert the reducer into the ActionReducerMap
+ */
+export function addReducerToActionReducerMap(
+  source: ts.SourceFile,
+  reducersPath: string,
+  options: { name: string; plural: boolean }
+): Change {
+  let initializer: any;
+  const actionReducerMap: any = source.statements
+    .filter((stm) => stm.kind === ts.SyntaxKind.VariableStatement)
+    .filter((stm: any) => !!stm.declarationList)
+    .map((stm: any) => {
+      const {
+        declarations,
+      }: {
+        declarations: ts.SyntaxKind.VariableDeclarationList[];
+      } = stm.declarationList;
+      const variable: any = declarations.find(
+        (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration
+      );
+      const type = variable ? variable.type : {};
+
+      return { initializer: variable.initializer, type };
+    })
+    .filter((initWithType) => initWithType.type !== undefined)
+    .find(({ type }) => type.typeName.text === 'ActionReducerMap');
+
+  if (!actionReducerMap || !actionReducerMap.initializer) {
+    return new NoopChange();
+  }
+
+  let node = actionReducerMap.initializer;
+
+  const state = options.plural
+    ? stringUtils.pluralize(options.name)
+    : stringUtils.camelize(options.name);
+
+  const keyInsert = `[from${stringUtils.classify(
+    options.name
+  )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify(
+    options.name
+  )}.reducer,`;
+  const expr = node as any;
+  let position;
+  let toInsert;
+
+  if (expr.properties.length === 0) {
+    position = expr.getEnd() - 1;
+    toInsert = `  ${keyInsert}\n`;
+  } else {
+    node = expr.properties[expr.properties.length - 1];
+    position = node.getEnd() + 1;
+    // Get the indentation of the last element, if any.
+    const text = node.getFullText(source);
+    const matches = text.match(/^\r?\n+(\s*)/);
+
+    if (matches.length > 0) {
+      toInsert = `\n${matches![1]}${keyInsert}`;
+    } else {
+      toInsert = `\n${keyInsert}`;
+    }
+  }
+
+  return new InsertChange(reducersPath, position, toInsert);
+}
+
+/**
+ * Add reducer feature to NgModule
+ */
+export function addReducerImportToNgModule(options: any): Rule {
+  return (host: Tree) => {
+    if (!options.module) {
+      return host;
+    }
+
+    const modulePath = options.module;
+    if (!host.exists(options.module)) {
+      throw new Error(`Specified module path ${modulePath} does not exist`);
+    }
+
+    const text = host.read(modulePath);
+    if (text === null) {
+      throw new SchematicsException(`File ${modulePath} does not exist.`);
+    }
+    const sourceText = text.toString('utf-8');
+
+    const source = ts.createSourceFile(
+      modulePath,
+      sourceText,
+      ts.ScriptTarget.Latest,
+      true
+    );
+
+    const commonImports = [
+      insertImport(source, modulePath, 'StoreModule', '@ngrx/store'),
+    ];
+
+    const reducerPath =
+      `/${options.path}/` +
+      (options.flat ? '' : stringUtils.dasherize(options.name) + '/') +
+      (options.group ? 'reducers/' : '') +
+      stringUtils.dasherize(options.name) +
+      '.reducer';
+    const relativePath = buildRelativePath(modulePath, reducerPath);
+    const reducerImport = insertImport(
+      source,
+      modulePath,
+      `* as from${stringUtils.classify(options.name)}`,
+      relativePath,
+      true
+    );
+    const state = options.plural
+      ? stringUtils.pluralize(options.name)
+      : stringUtils.camelize(options.name);
+    const [storeNgModuleImport] = addImportToModule(
+      source,
+      modulePath,
+      `StoreModule.forFeature(from${stringUtils.classify(
+        options.name
+      )}.${state}FeatureKey, from${stringUtils.classify(
+        options.name
+      )}.reducer)`,
+      relativePath
+    );
+    const changes = [...commonImports, reducerImport, storeNgModuleImport];
+    const recorder = host.beginUpdate(modulePath);
+    for (const change of changes) {
+      if (change instanceof InsertChange) {
+        recorder.insertLeft(change.pos, change.toAdd);
+      }
+    }
+    host.commitUpdate(recorder);
+
+    return host;
+  };
+}
+
+export function omit<T extends { [key: string]: any }>(
+  object: T,
+  keyToRemove: keyof T
+): Partial<T> {
+  return Object.keys(object)
+    .filter((key) => key !== keyToRemove)
+    .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {});
+}
diff --git a/modules/component-store/schematics-core/utility/package.ts b/modules/component-store/schematics-core/utility/package.ts
new file mode 100644
index 0000000000..ecbc74b463
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/package.ts
@@ -0,0 +1,27 @@
+import { Tree } from '@angular-devkit/schematics';
+
+/**
+ * Adds a package to the package.json
+ */
+export function addPackageToPackageJson(
+  host: Tree,
+  type: string,
+  pkg: string,
+  version: string
+): Tree {
+  if (host.exists('package.json')) {
+    const sourceText = host.read('package.json')!.toString('utf-8');
+    const json = JSON.parse(sourceText);
+    if (!json[type]) {
+      json[type] = {};
+    }
+
+    if (!json[type][pkg]) {
+      json[type][pkg] = version;
+    }
+
+    host.overwrite('package.json', JSON.stringify(json, null, 2));
+  }
+
+  return host;
+}
diff --git a/modules/component-store/schematics-core/utility/parse-name.ts b/modules/component-store/schematics-core/utility/parse-name.ts
new file mode 100644
index 0000000000..a48f56b8ca
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/parse-name.ts
@@ -0,0 +1,16 @@
+import { Path, basename, dirname, normalize } from '@angular-devkit/core';
+
+export interface Location {
+  name: string;
+  path: Path;
+}
+
+export function parseName(path: string, name: string): Location {
+  const nameWithoutPath = basename(name as Path);
+  const namePath = dirname((path + '/' + name) as Path);
+
+  return {
+    name: nameWithoutPath,
+    path: normalize('/' + namePath),
+  };
+}
diff --git a/modules/component-store/schematics-core/utility/project.ts b/modules/component-store/schematics-core/utility/project.ts
new file mode 100644
index 0000000000..43145d20fd
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/project.ts
@@ -0,0 +1,52 @@
+import { getWorkspace } from './config';
+import { Tree } from '@angular-devkit/schematics';
+
+export interface WorkspaceProject {
+  root: string;
+  projectType: string;
+}
+
+export function getProject(
+  host: Tree,
+  options: { project?: string | undefined; path?: string | undefined }
+): WorkspaceProject {
+  const workspace = getWorkspace(host);
+
+  if (!options.project) {
+    options.project =
+      workspace.defaultProject !== undefined
+        ? workspace.defaultProject
+        : Object.keys(workspace.projects)[0];
+  }
+
+  return workspace.projects[options.project];
+}
+
+export function getProjectPath(
+  host: Tree,
+  options: { project?: string | undefined; path?: string | undefined }
+) {
+  const project = getProject(host, options);
+
+  if (project.root.substr(-1) === '/') {
+    project.root = project.root.substr(0, project.root.length - 1);
+  }
+
+  if (options.path === undefined) {
+    const projectDirName =
+      project.projectType === 'application' ? 'app' : 'lib';
+
+    return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`;
+  }
+
+  return options.path;
+}
+
+export function isLib(
+  host: Tree,
+  options: { project?: string | undefined; path?: string | undefined }
+) {
+  const project = getProject(host, options);
+
+  return project.projectType === 'library';
+}
diff --git a/modules/component-store/schematics-core/utility/strings.ts b/modules/component-store/schematics-core/utility/strings.ts
new file mode 100644
index 0000000000..361698f1db
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/strings.ts
@@ -0,0 +1,147 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+const STRING_DASHERIZE_REGEXP = /[ _]/g;
+const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g;
+const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g;
+const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g;
+const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g;
+
+/**
+ * Converts a camelized string into all lower case separated by underscores.
+ *
+ ```javascript
+ decamelize('innerHTML');         // 'inner_html'
+ decamelize('action_name');       // 'action_name'
+ decamelize('css-class-name');    // 'css-class-name'
+ decamelize('my favorite items'); // 'my favorite items'
+ ```
+ */
+export function decamelize(str: string): string {
+  return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase();
+}
+
+/**
+ Replaces underscores, spaces, or camelCase with dashes.
+
+ ```javascript
+ dasherize('innerHTML');         // 'inner-html'
+ dasherize('action_name');       // 'action-name'
+ dasherize('css-class-name');    // 'css-class-name'
+ dasherize('my favorite items'); // 'my-favorite-items'
+ ```
+ */
+export function dasherize(str?: string): string {
+  return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-');
+}
+
+/**
+ Returns the lowerCamelCase form of a string.
+
+ ```javascript
+ camelize('innerHTML');          // 'innerHTML'
+ camelize('action_name');        // 'actionName'
+ camelize('css-class-name');     // 'cssClassName'
+ camelize('my favorite items');  // 'myFavoriteItems'
+ camelize('My Favorite Items');  // 'myFavoriteItems'
+ ```
+ */
+export function camelize(str: string): string {
+  return str
+    .replace(
+      STRING_CAMELIZE_REGEXP,
+      (_match: string, _separator: string, chr: string) => {
+        return chr ? chr.toUpperCase() : '';
+      }
+    )
+    .replace(/^([A-Z])/, (match: string) => match.toLowerCase());
+}
+
+/**
+ Returns the UpperCamelCase form of a string.
+
+ ```javascript
+ 'innerHTML'.classify();          // 'InnerHTML'
+ 'action_name'.classify();        // 'ActionName'
+ 'css-class-name'.classify();     // 'CssClassName'
+ 'my favorite items'.classify();  // 'MyFavoriteItems'
+ ```
+ */
+export function classify(str: string): string {
+  return str
+    .split('.')
+    .map((part) => capitalize(camelize(part)))
+    .join('.');
+}
+
+/**
+ More general than decamelize. Returns the lower\_case\_and\_underscored
+ form of a string.
+
+ ```javascript
+ 'innerHTML'.underscore();          // 'inner_html'
+ 'action_name'.underscore();        // 'action_name'
+ 'css-class-name'.underscore();     // 'css_class_name'
+ 'my favorite items'.underscore();  // 'my_favorite_items'
+ ```
+ */
+export function underscore(str: string): string {
+  return str
+    .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2')
+    .replace(STRING_UNDERSCORE_REGEXP_2, '_')
+    .toLowerCase();
+}
+
+/**
+ Returns the Capitalized form of a string
+
+ ```javascript
+ 'innerHTML'.capitalize()         // 'InnerHTML'
+ 'action_name'.capitalize()       // 'Action_name'
+ 'css-class-name'.capitalize()    // 'Css-class-name'
+ 'my favorite items'.capitalize() // 'My favorite items'
+ ```
+ */
+export function capitalize(str: string): string {
+  return str.charAt(0).toUpperCase() + str.substr(1);
+}
+
+/**
+ Returns the plural form of a string
+
+ ```javascript
+ 'innerHTML'.pluralize()         // 'innerHTMLs'
+ 'action_name'.pluralize()       // 'actionNames'
+ 'css-class-name'.pluralize()    // 'cssClassNames'
+ 'regex'.pluralize()            // 'regexes'
+ 'user'.pluralize()             // 'users'
+ ```
+ */
+export function pluralize(str: string): string {
+  return camelize(
+    [/([^aeiou])y$/, /()fe?$/, /([^aeiou]o|[sxz]|[cs]h)$/].map(
+      (c, i) => (str = str.replace(c, `$1${'iv'[i] || ''}e`))
+    ) && str + 's'
+  );
+}
+
+export function group(name: string, group: string | undefined) {
+  return group ? `${group}/${name}` : name;
+}
+
+export function featurePath(
+  group: boolean | undefined,
+  flat: boolean | undefined,
+  path: string,
+  name: string
+) {
+  if (group && !flat) {
+    return `../../${path}/${name}/`;
+  }
+
+  return group ? `../${path}/` : './';
+}
diff --git a/modules/component-store/schematics-core/utility/update.ts b/modules/component-store/schematics-core/utility/update.ts
new file mode 100644
index 0000000000..7e79cda781
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/update.ts
@@ -0,0 +1,43 @@
+import {
+  Rule,
+  SchematicContext,
+  Tree,
+  SchematicsException,
+} from '@angular-devkit/schematics';
+
+export function updatePackage(name: string): Rule {
+  return (tree: Tree, context: SchematicContext) => {
+    const pkgPath = '/package.json';
+    const buffer = tree.read(pkgPath);
+    if (buffer === null) {
+      throw new SchematicsException('Could not read package.json');
+    }
+    const content = buffer.toString();
+    const pkg = JSON.parse(content);
+
+    if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) {
+      throw new SchematicsException('Error reading package.json');
+    }
+
+    const dependencyCategories = ['dependencies', 'devDependencies'];
+
+    dependencyCategories.forEach((category) => {
+      const packageName = `@ngrx/${name}`;
+
+      if (pkg[category] && pkg[category][packageName]) {
+        const firstChar = pkg[category][packageName][0];
+        const suffix = match(firstChar, '^') || match(firstChar, '~');
+
+        pkg[category][packageName] = `${suffix}6.0.0`;
+      }
+    });
+
+    tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
+
+    return tree;
+  };
+}
+
+function match(value: string, test: string) {
+  return value === test ? test : '';
+}
diff --git a/modules/component-store/schematics-core/utility/visitors.ts b/modules/component-store/schematics-core/utility/visitors.ts
new file mode 100644
index 0000000000..c4a6af6e83
--- /dev/null
+++ b/modules/component-store/schematics-core/utility/visitors.ts
@@ -0,0 +1,227 @@
+import * as ts from 'typescript';
+import { normalize, resolve } from '@angular-devkit/core';
+import { Tree, DirEntry } from '@angular-devkit/schematics';
+
+export function visitTSSourceFiles<Result = void>(
+  tree: Tree,
+  visitor: (
+    sourceFile: ts.SourceFile,
+    tree: Tree,
+    result?: Result
+  ) => Result | undefined
+): Result | undefined {
+  let result: Result | undefined = undefined;
+  for (const sourceFile of visit(tree.root)) {
+    result = visitor(sourceFile, tree, result);
+  }
+
+  return result;
+}
+
+export function visitTemplates(
+  tree: Tree,
+  visitor: (
+    template: {
+      fileName: string;
+      content: string;
+      inline: boolean;
+      start: number;
+    },
+    tree: Tree
+  ) => void
+): void {
+  visitTSSourceFiles(tree, (source) => {
+    visitComponents(source, (_, decoratorExpressionNode) => {
+      ts.forEachChild(decoratorExpressionNode, function findTemplates(n) {
+        if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) {
+          if (
+            n.name.text === 'template' &&
+            ts.isStringLiteralLike(n.initializer)
+          ) {
+            // Need to add an offset of one to the start because the template quotes are
+            // not part of the template content.
+            const templateStartIdx = n.initializer.getStart() + 1;
+            visitor(
+              {
+                fileName: source.fileName,
+                content: n.initializer.text,
+                inline: true,
+                start: templateStartIdx,
+              },
+              tree
+            );
+            return;
+          } else if (
+            n.name.text === 'templateUrl' &&
+            ts.isStringLiteralLike(n.initializer)
+          ) {
+            const parts = normalize(source.fileName).split('/').slice(0, -1);
+            const templatePath = resolve(
+              normalize(parts.join('/')),
+              normalize(n.initializer.text)
+            );
+            if (!tree.exists(templatePath)) {
+              return;
+            }
+
+            const fileContent = tree.read(templatePath);
+            if (!fileContent) {
+              return;
+            }
+
+            visitor(
+              {
+                fileName: templatePath,
+                content: fileContent.toString(),
+                inline: false,
+                start: 0,
+              },
+              tree
+            );
+            return;
+          }
+        }
+
+        ts.forEachChild(n, findTemplates);
+      });
+    });
+  });
+}
+
+export function visitNgModuleImports(
+  sourceFile: ts.SourceFile,
+  callback: (
+    importNode: ts.PropertyAssignment,
+    elementExpressions: ts.NodeArray<ts.Expression>
+  ) => void
+) {
+  visitNgModuleProperty(sourceFile, callback, 'imports');
+}
+
+export function visitNgModuleExports(
+  sourceFile: ts.SourceFile,
+  callback: (
+    exportNode: ts.PropertyAssignment,
+    elementExpressions: ts.NodeArray<ts.Expression>
+  ) => void
+) {
+  visitNgModuleProperty(sourceFile, callback, 'exports');
+}
+
+function visitNgModuleProperty(
+  sourceFile: ts.SourceFile,
+  callback: (
+    nodes: ts.PropertyAssignment,
+    elementExpressions: ts.NodeArray<ts.Expression>
+  ) => void,
+  property: string
+) {
+  visitNgModules(sourceFile, (_, decoratorExpressionNode) => {
+    ts.forEachChild(decoratorExpressionNode, function findTemplates(n) {
+      if (
+        ts.isPropertyAssignment(n) &&
+        ts.isIdentifier(n.name) &&
+        n.name.text === property &&
+        ts.isArrayLiteralExpression(n.initializer)
+      ) {
+        callback(n, n.initializer.elements);
+        return;
+      }
+
+      ts.forEachChild(n, findTemplates);
+    });
+  });
+}
+export function visitComponents(
+  sourceFile: ts.SourceFile,
+  callback: (
+    classDeclarationNode: ts.ClassDeclaration,
+    decoratorExpressionNode: ts.ObjectLiteralExpression
+  ) => void
+) {
+  visitDecorator(sourceFile, 'Component', callback);
+}
+
+export function visitNgModules(
+  sourceFile: ts.SourceFile,
+  callback: (
+    classDeclarationNode: ts.ClassDeclaration,
+    decoratorExpressionNode: ts.ObjectLiteralExpression
+  ) => void
+) {
+  visitDecorator(sourceFile, 'NgModule', callback);
+}
+
+export function visitDecorator(
+  sourceFile: ts.SourceFile,
+  decoratorName: string,
+  callback: (
+    classDeclarationNode: ts.ClassDeclaration,
+    decoratorExpressionNode: ts.ObjectLiteralExpression
+  ) => void
+) {
+  ts.forEachChild(sourceFile, function findClassDeclaration(node) {
+    if (!ts.isClassDeclaration(node)) {
+      ts.forEachChild(node, findClassDeclaration);
+    }
+
+    const classDeclarationNode = node as ts.ClassDeclaration;
+
+    if (
+      !classDeclarationNode.decorators ||
+      !classDeclarationNode.decorators.length
+    ) {
+      return;
+    }
+
+    const componentDecorator = classDeclarationNode.decorators.find((d) => {
+      return (
+        ts.isCallExpression(d.expression) &&
+        ts.isIdentifier(d.expression.expression) &&
+        d.expression.expression.text === decoratorName
+      );
+    });
+
+    if (!componentDecorator) {
+      return;
+    }
+
+    const { expression } = componentDecorator;
+    if (!ts.isCallExpression(expression)) {
+      return;
+    }
+
+    const [arg] = expression.arguments;
+    if (!ts.isObjectLiteralExpression(arg)) {
+      return;
+    }
+
+    callback(classDeclarationNode, arg);
+  });
+}
+
+function* visit(directory: DirEntry): IterableIterator<ts.SourceFile> {
+  for (const path of directory.subfiles) {
+    if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
+      const entry = directory.file(path);
+      if (entry) {
+        const content = entry.content;
+        const source = ts.createSourceFile(
+          entry.path,
+          content.toString().replace(/^\uFEFF/, ''),
+          ts.ScriptTarget.Latest,
+          true
+        );
+        yield source;
+      }
+    }
+  }
+
+  for (const path of directory.subdirs) {
+    if (path === 'node_modules') {
+      continue;
+    }
+
+    yield* visit(directory.dir(path));
+  }
+}
diff --git a/modules/component-store/schematics/BUILD b/modules/component-store/schematics/BUILD
new file mode 100644
index 0000000000..a02f58955f
--- /dev/null
+++ b/modules/component-store/schematics/BUILD
@@ -0,0 +1,36 @@
+load("//tools:defaults.bzl", "pkg_npm", "ts_library")
+
+package(default_visibility = ["//visibility:public"])
+
+ts_library(
+    name = "schematics",
+    srcs = glob(
+        [
+            "**/*.ts",
+        ],
+        exclude = [
+            "**/*.spec.ts",
+            "**/files/**/*",
+        ],
+    ),
+    module_name = "@ngrx/component-store/schematics",
+    deps = [
+        "//modules/component-store/schematics-core",
+        "@npm//@angular-devkit/core",
+        "@npm//@angular-devkit/schematics",
+        "@npm//typescript",
+    ],
+)
+
+pkg_npm(
+    name = "npm_package",
+    srcs = [
+        ":collection.json",
+    ] + glob([
+        "**/files/**/*",
+        "**/schema.json",
+    ]),
+    deps = [
+        ":schematics",
+    ],
+)
diff --git a/modules/component-store/schematics/collection.json b/modules/component-store/schematics/collection.json
new file mode 100644
index 0000000000..095e4b7271
--- /dev/null
+++ b/modules/component-store/schematics/collection.json
@@ -0,0 +1,10 @@
+{
+  "schematics": {
+    "ng-add": {
+      "aliases": ["init"],
+      "factory": "./ng-add",
+      "schema": "./ng-add/schema.json",
+      "description": "Add @ngrx/component-store to your application"
+    }
+  }
+}
diff --git a/modules/component-store/schematics/ng-add/index.spec.ts b/modules/component-store/schematics/ng-add/index.spec.ts
new file mode 100644
index 0000000000..c3e5900c12
--- /dev/null
+++ b/modules/component-store/schematics/ng-add/index.spec.ts
@@ -0,0 +1,41 @@
+import {
+  SchematicTestRunner,
+  UnitTestTree,
+} from '@angular-devkit/schematics/testing';
+import * as path from 'path';
+import { Schema as SchemaOptions } from './schema';
+import { createWorkspace } from '../../../schematics-core/testing';
+
+describe('Component store ng-add Schematic', () => {
+  const schematicRunner = new SchematicTestRunner(
+    '@ngrx/component-store',
+    path.join(__dirname, '../collection.json')
+  );
+  const defaultOptions: SchemaOptions = {
+    skipPackageJson: false,
+  };
+
+  let appTree: UnitTestTree;
+
+  beforeEach(async () => {
+    appTree = await createWorkspace(schematicRunner, appTree);
+  });
+
+  it('should update package.json', () => {
+    const options = { ...defaultOptions };
+
+    const tree = schematicRunner.runSchematic('ng-add', options, appTree);
+    const packageJson = JSON.parse(tree.readContent('/package.json'));
+
+    expect(packageJson.dependencies['@ngrx/component-store']).toBeDefined();
+  });
+
+  it('should skip package.json update', () => {
+    const options = { ...defaultOptions, skipPackageJson: true };
+
+    const tree = schematicRunner.runSchematic('ng-add', options, appTree);
+    const packageJson = JSON.parse(tree.readContent('/package.json'));
+
+    expect(packageJson.dependencies['@ngrx/component-store']).toBeUndefined();
+  });
+});
diff --git a/modules/component-store/schematics/ng-add/index.ts b/modules/component-store/schematics/ng-add/index.ts
new file mode 100644
index 0000000000..b346c43bca
--- /dev/null
+++ b/modules/component-store/schematics/ng-add/index.ts
@@ -0,0 +1,34 @@
+import {
+  Rule,
+  SchematicContext,
+  Tree,
+  chain,
+  noop,
+} from '@angular-devkit/schematics';
+import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
+import {
+  addPackageToPackageJson,
+  platformVersion,
+} from '@ngrx/component-store/schematics-core';
+import { Schema as SchemaOptions } from './schema';
+
+function addModuleToPackageJson() {
+  return (host: Tree, context: SchematicContext) => {
+    addPackageToPackageJson(
+      host,
+      'dependencies',
+      '@ngrx/component-store',
+      platformVersion
+    );
+    context.addTask(new NodePackageInstallTask());
+    return host;
+  };
+}
+
+export default function (options: SchemaOptions): Rule {
+  return (host: Tree, context: SchematicContext) => {
+    return chain([
+      options && options.skipPackageJson ? noop() : addModuleToPackageJson(),
+    ])(host, context);
+  };
+}
diff --git a/modules/component-store/schematics/ng-add/schema.json b/modules/component-store/schematics/ng-add/schema.json
new file mode 100644
index 0000000000..06da1ff5a8
--- /dev/null
+++ b/modules/component-store/schematics/ng-add/schema.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/schema",
+  "id": "SchematicsNgRxComponentStore",
+  "title": "NgRx Component Store Schema",
+  "type": "object",
+  "properties": {
+    "skipPackageJson": {
+      "type": "boolean",
+      "default": false,
+      "description": "Do not add @ngrx/component-store as dependency to package.json (e.g., --skipPackageJson)."
+    }
+  },
+  "required": []
+}
diff --git a/modules/component-store/schematics/ng-add/schema.ts b/modules/component-store/schematics/ng-add/schema.ts
new file mode 100644
index 0000000000..c629d7fae1
--- /dev/null
+++ b/modules/component-store/schematics/ng-add/schema.ts
@@ -0,0 +1,3 @@
+export interface Schema {
+  skipPackageJson?: boolean;
+}
diff --git a/modules/data/package.json b/modules/data/package.json
index a520a8b400..f7d9009564 100644
--- a/modules/data/package.json
+++ b/modules/data/package.json
@@ -36,7 +36,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/modules/effects/package.json b/modules/effects/package.json
index 0d86c213f8..fae1d25add 100644
--- a/modules/effects/package.json
+++ b/modules/effects/package.json
@@ -34,7 +34,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/modules/entity/package.json b/modules/entity/package.json
index c0bb1cd260..1859aa793f 100644
--- a/modules/entity/package.json
+++ b/modules/entity/package.json
@@ -33,7 +33,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/modules/router-store/package.json b/modules/router-store/package.json
index e5267ea436..a052b5011b 100644
--- a/modules/router-store/package.json
+++ b/modules/router-store/package.json
@@ -35,7 +35,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/modules/schematics/package.json b/modules/schematics/package.json
index 6bce48e81b..d303fd4660 100644
--- a/modules/schematics/package.json
+++ b/modules/schematics/package.json
@@ -29,7 +29,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   }
diff --git a/modules/store-devtools/package.json b/modules/store-devtools/package.json
index 9a3ce45198..e1b3cdc9af 100644
--- a/modules/store-devtools/package.json
+++ b/modules/store-devtools/package.json
@@ -32,7 +32,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/modules/store/package.json b/modules/store/package.json
index a05b0dff6e..64d9ba966a 100644
--- a/modules/store/package.json
+++ b/modules/store/package.json
@@ -33,7 +33,8 @@
       "@ngrx/router-store",
       "@ngrx/data",
       "@ngrx/schematics",
-      "@ngrx/store-devtools"
+      "@ngrx/store-devtools",
+      "@ngrx/component-store"
     ],
     "migrations": "./migrations/migration.json"
   },
diff --git a/tsconfig.json b/tsconfig.json
index cc7a086ea6..7cf3218e5f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,6 +18,9 @@
     "paths": {
       "@ngrx/component": ["modules/component"],
       "@ngrx/component-store": ["modules/component-store"],
+      "@ngrx/component-store/schematics-core": [
+        "modules/component-store/schematics-core"
+      ],
       "@ngrx/data": ["./modules/data"],
       "@ngrx/data/schematics-core": ["./modules/data/schematics-core"],
       "@ngrx/effects": ["./modules/effects"],

From cd012f05af5a4b83d9e5ab79c56b5ff9b8a97940 Mon Sep 17 00:00:00 2001
From: timdeschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 29 Jun 2020 17:17:45 +0200
Subject: [PATCH 2/3] build(component-store): add schematic commands

---
 angular.json | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/angular.json b/angular.json
index 7a047fd70d..6da8bf3737 100644
--- a/angular.json
+++ b/angular.json
@@ -579,6 +579,9 @@
             "commands": [
               {
                 "command": "ng run component-store:build-package"
+              },
+              {
+                "command": "yarn tsc -p modules/component-store/tsconfig.schematics.json"
               }
             ]
           }

From c89844a702626b03bd007279e3ffd926be706e9e Mon Sep 17 00:00:00 2001
From: timdeschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Thu, 2 Jul 2020 07:29:58 +0200
Subject: [PATCH 3/3] build(component-store): add schematics-core path

---
 modules/component-store/tsconfig.schematics.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/component-store/tsconfig.schematics.json b/modules/component-store/tsconfig.schematics.json
index c18c7acf71..6ce954388f 100644
--- a/modules/component-store/tsconfig.schematics.json
+++ b/modules/component-store/tsconfig.schematics.json
@@ -7,7 +7,9 @@
     "moduleResolution": "node",
     "downlevelIteration": true,
     "outDir": "../../dist/modules/component-store",
-    "paths": {},
+    "paths": {
+      "@ngrx/component-store/schematics-core": ["./schematics-core"]
+    },
     "sourceMap": true,
     "inlineSources": true,
     "lib": ["es2018", "dom"],