From 2a22211d5948d033df108cd71223ba7a69536ee8 Mon Sep 17 00:00:00 2001
From: Mike Ryan <mike.ryan52@gmail.com>
Date: Tue, 31 Oct 2017 09:11:03 -0500
Subject: [PATCH] feat(Codegen): Add base code and build for @ngrx/codegen
 (#534)

---
 build/config.ts                               |  4 ++
 modules/codegen/README.md                     |  6 ++
 modules/codegen/index.ts                      |  7 ++
 modules/codegen/package.json                  | 21 ++++++
 modules/codegen/public_api.ts                 |  1 +
 modules/codegen/rollup.config.js              | 10 +++
 modules/codegen/src/action-interface.ts       | 61 +++++++++++++++++
 modules/codegen/src/codegen.ts                | 66 +++++++++++++++++++
 modules/codegen/src/collect-metadata.ts       | 65 ++++++++++++++++++
 modules/codegen/src/find-files.ts             | 18 +++++
 modules/codegen/src/index.ts                  |  1 +
 .../src/metadata/get-optional-properties.ts   | 11 ++++
 .../codegen/src/metadata/get-properties.ts    |  7 ++
 .../src/metadata/get-required-properties.ts   | 14 ++++
 modules/codegen/src/metadata/get-type.ts      | 20 ++++++
 modules/codegen/src/metadata/index.ts         |  6 ++
 .../src/metadata/is-action-descendent.ts      | 21 ++++++
 modules/codegen/src/metadata/is-exported.ts   | 13 ++++
 .../printers/action-factory-declaration.ts    | 52 +++++++++++++++
 .../codegen/src/printers/enum-declaration.ts  | 25 +++++++
 .../src/printers/import-declaration.ts        | 23 +++++++
 modules/codegen/src/printers/index.ts         |  5 ++
 .../printers/type-dictionary-declaration.ts   | 29 ++++++++
 .../src/printers/type-union-declaration.ts    | 22 +++++++
 modules/codegen/tsconfig-build.json           | 28 ++++++++
 package.json                                  | 13 ++--
 yarn.lock                                     | 20 +++---
 27 files changed, 556 insertions(+), 13 deletions(-)
 create mode 100644 modules/codegen/README.md
 create mode 100644 modules/codegen/index.ts
 create mode 100644 modules/codegen/package.json
 create mode 100644 modules/codegen/public_api.ts
 create mode 100644 modules/codegen/rollup.config.js
 create mode 100644 modules/codegen/src/action-interface.ts
 create mode 100644 modules/codegen/src/codegen.ts
 create mode 100644 modules/codegen/src/collect-metadata.ts
 create mode 100644 modules/codegen/src/find-files.ts
 create mode 100644 modules/codegen/src/index.ts
 create mode 100644 modules/codegen/src/metadata/get-optional-properties.ts
 create mode 100644 modules/codegen/src/metadata/get-properties.ts
 create mode 100644 modules/codegen/src/metadata/get-required-properties.ts
 create mode 100644 modules/codegen/src/metadata/get-type.ts
 create mode 100644 modules/codegen/src/metadata/index.ts
 create mode 100644 modules/codegen/src/metadata/is-action-descendent.ts
 create mode 100644 modules/codegen/src/metadata/is-exported.ts
 create mode 100644 modules/codegen/src/printers/action-factory-declaration.ts
 create mode 100644 modules/codegen/src/printers/enum-declaration.ts
 create mode 100644 modules/codegen/src/printers/import-declaration.ts
 create mode 100644 modules/codegen/src/printers/index.ts
 create mode 100644 modules/codegen/src/printers/type-dictionary-declaration.ts
 create mode 100644 modules/codegen/src/printers/type-union-declaration.ts
 create mode 100644 modules/codegen/tsconfig-build.json

diff --git a/build/config.ts b/build/config.ts
index fdda5e781c..18e1c06a6f 100644
--- a/build/config.ts
+++ b/build/config.ts
@@ -29,4 +29,8 @@ export const packages: PackageDescription[] = [
     name: 'entity',
     hasTestingModule: false,
   },
+  {
+    name: 'codegen',
+    hasTestingModule: false,
+  },
 ];
