Skip to content

Commit a7c4b18

Browse files
authored
Merge pull request #61 from TikiTDO/feature/batch-generate
Non-interactive mode
2 parents 9644adb + fd961f7 commit a7c4b18

File tree

9 files changed

+254
-26
lines changed

9 files changed

+254
-26
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ root = true
44
[*]
55
charset = utf-8
66
indent_style = space
7-
indent_size = 4
7+
indent_size = 2
88
insert_final_newline = true
99
trim_trailing_whitespace = true
1010

README.md

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,31 +209,79 @@ Example
209209
Here is the string `Lives down BY the River` with each of the converters:
210210

211211
```js
212-
// If you typed in 'Lives down BY the River' for the a Replacer Slot named '__replacerSlot__' and
212+
// If you typed in 'Lives down BY the River' for the a Replacer Slot named '__replacerSlot__' and
213213
// used one of the optional Case Converters you would get the following:
214214

215-
__replacerSlot__(noCase) // Lives down BY the River
216-
__replacerSlot__(camelCase) // livesDownByTheRiver
217-
__replacerSlot__(constantCase) // LIVES_DOWN_BY_THE_RIVER
218-
__replacerSlot__(dotCase) // lives.down.by.the.river
219-
__replacerSlot__(kebabCase) // lives-down-by-the-river
220-
__replacerSlot__(lowerCase) // livesdownbytheriver
221-
__replacerSlot__(pascalCase) // LivesDownByTheRiver
222-
__replacerSlot__(pathCase) // lives/down/by/the/river
223-
__replacerSlot__(sentenceCase) // Lives down by the river
224-
__replacerSlot__(snakeCase) // lives_down_by_the_river
225-
__replacerSlot__(titleCase) // Lives Down By The River
226-
227-
// Note: you can set a 'defaultCase' converter in IConfigItem so all
215+
__replacerSlot__(noCase); // Lives down BY the River
216+
__replacerSlot__(camelCase); // livesDownByTheRiver
217+
__replacerSlot__(constantCase); // LIVES_DOWN_BY_THE_RIVER
218+
__replacerSlot__(dotCase); // lives.down.by.the.river
219+
__replacerSlot__(kebabCase); // lives-down-by-the-river
220+
__replacerSlot__(lowerCase); // livesdownbytheriver
221+
__replacerSlot__(pascalCase); // LivesDownByTheRiver
222+
__replacerSlot__(pathCase); // lives/down/by/the/river
223+
__replacerSlot__(sentenceCase); // Lives down by the river
224+
__replacerSlot__(snakeCase); // lives_down_by_the_river
225+
__replacerSlot__(titleCase); // Lives Down By The River
226+
227+
// Note: you can set a 'defaultCase' converter in IConfigItem so all
228228
// Replacer Slots without a Case Converter will be transformed the same way.
229-
__replacerSlot__ // LivesDownByTheRiver
229+
__replacerSlot__; // LivesDownByTheRiver
230230
```
231231

