Skip to content

Commit

Permalink
feat(Codegen): Add base code and build for @ngrx/codegen (#534)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeRyanDev authored and brandonroberts committed Oct 31, 2017
1 parent 88f672c commit 2a22211
Show file tree
Hide file tree
Showing 27 changed files with 556 additions and 13 deletions.
4 changes: 4 additions & 0 deletions build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ export const packages: PackageDescription[] = [
name: 'entity',
hasTestingModule: false,
},
{
name: 'codegen',
hasTestingModule: false,
},
];
6 changes: 6 additions & 0 deletions modules/codegen/README.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions modules/codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* DO NOT EDIT
*
* This file is automatically generated at build
*/

export * from './public_api';
21 changes: 21 additions & 0 deletions modules/codegen/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions modules/codegen/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/index';
10 changes: 10 additions & 0 deletions modules/codegen/rollup.config.js
Original file line number Diff line number Diff line change
@@ -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: {

}
}
61 changes: 61 additions & 0 deletions modules/codegen/src/action-interface.ts
Original file line number Diff line number Diff line change
@@ -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}`
);
66 changes: 66 additions & 0 deletions modules/codegen/src/codegen.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
65 changes: 65 additions & 0 deletions modules/codegen/src/collect-metadata.ts
Original file line number Diff line number Diff line change
@@ -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');
}
18 changes: 18 additions & 0 deletions modules/codegen/src/find-files.ts
Original file line number Diff line number Diff line change
@@ -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);
}
);
});
}
1 change: 1 addition & 0 deletions modules/codegen/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { codegen } from './codegen';
11 changes: 11 additions & 0 deletions modules/codegen/src/metadata/get-optional-properties.ts
Original file line number Diff line number Diff line change
@@ -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,
}));
}
7 changes: 7 additions & 0 deletions modules/codegen/src/metadata/get-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as ts from 'typescript';

export function getProperties(
node: ts.InterfaceDeclaration
): ts.PropertySignature[] {
return node.members.filter(ts.isPropertySignature);
}
14 changes: 14 additions & 0 deletions modules/codegen/src/metadata/get-required-properties.ts
Original file line number Diff line number Diff line change
@@ -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');
}
20 changes: 20 additions & 0 deletions modules/codegen/src/metadata/get-type.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 6 additions & 0 deletions modules/codegen/src/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -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';
21 changes: 21 additions & 0 deletions modules/codegen/src/metadata/is-action-descendent.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions modules/codegen/src/metadata/is-exported.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 2a22211

Please sign in to comment.