gaffer-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Dawson Toth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Gaffer-Generator
2
+
3
+ My old Gaffer is tired of writing code, so he generates it instead! This is how he does it.
4
+
5
+ ![](https://media.giphy.com/media/Tf1wxPaCbXzCE/giphy.gif)
6
+
7
+ ## Requirements
8
+
9
+ Install the latest LTS of NodeJS. Or a version around v24. Or try whatever you've got installed already, maybe it will work!
10
+ https://nodejs.org/en/
11
+
12
+ Also install `yarn`. Or don't, and use `npm` instead.
13
+ https://yarnpkg.com/en/
14
+
15
+ ```$bash
16
+ yarn global add gaffer-generator
17
+ ```
18
+ or
19
+ ```$bash
20
+ npm install -g gaffer-generator
21
+ ```
22
+
23
+ Alternatively, `npx` is a useful way to run this.
24
+ https://www.npmjs.com/package/npx
25
+
26
+ ## Let's see it in action!
27
+
28
+ This project is written in a platform and language agnostic way. It contains several limited scripts that find your templates, and then it generates code based on them.
29
+
30
+ You'll have an easier time understanding it if you see it in action, so do the following:
31
+
32
+ ```$bash
33
+ > mkdir testing-gaffer-generator
34
+ > cd testing-gaffer-generator
35
+ > gaffer-generator create
36
+ > gaffer-generator generate
37
+ ```
38
+
39
+ You should see output similar to the following:
40
+
41
+ ```$bash
42
+
43
+ > gaffer-generator generate
44
+
45
+ [15:20:31] creating: sample.output/date-parser.ts
46
+ [15:20:31] creating: sample.output/is-set.ts
47
+ [15:20:31] creating: sample.output/models/day-of-week.ts
48
+ [15:20:31] creating: sample.output/models/address.ts
49
+ [15:20:31] creating: sample.output/models/geo-point.ts
50
+ [15:20:31] creating: sample.output/models/geo-polygon.ts
51
+ [15:20:31] creating: sample.output/models/geo-rectangle.ts
52
+ [15:20:31] creating: sample.output/models/page-index.ts
53
+ [15:20:31] creating: sample.output/models/paging.ts
54
+ [15:20:31] creating: sample.output/models/index.ts
55
+ [15:20:31] creating: sample.output/services/address.service.ts
56
+ [15:20:31] creating: sample.output/services/index.ts
57
+ [15:20:31] Finished 'default' after 341 ms
58
+ ```
59
+
60
+ ## What happened?
61
+
62
+ Let's break down the discrete steps that happened above:
63
+
64
+ 1. We search the current directory recursively for `.templateroot` directories.
65
+ 2. When one is found, we read in the `template.cjs` (or `template.js`) file (it should be a CommonJS module).
66
+ 3. Call the `download` method of this file, which should return a promise that eventually resolves to any JSON describing what you want to generate.
67
+ 4. Using the exported `into` property from the `template.cjs` (or `template.js`), we'll recursively look at all of the other files within the `.templateroot`.
68
+ 5. When we find a directory, recurse in to it!
69
+ 6. When we find a file, parse it as a [lodash template](https://lodash.com/docs/4.17.11#template). The context will be whatever was returned from the `download` promise above, plus any exported methods of our `template.cjs` (or `template.js`) will be exposed on the `utils` object.
70
+ 7. When we find a variable in the path, like `_fileName_.ts`, evaluate it based on our context from `download`.
71
+ 8. When we find a `_each` in the path, like `_eachEnum.fileName_.ts`, expand it out. This lets us generate many files from a single template.
72
+
73
+ ## How do I get started?
74
+
75
+ Create a `sample.templateroot` in your project:
76
+
77
+ ```$bash
78
+ gaffer-generator create path/where/you/want/your/clientsdk.templateroot
79
+ ```
80
+
81
+ Keep it under source control, and update it to generate based on your own metadata JSON, and desired client language.
82
+
83
+ It's also recommended that the generated output be placed under source control. This way, whenever you generate new code, you can see exactly what changed.
84
+
85
+ When you want to generate code, run `gaffer-generator generate` within that project. Point it at the root of your project, and enjoy your newly generated code! (Tip: you can have as many `.templateroot`s as you need.)
86
+
87
+ ## Flags
88
+
89
+ There are several flags you can pass. To see them, run `gaffer-generator --help`!
90
+
91
+ ## Testing
92
+
93
+ ```$bash
94
+ npm run test
95
+ ```
package/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import yargs from 'yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+ import { setArgv } from './lib/argv.js';
5
+
6
+ setArgv(
7
+ yargs(hideBin(process.argv))
8
+ .command({
9
+ command: 'generate [directory]',
10
+ aliases: ['gen', 'g'],
11
+ desc: 'Recursively looks for .templateroots in the current directory, or supplied directory',
12
+ builder: yargs => yargs.default('directory', './'),
13
+ handler: async argv => (await import('./lib/generate/root.js')).run(argv.directory),
14
+ })
15
+ .command({
16
+ command: 'create [directory] [--overwrite]',
17
+ aliases: ['c'],
18
+ desc: 'Create a starting .templateroot in to the current directory, or supplied directory',
19
+ builder: yargs => yargs.default('directory', './example.templateroot'),
20
+ handler: async argv => (await import('./lib/create.js')).run(argv.directory),
21
+ })
22
+ .options({
23
+ 'dry-run': {
24
+ boolean: true,
25
+ describe: 'Logs the changes that would be made without actually touching the file system.',
26
+ },
27
+ 'into': {
28
+ string: true,
29
+ describe: 'Overrides the directory that .templateroots will target. Useful for targeting dynamic directories.',
30
+ },
31
+ 'silent': {
32
+ boolean: true,
33
+ describe: 'Disables console logging.',
34
+ },
35
+ 'no-colors': {
36
+ boolean: true,
37
+ describe: 'Turns off console coloring of text.',
38
+ },
39
+ })
40
+ .demandCommand()
41
+ .help()
42
+ .wrap(72)
43
+ .argv,
44
+ );
package/lib/argv.js ADDED
@@ -0,0 +1,9 @@
1
+ let argv;
2
+
3
+ export function setArgv(newArgv) {
4
+ argv = newArgv;
5
+ }
6
+
7
+ export function getArgv() {
8
+ return argv;
9
+ }
package/lib/create.js ADDED
@@ -0,0 +1,40 @@
1
+ import colors from 'colors/safe.js';
2
+ import path from 'path';
3
+ import copy from 'recursive-copy';
4
+ import { getArgv } from './argv.js';
5
+ import * as utils from './utils.js';
6
+ import { logChange } from './utils/logChange.js';
7
+ import { logError } from './utils/logError.js';
8
+
9
+ const dryRun = getArgv().dryRun || false;
10
+
11
+ /*
12
+ Public API.
13
+ */
14
+ export { run };
15
+
16
+ /*
17
+ Implementation.
18
+ */
19
+ function run(directory) {
20
+ const from = path.join(import.meta.dirname, '..', 'example', 'sample.templateroot');
21
+ const to = directory;
22
+ const options = {
23
+ overwrite: getArgv().overwrite,
24
+ };
25
+ logChange(to);
26
+ if (dryRun) {
27
+ return;
28
+ }
29
+ copy(from, to, options)
30
+ .on(copy.events.ERROR, (error, copyOperation) => {
31
+ logError(colors.red('Unable to copy to ') + colors.magenta(copyOperation.dest));
32
+ })
33
+ .then(results => {
34
+ utils.log(results.length + ' file(s) copied');
35
+ })
36
+ .catch(function(error) {
37
+ logError(colors.red(error));
38
+ process.exit(1);
39
+ });
40
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import globule from 'globule';
3
+ import path from 'path';
4
+ import { getArgv } from '../argv.js';
5
+ import * as utils from '../utils.js';
6
+ import { logRemoval } from '../utils/logRemoval.js';
7
+ import * as node from './node.js';
8
+ import difference from 'lodash.difference';
9
+ import uniq from 'lodash.uniq';
10
+
11
+ const dryRun = getArgv().dryRun || false;
12
+ const reservedItems = ['template.cjs', 'template.js', 'template.mjs', 'template.ts', 'template', '.DS_Store'];
13
+
14
+ /*
15
+ Public API.
16
+ */
17
+ export { visit };
18
+
19
+ /*
20
+ Implementation.
21
+ */
22
+ function visit(items, fromPath, templateSettings) {
23
+ const directoryItems = globule.find('*', { cwd: fromPath, dot: true })
24
+ .filter(i => reservedItems.indexOf(i) === -1);
25
+ const changedFiles = [];
26
+ for (const item of items) {
27
+ for (const directoryItem of directoryItems) {
28
+ node.visit(
29
+ item.context,
30
+ path.join(fromPath, directoryItem), path.join(item.path, directoryItem),
31
+ templateSettings,
32
+ changedFiles);
33
+ }
34
+ }
35
+ !templateSettings.skipCleaning && cleanDirectory(changedFiles);
36
+ }
37
+
38
+ function cleanDirectory(changedFiles) {
39
+ if (changedFiles.length) {
40
+ const changedDirs = uniq(changedFiles.map(path.dirname));
41
+ changedDirs.forEach(changedDir => {
42
+ const allFiles = globule.find('*', {
43
+ nodir: true,
44
+ cwd: changedDir,
45
+ prefixBase: true,
46
+ dot: true,
47
+ }).map(utils.normalizePath);
48
+ const obsoleteFiles = difference(allFiles, changedFiles.map(utils.normalizePath));
49
+ for (let obsoleteFile of obsoleteFiles) {
50
+ logRemoval(obsoleteFile);
51
+ !dryRun && fs.unlinkSync(obsoleteFile);
52
+ }
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,61 @@
1
+ import colors from 'colors/safe.js';
2
+ import defaults from 'lodash.defaults';
3
+ import template from 'lodash.template';
4
+ import { getArgv } from '../argv.js';
5
+ import { templateArgs } from '../templateArgs.js';
6
+ import * as utils from '../utils.js';
7
+
8
+ const dryRun = getArgv().dryRun || false;
9
+
10
+ /*
11
+ Public API.
12
+ */
13
+ export { visit };
14
+
15
+ /*
16
+ Implementation.
17
+ */
18
+ function visit(items, fromPath, toPath, templateSettings, changedFiles) {
19
+ const templateUtils = defaults({}, templateSettings, utils);
20
+ let compiledTemplate;
21
+
22
+ try {
23
+ compiledTemplate = template(
24
+ templateUtils.safeRead(fromPath),
25
+ defaults({
26
+ sourceURL: fromPath,
27
+ }, templateSettings.templateArgs || templateArgs),
28
+ );
29
+ }
30
+ catch (err) {
31
+ templateUtils.logError(
32
+ colors.red('Hit error when compiling template:\n')
33
+ + colors.cyan(fromPath),
34
+ err);
35
+ return;
36
+ }
37
+
38
+ for (const item of items) {
39
+ try {
40
+ const instanceData = defaults({ utils: templateUtils }, item.context);
41
+ const existingContents = templateUtils.safeRead(item.path);
42
+ let newContents = compiledTemplate(instanceData);
43
+ if (templateUtils.mapContents) {
44
+ newContents = templateUtils.mapContents(newContents, instanceData, item);
45
+ }
46
+ if (newContents) {
47
+ changedFiles.push(item.path);
48
+ if (templateUtils.contentsDiffer(existingContents, newContents)) {
49
+ templateUtils.logChange(item.path);
50
+ !dryRun && templateUtils.safeWrite(item.path, newContents);
51
+ }
52
+ }
53
+ }
54
+ catch (err) {
55
+ templateUtils.logError(
56
+ colors.red('Hit error when running template:\n')
57
+ + colors.cyan(fromPath) + colors.gray(' => ') + colors.cyan(item.path),
58
+ err);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import defaults from 'lodash.defaults';
3
+ import * as utils from '../utils.js';
4
+ import * as directory from './directory.js';
5
+ import * as file from './file.js';
6
+
7
+ /*
8
+ Public API.
9
+ */
10
+ export { visit };
11
+
12
+ /*
13
+ Implementation.
14
+ */
15
+ function visit(context, fromPath, toPath, templateSettings, changedFiles) {
16
+ const stat = fs.lstatSync(fromPath);
17
+ const items = findArrayIterationInPath(context, toPath);
18
+ if (stat.isDirectory()) {
19
+ directory.visit(items, fromPath, templateSettings);
20
+ }
21
+ else if (stat.isFile()) {
22
+ file.visit(items, fromPath, toPath, templateSettings, changedFiles);
23
+ }
24
+ }
25
+
26
+ function findArrayIterationInPath(context, itemPath) {
27
+ const match = itemPath.match(/_[Ee]ach([A-Z][a-z]+)/);
28
+ const type = match && match[1];
29
+ return (type && context[type.toLowerCase() + 's'] || [context])
30
+ .map(item => {
31
+ const subContext = type ? defaults(
32
+ {
33
+ [type]: item,
34
+ [type.toLowerCase()]: item,
35
+ }, item, context) : context;
36
+ const rawPath = itemPath
37
+ .replace(/_[Ee]ach[A-Z][A-Za-z]+\./, a => '_' + a.substr(5).toLowerCase())
38
+ .replace(/\.templateroot$/, '');
39
+ return {
40
+ context: subContext,
41
+ path: utils.parameterizeString(rawPath, subContext),
42
+ };
43
+ });
44
+ }
@@ -0,0 +1,95 @@
1
+ import colors from 'colors/safe.js';
2
+ import fs from 'fs';
3
+ import globule from 'globule';
4
+ import { createRequire } from 'module';
5
+ import path from 'path';
6
+ import { getArgv } from '../argv.js';
7
+ import * as utils from '../utils.js';
8
+ import { logError } from '../utils/logError.js';
9
+ import * as node from './node.js';
10
+
11
+ // Create a CommonJS-compatible require that works from ESM context
12
+ const requireCompat = createRequire(import.meta.url);
13
+
14
+ /*
15
+ Public API.
16
+ */
17
+ export { run, visit };
18
+
19
+ /*
20
+ Implementation.
21
+ */
22
+ async function run(directory) {
23
+ const matches = globule.find({
24
+ src: path.join(directory, '**/*.templateroot'),
25
+ filter: match => match.indexOf('node_modules') === -1,
26
+ dot: true,
27
+ });
28
+ for (const match of matches) {
29
+ await visit(path.resolve(match));
30
+ }
31
+ }
32
+
33
+ async function visit(rootPath) {
34
+ const templateSettingsPath = determineTemplateFile(rootPath);
35
+ if (!templateSettingsPath) {
36
+ logError(
37
+ colors.red('Found .templateroot without a template.cjs, template.mjs, template.js or template.ts file:\n')
38
+ + colors.cyan(rootPath));
39
+ return;
40
+ }
41
+ // TODO:
42
+ // const templateSettings = requireCompat(templateSettingsPath);
43
+ const templateSettings = await import(templateSettingsPath);
44
+ if (getArgv().into) {
45
+ templateSettings.into = getArgv().into;
46
+ }
47
+ if (!templateSettings.into) {
48
+ logError(
49
+ colors.red('Found .templateroot that does not have a "into" export in template file:\n')
50
+ + colors.cyan(templateSettingsPath));
51
+ return;
52
+ }
53
+ if (!templateSettings.download) {
54
+ logError(
55
+ colors.red('Found .templateroot that does not have a "download" export in template file:\n')
56
+ + colors.cyan(templateSettingsPath));
57
+ return;
58
+ }
59
+ const toPath = templateSettings.into[0] === '/' ? templateSettings.into : path.join(rootPath, templateSettings.into);
60
+ utils.log(colors.green('Running download from ') + colors.magenta(templateSettingsPath));
61
+ templateSettings.download(utils.fetch)
62
+ .catch(err => {
63
+ logError(
64
+ colors.red('Hit error when downloading for .templateroot:\n')
65
+ + colors.cyan(templateSettingsPath),
66
+ err);
67
+ })
68
+ .then(json => json && node.visit(json, rootPath, toPath, templateSettings));
69
+ }
70
+
71
+ function determineTemplateFile(rootPath) {
72
+ const cjsTemplate = path.join(rootPath, 'template.cjs');
73
+ const jsTemplate = path.join(rootPath, 'template.js');
74
+ const mjsTemplate = path.join(rootPath, 'template.mjs');
75
+ const tsTemplate = path.join(rootPath, 'template.ts');
76
+ if (fs.existsSync(cjsTemplate)) {
77
+ return cjsTemplate;
78
+ }
79
+ if (fs.existsSync(mjsTemplate)) {
80
+ return mjsTemplate;
81
+ }
82
+ if (fs.existsSync(jsTemplate)) {
83
+ return jsTemplate;
84
+ }
85
+ if (fs.existsSync(tsTemplate)) {
86
+ try {
87
+ requireCompat('ts-node').register();
88
+ }
89
+ catch (err) {
90
+ console.warn(colors.gray('Detected template.ts, but ts-node failed to register:'), err);
91
+ }
92
+ return tsTemplate;
93
+ }
94
+ return null;
95
+ }
@@ -0,0 +1,5 @@
1
+ export const templateArgs = {
2
+ evaluate: /<%([\s\S]+?)%>/g,
3
+ escape: /<%-([\s\S]+?)%>/g,
4
+ interpolate: /<%=([\s\S]+?)%>/g,
5
+ };
@@ -0,0 +1,20 @@
1
+ import { normalizeLineEndings } from './normalizeLineEndings.js';
2
+
3
+ /**
4
+ * Look at two strings to see if they contain the same content, ignoring line endings.
5
+ * @param a
6
+ * @param b
7
+ * @returns {boolean}
8
+ */
9
+ export function contentsDiffer(a, b) {
10
+ // Are a and b both empty?
11
+ if (!a && !b) {
12
+ return false;
13
+ }
14
+ // Is only one of them empty?
15
+ if (!a || !b) {
16
+ return true;
17
+ }
18
+ // Otherwise, compare them while ignoring line endings to see if they're equivalent.
19
+ return normalizeLineEndings(a) !== normalizeLineEndings(b);
20
+ }
@@ -0,0 +1,26 @@
1
+ import colors from 'colors/safe.js';
2
+ import { getArgv } from '../argv.js';
3
+
4
+ /**
5
+ * Logs a message to the console with a nice prefix.
6
+ * @param text The text to write.
7
+ * @param [error] The optional error to log
8
+ * @param writeAsError If we should use console.error or console.log.
9
+ */
10
+ export function log(text, error = null, writeAsError = false) {
11
+ if (getArgv().silent) {
12
+ return;
13
+ }
14
+ let prefix = colors.gray(`[${new Date().toTimeString().split(' ')[0]}] `);
15
+ if (writeAsError) {
16
+ if (error) {
17
+ console.error(prefix + text, error);
18
+ }
19
+ else {
20
+ console.error(prefix + text);
21
+ }
22
+ }
23
+ else {
24
+ console.log(prefix + text);
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ import colors from 'colors/safe.js';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { log } from './log.js';
5
+ import { normalizePath } from './normalizePath.js';
6
+
7
+ /**
8
+ * If the file doesn't exist, logs a message saying it is being created. Otherwise, it's being updated.
9
+ * @param file
10
+ */
11
+ export function logChange(file) {
12
+ let url = normalizePath(typeof file === 'string'
13
+ ? file
14
+ : path.join(file.dirname, file.basename));
15
+ log(colors.cyan(`${fs.existsSync(url) ? 'updating' : 'creating'}: `) + colors.magenta(url));
16
+ }
@@ -0,0 +1,10 @@
1
+ import { log } from './log.js';
2
+
3
+ /**
4
+ * Logs an error.
5
+ * @param text
6
+ * @param [err]
7
+ */
8
+ export function logError(text, err) {
9
+ log(text, err, true);
10
+ }
@@ -0,0 +1,10 @@
1
+ import colors from 'colors/safe.js';
2
+ import { log } from './log.js';
3
+
4
+ /**
5
+ * Logs a message saying the file is being removed.
6
+ * @param url
7
+ */
8
+ export function logRemoval(url) {
9
+ log(colors.red(`removing: `) + colors.magenta(url));
10
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lowers the first letter of the provided string, leaving the rest of it alone.
3
+ * @param val
4
+ * @returns {string}
5
+ */
6
+ export function lowerCaseFirst(val) {
7
+ if (!val || !val.toLowerCase) {
8
+ return val;
9
+ }
10
+ return val[0].toLowerCase() + val.slice(1);
11
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalizes line endings for comparison purposes.
3
+ * @param contents
4
+ * @returns {string}
5
+ */
6
+ export function normalizeLineEndings(contents) {
7
+ return contents.replace(/\r\n?/g, '\n');
8
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Normalizes a path across platforms. (Root / paths are assumed to be on C:\ on windows).
5
+ * @param ref
6
+ * @returns {string}
7
+ */
8
+ export function normalizePath(ref) {
9
+ return ref && ref.startsWith('/') ? path.resolve(ref) : path.normalize(ref);
10
+ }
@@ -0,0 +1,19 @@
1
+ import get from 'lodash.get';
2
+
3
+ /**
4
+ * Given a string with _parameters_ within it, substitute them with the real values from the given context.
5
+ * @param str
6
+ * @param context
7
+ * @returns {string}
8
+ */
9
+ export function parameterizeString(str, context) {
10
+ return str.replace(
11
+ /_[a-z]*\.?[a-z]+_/ig,
12
+ match => {
13
+ const variableName = match.slice(1, -1);
14
+ return get(context, variableName)
15
+ || get(context, variableName.toLowerCase())
16
+ || get(context, variableName.toUpperCase())
17
+ || match;
18
+ });
19
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { normalizePath } from './normalizePath.js';
4
+
5
+ /**
6
+ * Attempts to read in the provided file, returning the string contents if it exists, or null.
7
+ * @param file
8
+ * @returns {string}
9
+ */
10
+ export function safeRead(file) {
11
+ let url = normalizePath(typeof file === 'string'
12
+ ? file
13
+ : path.join(file.dirname, file.basename));
14
+ return fs.existsSync(url)
15
+ ? fs.readFileSync(url, 'utf-8')
16
+ : null;
17
+ }
@@ -0,0 +1,23 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { normalizePath } from './normalizePath.js';
4
+
5
+ /**
6
+ * Writes the contents to the file, making sure the parent directories of the file exist.
7
+ * @param file
8
+ * @param contents
9
+ */
10
+ export function safeWrite(file, contents) {
11
+ const url = normalizePath(typeof file === 'string'
12
+ ? file
13
+ : path.join(file.dirname, file.basename));
14
+ const dirs = url.split(path.sep).slice(0, -1);
15
+ // Note: we start at 1 to avoid trying to create the root directory.
16
+ for (let i = 1; i < dirs.length; i++) {
17
+ let collectivePath = dirs.slice(0, i + 1).join(path.sep);
18
+ if (!fs.existsSync(collectivePath)) {
19
+ fs.mkdirSync(collectivePath);
20
+ }
21
+ }
22
+ fs.writeFileSync(url, contents, 'utf-8');
23
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,16 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ /*
4
+ Public API.
5
+ */
6
+ export { contentsDiffer } from './utils/contentsDiffer.js';
7
+ export { safeRead } from './utils/safeRead.js';
8
+ export { safeWrite } from './utils/safeWrite.js';
9
+ export { log } from './utils/log.js';
10
+ export { logChange } from './utils/logChange.js';
11
+ export { logRemoval } from './utils/logRemoval.js';
12
+ export { logError } from './utils/logError.js';
13
+ export { parameterizeString } from './utils/parameterizeString.js';
14
+ export { lowerCaseFirst } from './utils/lowerCaseFirst.js';
15
+ export { fetch };
16
+ export { normalizePath } from './utils/normalizePath.js';
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "name": "gaffer-generator",
4
+ "description": "Generates code across files based on your templates",
5
+ "author": "Dawson Toth",
6
+ "keywords": [],
7
+ "homepage": ".",
8
+ "type": "module",
9
+ "license": "ISC",
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/",
12
+ "tag": "latest",
13
+ "provenance": true
14
+ },
15
+ "repository": "github:dawsontoth/gaffer-generator",
16
+ "main": "./cli.js",
17
+ "bin": {
18
+ "gaffer-generator": "./cli.js"
19
+ },
20
+ "scripts": {
21
+ "test": "vitest",
22
+ "test:ci": "vitest run",
23
+ "test:coverage": "vitest run --coverage",
24
+ "lint": "eslint .",
25
+ "lint:fix": "eslint . --fix"
26
+ },
27
+ "dependencies": {
28
+ "colors": "^1.4.0",
29
+ "globule": "^1.3.4",
30
+ "lodash.defaults": "^4.2.0",
31
+ "lodash.difference": "^4.5.0",
32
+ "lodash.get": "^4.4.2",
33
+ "lodash.template": "^4.5.0",
34
+ "lodash.uniq": "^4.5.0",
35
+ "node-fetch": "^2.6.7",
36
+ "recursive-copy": "^2.0.14",
37
+ "yargs": "^18.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "^9.39.1",
41
+ "@semantic-release/commit-analyzer": "^13.0.1",
42
+ "@semantic-release/git": "^10.0.1",
43
+ "@semantic-release/github": "^12.0.2",
44
+ "@semantic-release/npm": "^13.1.1",
45
+ "@semantic-release/release-notes-generator": "^14.1.0",
46
+ "@vitest/coverage-v8": "^4.0.8",
47
+ "conventional-changelog-conventionalcommits": "^9.1.0",
48
+ "eslint": "^9.39.1",
49
+ "globals": "^16.5.0",
50
+ "semantic-release": "^25.0.2",
51
+ "ts-node": "^10.9.2",
52
+ "vitest": "^4.0.8"
53
+ }
54
+ }