232232
One Rule: no spaces between the [Replacer Slots](#replacer-slots-or-ireplacerslotquestion) and [Case Converters](#case-converters). If there is a space, [Case Converters](#case-converters) will not work.
233233

234234
- :white_check_mark: `__name__(camelCase)`
235235
- :warning: `__name__ (camelCase)`
236236

237+
## Batch Usage
238+
239+
You can use `generate-template-files` to generate your template files programmatically, without any interactive prompts. This mode does not support `stringReplacers`.
240+
241+
The following example will generate the component, unit tests, and the SCSS module in one do.
242+
243+
```js
244+
// generateTemplateFile.js
245+
const { generateTemplateFilesBatch } = require('generate-template-files');
246+
247+
const componentWithInterface = (componentName, componentScope = 'common') => {
248+
generateTemplateFilesBatch([
249+
{
250+
option: 'Component',
251+
defaultCase: '(pascalCase)',
252+
entry: {
253+
folderPath: './tools/templates/react/component',
254+
},
255+
dynamicReplacers: [
256+
{ slot: '__name__', slotValue: componentName },
257+
{ slot: '__scope__', slotValue: componentScope },
258+
],
259+
output: {
260+
path: `./src/component/__scope__(camelCase)`,
261+
pathAndFileNameDefaultCase: '(pascalCase)',
262+
},
263+
},
264+
{
265+
option: 'Component Interface',
266+
defaultCase: '(pascalCase)',
267+
entry: {
268+
folderPath: './tools/templates/react/I__interface__.ts',
269+
},
270+
dynamicReplacers: [
271+
{ slot: '__interface__', slotValue: componentName },
272+
{ slot: '__scope__', slotValue: componentScope },
273+
],
274+
output: {
275+
path: `./src/component/__scope__(camelCase)/I__interface__.ts`,
276+
pathAndFileNameDefaultCase: '(pascalCase)',
277+
},
278+
},
279+
]).catch(() => {
280+
console.log('Build Error');
281+
});
282+
};
283+
```
284+
237285
## Command Line Usage
238286

239287
You can use `generate-template-files` with the command line to generate your template files.

examples/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"---------- Normal Example -------------------------------------------": "",
77
"generate": "node ./tools/generate.js",
8+
"batchGenerate": "node ./tools/batchGenerate.js",
89
"---------- Command Line Example -------------------------------------": "",
910
"commandlineSimple": "node ./tools/commandLine.js angular-ngrx-store __name__=some-name __model__=some-other-name",
1011
"commandline": "node ./tools/commandLine.js angular-ngrx-store __name__=some-name __model__=some-other-name --outputpath=./src/here --overwrite"

examples/tools/batchGenerate.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// generateTemplateFile.js
2+
const {generateTemplateFilesBatch, CaseConverterEnum} = require('../../dist/generate-template-files.cjs');
3+
// Note: In your file it will be like this:
4+
// const {generateTemplateFilesBatch, CaseConverterEnum} = require('generate-template-files');
5+
6+
const componentName = "Example"
7+
const componentScope = "common"
8+
9+
generateTemplateFilesBatch([
10+
{
11+
option: 'Component',
12+
defaultCase: CaseConverterEnum.PascalCase,
13+
entry: {
14+
folderPath: './tools/templates/react/component',
15+
},
16+
dynamicReplacers: [
17+
{ slot: '__name__', slotValue: componentName },
18+
{ slot: '__scope__', slotValue: componentScope },
19+
],
20+
output: {
21+
path: `./src/component/__scope__(camelCase)`,
22+
pathAndFileNameDefaultCase: CaseConverterEnum.PascalCase,
23+
},
24+
},
25+
{
26+
option: 'Component Interface',
27+
defaultCase: CaseConverterEnum.PascalCase,
28+
entry: {
29+
folderPath: './tools/templates/react/I__interface__.ts',
30+
},
31+
dynamicReplacers: [
32+
{ slot: '__interface__', slotValue: componentName },
33+
{ slot: '__scope__', slotValue: componentScope },
34+
],
35+
output: {
36+
path: `./src/component/__scope__(camelCase)/I__interface__.ts`,
37+
pathAndFileNameDefaultCase: CaseConverterEnum.PascalCase,
38+
},
39+
},
40+
]);
41+

src/GenerateTemplateFiles.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as enquirer from 'enquirer';
1+
import enquirer from 'enquirer';
22
import recursiveCopy from 'recursive-copy';
33
import pathExists from 'path-exists';
44
import through from 'through2';
@@ -11,9 +11,10 @@ import IResults from './models/IResults';
1111
import IDefaultCaseConverter from './models/IDefaultCaseConverter';
1212
import {
1313
throwErrorIfNoConfigItems,
14-
throwErrorIfOptionNameIsNotFound,
1514
throwErrorIfNoStringOrDynamicReplacers,
15+
throwErrorIfOptionNameIsNotFound,
1616
throwErrorIfStringReplacersDoNotMatch,
17+
throwErrorIfStringReplacersExistOrNoDynamicReplacers,
1718
displayError,
1819
displayWarning,
1920
displaySuccess,
@@ -23,6 +24,7 @@ import yargs from 'yargs';
2324

2425
export default class GenerateTemplateFiles {
2526
private _isCommandLine: boolean = false;
27+
private _isBatch: boolean = false;
2628

2729
/**
2830
* Main method to create your template files. Accepts an array of `IConfigItem` items.
@@ -88,6 +90,23 @@ export default class GenerateTemplateFiles {
8890
}
8991
}
9092

93+
/**
94+
* Main method to run generate on multiple templates at once, without any interactive prompts
95+
*/
96+
public async batchGenerate(options: Omit<IConfigItem, 'stringReplacers'>[]) {
97+
this._isBatch = true;
98+
99+
throwErrorIfNoConfigItems(options);
100+
throwErrorIfStringReplacersExistOrNoDynamicReplacers(options);
101+
102+
for (const selectedConfigItem of options) {
103+
const answeredReplacers: IReplacer[] = await this._getDynamicReplacerSlotValues(
104+
selectedConfigItem
105+
);
106+
await this._outputFiles(selectedConfigItem, answeredReplacers);
107+
}
108+
}
109+
91110
private async _outputFiles(
92111
selectedConfigItem: IConfigItem,
93112
replacers: IReplacer[]
@@ -184,11 +203,22 @@ export default class GenerateTemplateFiles {
184203
};
185204
}
186205
);
187-
const dynamicReplacers: IReplacer[] = selectedConfigItem.dynamicReplacers || [];
206+
const dynamicReplacers = await this._getDynamicReplacerSlotValues(selectedConfigItem);
188207

189208
return [...replacers, ...dynamicReplacers];
190209
}
191210

211+
/**
212+
* Dynamic replacer values, used for interactive and batch generation
213+
*/
214+
private async _getDynamicReplacerSlotValues(
215+
selectedConfigItem: IConfigItem
216+
): Promise<IReplacer[]> {
217+
const dynamicReplacers: IReplacer[] = selectedConfigItem.dynamicReplacers || [];
218+
219+
return dynamicReplacers;
220+
}
221+
192222
/**
193223
* Create every variation for the for the replacement keys
194224
*/
@@ -239,6 +269,10 @@ export default class GenerateTemplateFiles {
239269
return outputPath ?? outputPathFormatted;
240270
}
241271

272+
if (this._isBatch) {
273+
return outputPathFormatted;
274+
}
275+
242276
const outputPathAnswer: any = await enquirer.prompt({
243277
type: 'input',
244278
name: 'outputPath',
@@ -269,6 +303,10 @@ export default class GenerateTemplateFiles {
269303
return selectedConfigItem.output.overwrite || yargs.argv.overwrite === true;
270304
}
271305

306+
if (this._isBatch) {
307+
return Boolean(selectedConfigItem.output.overwrite);
308+
}
309+
272310
const overwriteFilesAnswer: any = await enquirer.prompt({
273311
name: 'overwrite',
274312
message: 'Overwrite files, continue?',

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,12 @@ export function generateTemplateFiles(data: IConfigItem[]): Promise<void> {
2828
export function generateTemplateFilesCommandLine(data: IConfigItem[]): Promise<void> {
2929
return new GenerateTemplateFiles().commandLine(data);
3030
}
31+
32+
/**
33+
* Main method to run generate on multiple templates at once, without any interactive prompts.
34+
*/
35+
export function generateTemplateFilesBatch(
36+
data: Omit<IConfigItem, 'stringReplacers'>[]
37+
): Promise<void> {
38+
return new GenerateTemplateFiles().batchGenerate(data);
39+
}

src/utilities/CheckUtility.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,16 @@ export const throwErrorIfNoStringOrDynamicReplacers = (options: IConfigItem[]) =
7272
throw new Error('IConfigItem needs to have a stringReplacers or dynamicReplacers.');
7373
}
7474
};
75+
76+
export const throwErrorIfStringReplacersExistOrNoDynamicReplacers = (options: IConfigItem[]) => {
77+
const allValidBatchEntries =
78+
options.every((item: IConfigItem) => {
79+
return !Boolean(item?.stringReplacers?.length) && Boolean(item?.dynamicReplacers?.length);
80+
}) && options.length > 0;
81+
82+
if (!allValidBatchEntries) {
83+
throw new Error(
84+
'IConfigItem for batchGenerate does not support stringReplacers, and must have dynamicReplacers.'
85+
);
86+
}
87+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import GenerateTemplateFiles from '../GenerateTemplateFiles';
2+
import { IConfigItem } from '../index';
3+
import CaseConverterEnum from '../constants/CaseConverterEnum';
4+
5+
describe('GenerateTemplateFiles - Batch', () => {
6+
test('should throw an error if no IConfigItem items', () => {
7+
const items: IConfigItem[] = [];
8+
const gtf = new GenerateTemplateFiles();
9+
10+
expect(() => gtf.batchGenerate(items)).rejects.toThrowError(
11+
'There was no IConfigItem items found.'
12+
);
13+
});
14+
15+
test('should throw an error if no stringReplacers or dynamicReplacers', async () => {
16+
const items: IConfigItem[] = [
17+
{
18+
option: 'some-template',
19+
defaultCase: CaseConverterEnum.PascalCase,
20+
entry: {
21+
folderPath: 'path',
22+
},
23+
output: {
24+
path: 'path',
25+
},
26+
},
27+
];
28+
const gtf = new GenerateTemplateFiles();
29+
30+
await expect(() => gtf.batchGenerate(items)).rejects.toThrowError(
31+
'IConfigItem for batchGenerate does not support stringReplacers, and must have dynamicReplacers'
32+
);
33+
});
34+
35+
test('should throw an error if batch IConfigItem is not found for option name', async () => {
36+
const items: IConfigItem[] = [
37+
{
38+
option: 'some-template',
39+
defaultCase: CaseConverterEnum.PascalCase,
40+
stringReplacers: ['__name__'],
41+
entry: {
42+
folderPath: 'path',
43+
},
44+
output: {
45+
path: 'path',
46+
},
47+
},
48+
];
49+
const gtf = new GenerateTemplateFiles();
50+
51+
await expect(() => gtf.batchGenerate(items)).rejects.toThrowError(
52+
`IConfigItem for batchGenerate does not support stringReplacers, and must have dynamicReplacers.`
53+
);
54+
});
55+
});

src/utilities/GenerateTemplateFiles.commandline.test.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,27 @@ import GenerateTemplateFiles from '../GenerateTemplateFiles';
22
import { IConfigItem } from '../index';
33
import CaseConverterEnum from '../constants/CaseConverterEnum';
44
import yargs from 'yargs';
5+
import colors from 'colors';
6+
7+
let consoleInfoSpy: jest.SpyInstance;
8+
describe('GenerateTemplateFiles - Command Line', () => {
9+
beforeEach(() => {
10+
consoleInfoSpy = jest.spyOn(global.console, 'info').mockImplementation(() => null);
11+
});
12+
13+
afterEach(() => {
14+
consoleInfoSpy.mockRestore();
15+
});
516

6-
describe.skip('GenerateTemplateFiles - Command Line', () => {
717
test('should throw an error if no IConfigItem items', () => {
818
const items: IConfigItem[] = [];
919
const gtf = new GenerateTemplateFiles();
1020

11-
expect(() => gtf.commandLine(items)).rejects.toThrowError(
12-
'There was no IConfigItem items found.'
21+
gtf.commandLine(items);
22+
expect(consoleInfoSpy).toBeCalledWith(
23+
colors.bold.red(
24+
`[Error in generate-template-files]: ${colors.red('There was no IConfigItem items found.')}`
25+
)
1326
);
1427
});
1528

@@ -33,8 +46,13 @@ describe.skip('GenerateTemplateFiles - Command Line', () => {
3346
];
3447
const gtf = new GenerateTemplateFiles();
3548

36-
expect(() => gtf.commandLine(items)).rejects.toThrowError(
37-
`No IConfigItem found for ${notFoundOptionName}`
49+
gtf.commandLine(items);
50+
expect(consoleInfoSpy).toBeCalledWith(
51+
colors.bold.red(
52+
`[Error in generate-template-files]: ${colors.red(
53+
`No IConfigItem found for ${notFoundOptionName}`
54+
)}`
55+
)
3856
);
3957
});
4058

@@ -53,8 +71,13 @@ describe.skip('GenerateTemplateFiles - Command Line', () => {
5371
];
5472
const gtf = new GenerateTemplateFiles();
5573

56-
expect(() => gtf.commandLine(items)).rejects.toThrowError(
57-
'IConfigItem needs to have a stringReplacers or dynamicReplacers.'
74+
gtf.commandLine(items);
75+
expect(consoleInfoSpy).toBeCalledWith(
76+
colors.bold.red(
77+
`[Error in generate-template-files]: ${colors.red(
78+
'IConfigItem needs to have a stringReplacers or dynamicReplacers.'
79+
)}`
80+
)
5881
);
5982
});
6083
});

0 commit comments

Comments
 (0)