How to overwrite file with angular schematics?
Asked Answered
S

5

16

I want to write a Rule that overwrites a file every time. In the following and have MergeStrategy set to Overwrite:

collection.json

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "function": {
      "aliases": [ "fn" ],
      "factory": "./function",
      "description": "Create a function.",
      "schema": "./function/schema.json"
    }
  }
}

function/index.ts

export default function(options: FunctionOptions): Rule {
  options.path = options.path ? normalize(options.path) : options.path;
  const sourceDir = options.sourceDir;
  if (!sourceDir) {
    throw new SchematicsException(`sourceDir option is required.`);
  }

  const templateSource: Source = apply(
    url('./files'),
    [
      template({
        ...strings,
        ...options,
      }),
      move(sourceDir),
    ]
  )

  return mergeWith(templateSource, MergeStrategy.Overwrite);

}

files/__path__/__name@dasherize__.ts

export function <%= camelize(name) %>(): void {
}

I run schematics .:function --name=test --dry-run=false I get

CREATE /src/app/test.ts (33 bytes)

but then, the second time.

ERROR! /src/app/test.ts already exists.

Should it not overwrite the file test.ts with out error?

Edit:

All of the answers work and are great but it seems they are workarounds and no obvious "right" answer and possibly based on preference / opinionated. So not sure how to mark as answered.

Seidel answered 23/2, 2018 at 22:31 Comment(7)
And actually no, the default behavior is to leave files as they are. you could use the --force flag to enforce overwriting files, like so: schematics .:function --name=test --dry-run=false --force. BTW: What did you import for ...strings in template function?Portamento
I probably don't understand to meaning of MergeStrategy.Overwrite then, if that is not the behavior then what is the point/meaning of MergeStrategy.Overwrite.Seidel
Oh, I'm sorry, I totally missed the MergeStrategy.Overwrite, now I agree it should totally work. At least there was once an issue for it in the archived angular/dev-kit project: github.com/angular/devkit/issues/745Portamento
github.com/angular/angular-cli/issues/11337Seidel
@Portamento import { strings } from '@angular-devkit/core';Seidel
Try running schematics .:function --name=test --dry-run=false --forceTonometer
08.2019 - still an issue. Guthub issue to track progress: github.com/angular/angular-cli/issues/11337Sierrasiesser
S
6

I experienced the same issue. By accident I found that adding a forEach into the apply allowed the files to be deleted and the new files created. This is with @angular-devkit/[email protected].

export function indexPage(options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        const rule = mergeWith(
            apply(url('./files'), [
                template({ ...options }),
                forEach((fileEntry: FileEntry) => {
                    // Just by adding this is allows the file to be overwritten if it already exists
                    if (tree.exists(fileEntry.path)) return null;
                    return fileEntry;
                })

            ])
        );

        return rule(tree, _context);
    };
}
Scoggins answered 7/7, 2018 at 0:3 Comment(4)
Damn this doesnt work with nested folders structures. Beware!Dumond
I'm using it with nested folders, two levels deep. But I did find it would error if the file did not already exist, so I had to add a check for existence first. I'll update my answer.Scoggins
important for the forEach() to be last, I found ie after move()Seidel
This answer is incorrect. It doesn't overwrite existing files - only ignores them without throwing 'Schematics process failed' error. The answer by @Dumond does overwrite the existing files.Sierrasiesser
D
13

Modifying chris' answer I was able to come up with the following solution:

export function applyWithOverwrite(source: Source, rules: Rule[]): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const rule = mergeWith(
      apply(source, [
        ...rules,
        forEach((fileEntry) => {
          if (tree.exists(fileEntry.path)) {
            tree.overwrite(fileEntry.path, fileEntry.content);
            return null;
          }
          return fileEntry;
        }),

      ]),
    );

    return rule(tree, _context);
  };
}

Replace your usage of apply with calls to this function.

 applyWithOverwrite(url('./files/stylelint'), [
        template({
          dot: '.',
        }),
      ]),
Dumond answered 9/11, 2018 at 16:49 Comment(2)
This solution actually overwrites the existing files (as opposed to the accepted one), thank you :) N.B. for some reason WebStorm doesn't see where it can import forEach and FileEntry from; it should be imported from '@angular-devkit/schematics'Sierrasiesser
If one of the ...rules prior to the forEach created the file, then tree.exists(fileEntry.path) will evaluate to true. In other words, the file might not actually exist in your file system yet but only in the virtual tree being built by the schematic. This may or may not matter given your use case.Dari
S
6

I experienced the same issue. By accident I found that adding a forEach into the apply allowed the files to be deleted and the new files created. This is with @angular-devkit/[email protected].

export function indexPage(options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        const rule = mergeWith(
            apply(url('./files'), [
                template({ ...options }),
                forEach((fileEntry: FileEntry) => {
                    // Just by adding this is allows the file to be overwritten if it already exists
                    if (tree.exists(fileEntry.path)) return null;
                    return fileEntry;
                })

            ])
        );

        return rule(tree, _context);
    };
}
Scoggins answered 7/7, 2018 at 0:3 Comment(4)
Damn this doesnt work with nested folders structures. Beware!Dumond
I'm using it with nested folders, two levels deep. But I did find it would error if the file did not already exist, so I had to add a check for existence first. I'll update my answer.Scoggins
important for the forEach() to be last, I found ie after move()Seidel
This answer is incorrect. It doesn't overwrite existing files - only ignores them without throwing 'Schematics process failed' error. The answer by @Dumond does overwrite the existing files.Sierrasiesser
Q
3

This may not be ideal if you are replacing many files. I came across this issue replacing the favicon.ico while adding boilerplate to the project. The solution I use is to explicitly delete it first.

export function scaffold(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
  
    tree.delete('src/favicon.ico');

    const templateSource = apply(url('./files'), [
      template({
        ...options,
      }),
      move('.'),
    ]);

    return chain([
      branchAndMerge(chain([
        mergeWith(templateSource, MergeStrategy.Overwrite),
      ]), MergeStrategy.AllowOverwriteConflict),
    ])(tree, _context);
  };
}

NOTE: This approach no longer works with current versions of Schematics.

The following seems to work for the 0.6.8 schematics.

export function scaffold(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
  
    const templateSource = apply(url('./files'), [
      template({
        ...options,
      }),
      move('.'),
    ]);

    return chain([
      branchAndMerge(chain([
        mergeWith(templateSource, MergeStrategy.Overwrite),
      ]), MergeStrategy.Overwrite),
    ])(tree, _context);
  };
}

As a bonus, I no longer need to explicitly delete the file.

Quarrel answered 11/4, 2018 at 15:15 Comment(0)
R
0

Thanks @cgatian for that hint!
Unfortunately it didn't worked to move the templates to some location, so I had to add:

forEach(fileEntry => {
  const destPath = join(options.output, fileEntry.path);
  if (tree.exists(destPath)) {
    tree.overwrite(destPath, fileEntry.content);
  } else {
    tree.create(destPath, fileEntry.content);
  }
  return null;
})

Until the MergeStrategy works as expected,
this will do the trick passing the destination path in options.output!

Thanks again!

Resistencia answered 11/2, 2019 at 5:20 Comment(0)
S
-1

Use --force argument: ng g c component-name --force

Syriac answered 28/10, 2020 at 22:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.