Skip to content

Commit

Permalink
feat(playground): components list (#1106)
Browse files Browse the repository at this point in the history
Closes #1077
  • Loading branch information
yggg authored and nnixaa committed Dec 26, 2018
1 parent 7511bb0 commit 4ab7508
Show file tree
Hide file tree
Showing 19 changed files with 1,926 additions and 15 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"gulp": "gulp",
"firebase": "firebase",
"conventional-changelog": "conventional-changelog",
"start": "run-p \"ng -- serve {@}\" \"gulp -- watch:gen:playground-modules\" -- ",
"prestart": "run-s build:schematics gen:playground",
"start": "run-p \"ng -- serve {@}\" \"gulp -- watch:gen:playground\" -- ",
"start:prod": "npm run start -- --prod --aot",
"start:wp": "npm run start:prod -- playground-wp",
"docs:parse": "gulp docs",
Expand Down Expand Up @@ -71,7 +72,9 @@
"publish": "./scripts/publish.sh",
"cli:firefox-fix": "rimraf node_modules/@angular-devkit/build-angular/node_modules/uglify-es && rimraf node_modules/@angular-devkit/build-angular/node_modules/uglifyjs-webpack-plugin",
"postinstall": "npm run cli:firefox-fix",
"gen:playground-module": "ng g .:playground-module"
"gen:playground": "ng g .:playground",
"gen:playground-module": "ng g .:playground-module",
"gen:playground-components": "ng g .:playground-components"
},
"keywords": [
"angular",
Expand Down
8 changes: 8 additions & 0 deletions schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
"factory": "./new-component/index#newComponent",
"schema": "./new-component/schema.json"
},
"playground": {
"description": "A schematic to call other playground schematics in order.",
"factory": "./playground/index#generatePlayground"
},
"playground-module": {
"description": "A schematic to add all playground components to a given module.",
"factory": "./playground-module/index#playgroundModule"
},
"playground-components": {
"description": "A schematic to generate routes array for the app.",
"factory": "./playground-components/index#playgroundComponents"
}
}
}
173 changes: 173 additions & 0 deletions schematics/playground-components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import * as ts from 'typescript';
import { dirname, join, normalize, Path, PathFragment } from '@angular-devkit/core';
import { DirEntry, SchematicsException, Tree, Rule } from '@angular-devkit/schematics';
import { getSourceFile } from '@angular/cdk/schematics';
import {
addTrailingCommas,
applyReplaceChange,
findDeclarationByIdentifier,
findRoutesArray,
getPlaygroundRootDir,
getRouteChildren,
getRouteComponent,
getRouteLazyModule,
getRoutePath,
getRoutesFromArray,
isComponentRoute,
isLazyRoute,
isRoutingModule,
lazyRoutePathToFilePath,
removePropsQuotes,
singleQuotes,
splitClassName,
trimQuotes,
} from '../utils';

const COMPONENTS_LIST_FILE_PATH = normalize('/src/app/playground-components.ts');
const COMPONENTS_VARIABLE_NAME = 'PLAYGROUND_COMPONENTS';

interface ComponentLink {
path: string;
name?: string;
component?: string;
link?: any[] | string;
children?: ComponentLink[];
}

export function playgroundComponents(): Rule {
return generateComponentsList;
}

function generateComponentsList(tree: Tree): void {
const componentsListFile = getSourceFile(tree, COMPONENTS_LIST_FILE_PATH);
const componentsListArray = getComponentsListArray(componentsListFile);
const routes = removeRoutesWithoutPath(findRoutesInDir(tree, getPlaygroundRootDir(tree)));
updateComponentsFile(tree, componentsListFile, componentsListArray, routes);
}

function getComponentsListArray(fileSource: ts.SourceFile): ts.ArrayLiteralExpression {
const listDeclaration = findDeclarationByIdentifier(fileSource, COMPONENTS_VARIABLE_NAME);
if (!listDeclaration) {
throw new SchematicsException(`Error in ${COMPONENTS_LIST_FILE_PATH}. Can't find components list variable.`);
}
const initializer = (listDeclaration as ts.VariableDeclaration).initializer;
if (!initializer || initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
throw new SchematicsException(`Error in ${COMPONENTS_LIST_FILE_PATH}. List should be initialized with array.`);
}

return initializer as ts.ArrayLiteralExpression;
}

