diff --git a/package-lock.json b/package-lock.json index db4cdeb..e869052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "listr": "0.14.3", "oclif": "3.3.1", "type-fns": "0.7.0", - "uuid": "9.0.0" + "uuid": "9.0.0", + "yaml": "2.2.1" }, "bin": { "sql-schema-generator": "bin/run" @@ -710,12 +711,15 @@ } }, "node_modules/@babel/runtime": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz", - "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==", + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/template": { @@ -7644,6 +7648,18 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/declapract/node_modules/yaml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz", + "integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.4.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -16422,9 +16438,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", - "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, "node_modules/regexp.prototype.flags": { @@ -18092,15 +18108,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz", - "integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.4.5" - }, + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -19430,12 +19442,12 @@ } }, "@babel/runtime": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz", - "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==", + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", "dev": true, "requires": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.11" } }, "@babel/template": { @@ -24654,6 +24666,15 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "dev": true + }, + "yaml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz", + "integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5" + } } } }, @@ -31194,9 +31215,9 @@ } }, "regenerator-runtime": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", - "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, "regexp.prototype.flags": { @@ -32466,13 +32487,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz", - "integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.4.5" - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==" }, "yargs": { "version": "17.6.2", diff --git a/package.json b/package.json index b149ac4..2a31995 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "listr": "0.14.3", "oclif": "3.3.1", "type-fns": "0.7.0", - "uuid": "9.0.0" + "uuid": "9.0.0", + "yaml": "2.2.1" }, "devDependencies": { "@commitlint/cli": "13.1.0", diff --git a/src/contract/__test_assets__/codegen.sql.schema.yml b/src/contract/__test_assets__/codegen.sql.schema.yml new file mode 100644 index 0000000..35cf9d8 --- /dev/null +++ b/src/contract/__test_assets__/codegen.sql.schema.yml @@ -0,0 +1,6 @@ +language: postgres +dialect: 10.7 +declarations: domain.ts +generates: + sql: + to: generated diff --git a/src/contract/commands/generate.integration.test.ts b/src/contract/commands/generate.integration.test.ts index 480f5a1..fc218ec 100644 --- a/src/contract/commands/generate.integration.test.ts +++ b/src/contract/commands/generate.integration.test.ts @@ -3,10 +3,8 @@ import Generate from './generate'; describe('command', () => { it('should be able to generate schema for valid entities declaration', async () => { await Generate.run([ - '-d', - `${__dirname}/../__test_assets__/domain.ts`, - '-t', - `${__dirname}/../__test_assets__/generated`, + '-c', + `${__dirname}/../__test_assets__/codegen.sql.schema.yml`, ]); }); }); diff --git a/src/contract/commands/generate.ts b/src/contract/commands/generate.ts index fb6807a..7161f99 100644 --- a/src/contract/commands/generate.ts +++ b/src/contract/commands/generate.ts @@ -9,29 +9,21 @@ export default class Generate extends Command { public static flags = { help: Flags.help({ char: 'h' }), - declarations: Flags.string({ - char: 'd', - description: 'path to config file, containing entity definitions', - required: true, - default: 'declarations.ts', - }), - target: Flags.string({ - char: 't', - description: 'target directory to record generated schema into', - required: true, - default: 'generated', + config: Flags.string({ + char: 'c', + description: 'path to config file', + required: false, + default: 'codegen.sql.schema.yml', }), }; public async run() { const { flags } = await this.parse(Generate); - const config = flags.declarations!; - const target = flags.target!; + const config = flags.config; + // get and display the plans const configPath = config.slice(0, 1) === '/' ? config : `${process.cwd()}/${config}`; // if starts with /, consider it as an absolute path - const targetDir = - target.slice(0, 1) === '/' ? target : `${process.cwd()}/${target}`; // if starts with /, consider it as an absolute path - await generateSchema({ configPath, targetDirPath: targetDir }); + await generateSchema({ configPath }); } } diff --git a/src/domain/objects/GeneratorConfig.ts b/src/domain/objects/GeneratorConfig.ts new file mode 100644 index 0000000..2977a28 --- /dev/null +++ b/src/domain/objects/GeneratorConfig.ts @@ -0,0 +1,29 @@ +import { DomainObject } from 'domain-objects'; +import Joi from 'joi'; + +export enum DatabaseLanguage { + MYSQL = 'mysql', + POSTGRES = 'postgres', +} + +const schema = Joi.object().keys({ + rootDir: Joi.string().required(), // dir of config file, to which all config paths are relative + language: Joi.string().valid(...Object.values(DatabaseLanguage)), + dialect: Joi.string().required(), + declarationsPath: Joi.string().required(), + targetDirPath: Joi.string().required(), +}); + +export interface GeneratorConfig { + rootDir: string; + language: DatabaseLanguage; + dialect: string; + declarationsPath: string; + targetDirPath: string; +} +export class GeneratorConfig + extends DomainObject + implements GeneratorConfig +{ + public static schema = schema; +} diff --git a/src/logic/compose/generateSchema/generateSchema.test.ts b/src/logic/compose/generateSchema/generateSchema.test.ts index d626445..8496a94 100644 --- a/src/logic/compose/generateSchema/generateSchema.test.ts +++ b/src/logic/compose/generateSchema/generateSchema.test.ts @@ -1,13 +1,19 @@ import { generateAndRecordEntitySchema } from './generateAndRecordEntitySchema'; import { generateSchema } from './generateSchema'; import { normalizeDeclarationContents } from './normalizeDeclarationContents'; +import { readConfig } from './readConfig'; import { readDeclarationFile } from './readDeclarationFile'; -// we ignore this line since we can't define a type declaration for a relative module +jest.mock('./readConfig'); +const readConfigMock = readConfig as jest.Mock; +readConfigMock.mockReturnValue({ + declarationsPath: '__DECLARATIONS_PATH__', + targetDirPath: '__TARGET_DIR_PATH__', +}); jest.mock('./readDeclarationFile'); const readDeclarationFileMock = readDeclarationFile as jest.Mock; -readDeclarationFileMock.mockResolvedValue('__CONFIG_CONTENTS__'); +readDeclarationFileMock.mockResolvedValue('__DECLARATION_CONTENTS__'); jest.mock('./normalizeDeclarationContents'); const normalizeDeclarationContentsMock = @@ -22,39 +28,36 @@ const generateAndRecordEntitySchemaMock = describe('generateSchema', () => { beforeEach(() => jest.clearAllMocks()); - it('should read the config file', async () => { + it('should read the declaration file', async () => { await generateSchema({ configPath: '__CONFIG_PATH__', - targetDirPath: '__TARGET_DIR_PATH__', }); expect(readDeclarationFileMock.mock.calls.length).toEqual(1); expect(readDeclarationFileMock.mock.calls[0][0]).toMatchObject({ - configPath: '__CONFIG_PATH__', + declarationsPath: '__DECLARATIONS_PATH__', }); }); - it('should extract entities from the contents of the config file', async () => { + it('should extract entities from the contents of the declaration file', async () => { await generateSchema({ configPath: '__CONFIG_PATH__', - targetDirPath: '__TARGET_DIR_PATH__', }); expect(normalizeDeclarationContentsMock.mock.calls.length).toEqual(1); expect(normalizeDeclarationContentsMock.mock.calls[0][0]).toMatchObject({ - contents: '__CONFIG_CONTENTS__', + contents: '__DECLARATION_CONTENTS__', }); }); it('should generateAndRecordEntitySchema for each entity defined in the config', async () => { await generateSchema({ configPath: '__CONFIG_PATH__', - targetDirPath: '__TARGET_DIR_PATH__', }); expect(generateAndRecordEntitySchemaMock.mock.calls.length).toEqual(2); expect(generateAndRecordEntitySchemaMock.mock.calls[0][0]).toMatchObject({ - targetDirPath: '__TARGET_DIR_PATH__', entity: { name: '__ENTITY_ONE__' }, + targetDirPath: '__TARGET_DIR_PATH__', }); expect(generateAndRecordEntitySchemaMock.mock.calls[1][0]).toMatchObject({ - targetDirPath: '__TARGET_DIR_PATH__', entity: { name: '__ENTITY_TWO__' }, + targetDirPath: '__TARGET_DIR_PATH__', }); }); }); diff --git a/src/logic/compose/generateSchema/generateSchema.ts b/src/logic/compose/generateSchema/generateSchema.ts index f69be91..f144cf4 100644 --- a/src/logic/compose/generateSchema/generateSchema.ts +++ b/src/logic/compose/generateSchema/generateSchema.ts @@ -3,6 +3,7 @@ import Listr from 'listr'; import { generateAndRecordEntitySchema } from './generateAndRecordEntitySchema'; import { normalizeDeclarationContents } from './normalizeDeclarationContents'; +import { readConfig } from './readConfig'; import { readDeclarationFile } from './readDeclarationFile'; /* @@ -12,13 +13,14 @@ import { readDeclarationFile } from './readDeclarationFile'; */ export const generateSchema = async ({ configPath, - targetDirPath, }: { configPath: string; - targetDirPath: string; }) => { + // 0. read the config file + const { declarationsPath, targetDirPath } = await readConfig({ configPath }); + // 1. read the entities from source file - const contents = await readDeclarationFile({ configPath }); + const contents = await readDeclarationFile({ declarationsPath }); const { entities } = await normalizeDeclarationContents({ contents }); // 2. for each entity: generate and record resources diff --git a/src/logic/compose/generateSchema/readConfig.ts b/src/logic/compose/generateSchema/readConfig.ts new file mode 100644 index 0000000..47b4bdc --- /dev/null +++ b/src/logic/compose/generateSchema/readConfig.ts @@ -0,0 +1,62 @@ +import { + DatabaseLanguage, + GeneratorConfig, +} from '../../../domain/objects/GeneratorConfig'; +import { UserInputError } from '../../../utils/errors/UserInputError'; +import { readYmlFile } from '../../../utils/fileio/readYmlFile'; +import { getDirOfPath } from '../../../utils/filepaths/getDirOfPath'; + +/* + 1. read the config + 2. validate the config +*/ +export const readConfig = async ({ + configPath, +}: { + configPath: string; +}): Promise => { + const configDir = getDirOfPath(configPath); + const getAbsolutePathFromRelativeToConfigPath = (relpath: string) => + `${configDir}/${relpath}`; + + // get the yml + const contents = await readYmlFile({ filePath: configPath }); + + // get the language and dialect + if (!contents.language) + throw new UserInputError({ reason: 'config.language must be defined' }); + const language = contents.language; + if (contents.language && contents.language !== DatabaseLanguage.POSTGRES) + throw new UserInputError({ + reason: + 'dao generator only supports postgres. please update the `language` option in your config to `postgres` to continue', + }); + if (!contents.dialect) + throw new UserInputError({ reason: 'config.dialect must be defined' }); + const dialect = `${contents.dialect}`; // ensure that we read it as a string, as it could be a number + + // validate the output config + if (!contents.declarations) + throw new UserInputError({ + reason: + 'config.declarations must specify path to the file containing declarations', + }); + if (!contents.generates.sql?.to) + throw new UserInputError({ + reason: + 'config.generates.sql.to must specify where to output the generated sql', + }); + + // return the results + return new GeneratorConfig({ + language, + dialect, + rootDir: configDir, + declarationsPath: getAbsolutePathFromRelativeToConfigPath( + contents.declarations, + ), + targetDirPath: getAbsolutePathFromRelativeToConfigPath( + contents.generates.sql.to, + ), + }); +}; diff --git a/src/logic/compose/generateSchema/readDeclarationFile.ts b/src/logic/compose/generateSchema/readDeclarationFile.ts index d575c98..63e1f97 100644 --- a/src/logic/compose/generateSchema/readDeclarationFile.ts +++ b/src/logic/compose/generateSchema/readDeclarationFile.ts @@ -1,8 +1,8 @@ // split out to make tit easier to test, and for historical reasons; can be merged if desired export const readDeclarationFile = async ({ - configPath, + declarationsPath, }: { - configPath: string; + declarationsPath: string; }) => { - return require(configPath); + return require(declarationsPath); }; diff --git a/src/utils/errors/UserInputError.ts b/src/utils/errors/UserInputError.ts new file mode 100644 index 0000000..7ed7c13 --- /dev/null +++ b/src/utils/errors/UserInputError.ts @@ -0,0 +1,23 @@ +export class UserInputError extends Error { + constructor({ + reason, + domainObjectName, + domainObjectPropertyName, + potentialSolution, + }: { + reason: string; + domainObjectName?: string; + domainObjectPropertyName?: string; + potentialSolution?: string; + }) { + super( + ` +User input error. ${reason.replace(/\.$/, '')}. '${domainObjectName}${ + domainObjectPropertyName ? `.${domainObjectPropertyName}` : '' + }' does not meet this criteria. Please correct this and try again.${ + potentialSolution ? `\n\n${potentialSolution}` : '' + } + `.trim(), + ); + } +} diff --git a/src/utils/fileio/makeDirAsync.ts b/src/utils/fileio/makeDirAsync.ts new file mode 100644 index 0000000..ee471c5 --- /dev/null +++ b/src/utils/fileio/makeDirAsync.ts @@ -0,0 +1,11 @@ +import fs from 'fs'; +import util from 'util'; + +// export these from a separate file to make testing easier (i.e., easier to define the mocks) +const mkdir = util.promisify(fs.mkdir); + +export const makeDirectoryAsync = async ({ + directoryPath, +}: { + directoryPath: string; +}) => mkdir(directoryPath, { recursive: true }); diff --git a/src/utils/fileio/readFileAsync.ts b/src/utils/fileio/readFileAsync.ts new file mode 100644 index 0000000..f7d8fb7 --- /dev/null +++ b/src/utils/fileio/readFileAsync.ts @@ -0,0 +1,10 @@ +import fs from 'fs'; +import util from 'util'; + +export const readFile = util.promisify(fs.readFile); + +export const readFileAsync = ({ + filePath, +}: { + filePath: string; +}): Promise => readFile(filePath, 'utf8'); diff --git a/src/utils/fileio/readYmlFile.test.ts b/src/utils/fileio/readYmlFile.test.ts new file mode 100644 index 0000000..469725e --- /dev/null +++ b/src/utils/fileio/readYmlFile.test.ts @@ -0,0 +1,87 @@ +import YAML from 'yaml'; + +import { readFileAsync } from './readFileAsync'; +import { readYmlFile } from './readYmlFile'; + +jest.mock('./readFileAsync'); +const readFileAsyncMock = readFileAsync as jest.Mock; +readFileAsyncMock.mockResolvedValue(` +- type: change + path: 'init/service_user.sql' + id: 'init_20190619_1' + reapplyOnUpdate: false # we'll do it manually for now + +- type: change + id: 'data_20190619_1' + path: 'data/data_sources.sql' + reapplyOnUpdate: false # only run each insert once + +- type: change + id: 'procedures_20190619_1' + path: 'procedures/awesome_sproc.sql' + reapplyOnUpdate: true +`); + +describe('readDefinitionsFileRecursive', () => { + beforeEach(() => jest.clearAllMocks()); + it('should throw an error if the filepath defined does not point to a yml file', async () => { + try { + await readYmlFile({ filePath: 'some.json' }); + throw new Error('should not reach here'); + } catch (error) { + expect(error.message).toMatch('file path point to a .yml file.'); + } + }); + it('should attempt to read the file', async () => { + await readYmlFile({ filePath: 'some.yml' }); + expect(readFileAsyncMock.mock.calls.length).toEqual(1); + expect(readFileAsyncMock.mock.calls[0][0]).toEqual({ + filePath: 'some.yml', + }); + }); + it('should parse the contents into yml with the YAML library', async () => { + const spy = jest.spyOn(YAML, 'parse'); + expect(spy.mock.calls.length).toEqual(0); + await readYmlFile({ filePath: 'some.yml' }); + expect(spy.mock.calls.length).toEqual(1); + }); + it('should return the parsed yml content', async () => { + readFileAsyncMock.mockResolvedValueOnce(` + - type: change + path: 'init/service_user.sql' + id: 'init_20190619_1' + reapplyOnUpdate: false # we'll do it manually for now + + - type: change + id: 'data_20190619_1' + path: 'data/data_sources.sql' + reapplyOnUpdate: false # only run each insert once + + - type: change + id: 'procedures_20190619_1' + path: 'procedures/awesome_sproc.sql' + reapplyOnUpdate: true + `); + const result = await readYmlFile({ filePath: 'some.yml' }); + expect(result).toMatchObject([ + { + type: 'change', + path: 'init/service_user.sql', + id: 'init_20190619_1', + reapplyOnUpdate: false, + }, + { + type: 'change', + id: 'data_20190619_1', + path: 'data/data_sources.sql', + reapplyOnUpdate: false, + }, + { + type: 'change', + id: 'procedures_20190619_1', + path: 'procedures/awesome_sproc.sql', + reapplyOnUpdate: true, + }, + ]); + }); +}); diff --git a/src/utils/fileio/readYmlFile.ts b/src/utils/fileio/readYmlFile.ts new file mode 100644 index 0000000..cbaf61b --- /dev/null +++ b/src/utils/fileio/readYmlFile.ts @@ -0,0 +1,18 @@ +import YAML from 'yaml'; + +import { readFileAsync } from './readFileAsync'; + +export const readYmlFile = async ({ filePath }: { filePath: string }) => { + // check path is for yml file + if (filePath.slice(-4) !== '.yml') + throw new Error(`file path point to a .yml file. error: ${filePath}`); + + // get file contents + const stringContent = await readFileAsync({ filePath }); + + // parse the string content into yml + const content = YAML.parse(stringContent); + + // return the content + return content; +}; diff --git a/src/utils/fileio/writeFileAsync.ts b/src/utils/fileio/writeFileAsync.ts new file mode 100644 index 0000000..d898ae6 --- /dev/null +++ b/src/utils/fileio/writeFileAsync.ts @@ -0,0 +1,14 @@ +import fs from 'fs'; +import util from 'util'; + +// export these from a seperate file to make testing easier (i.e., easier to define the mocks) +export const mkdir = util.promisify(fs.mkdir); +const writeFile = util.promisify(fs.writeFile); + +export const writeFileAsync = async ({ + path, + content, +}: { + path: string; + content: string; +}) => writeFile(path, content); diff --git a/src/utils/filepaths/getDirOfPath.ts b/src/utils/filepaths/getDirOfPath.ts new file mode 100644 index 0000000..e207026 --- /dev/null +++ b/src/utils/filepaths/getDirOfPath.ts @@ -0,0 +1,2 @@ +export const getDirOfPath = (path: string) => + path.split('/').slice(0, -1).join('/'); // drops the file name