diff --git a/modules/codegen/README.md b/modules/codegen/README.md
new file mode 100644
index 0000000000..6f552543d4
--- /dev/null
+++ b/modules/codegen/README.md
@@ -0,0 +1,6 @@
+@ngrx/codegen
+=======
+
+The sources for this package are in the main [ngrx/platform](https://github.com/ngrx/platform) repo. Please file issues and pull requests against that repo.
+
+License: MIT
diff --git a/modules/codegen/index.ts b/modules/codegen/index.ts
new file mode 100644
index 0000000000..637e1cf2bf
--- /dev/null
+++ b/modules/codegen/index.ts
@@ -0,0 +1,7 @@
+/**
+ * DO NOT EDIT
+ *
+ * This file is automatically generated at build
+ */
+
+export * from './public_api';
diff --git a/modules/codegen/package.json b/modules/codegen/package.json
new file mode 100644
index 0000000000..3d21f2209c
--- /dev/null
+++ b/modules/codegen/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "@ngrx/codegen",
+  "version": "4.1.0",
+  "description": "Codegen for Ngrx and Redux actions",
+  "module": "@ngrx/codegen.es5.js",
+  "es2015": "@ngrx/codegen.js",
+  "main": "bundles/codegen.umd.js",
+  "typings": "codegen.d.ts",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/ngrx/platform.git"
+  },
+  "authors": ["Mike Ryan"],
+  "license": "MIT",
+  "dependencies": {
+    "glob": "^7.1.2",
+    "lodash": "^4.17.4",
+    "ora": "^1.3.0",
+    "typescript": "^2.4.0"
+  }
+}
diff --git a/modules/codegen/public_api.ts b/modules/codegen/public_api.ts
new file mode 100644
index 0000000000..cba1843545
--- /dev/null
+++ b/modules/codegen/public_api.ts
@@ -0,0 +1 @@
+export * from './src/index';
diff --git a/modules/codegen/rollup.config.js b/modules/codegen/rollup.config.js
new file mode 100644
index 0000000000..df9a013fae
--- /dev/null
+++ b/modules/codegen/rollup.config.js
@@ -0,0 +1,10 @@
+export default {
+  entry: './dist/codegen/@ngrx/codegen.es5.js',
+  dest: './dist/codegen/bundles/codegen.umd.js',
+  format: 'umd',
+  exports: 'named',
+  moduleName: 'ngrx.codegen',
+  globals: {
+    
+  }
+}
diff --git a/modules/codegen/src/action-interface.ts b/modules/codegen/src/action-interface.ts
new file mode 100644
index 0000000000..66835a4a80
--- /dev/null
+++ b/modules/codegen/src/action-interface.ts
@@ -0,0 +1,61 @@
+import * as _ from 'lodash';
+
+export interface ActionInterfaceProperty {
+  name: string;
+  required: boolean;
+}
+
+export interface ActionInterface {
+  name: string;
+  actionType: string;
+  properties: ActionInterfaceProperty[];
+}
+
+const actionTypeRegex = new RegExp(/\[(.*?)\](.*)/);
+function parseActionType(type: string) {
+  const result = actionTypeRegex.exec(type);
+
+  if (result === null) {
+    throw new Error(`Could not parse action type "${type}"`);
+  }
+
+  return {
+    category: result[1] as string,
+    name: result[2] as string,
+  };
+}
+
+export const getActionType = (enterface: ActionInterface) =>
+  enterface.actionType;
+export const getActionName = (enterface: ActionInterface) => enterface.name;
+export const getActionCategory = _.flow(
+  getActionType,
+  parseActionType,
+  v => v.category
+);
+export const getActionCategoryToken = _.flow(
+  getActionCategory,
+  _.camelCase,
+  _.upperFirst
+);
+export const getActionEnumName = _.flow(
+  getActionCategoryToken,
+  v => `${v}ActionType`
+);
+export const getActionEnumPropName = _.flow(getActionName, _.snakeCase, v =>
+  v.toUpperCase()
+);
+export const getActionUnionName = _.flow(
+  getActionCategoryToken,
+  v => `${v}Actions`
+);
+export const getActionLookupName = _.flow(
+  getActionCategoryToken,
+  v => `${v}ActionLookup`
+);
+export const getActionFactoryName = _.flow(
+  getActionName,
+  _.camelCase,
+  _.upperFirst,
+  v => `create${v}`
+);
diff --git a/modules/codegen/src/codegen.ts b/modules/codegen/src/codegen.ts
new file mode 100644
index 0000000000..9f8c219456
--- /dev/null
+++ b/modules/codegen/src/codegen.ts
@@ -0,0 +1,66 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as ts from 'typescript';
+import { collectMetadata, printActionFactory } from './collect-metadata';
+import { findFiles } from './find-files';
+const ora = require('ora');
+
+async function readFile(file: string): Promise<string> {
+  return new Promise<string>((resolve, reject) => {
+    fs.readFile(file, 'utf8', (error, data) => {
+      if (error) {
+        reject(error);
+      } else {
+        resolve(data);
+      }
+    });
+  });
+}
+
+async function writeFile(file: string, contents: string): Promise<any> {
+  return new Promise((resolve, reject) => {
+    fs.writeFile(file, contents, { encoding: 'utf8' }, error => {
+      if (error) {
+        reject(error);
+      } else {
+        resolve();
+      }
+    });
+  });
+}
+
+function createSourceFile(data: string) {
+  return ts.createSourceFile('', data, ts.ScriptTarget.ES2015, true);
+}
+
+export async function codegen(glob: string) {
+  const filesIndicator = ora(`Searching for files matching "${glob}"`).start();
+  const files = await findFiles(glob);
+  filesIndicator.succeed(`Found ${files.length} files for pattern "${glob}"`);
+
+  for (let file of files) {
+    const indicator = ora(file).start();
+
+    try {
+      const parsedPath = path.parse(file);
+      const contents = await readFile(file);
+      const sourceFile = createSourceFile(contents);
+      const ast = collectMetadata(parsedPath.name, sourceFile);
+
+      if (!ast) {
+        throw new Error(`No actions found for file "${file}"`);
+      }
+
+      const output = printActionFactory(ast);
+      const target = path.resolve(
+        parsedPath.dir,
+        `./${parsedPath.name}.helpers.ts`
+      );
+      await writeFile(target, output);
+
+      indicator.succeed(`Found ${ast.length} actions in ${file}`);
+    } catch (e) {
+      indicator.fail((e as Error).message);
+    }
+  }
+}
diff --git a/modules/codegen/src/collect-metadata.ts b/modules/codegen/src/collect-metadata.ts
new file mode 100644
index 0000000000..40eae50a04
--- /dev/null
+++ b/modules/codegen/src/collect-metadata.ts
@@ -0,0 +1,65 @@
+import * as ts from 'typescript';
+import * as _ from 'lodash';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as collector from './metadata/index';
+import * as printers from './printers/index';
+import { ActionInterface } from './action-interface';
+
+export interface ActionMetadata {
+  name: string;
+  type: string;
+  properties: { name: string; optional: boolean }[];
+}
+
+export function collectMetadata(
+  fileName: string,
+  sourceFile: ts.SourceFile
+): ts.Node[] | undefined {
+  const interfaces = sourceFile.statements
+    .filter(ts.isInterfaceDeclaration)
+    .filter(collector.isExported)
+    .filter(collector.isActionDescendent)
+    .filter(m => !!collector.getType(m))
+    .map((enterface): ActionInterface => ({
+      name: enterface.name.getText(),
+      actionType: _.trim(
+        collector.getType(enterface)!.literal.getFullText(),
+        ' \'"`'
+      ),
+      properties: [
+        ...collector.getRequiredProperties(collector.getProperties(enterface)),
+        ...collector.getOptionalProperties(collector.getProperties(enterface)),
+      ],
+    }));
+
+  if (interfaces.length === 0) {
+    undefined;
+  }
+
+  return [
+    printers.printImportDeclaration(fileName, interfaces),
+    printers.printEnumDeclaration(interfaces),
+    printers.printTypeUnionDeclaration(interfaces),
+    printers.printTypeDictionaryDeclaration(interfaces),
+    ...interfaces.map(action => printers.printActionFactoryDeclaration(action)),
+  ];
+}
+
+export function printActionFactory(ast: ts.Node[]) {
+  const resultFile = ts.createSourceFile(
+    '',
+    '',
+    ts.ScriptTarget.ES2015,
+    false,
+    ts.ScriptKind.TS
+  );
+
+  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
+
+  return ast
+    .map(statement =>
+      printer.printNode(ts.EmitHint.Unspecified, statement, resultFile)
+    )
+    .join('\n\n');
+}
diff --git a/modules/codegen/src/find-files.ts b/modules/codegen/src/find-files.ts
new file mode 100644
index 0000000000..51b35f87e8
--- /dev/null
+++ b/modules/codegen/src/find-files.ts
@@ -0,0 +1,18 @@
+import * as path from 'path';
+const glob = require('glob');
+
+export function findFiles(globPattern: string): Promise<string[]> {
+  return new Promise((resolve, reject) => {
+    glob(
+      globPattern,
+      { cwd: process.cwd(), ignore: ['**/node_modules/**'] },
+      (error: any, files: string[]) => {
+        if (error) {
+          return reject(error);
+        }
+
+        resolve(files);
+      }
+    );
+  });
+}
diff --git a/modules/codegen/src/index.ts b/modules/codegen/src/index.ts
new file mode 100644
index 0000000000..bde140c125
--- /dev/null
+++ b/modules/codegen/src/index.ts
@@ -0,0 +1 @@
+export { codegen } from './codegen';
diff --git a/modules/codegen/src/metadata/get-optional-properties.ts b/modules/codegen/src/metadata/get-optional-properties.ts
new file mode 100644
index 0000000000..9e53e1212b
--- /dev/null
+++ b/modules/codegen/src/metadata/get-optional-properties.ts
@@ -0,0 +1,11 @@
+import * as ts from 'typescript';
+import { ActionInterfaceProperty } from '../action-interface';
+
+export function getOptionalProperties(
+  props: ts.PropertySignature[]
+): ActionInterfaceProperty[] {
+  return props.filter(prop => prop.questionToken).map(prop => ({
+    name: prop.name.getText(),
+    required: false,
+  }));
+}
diff --git a/modules/codegen/src/metadata/get-properties.ts b/modules/codegen/src/metadata/get-properties.ts
new file mode 100644
index 0000000000..1aa7ee9963
--- /dev/null
+++ b/modules/codegen/src/metadata/get-properties.ts
@@ -0,0 +1,7 @@
+import * as ts from 'typescript';
+
+export function getProperties(
+  node: ts.InterfaceDeclaration
+): ts.PropertySignature[] {
+  return node.members.filter(ts.isPropertySignature);
+}
diff --git a/modules/codegen/src/metadata/get-required-properties.ts b/modules/codegen/src/metadata/get-required-properties.ts
new file mode 100644
index 0000000000..a2ad0a6aaf
--- /dev/null
+++ b/modules/codegen/src/metadata/get-required-properties.ts
@@ -0,0 +1,14 @@
+import * as ts from 'typescript';
+import { ActionInterfaceProperty } from '../action-interface';
+
+export function getRequiredProperties(
+  props: ts.PropertySignature[]
+): ActionInterfaceProperty[] {
+  return props
+    .filter(prop => !prop.questionToken)
+    .map(prop => ({
+      name: prop.name.getText(),
+      required: true,
+    }))
+    .filter(({ name }) => name !== 'type');
+}
diff --git a/modules/codegen/src/metadata/get-type.ts b/modules/codegen/src/metadata/get-type.ts
new file mode 100644
index 0000000000..18b10a9311
--- /dev/null
+++ b/modules/codegen/src/metadata/get-type.ts
@@ -0,0 +1,20 @@
+import * as ts from 'typescript';
+import { getProperties } from './get-properties';
+
+export function getType(
+  action: ts.InterfaceDeclaration
+): ts.LiteralTypeNode | undefined {
+  const typeProperty = getProperties(action).find(
+    property => property.name.getText() === 'type'
+  );
+
+  if (!typeProperty) {
+    return undefined;
+  }
+
+  return ts.isLiteralTypeNode(typeProperty.type as any)
+    ? typeProperty.type as any
+    : undefined;
+
+  // return !!typeProperty && ts.isLiteralTypeNode(typeProperty.type) ? typeProperty.type : undefined;
+}
diff --git a/modules/codegen/src/metadata/index.ts b/modules/codegen/src/metadata/index.ts
new file mode 100644
index 0000000000..5f243ec59a
--- /dev/null
+++ b/modules/codegen/src/metadata/index.ts
@@ -0,0 +1,6 @@
+export * from './get-optional-properties';
+export * from './get-properties';
+export * from './get-required-properties';
+export * from './get-type';
+export * from './is-action-descendent';
+export * from './is-exported';
diff --git a/modules/codegen/src/metadata/is-action-descendent.ts b/modules/codegen/src/metadata/is-action-descendent.ts
new file mode 100644
index 0000000000..392789370d
--- /dev/null
+++ b/modules/codegen/src/metadata/is-action-descendent.ts
@@ -0,0 +1,21 @@
+import * as ts from 'typescript';
+
+export function isActionDescendent(
+  statement: ts.InterfaceDeclaration
+): boolean {
+  const heritageClauses = statement.heritageClauses;
+
+  if (heritageClauses) {
+    return heritageClauses.some(clause => {
+      /**
+       * TODO: This breaks if the interface looks like this:
+       * 
+       *   interface MyAction extends ngrx.Action { }
+       * 
+       */
+      return clause.types.some(type => type.expression.getText() === 'Action');
+    });
+  }
+
+  return false;
+}
diff --git a/modules/codegen/src/metadata/is-exported.ts b/modules/codegen/src/metadata/is-exported.ts
new file mode 100644
index 0000000000..642a92fac1
--- /dev/null
+++ b/modules/codegen/src/metadata/is-exported.ts
@@ -0,0 +1,13 @@
+import * as ts from 'typescript';
+
+function hasExportModifier(node: ts.Node): boolean {
+  return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
+}
+
+function isTopLevel(node: ts.Node): boolean {
+  return !!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile;
+}
+
+export function isExported(node: ts.Node): boolean {
+  return hasExportModifier(node) && isTopLevel(node);
+}
diff --git a/modules/codegen/src/printers/action-factory-declaration.ts b/modules/codegen/src/printers/action-factory-declaration.ts
new file mode 100644
index 0000000000..6e2017e638
--- /dev/null
+++ b/modules/codegen/src/printers/action-factory-declaration.ts
@@ -0,0 +1,52 @@
+import * as ts from 'typescript';
+import {
+  ActionInterface,
+  getActionFactoryName,
+  getActionName,
+  getActionEnumName,
+  getActionEnumPropName,
+} from '../action-interface';
+
+export function printActionFactoryDeclaration(action: ActionInterface) {
+  return ts.createFunctionDeclaration(
+    undefined,
+    [ts.createToken(ts.SyntaxKind.ExportKeyword)],
+    undefined,
+    getActionFactoryName(action),
+    undefined,
+    action.properties.map(({ name, required }) => {
+      return ts.createParameter(
+        undefined,
+        undefined,
+        undefined,
+        name,
+        required ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken),
+        ts.createTypeReferenceNode(
+          `${getActionName(action)}["${name}"]`,
+          undefined
+        ),
+        undefined
+      );
+    }),
+    ts.createTypeReferenceNode(getActionName(action), undefined),
+    ts.createBlock(
+      [
+        ts.createReturn(
+          ts.createObjectLiteral([
+            ts.createPropertyAssignment(
+              'type',
+              ts.createPropertyAccess(
+                ts.createIdentifier(getActionEnumName(action)),
+                ts.createIdentifier(getActionEnumPropName(action))
+              )
+            ),
+            ...action.properties.map(({ name }) => {
+              return ts.createShorthandPropertyAssignment(name, undefined);
+            }),
+          ])
+        ),
+      ],
+      true
+    )
+  );
+}
diff --git a/modules/codegen/src/printers/enum-declaration.ts b/modules/codegen/src/printers/enum-declaration.ts
new file mode 100644
index 0000000000..45e1361b95
--- /dev/null
+++ b/modules/codegen/src/printers/enum-declaration.ts
@@ -0,0 +1,25 @@
+import * as ts from 'typescript';
+import {
+  ActionInterface,
+  getActionEnumName,
+  getActionEnumPropName,
+  getActionType,
+} from '../action-interface';
+
+export function printEnumDeclaration(actions: ActionInterface[]) {
+  const [firstInterface] = actions;
+
+  return ts.createEnumDeclaration(
+    undefined,
+    [ts.createToken(ts.SyntaxKind.ExportKeyword)],
+    getActionEnumName(firstInterface),
+    actions
+      .map(action => ({
+        prop: getActionEnumPropName(action),
+        value: getActionType(action),
+      }))
+      .map(({ prop, value }) => {
+        return ts.createEnumMember(prop, ts.createLiteral(value));
+      })
+  );
+}
diff --git a/modules/codegen/src/printers/import-declaration.ts b/modules/codegen/src/printers/import-declaration.ts
new file mode 100644
index 0000000000..3f43ce419f
--- /dev/null
+++ b/modules/codegen/src/printers/import-declaration.ts
@@ -0,0 +1,23 @@
+import * as ts from 'typescript';
+import { ActionInterface, getActionName } from '../action-interface';
+
+export function printImportDeclaration(
+  filename: string,
+  actions: ActionInterface[]
+) {
+  return ts.createImportDeclaration(
+    undefined,
+    undefined,
+    ts.createImportClause(
+      undefined,
+      ts.createNamedImports(
+        actions
+          .map(getActionName)
+          .map(name =>
+            ts.createImportSpecifier(undefined, ts.createIdentifier(name))
+          )
+      )
+    ),
+    ts.createIdentifier(`'./${filename}'`)
+  );
+}
diff --git a/modules/codegen/src/printers/index.ts b/modules/codegen/src/printers/index.ts
new file mode 100644
index 0000000000..ddd63b1917
--- /dev/null
+++ b/modules/codegen/src/printers/index.ts
@@ -0,0 +1,5 @@
+export * from './action-factory-declaration';
+export * from './enum-declaration';
+export * from './import-declaration';
+export * from './type-dictionary-declaration';
+export * from './type-union-declaration';
diff --git a/modules/codegen/src/printers/type-dictionary-declaration.ts b/modules/codegen/src/printers/type-dictionary-declaration.ts
new file mode 100644
index 0000000000..0f0ca8f9da
--- /dev/null
+++ b/modules/codegen/src/printers/type-dictionary-declaration.ts
@@ -0,0 +1,29 @@
+import * as ts from 'typescript';
+import {
+  ActionInterface,
+  getActionLookupName,
+  getActionType,
+  getActionName,
+} from '../action-interface';
+
+export function printTypeDictionaryDeclaration(actions: ActionInterface[]) {
+  const [firstAction] = actions;
+
+  return ts.createTypeAliasDeclaration(
+    undefined,
+    [ts.createToken(ts.SyntaxKind.ExportKeyword)],
+    getActionLookupName(firstAction),
+    undefined,
+    ts.createTypeLiteralNode(
+      actions.map(action => {
+        return ts.createPropertySignature(
+          undefined,
+          JSON.stringify(getActionType(action)),
+          undefined,
+          ts.createTypeReferenceNode(getActionName(action), undefined),
+          undefined
+        );
+      })
+    )
+  );
+}
diff --git a/modules/codegen/src/printers/type-union-declaration.ts b/modules/codegen/src/printers/type-union-declaration.ts
new file mode 100644
index 0000000000..06e9bfa851
--- /dev/null
+++ b/modules/codegen/src/printers/type-union-declaration.ts
@@ -0,0 +1,22 @@
+import * as ts from 'typescript';
+import {
+  ActionInterface,
+  getActionName,
+  getActionUnionName,
+} from '../action-interface';
+
+export function printTypeUnionDeclaration(actions: ActionInterface[]) {
+  const [firstAction] = actions;
+
+  return ts.createTypeAliasDeclaration(
+    undefined,
+    [ts.createToken(ts.SyntaxKind.ExportKeyword)],
+    getActionUnionName(firstAction),
+    undefined,
+    ts.createUnionTypeNode(
+      actions
+        .map(getActionName)
+        .map(name => ts.createTypeReferenceNode(name, undefined))
+    )
+  );
+}
diff --git a/modules/codegen/tsconfig-build.json b/modules/codegen/tsconfig-build.json
new file mode 100644
index 0000000000..59e1755443
--- /dev/null
+++ b/modules/codegen/tsconfig-build.json
@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "declaration": true,
+    "stripInternal": true,
+    "experimentalDecorators": true,
+    "module": "es2015",
+    "moduleResolution": "node",
+    "outDir": "../../dist/packages/codegen",
+    "paths": { },
+    "rootDir": ".",
+    "sourceMap": true,
+    "inlineSources": true,
+    "target": "es2015",
+    "lib": ["es2015", "dom"],
+    "skipLibCheck": true,
+    "strict": true
+  },
+  "files": [
+    "public_api.ts"
+  ],
+  "angularCompilerOptions": {
+    "annotateForClosureCompiler": true,
+    "strictMetadataEmit": true,
+    "flatModuleOutFile": "index.js",
+    "flatModuleId": "@ngrx/codegen"
+  }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 4a4c5626bf..cbde463e7f 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
     "watch:tests": "chokidar 'modules/**/*.ts' --initial -c 'nyc --reporter=text --reporter=html yarn run test:unit'",
     "postinstall": "opencollective postinstall",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