function findRoutesInDir(tree: Tree, dir: DirEntry): ComponentLink[] {
const routingModuleFile = dir.subfiles.find(isRoutingModule);
if (routingModuleFile) {
const routingModulePath = join(dir.path, routingModuleFile);
const routes = getRoutesFromArray(findRoutesArray(tree, routingModulePath));
return parseRoutes(tree, dir, routes);
}
return [];
}

function parseRoutes(tree: Tree, dir: DirEntry, routeEntries: ts.ObjectLiteralExpression[]): ComponentLink[] {
const foundRoutes: ComponentLink[] = [];
const routesWithPath = routeEntries
.filter(r => r.properties.length > 0)
.filter(r => !!getRoutePath(r))
.filter(r => isLazyRoute(r) || isComponentRoute(r));

for (const route of routesWithPath) {
const component = getComponentRoute(route);
const routeChildren = getChildRoutes(tree, dir, route);
const lazyChildren = getLazyModuleRoutes(tree, dir, route);
const children = routeChildren.concat(lazyChildren);
const routePath = trimQuotes((getRoutePath(route) as ts.PropertyAssignment).initializer.getText());
foundRoutes.push({ path: routePath, component, children });
}

return foundRoutes;
}

function getComponentRoute(route: ts.ObjectLiteralExpression): string | undefined {
const componentProp = getRouteComponent(route);
if (componentProp) {
return componentProp.initializer.getText();
}
}

function getChildRoutes(
tree: Tree,
routingModuleDir: DirEntry,
route: ts.ObjectLiteralExpression,
): ComponentLink[] {
const childrenProp = getRouteChildren(route);
if (childrenProp) {
return parseRoutes(tree, routingModuleDir, getRoutesFromArray(childrenProp));
}
return [];
}

function getLazyModuleRoutes(
tree: Tree,
routingModuleDir: DirEntry,
route: ts.ObjectLiteralExpression,
): ComponentLink[] {
const lazyModule = getRouteLazyModule(route);
if (lazyModule) {
const lazyModulePath = lazyModule && trimQuotes(lazyModule.initializer.getText());
const moduleDirPath = dirname(lazyRoutePathToFilePath(lazyModulePath) as Path) as PathFragment;
return findRoutesInDir(tree, routingModuleDir.dir(moduleDirPath));
}
return [];
}

function removeRoutesWithoutPath(routes: ComponentLink[], startPath: string = ''): ComponentLink[] {
const routesWithPath: ComponentLink[] = [];

for (const { path, component, children } of routes) {
const fullPath = path ? startPath + '/' + path : startPath;
let routeChildren;
if (children) {
routeChildren = removeRoutesWithoutPath(children, fullPath);
}

const toAdd: ComponentLink[] = [];
if (path) {
const link = component ? fullPath : undefined;
const name = component ? splitClassName(component) : undefined;
const childRoutes = routeChildren && routeChildren.length ? routeChildren : undefined;
toAdd.push({ path, link, component, name, children: childRoutes });
} else if (routeChildren) {
toAdd.push(...routeChildren);
}

routesWithPath.push(...toAdd);
}

return routesWithPath;
}

function updateComponentsFile(
tree: Tree,
componentsListFile: ts.SourceFile,
list: ts.ArrayLiteralExpression,
routes: ComponentLink[],
) {
const pos = list.getFullStart();
const endPos = pos + list.getFullText().length;
const oldText = componentsListFile.getFullText().slice(pos, endPos);
const newText = generateRoutesString(routes);

applyReplaceChange(tree, COMPONENTS_LIST_FILE_PATH, { pos, oldText, newText });
}

function generateRoutesString(routes: ComponentLink[]): string {
const jsonRoutes = JSON.stringify(routes, null, 2);

return ` ${addTrailingCommas(removePropsQuotes(singleQuotes(jsonRoutes)))}`;
}
7 changes: 7 additions & 0 deletions schematics/playground-components/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "http:https://json-schema.org/schema",
"id": "playground-components",
"type": "object",
"properties": {
}
}
8 changes: 8 additions & 0 deletions schematics/playground/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Rule, chain, schematic } from '@angular-devkit/schematics';

export function generatePlayground(): Rule {
return chain([
schematic('playground-module', {}),
schematic('playground-components', {}),
]);
}
5 changes: 5 additions & 0 deletions schematics/playground/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http:https://json-schema.org/schema",
"id": "playground",
"type": "object"
}
1 change: 1 addition & 0 deletions schematics/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './playground';
export * from './path';
export * from './routing';
export * from './change';
export * from './strings';
8 changes: 7 additions & 1 deletion schematics/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { parse } from 'path';
import { NormalizedSep, Path, relative, dirname, join, normalize, basename } from '@angular-devkit/core';
import { NormalizedSep, Path, relative, dirname, join, normalize, basename, PathFragment } from '@angular-devkit/core';

