15

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.

Wilhelmina Lohan
  • 2,803
  • 2
  • 29
  • 58
  • 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? – jlang Jun 20 '18 at 08:11
  • 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`. – Wilhelmina Lohan Jun 20 '18 at 19:59
  • 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: https://github.com/angular/devkit/issues/745 – jlang Jun 21 '18 at 05:26
  • 2
    https://github.com/angular/angular-cli/issues/11337 – Wilhelmina Lohan Jun 21 '18 at 16:37
  • @jlang `import { strings } from '@angular-devkit/core';` – Wilhelmina Lohan Jun 21 '18 at 23:45
  • Try running `schematics .:function --name=test --dry-run=false --force` – eko Jul 13 '18 at 07:01
  • 08.2019 - still an issue. Guthub issue to track progress: https://github.com/angular/angular-cli/issues/11337 – grreeenn Aug 26 '19 at 12:49

5 Answers5

12

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: '.',
        }),
      ]),
cgatian
  • 22,047
  • 9
  • 56
  • 76
  • 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' – grreeenn Sep 09 '19 at 14:26
  • 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. – Eric Dec 13 '20 at 23:29
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/schematics-cli@0.6.8.

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);
    };
}
chris
  • 2,740
  • 3
  • 25
  • 23
  • 1
    Damn this doesnt work with nested folders structures. Beware! – cgatian Aug 02 '18 at 18:52
  • 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. – chris Aug 02 '18 at 22:04
  • 2
    important for the forEach() to be last, I found ie after move() – Wilhelmina Lohan Feb 01 '19 at 22:30
  • 2
    This answer is incorrect. It doesn't overwrite existing files - only ignores them without throwing 'Schematics process failed' error. The answer by @cgatian does overwrite the existing files. – grreeenn Sep 09 '19 at 14:26
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.

Martin
  • 15,820
  • 4
  • 47
  • 56
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!

Mateo Tibaquira
  • 2,059
  • 22
  • 23
-1

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

Brackets
  • 464
  • 7
  • 12