-    "release": "lerna publish --skip-npm --conventional-commits && npm run build"
+    "release": "lerna publish --skip-npm --conventional-commits && npm run build",
+    "codegen": "ts-node modules/codegen/src/index.ts"
   },
   "engines": {
     "node": ">=6.9.5",
@@ -70,12 +71,13 @@
     "@angular/router": "^4.2.0",
     "@ngrx/db": "^2.0.1",
     "@types/fs-extra": "^2.1.0",
-    "@types/glob": "^5.0.30",
+    "@types/glob": "^5.0.33",
     "@types/jasmine": "2.5.45",
     "@types/jasminewd2": "^2.0.2",
     "@types/jest": "^20.0.2",
+    "@types/lodash": "^4.14.80",
     "@types/node": "^7.0.5",
-    "@types/ora": "^0.3.31",
+    "@types/ora": "^1.3.1",
     "@types/rimraf": "^0.0.28",
     "chokidar": "^1.7.0",
     "chokidar-cli": "^1.2.0",
@@ -86,7 +88,7 @@
     "cpy-cli": "^1.0.1",
     "deep-freeze": "^0.0.1",
     "fs-extra": "^2.1.2",
-    "glob": "^7.1.1",
+    "glob": "^7.1.2",
     "hammerjs": "^2.0.8",
     "husky": "^0.14.3",
     "jasmine": "^2.5.3",
@@ -107,7 +109,7 @@
     "module-alias": "^2.0.0",
     "ngrx-store-freeze": "^0.2.0",
     "nyc": "^10.1.2",
-    "ora": "^1.2.0",
+    "ora": "^1.3.0",
     "prettier": "^1.5.2",
     "protractor": "~5.1.0",
     "reflect-metadata": "^0.1.9",
@@ -124,6 +126,7 @@
   },
   "dependencies": {
     "@angular/cdk": "^2.0.0-beta.8",
+    "lodash": "^4.17.4",
     "opencollective": "^1.0.3"
   },
   "collective": {
diff --git a/yarn.lock b/yarn.lock
index a28e354c7d..f1c7813951 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -180,9 +180,9 @@
   dependencies:
     "@types/node" "*"
 
-"@types/glob@^5.0.30":
-  version "5.0.30"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.30.tgz#1026409c5625a8689074602808d082b2867b8a51"
+"@types/glob@^5.0.33":
+  version "5.0.33"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.33.tgz#3dff7c6ce09d65abe919c7961dc3dee016f36ad7"
   dependencies:
     "@types/minimatch" "*"
     "@types/node" "*"
@@ -201,6 +201,10 @@
   version "20.0.8"
   resolved "https://registry.yarnpkg.com/@types/jest/-/jest-20.0.8.tgz#7f8c97f73d20d3bf5448fbe33661a342002b5954"
 
+"@types/lodash@^4.14.80":
+  version "4.14.80"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.80.tgz#a6b8b7900e6a7dcbc2e90d9b6dfbe3f6a7f69951"
+
 "@types/minimatch@*":
   version "2.0.29"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a"
@@ -213,9 +217,9 @@
   version "6.0.68"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.68.tgz#0c43b6b8b9445feb86a0fbd3457e3f4bc591e66d"
 
-"@types/ora@^0.3.31":
-  version "0.3.31"
-  resolved "https://registry.yarnpkg.com/@types/ora/-/ora-0.3.31.tgz#1a4bf16bd62ec2764b8f40b0e2f4d85c21292f83"
+"@types/ora@^1.3.1":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@types/ora/-/ora-1.3.1.tgz#53db0e10b7ea2f014548e87ea81c755986502e52"
   dependencies:
     "@types/node" "*"
 
@@ -5162,7 +5166,7 @@ ora@^0.2.3:
     cli-spinners "^0.1.2"
     object-assign "^4.0.1"
 
-ora@^1.2.0:
+ora@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/ora/-/ora-1.3.0.tgz#80078dd2b92a934af66a3ad72a5b910694ede51a"
   dependencies:
@@ -7873,4 +7877,4 @@ zone.js@^0.8.12:
 
 zone.js@^0.8.14:
   version "0.8.18"
-  resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.18.tgz#8cecb3977fcd1b3090562ff4570e2847e752b48d"
\ No newline at end of file
+  resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.18.tgz#8cecb3977fcd1b3090562ff4570e2847e752b48d"