export function removeExtension(filePath: Path): string {
return parse(filePath).name;
Expand Down Expand Up @@ -39,3 +39,9 @@ export function importPath(from: Path, to: Path): string {

return generateCurrentDirImport(join(relativePath, basename(to)));
}

export function lazyRoutePathToFilePath(lazyRoutePath: string): string {
const path = lazyRoutePath.slice(0, lazyRoutePath.indexOf('#'));

return path + '.ts' as PathFragment;
}
8 changes: 8 additions & 0 deletions schematics/utils/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,11 @@ export function rootRoutePredicate(modulePath: Path): RoutePredicate {

return (route: ts.ObjectLiteralExpression) => lazyModulePredicate(lazyModulePath, route);
}

export function isLazyRoute(route: ts.ObjectLiteralExpression): boolean {
return !!getRouteLazyModule(route);
}

export function isComponentRoute(route: ts.ObjectLiteralExpression): boolean {
return !!getRouteComponent(route);
}
49 changes: 49 additions & 0 deletions schematics/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const QUOTES = [ `'`, `"`, '`' ];

export function trimQuotes(stringLiteral: string): string {
if (stringLiteral.length === 0) {
return stringLiteral;
}

let resultingString = stringLiteral;

if (QUOTES.includes(resultingString[0])) {
resultingString = resultingString.slice(1);
}
if (QUOTES.includes(resultingString[resultingString.length - 1])) {
resultingString = resultingString.slice(0, resultingString.length - 1);
}

return resultingString;
}

const COMPONENT_SUFFIX = 'Component';

/**
* Splits string in words by capital letters. Also removes 'Component' suffix.
*/
export function splitClassName(className: string): string {
const withoutSuffix = className.endsWith(COMPONENT_SUFFIX)
? className.replace(COMPONENT_SUFFIX, '')
: className;

return withoutSuffix.replace(/([a-z])([A-Z])/g, '$1 $2');
}

export function singleQuotes(json: string) {
return json.replace(/\"/gm, `'`);
}

export function addTrailingCommas(json: string): string {
let withCommas = json.replace(/(['}\]])$/gm, '$1,');

if (withCommas.endsWith(',')) {
withCommas = withCommas.slice(0, withCommas.length - 1);
}

return withCommas;
}

export function removePropsQuotes(json: string): string {
return json.replace(/^(\s*)['"](\S+)['"]:/gm, '$1$2:');
}
2 changes: 1 addition & 1 deletion scripts/gulp/gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import './tasks/bundle/bundle';
import './tasks/docs/docs';
import './tasks/copy-sources';
import './tasks/bump-versions';
import './tasks/playground/playground';
import './tasks/playground/playground-schematic';

task('default', ['copy-sources']);
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ import { watch } from 'chokidar';
import { PLAYGROUND_ROOT } from '../config';

const PG_GLOB = PLAYGROUND_ROOT + '**/*.ts';
const DEBOUNCE_TIME = 3000;

function debounce(callback, delay: number = DEBOUNCE_TIME) {
let timeoutId;
return function debounced() {
clearTimeout(timeoutId);
timeoutId = setTimeout(callback, delay);
}
}

function startWatch() {
const watcher = watch(PG_GLOB, { awaitWriteFinish: true, ignoreInitial: true });
const cb = stopWatchRunSchematic.bind(null, watcher);
watcher.on('add', cb);
watcher.on('change', cb);
const watcher = watch(PG_GLOB, { ignoreInitial: true });
const debouncedSchematic = debounce(() => stopWatchRunSchematic(watcher));
watcher.on('add', debouncedSchematic);
watcher.on('change', debouncedSchematic);
}

function stopWatchRunSchematic(watcher) {
watcher.close();
exec('npm run gen:playground-module', logAndRestart);
exec('npm run gen:playground', logAndRestart);
}

function logAndRestart(error: Error, stdout: string, stderr: string): void {
Expand All @@ -31,4 +40,4 @@ function logAndRestart(error: Error, stdout: string, stderr: string): void {
startWatch();
}

task('watch:gen:playground-modules', startWatch);
task('watch:gen:playground', startWatch);
Loading

0 comments on commit 4ab7508

Please sign in to comment.