2

I would like to create a custom schematic for Angular that will create a file in the same directory as the schematic was executed from. I followed this article, but I'm not sure how I would get the new file to be added in the desired directory. (https://developer.okta.com/blog/2019/02/13/angular-schematics#create-your-first-schematic)

For example if I have the following directory structure:

src/app/ !-- Angular Root
|- modules
   |_ foo
      |- foo.component.ts
      |_ foo.component.html
|- app.component.ts
|- app.component.html
|_ app.module.ts

If I were do do the following commands:

> cd /src/app/modules/foo
> ng g my-component:my-component

I would like the newly created/updated files to be inside the /src/app/modules/foo directory instead of the root directory. Think of how ng generate works. If I execute ng g component bar from inside the directory /src/app/modules/foo then a new component will be generated inside that directory. This is the behavior I need to replicate.

Here's my factory. Right now it's obviously targeting the root directory with project.root however there isn't any alternative that I've found, and if I don't provide a path then I get an error. How do I get the current path (src/app/modules/foo) to store in options.path?

export function setupOptions(host: Tree, options: any): Tree {
  const workspace = getWorkspace(host);
  if (!options.project) {
    options.project = Object.keys(workspace.projects)[0];
  }
  const project = workspace.projects[options.project];
  options.path = join(normalize(project.root), 'src');
  return host;
}

// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function myComponent(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    setupOptions(tree, _options);
    const movePath = normalize(_options.path + '/');
    const templateSource = apply(url('./files/src'), [
      template({ ..._options }),
      move(movePath),
      forEach((fileEntry: FileEntry) => {
        if (tree.exists(fileEntry.path)) {
          tree.overwrite(fileEntry.path, fileEntry.content);
        }
        return fileEntry;
      })
    ]);
    const rule = mergeWith(templateSource, MergeStrategy.Overwrite);
    return rule(tree, _context);
  };
}
efarley
  • 8,371
  • 12
  • 42
  • 65

4 Answers4

1

Add this to your schema.json

{
 ...,
 "properties": {
  ..
  "path": {
   "type": "string",
   "format": "path",
   "description": "The path to create the simple schematic within.",
   "visible": false
  }
Chris Gibb
  • 845
  • 8
  • 11
1

Follow the steps mentioned in the angular docs

You will run into a bit of issues. For example

1) const workspaceConfig = tree.read('/angular.json');

// will be null when using the 'schematics' command but will work when using the 'ng g' commad.

2) Similarly 'options.path' will be undefined when using the 'schematics' command but will work when using the 'ng g' command.

The above reply is correct, you need to add path to schema.json file and then in your function

'export function myComponent(_options: any): Rule {'

you should be able to use 'options.path' to get the current location. However as I mentioned I was not able to get it to work when using the 'schematics' command. I was only able to get it to work when using the 'ng g' command.

So as an example here are my files

1) ..schematics/ng-generate/customComponent/schema.json

{
    "$schema": "http://json-schema.org/schema",
    "id": "GenerateCustomComponent",
    "title": "Generate Custom Component",
    "type": "object",
    "properties": {
        "name": {
            "description": "The name for the custom component.",
            "type": "string",
            "x-prompt": "What is the name for the custom component?"
        },
        "path": {
            "type": "string",
            "format": "path",
            "description": "The path at which to create the component file, relative to the current workspace. Default is a folder with the same name as the component in the project root.",
            "visible": false
        }
    },
    "required": [
        "name"
    ]
}

2) ..schematics/ng-generate/customComponent/schema.ts

import { Schema as ComponentSChema } from '@schematics/angular/component/schema';

export interface Schema extends ComponentSChema {
    // The name of the custom component
    name: string;
}

2) ..schematics/ng-generate/customComponent/index.ts

import {
  Rule, Tree, SchematicsException,
  apply, url, applyTemplates, move,
  chain, mergeWith
} from '@angular-devkit/schematics';

import { strings, experimental, normalize } from '@angular-devkit/core';

import { Schema as CustomSchema } from './schema';

export function generate(options: CustomSchema): Rule {
    return (tree: Tree) => {
        const workspaceConfig = tree.read('/angular.json'); // will return null when using schematics command but will work when using ng g
        console.log('workspaceConfig::', workspaceConfig);
        console.log('path:', options.path); // will be undefined when using schematics command but will work when using ng g
        
        // from now following along with angular docs with slight modifications. 
        if (workspaceConfig && !options.path) {
            const workspaceContent = workspaceConfig.toString();
            console.log('workspaceContent::', workspaceContent);
            
            const workspace: experimental.workspace.WorkspaceSchema = JSON.parse(workspaceContent);
            console.log('workspace', workspace);
            
            options.project = workspace.defaultProject;
            const projectName = options.project as string;
            const project = workspace.projects[projectName];
            const projectType = project.projectType === 'application' ? 'app' : 'lib';
            console.log('projectType::', projectType);
            
            options.path = `${project.sourceRoot}/${projectType}`;
        }

        
        if (options.path) { 
           // this will be used by the ng g command
            const templateSource = apply(url('./files'), [
                applyTemplates({
                    classify: strings.classify,
                    dasherize: strings.dasherize,
                    name: options.name
                }),
                move(normalize(options.path as string))
            ]);
            return chain([
                mergeWith(templateSource)
            ]);
        } else {
            // this will be used by the schematics command
            const templateSource = apply(url('./files'), [
                applyTemplates({
                    classify: strings.classify,
                    dasherize: strings.dasherize,
                    name: options.name
                })
            ]);
            return chain([
                mergeWith(templateSource)
            ]);
        }
    };
}
ecosystem31
  • 276
  • 2
  • 4
0

Something that was confusing me for a while and to watch out for.

'options.path' will be undefined when executing a schematics command in the Root of the angular project. Only if you have changed directory in to the sub directory of the root, will the path be available.

codeThinker123
  • 664
  • 1
  • 6
  • 16
0

may be this is the ugly solution but the only way i found it .

you must change the root directory of tree to access the parent of current directory

const currentPath = (tree as any)['_backend']['_root'] ;
(tree as any)['_backend']['_root'] = '/' ;

const currentDir = tree.getDir(currentPath);

const parent = currentDir.parent ;

know you can access to parent directory and can write own function to change every things

see sample

const rootModelDirectory  = 'model-form-request'


export function model(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const currentPath = (tree as any)['_backend']['_root'] ;
    (tree as any)['_backend']['_root'] = '/' ; 
    console.log(currentPath);
    const rootdir = findParentDirectoryPath(tree.getDir(currentPath) , rootModelDirectory);
    const templateSource = apply(url('./files'), [
    // filter(path => path.endsWith('__name@dasherize__.module.ts.template')),
    template({
      ...strings,
      ..._options
    }),
    move(normalize(`/${rootdir}/${dasherize(_options.name)}/`))
  ]);
  return mergeWith(templateSource);
  };
}

export function findParentDirectoryPath(location :ReturnType<Tree['getDir']>, directoryName : string): string {
  for(const dirPath of location.subdirs){
    if(new RegExp(directoryName).test(dirPath)){
      return `${location.path}/${dirPath}` ; 
    }
  }
  if(location.parent){
    return findParentDirectoryPath(location.parent , directoryName);
  }
  throw new Error('root model directory not found')

}