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 { + return new Promise((resolve, reject) => { + fs.readFile(file, 'utf8', (error, data) => { + if (error) { + reject(error); + } else { + resolve(data); + } + }); + }); +} + +async function writeFile(file: string, contents: string): Promise { + 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 { + 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"