Skip to content

Commit

Permalink
feat(devkit): allow to customize overwrite mode in generateFiles (#26354
Browse files Browse the repository at this point in the history
)

Adds a new capability to choose how to handle already existing target
files when using a generator.

See #17925

Co-authored-by: Michael Monerau <micmo@qontrol.io>
  • Loading branch information
AgentEnder and qortex authored Jun 4, 2024
1 parent 8800a30 commit dd6eda8
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 7 deletions.
29 changes: 29 additions & 0 deletions docs/generated/devkit/OverwriteStrategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Enumeration: OverwriteStrategy

Specify what should be done when a file is generated but already exists on the system

## Table of contents

### Enumeration Members

- [KeepExisting](../../devkit/documents/OverwriteStrategy#keepexisting)
- [Overwrite](../../devkit/documents/OverwriteStrategy#overwrite)
- [ThrowIfExisting](../../devkit/documents/OverwriteStrategy#throwifexisting)

## Enumeration Members

### KeepExisting

**KeepExisting** = `"keepExisting"`

---

### Overwrite

**Overwrite** = `"overwrite"`

---

### ThrowIfExisting

**ThrowIfExisting** = `"throwIfExisting"`
1 change: 1 addition & 0 deletions docs/generated/devkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ It only uses language primitives and immutable objects

- [ChangeType](../../devkit/documents/ChangeType)
- [DependencyType](../../devkit/documents/DependencyType)
- [OverwriteStrategy](../../devkit/documents/OverwriteStrategy)

### Classes

Expand Down
3 changes: 2 additions & 1 deletion docs/generated/devkit/generateFiles.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Function: generateFiles

**generateFiles**(`tree`, `srcFolder`, `target`, `substitutions`): `void`
**generateFiles**(`tree`, `srcFolder`, `target`, `substitutions`, `options?`): `void`

Generates a folder of files based on provided templates.

Expand Down Expand Up @@ -32,6 +32,7 @@ doesn't get confused about incorrect TypeScript files.
| `srcFolder` | `string` | the source folder of files (absolute path) |
| `target` | `string` | the target folder (relative to the tree root) |
| `substitutions` | `Object` | an object of key-value pairs |
| `options?` | `GenerateFilesOptions` | See GenerateFilesOptions |

#### Returns

Expand Down
1 change: 1 addition & 0 deletions docs/generated/packages/devkit/documents/nx_devkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ It only uses language primitives and immutable objects

- [ChangeType](../../devkit/documents/ChangeType)
- [DependencyType](../../devkit/documents/DependencyType)
- [OverwriteStrategy](../../devkit/documents/OverwriteStrategy)

### Classes

Expand Down
10 changes: 10 additions & 0 deletions docs/shared/recipes/generators/creating-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ Hello, my name is mylib!

If you want the generated file or folder name to contain variable values, use `__variable__`. So `NOTES-for-__name__.md` would be resolved to `NOTES_for_mylib.md` in the above example.

## Overwrite mode

By default, generators overwrite files when they already exist.

You can customize this behaviour with an optional argument to `generateFiles` that can take one of three values:

- `OverwriteStrategy.Overwrite` (default): all generated files are created and overwrite existing target files if any.
- `OverwriteStrategy.KeepExisting`: generated files are created only when target file does not exist. Existing target files are kept as is.
- `OverwriteStrategy.ThrowIfExisting`: if a target file already exists, an exception is thrown. Suitable when a pristine target environment is expected.

## EJS Syntax Quickstart

The [EJS syntax](https://ejs.co/) can do much more than replace variable names with values. Here are some common techniques.
Expand Down
5 changes: 4 additions & 1 deletion packages/devkit/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export { formatFiles } from './src/generators/format-files';
/**
* @category Generators
*/
export { generateFiles } from './src/generators/generate-files';
export {
generateFiles,
OverwriteStrategy,
} from './src/generators/generate-files';

/**
* @category Generators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ exports[`generateFiles should copy files from a directory into the tree 1`] = `
"
`;

exports[`generateFiles should overwrite files when option is overwrite 1`] = `
"file in directory contents
"
`;

exports[`generateFiles should remove ".template" from paths 1`] = `
"file with template suffix contents
"
Expand Down
77 changes: 74 additions & 3 deletions packages/devkit/src/generators/generate-files.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Tree } from 'nx/src/generators/tree';
import * as FileType from 'file-type';
import { createTree } from 'nx/src/generators/testing-utils/create-tree';
import { generateFiles } from './generate-files';
import type { Tree } from 'nx/src/generators/tree';
import { join } from 'path';
import * as FileType from 'file-type';
import { OverwriteStrategy, generateFiles } from './generate-files';

describe('generateFiles', () => {
let tree: Tree;
Expand Down Expand Up @@ -79,4 +79,75 @@ describe('generateFiles', () => {
mime: 'image/png',
});
});

it('should throw if overwrite is treated as an error', async () => {
expect(() => {
generateFiles(
tree,
join(__dirname, './test-files'),
'.',
{
foo: 'bar',
name: 'my-project',
projectName: 'my-project-name',
dot: '.',
},
{ overwriteStrategy: OverwriteStrategy.ThrowIfExisting }
);
}).toThrowError(
'Generated file already exists, not allowed by overwrite strategy in generator (directory/file-in-directory.txt)'
);
});

it('should overwrite files when option is overwrite', async () => {
// Write a custom file that will be overwritten
tree.write(
'directory/file-in-directory.txt',
'placeholder text to overwrite'
);

// Run generation again
generateFiles(
tree,
join(__dirname, './test-files'),
'.',
{
foo: 'bar',
name: 'my-project',
projectName: 'my-project-name',
dot: '.',
},
{ overwriteStrategy: OverwriteStrategy.Overwrite }
);

// File must have been overwritten
expect(
tree.read('directory/file-in-directory.txt', 'utf-8')
).toMatchSnapshot();
});

it('should keep files when option is to keep existing files', async () => {
// Write a custom file that will stay the same
const placeholder = 'placeholder text to keep';
tree.write('directory/file-in-directory.txt', placeholder);

// Run generation again
generateFiles(
tree,
join(__dirname, './test-files'),
'.',
{
foo: 'bar',
name: 'my-project',
projectName: 'my-project-name',
dot: '.',
},
{ overwriteStrategy: OverwriteStrategy.KeepExisting }
);

// File must have been kept
expect(tree.read('directory/file-in-directory.txt', 'utf-8')).toEqual(
placeholder
);
});
});
43 changes: 41 additions & 2 deletions packages/devkit/src/generators/generate-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@ import { readdirSync, readFileSync, statSync } from 'fs';
import * as path from 'path';
import { isBinaryPath } from '../utils/binary-extensions';

import { logger, Tree } from 'nx/src/devkit-exports';
import { logger, type Tree } from 'nx/src/devkit-exports';

/**
* Specify what should be done when a file is generated but already exists on the system
*/
export enum OverwriteStrategy {
Overwrite = 'overwrite',
KeepExisting = 'keepExisting',
ThrowIfExisting = 'throwIfExisting',
}

/**
* Options for the generateFiles function
*/
export interface GenerateFilesOptions {
/**
* Specify what should be done when a file is generated but already exists on the system
*/
overwriteStrategy?: OverwriteStrategy;
}

/**
* Generates a folder of files based on provided templates.
Expand All @@ -25,13 +44,20 @@ import { logger, Tree } from 'nx/src/devkit-exports';
* @param srcFolder - the source folder of files (absolute path)
* @param target - the target folder (relative to the tree root)
* @param substitutions - an object of key-value pairs
* @param options - See {@link GenerateFilesOptions}
*/
export function generateFiles(
tree: Tree,
srcFolder: string,
target: string,
substitutions: { [k: string]: any }
substitutions: { [k: string]: any },
options: GenerateFilesOptions = {
overwriteStrategy: OverwriteStrategy.Overwrite,
}
): void {
options ??= {};
options.overwriteStrategy ??= OverwriteStrategy.Overwrite;

const ejs: typeof import('ejs') = require('ejs');

const files = allFilesInDir(srcFolder);
Expand All @@ -49,6 +75,19 @@ export function generateFiles(
substitutions
);

if (tree.exists(computedPath)) {
if (options.overwriteStrategy === OverwriteStrategy.KeepExisting) {
return;
} else if (
options.overwriteStrategy === OverwriteStrategy.ThrowIfExisting
) {
throw new Error(
`Generated file already exists, not allowed by overwrite strategy in generator (${computedPath})`
);
}
// else: file should be overwritten, so just fall through to file generation
}

if (isBinaryPath(filePath)) {
newContent = readFileSync(filePath);
} else {
Expand Down

0 comments on commit dd6eda8

Please sign in to comment.