Skip to content

Commit

Permalink
feat(schematics): add component store schematics (#2886)
Browse files Browse the repository at this point in the history
Closes #2570
  • Loading branch information
stefanoww committed Mar 8, 2021
1 parent 9b23403 commit f086f80
Show file tree
Hide file tree
Showing 37 changed files with 3,665 additions and 0 deletions.
3 changes: 3 additions & 0 deletions modules/component-store/schematics-core/index.ts
Expand Up @@ -23,6 +23,7 @@ export {
addDeclarationToModule,
addExportToModule,
addImportToModule,
addProviderToComponent,
addProviderToModule,
replaceImport,
containsProperty,
Expand All @@ -42,6 +43,8 @@ export {

export { AppConfig, getWorkspace, getWorkspacePath } from './utility/config';

export { findComponentFromOptions } from './utility/find-component';

export {
findModule,
findModuleFromOptions,
Expand Down
168 changes: 168 additions & 0 deletions modules/component-store/schematics-core/utility/ast-utils.ts
Expand Up @@ -453,6 +453,156 @@ function _addSymbolToNgModuleMetadata(
return [insert, importInsert];
}

function _addSymbolToComponentMetadata(
source: ts.SourceFile,
componentPath: string,
metadataField: string,
symbolName: string,
importPath: string
): Change[] {
const nodes = getDecoratorMetadata(source, 'Component', '@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(
componentPath,
position,
toInsert
);
const newMetadataImport = insertImport(
source,
componentPath,
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 component 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];
}

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(componentPath, position, toInsert);
const importInsert: Change = insertImport(
source,
componentPath,
symbolName.replace(/\..*$/, ''),
importPath
);

return [insert, importInsert];
}

/**
* Custom function to insert a declaration (component, pipe, directive)
* into NgModule declarations. It also imports the component.
Expand Down Expand Up @@ -509,6 +659,24 @@ export function addProviderToModule(
);
}

/**
* Custom function to insert a provider into Component. It also imports it.
*/
export function addProviderToComponent(
source: ts.SourceFile,
componentPath: string,
classifiedName: string,
importPath: string
): Change[] {
return _addSymbolToComponentMetadata(
source,
componentPath,
'providers',
classifiedName,
importPath
);
}

/**
* Custom function to insert an export into NgModule. It also imports it.
*/
Expand Down
145 changes: 145 additions & 0 deletions modules/component-store/schematics-core/utility/find-component.ts
@@ -0,0 +1,145 @@
/**
* @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 ComponentOptions {
component?: string;
name: string;
flat?: boolean;
path?: string;
skipImport?: boolean;
}

/**
* Find the component referred by a set of options passed to the schematics.
*/
export function findComponentFromOptions(
host: Tree,
options: ComponentOptions
): Path | undefined {
if (options.hasOwnProperty('skipImport') && options.skipImport) {
return undefined;
}

if (!options.component) {
const pathToCheck =
(options.path || '') +
(options.flat ? '' : '/' + strings.dasherize(options.name));

return normalize(findComponent(host, pathToCheck));
} else {
const componentPath = normalize(
'/' + options.path + '/' + options.component
);
const componentBaseName = normalize(componentPath).split('/').pop();

if (host.exists(componentPath)) {
return normalize(componentPath);
} else if (host.exists(componentPath + '.ts')) {
return normalize(componentPath + '.ts');
} else if (host.exists(componentPath + '.component.ts')) {
return normalize(componentPath + '.component.ts');
} else if (
host.exists(componentPath + '/' + componentBaseName + '.component.ts')
) {
return normalize(
componentPath + '/' + componentBaseName + '.component.ts'
);
} else {
throw new Error(
`Specified component path ${componentPath} does not exist`
);
}
}
}

/**
* Function to find the "closest" component to a generated file's path.
*/
export function findComponent(host: Tree, generateDir: string): Path {
let dir: DirEntry | null = host.getDir('/' + generateDir);

const componentRe = /\.component\.ts$/;

while (dir) {
const matches = dir.subfiles.filter((p) => componentRe.test(p));

if (matches.length == 1) {
return join(dir.path, matches[0]);
} else if (matches.length > 1) {
throw new Error(
'More than one component matches. Use skip-import option to skip importing ' +
'the component store into the closest component.'
);
}

dir = dir.parent;
}

throw new Error(
'Could not find an Component. Use the skip-import ' +
'option to skip importing in Component.'
);
}

/**
* 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)$/, '') : '';
}
3 changes: 3 additions & 0 deletions modules/component/schematics-core/index.ts
Expand Up @@ -23,6 +23,7 @@ export {
addDeclarationToModule,
addExportToModule,
addImportToModule,
addProviderToComponent,
addProviderToModule,
replaceImport,
containsProperty,
Expand All @@ -42,6 +43,8 @@ export {

export { AppConfig, getWorkspace, getWorkspacePath } from './utility/config';

export { findComponentFromOptions } from './utility/find-component';

export {
findModule,
findModuleFromOptions,
Expand Down

0 comments on commit f086f80

Please sign in to comment.