From bfa8f4e85dbd47c01c47becf12bde8dfff14fb5d Mon Sep 17 00:00:00 2001 From: billybillydev Date: Wed, 19 Jun 2024 11:09:42 +0200 Subject: [PATCH] [feat]: added --input-dir argument --- CHANGELOG.md | 6 + bun.lockb | Bin 235259 -> 235612 bytes jsconfig.json | 2 +- package.json | 3 +- src/commands/convert.js | 33 ++-- src/commands/watch.js | 40 +++-- src/index.js | 2 +- src/lib/utils.js | 83 +++++++++++ src/types/index.js | 2 +- tests/convert.test.js | 112 ++++++++++++-- tests/e2e.test.js | 45 +++++- tests/expected-directory-to-json-output.js | 18 +++ tests/expected-e2e-output.js | 16 +- tests/expected-js-doc-output.js | 14 ++ tests/expected-ts-output.js | 38 ++++- tests/inputs/empty-object.json | 1 + tests/inputs/mocks/mock-three.json | 166 +++++++++++++++++++++ tests/inputs/mocks/mock.one.json | 6 + tests/inputs/mocks/mock_two.json | 6 + tests/outputs/mock-directory-over-file.ts | 18 +++ tests/outputs/mock-directory.ts | 18 +++ tests/outputs/mock2.js | 14 +- tests/utils.test.js | 28 ++++ 23 files changed, 620 insertions(+), 51 deletions(-) create mode 100644 tests/expected-directory-to-json-output.js create mode 100644 tests/inputs/empty-object.json create mode 100644 tests/inputs/mocks/mock-three.json create mode 100644 tests/inputs/mocks/mock.one.json create mode 100644 tests/inputs/mocks/mock_two.json create mode 100644 tests/outputs/mock-directory-over-file.ts create mode 100644 tests/outputs/mock-directory.ts create mode 100644 tests/utils.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc21a3..1b55f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # balukajs +## 1.4.0 + +### Minor Changes + +- added -I / --input-dir to manage directory conversion. + ## 1.3.0 ### Minor Changes diff --git a/bun.lockb b/bun.lockb index faca4cd12a3b4278e5190bc8ca0e87c349bd5c31..eeefd904b8108af076b56faddf75a86158916dc4 100755 GIT binary patch delta 41431 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 diff --git a/jsconfig.json b/jsconfig.json index 32e74f1..a071476 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "CommonJS", + "module": "ESNext", "target": "ES6", /* Bundler mode */ diff --git a/package.json b/package.json index 51dc964..1656da3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "balukajs", - "version": "1.3.0", + "version": "1.4.0", "main": "src/index.js", "author": { "name": "billybillydev", @@ -57,6 +57,7 @@ "change-case": "^5.4.4", "chokidar": "^3.6.0", "commander": "^12.1.0", + "dir-to-json": "^1.0.0", "jest": "^29.7.0", "tsup": "^8.0.2" } diff --git a/src/commands/convert.js b/src/commands/convert.js index 9ff367a..9f0d08a 100644 --- a/src/commands/convert.js +++ b/src/commands/convert.js @@ -1,14 +1,17 @@ import { jsonToJSDocTypes } from "$lib/json-to-jsdoc"; import { buildJsonSchema } from "$lib/json-to-schema"; import { jsonToTSInterface } from "$lib/json-to-ts-interface"; +import { organizeToJSON } from "$lib/utils"; import chalk from "chalk"; import fs from "fs"; +import { lstat } from "node:fs/promises"; import path from "path"; /** * Function responsible for converting input into formatted output * @param {Object} data - * @param {string} data.input + * @param {string=} data.input + * @param {string=} data.inputDir * @param {string=} data.output * @param {string=} data.name * @param {FormatType=} data.format @@ -16,19 +19,27 @@ import path from "path"; * @example * convert({ input: "example.json", format: "jsdoc", name: "IExample" }) */ -export function convert({ input, output, name, format }) { - if (!input) { - throw new Error("Missing -i or --input option"); +export async function convert({ input, inputDir, output, name, format }) { + const source = inputDir ?? input; + if (!source) { + throw new Error("Missing -i / --input and -I / --input-dir arguments"); } - if (!fs.existsSync(input)) { - throw new Error("No file"); + if (!fs.existsSync(source)) { + throw new Error("No file or directory"); } - - if (path.extname(input) !== ".json") { - throw new Error("File is not json"); + let data; + const stats = await lstat(source); + if (stats.isDirectory()) { + data = JSON.stringify(await organizeToJSON(source)); + } else { + if (path.extname(input) !== ".json") { + throw new Error("File is not json"); + } + data = fs.readFileSync(input, "utf-8"); + } + if (!data) { + throw new Error("Data is null or undefined"); } - - const data = fs.readFileSync(input, "utf-8"); let result = ""; const defaultName = "MyType"; diff --git a/src/commands/watch.js b/src/commands/watch.js index 5b91747..2e38e2f 100644 --- a/src/commands/watch.js +++ b/src/commands/watch.js @@ -3,15 +3,37 @@ import chokidar from "chokidar"; /** * @param {Object} data - * @param {string} data.input - * @param {string=} data.output - * @param {string=} data.name - * @param {FormatType=} data.format + * @param {string} data.input + * @param {string} data.inputDir + * @param {string=} data.output + * @param {string=} data.name + * @param {FormatType=} data.format * @returns {void} */ -export function watch({ input, output, name, format }) { - chokidar.watch(input).on("change", () => { - console.log("File changed, re-running conversion..."); - convert({ input, output, name, format }); - }); +export function watch({ input, inputDir, output, name, format }) { + if (inputDir) { + chokidar + .watch(inputDir) + .on("ready", () => { + console.log("Scan completed, running conversion..."); + convert({ inputDir, output, name, format }); + }) + .on("change", (path) => { + console.log("%s updated, re-running conversion...", path); + convert({ inputDir, output, name, format }); + }) + .on("unlink", (path) => { + console.log("File at path: % removed, re-running conversion...", path); + convert({ inputDir, output, name, format }); + }) + .on("unlinkDir", (path) => { + console.log("Directory at path: %s removed, re-running conversion...", path); + convert({ inputDir, output, name, format }); + }); + } else if (input) { + chokidar.watch(input).on("change", (path) => { + console.log("File at path: %s changed, re-running conversion...", path); + convert({ input, output, name, format }); + }); + } } diff --git a/src/index.js b/src/index.js index caec8fc..e3e8713 100755 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ async function main() { "display the version number" ) .option("-i, --input ", "input JSON file") + .option("-I, --input-dir ", "input directory containing directories and json files") .option("-o, --output ", "output file") .option("--name ", "name of the type") .option("--format ", "output format (jsdoc, ts or schema)", "jsdoc") @@ -30,7 +31,6 @@ async function main() { if (process.argv.length <= 2) { program.help(); } - const { watch, ...restProps } = program.opts(); if (watch) { diff --git a/src/lib/utils.js b/src/lib/utils.js index 05136c0..1dd4d80 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,3 +1,22 @@ +import dirToJson from "dir-to-json"; +import path from "path"; +import fs from "fs"; +import { camelCase } from "change-case"; + +/** + * @typedef {object} StructureInfo + * @property {string} parent + * @property {string} path + * @property {string} name + * @property {"directory" | "file"} type + * @property {StructureInfo[]} [children] + */ + +/** + * @typedef {object} SimpleStructureInfo + * @type {Record} + */ + /** * @param {object[]} objectArray * @returns {string[]} @@ -17,3 +36,67 @@ export function detectOptionalProperties(objectArray) { return optionalProperties; } + +/** + * Takes a directory path and transform its structure to json tree object + * @param {string} directoryPath + * @returns {Object} + */ +export async function organizeToJSON(directoryPath) { + /** @type {StructureInfo} */ + const directoryObject = await dirToJson(directoryPath); + const simpleObject = await simplifyObject(directoryObject, directoryPath); + return sortObjectByKeys(simpleObject); +} + +/** + * Takes a StructureInfo object and returns a SimpleStructureInfo + * @param {StructureInfo} obj + * @param {string} rootPath + * @returns {SimpleStructureInfo} + */ +async function simplifyObject(obj, rootPath) { + const simpleObject = {}; + if (obj.type === "directory") { + for await (const child of obj.children) { + const childObject = await simplifyObject(child, rootPath); + if (childObject) { + if (child.type === "file") { + Object.entries(childObject).forEach(([k, v]) => { + simpleObject[k] = v; + }); + } else { + const name = + child.name.includes(".") || child.name.includes("-") + ? camelCase(child.name) + : child.name; + simpleObject[name] = childObject; + } + } + } + } else if (obj.type === "file") { + const filePath = path.join(rootPath, obj.path); + const isFileExists = await fs.existsSync(filePath); + const fileExtension = path.extname(filePath); + if (isFileExists) { + if (fileExtension !== ".json") { + return null; + } + const fileName = path.basename(filePath, fileExtension); + const file = await fs.readFileSync(filePath, "utf-8"); + const name = + fileName.includes(".") || fileName.includes("-") + ? camelCase(fileName) + : fileName; + simpleObject[name] = file; + } + } + return simpleObject; +} + +export function sortObjectByKeys(obj) { + // Convert object to an array of key-value pairs, sort it, and convert back to an object + return Object.fromEntries( + Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + ); +} \ No newline at end of file diff --git a/src/types/index.js b/src/types/index.js index fa96ece..a594efd 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -1,3 +1,3 @@ /** - * @typedef {"json" | "ts" | "schema"} FormatType + * @typedef {"jsdoc" | "ts" | "schema"} FormatType */ \ No newline at end of file diff --git a/tests/convert.test.js b/tests/convert.test.js index bcf7e26..cb85983 100644 --- a/tests/convert.test.js +++ b/tests/convert.test.js @@ -1,16 +1,31 @@ import { convert } from "$commands/convert"; -import { describe, expect, it, spyOn } from "bun:test"; +import { describe, expect, it, spyOn, beforeAll } from "bun:test"; import fs from "fs"; import { access } from "fs/promises"; import path from "path"; -import { expectedJSDocOutput2 } from "tests/expected-js-doc-output"; -import { expectedTsOutput2 } from "tests/expected-ts-output"; +import { + expectedDirectoryToJSDoc, + expectedJSDocOutput2, +} from "tests/expected-js-doc-output"; import expectedSchemaOutput from "tests/expected-schema-output.json"; +import { + expectedDirectoryOverFileToTs, + expectedDirectoryToTs, + expectedTsOutput2, +} from "tests/expected-ts-output"; + +beforeAll(async () => { + const emptyDir = path.join(__dirname, "./inputs/empty-directory"); + const isDirExists = await fs.existsSync(emptyDir); + if (!isDirExists) { + await fs.mkdirSync(emptyDir); + } +}); describe("ConvertCommandNoInput", () => { - it("should throw error if input is missing", () => { + it("should throw error if input or inputDir are missing", () => { expect(() => convert({ name: "MyType" })).toThrowError( - "Missing -i or --input option" + "Missing -i / --input and -I / --input-dir arguments" ); }); }); @@ -18,10 +33,30 @@ describe("ConvertCommandNoInput", () => { describe("ConvertCommandFileNotExist", () => { it("should throw error if file does not exist", () => { const input = path.join(__dirname, "./inputs/not-existed-file.txt"); - expect(() => convert({ input, name: "MyType" })).toThrowError("No file"); + expect(() => convert({ input, name: "MyType" })).toThrowError( + "No file or directory" + ); }); }); +describe("ConvertCommandDirectoryNotExist", () => { + it("should throw error if directory does not exist", () => { + const inputDir = path.join(__dirname, "./inputs/not-existed-directory"); + expect(() => convert({ inputDir, name: "MyType" })).toThrowError( + "No file or directory" + ); + }); +}); + +// describe("ConvertCommandDataIsNullOrUndefined", () => { +// it("should throw error if data is null or undefined", () => { +// const input = path.join(__dirname, "./inputs/empty-object.json"); +// expect(() => convert({ input, name: "MyType", format: "ts" })).toThrowError( +// "Data is null or undefined" +// ); +// }); +// }); + describe("ConvertCommandNonJsonFile", () => { it("should throw error if file is not json", () => { const input = path.join(__dirname, "./inputs/non-json-file.txt"); @@ -49,6 +84,14 @@ describe("ConvertCommandForJsdoc", () => { console.log(expectedJSDocOutput2); expect(consoleSpy).toHaveBeenCalledWith(expectedJSDocOutput2); }); + + it("should directory and output JSDoc type definitions", () => { + const consoleSpy = spyOn(console, "log"); + const inputDir = path.join(__dirname, "./inputs"); + convert({ inputDir, name: "MyType", format: "jsdoc" }); + console.log(expectedDirectoryToJSDoc); + expect(consoleSpy).toHaveBeenCalledWith(expectedDirectoryToJSDoc); + }); }); describe("ConvertCommandForTypescript", () => { @@ -59,23 +102,31 @@ describe("ConvertCommandForTypescript", () => { console.log(expectedTsOutput2); expect(consoleSpy).toHaveBeenCalledWith(expectedTsOutput2); }); + + it("should read directory and output typescript interfaces", () => { + const consoleSpy = spyOn(console, "log"); + const inputDir = path.join(__dirname, "./inputs"); + convert({ inputDir, name: "MyType", format: "ts" }); + console.log(expectedDirectoryToJSDoc); + expect(consoleSpy).toHaveBeenCalledWith(expectedDirectoryToJSDoc); + }); }); describe("ConvertCommandForSchema", () => { it("should read JSON file and output json schema", () => { const consoleSpy = spyOn(console, "log"); const input = path.join(__dirname, "./inputs/mock3.json"); - convert({ input, format: "ts" }); + convert({ input, format: "schema" }); console.log(expectedSchemaOutput); expect(consoleSpy).toHaveBeenCalledWith(expectedSchemaOutput); }); }); describe("ConvertCommandWithOutputFile", () => { - it("should create output file", async () => { + it("should create output file from input file", async () => { const inputPath = path.join(__dirname, "./inputs/mock2.json"); const outputPath = path.join(__dirname, "./outputs/mock2.ts"); - convert({ + await convert({ input: inputPath, name: "MyType", format: "ts", @@ -89,4 +140,47 @@ describe("ConvertCommandWithOutputFile", () => { expect(outputData.trim()).toBe(expectedTsOutput2.trim()); }); + + it("should create output file from input directory", async () => { + const inputDirPath = path.join(__dirname, "./inputs"); + const outputPath = path.join(__dirname, "./outputs/mock-directory.ts"); + await convert({ + inputDir: inputDirPath, + name: "MyDirectoryType", + format: "ts", + output: outputPath, + }); + const isOutputFileExists = await access(outputPath) + .then(() => true) + .catch(() => false); + expect(isOutputFileExists).toBeTrue(); + const outputData = fs.readFileSync(outputPath, "utf-8"); + + expect(outputData.trim()).toBe(expectedDirectoryToTs); + }); +}); + +describe("ConvertCommandDirectoryOverFile", () => { + it("should convert directory over file if passed -I and -i arguments", async () => { + const inputDirPath = path.join(__dirname, "./inputs"); + const inputPath = path.join(__dirname, "./inputs/mock2.json"); + const outputPath = path.join( + __dirname, + "./outputs/mock-directory-over-file.ts" + ); + await convert({ + inputDir: inputDirPath, + input: inputPath, + name: "MyDirectoryOverFileType", + format: "ts", + output: outputPath, + }); + const isOutputFileExists = await access(outputPath) + .then(() => true) + .catch(() => false); + expect(isOutputFileExists).toBeTrue(); + const outputData = fs.readFileSync(outputPath, "utf-8"); + + expect(outputData.trim().length).toEqual(expectedDirectoryOverFileToTs.trim().length); + }); }); diff --git a/tests/e2e.test.js b/tests/e2e.test.js index cbe24ca..d31acde 100644 --- a/tests/e2e.test.js +++ b/tests/e2e.test.js @@ -1,14 +1,30 @@ import { $ } from "bun"; -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, beforeAll } from "bun:test"; import fs from "fs"; +import path from "path"; import { expectedE2EOutput } from "tests/expected-e2e-output"; -import { expectedJSDocOutput2, expectedJSDocOutput3 } from "tests/expected-js-doc-output"; -import { expectedTsOutput3 } from "tests/expected-ts-output"; +import { + expectedDirectoryToJSDoc, + expectedJSDocOutput2, + expectedJSDocOutput3, +} from "tests/expected-js-doc-output"; +import { + expectedDirectoryToTs, + expectedTsOutput3, +} from "tests/expected-ts-output"; import expectedSchemaOutput from "tests/expected-schema-output.json"; +beforeAll(async () => { + const emptyDir = path.join(__dirname, "./inputs/empty-directory"); + const isDirExists = await fs.existsSync(emptyDir); + if (!isDirExists) { + await fs.mkdirSync(emptyDir); + } +}); + describe("End-to-End Tests", () => { it("should print help when zero arguments passed", async () => { - const output = (await $`bun run dist/index.js`.text()).trim(); + const output = (await $`bun start`.text()).trim(); const expectedOutput = expectedE2EOutput.trim(); expect(output).toBe(expectedOutput); }); @@ -22,6 +38,24 @@ describe("End-to-End Tests", () => { expect(output.trim()).toBe(expectedJSDocOutput2.trim()); }); + it("should convert directory to JSDoc types and write to output file when --format is missing", async () => { + const inputDirPath = "tests/inputs"; + const outputPath = "tests/outputs/mock2.js"; + await $`bun start -I ${inputDirPath} -o ${outputPath}`; + + const output = fs.readFileSync(outputPath, "utf-8"); + expect(output.trim()).toBe(expectedDirectoryToJSDoc.trim()); + }); + + it("should convert directory to TS types and write to output file when --format is ts", async () => { + const inputDirPath = "tests/inputs"; + const outputPath = "tests/outputs/mock-directory.ts"; + await $`bun start -I ${inputDirPath} -o ${outputPath} --format ts --name MyDirectoryType`; + + const output = fs.readFileSync(outputPath, "utf-8"); + expect(output.trim()).toBe(expectedDirectoryToTs.trim()); + }); + it("should convert JSON to JSDoc types and print output", async () => { const inputPath = "tests/inputs/mock2.json"; const output = await $`bun start -i ${inputPath} --format jsdoc`.text(); @@ -31,7 +65,8 @@ describe("End-to-End Tests", () => { it("should convert JSON to TS types and print output", async () => { const inputPath = "tests/inputs/mock3.json"; - const output = await $`bun start -i ${inputPath} --name IExample --format ts`.text(); + const output = + await $`bun start -i ${inputPath} --name IExample --format ts`.text(); expect(output.trim()).toBe(expectedTsOutput3.trim()); }); diff --git a/tests/expected-directory-to-json-output.js b/tests/expected-directory-to-json-output.js new file mode 100644 index 0000000..19d56af --- /dev/null +++ b/tests/expected-directory-to-json-output.js @@ -0,0 +1,18 @@ +export const expectedDirectoryToJsonOutput = { + mocks: { + mockThree: + '{\n "home": {\n "seo": {\n "title": "Agence Mosi"\n },\n "content": {\n "activities": ["évènementiel", "marketing", "communication digitale"],\n "motto": ["être meilleur", "pour les autres"],\n "communication": "communication"\n }\n },\n "about": {\n "seo": {\n "title": "A propos de nous"\n },\n "content": {\n "title": ["qui sommes", "nous ?"],\n "presentation": "MOSI Agency » L\'Expertise IT Services 36O°.",\n "specialization": "MOSI AGENCY est une entreprise spécialisée dans la communication globale offrant une vaste gamme de services, notamment la production de supports visuels diversifiés et des conseils en communication.",\n "expertise": "Notre expertise s\'étend également à l\'ingénierie informatique, permettant des solutions novatrices à moindre coût. Dans ce domaine, nos services incluent :",\n "sections": [\n {\n "label": "ingénierie informatique",\n "items": [\n "Développement de logiciels sur mesure",\n "Solutions informatiques adaptées aux besoins spécifiques",\n "Intégration de systèmes"\n ]\n },\n {\n "label": "services de communication",\n "items": [\n "Production de supports visuels variés",\n "Conseil en stratégies de communication",\n "Conception de publicités",\n "Reportages",\n "Community Management",\n "Création de sites web"\n ]\n }\n ]\n }\n },\n "contact": {\n "seo": {\n "title": "Contactez-nous"\n },\n "content": {\n "address": "Meudon la forêt, 92360",\n "telephone": "+33663741976",\n "email": "contact@mosi-agency",\n "partnership": {\n "label": "En partenariat avec :",\n "name": "icell technologies"\n }\n }\n },\n "services": {\n "seo": {\n "title": "Nos Services"\n },\n "content": {\n "title": "nos services",\n "items": [\n "organisation de célébrations festives",\n "organisation d\'événements culturels",\n "mise en place d\'un réseau d\'influence",\n "coordination d\'une compagne marketing",\n "optimisation sur google (seo)",\n "services google analytics",\n "google ads",\n "publicité payante sur les réseaux sociaux (meta ads)",\n "marketing des réseaux sociaux",\n "personal branding",\n "création de contenus",\n "conception et développement de sites web",\n "développement d\'applications mobiles",\n "hébergement web & nom de domaine",\n "adresses e-mails personnalisées",\n "design graphique (infographie)",\n "design de documents d\'entreprises (en-tête, factures,..)",\n "publicités & reportages",\n "infogérance (maintenance matérielle & logicielle)"\n ]\n }\n },\n "notFound": {\n "seo": {\n "title": "Page non trouvée"\n }\n },\n "menu": [\n { "key": "home", "link": "/", "text": "accueil" },\n { "key": "about", "link": "/a-propos-de-nous", "text": "a propos de nous" },\n { "key": "contact", "link": "/contact", "text": "contact" },\n { "key": "services", "link": "/services", "text": "nos services" },\n {\n "key": "pricing",\n "link": "/nos-tarifs",\n "text": "tarifs",\n "children": [\n {\n "key": "event-planning",\n "link": "/event-planning",\n "text": "service évènementiel"\n },\n {\n "key": "marketing-campaign",\n "link": "/marketing-campaign",\n "text": "campagnes marketing"\n },\n {\n "key": "design-and-multimedia",\n "link": "/design-and-multimedia",\n "text": "design & multimedia"\n },\n {\n "key": "social-media-management",\n "link": "/social-media-management",\n "text": "gestion social media"\n },\n {\n "key": "website-conception",\n "link": "/website-conception",\n "text": "création sites web"\n }\n ]\n }\n ],\n "footer": {\n "copyright": "Tout droits réservés",\n "navigation": [\n { "key": "sitemap", "link": "/sitemap", "text": "Plan du site" },\n {\n "key": "legal-notice",\n "link": "/legal-notice",\n "text": "Mentions légales"\n },\n {\n "key": "terms-conditions",\n "link": "/terms-conditions",\n "text": "Conditions générales"\n },\n {\n "key": "data-protection",\n "link": "/data-protection",\n "text": "Protection des données"\n },\n {\n "key": "cookie-management",\n "link": "/cookie-management",\n "text": "Paramètres des cookies"\n },\n {\n "key": "faq",\n "link": "/faq",\n "text": "FAQ"\n }\n ],\n "socialNetworks": [\n { "key": "twitter", "link": "#", "icon": "twitter-x-fill" },\n { "key": "instagram", "link": "#", "icon": "instagram-fill" },\n { "key": "linkedin", "link": "#", "icon": "linkedin-box-fill" }\n ]\n }\n}\n', + mockOne: + '{\n "userId": 1,\n "id": 1,\n "title": "delectus aut autem",\n "completed": false\n}\n', + mock_two: + '{\n "home": {\n "seo": { "title": "Test" },\n "content": { "activities": ["a", "b"] }\n }\n}\n', + }, + emptyDirectory: {}, + mock1: + '{\n "userId": 1,\n "id": 1,\n "title": "delectus aut autem",\n "completed": false\n}\n', + mock2: + '{\n "home": {\n "seo": { "title": "Test" },\n "content": { "activities": ["a", "b"] }\n }\n}\n', + emptyObject: "{}", + mock3: + '{\n "home": {\n "seo": {\n "title": "Agence Mosi"\n },\n "content": {\n "activities": ["évènementiel", "marketing", "communication digitale"],\n "motto": ["être meilleur", "pour les autres"],\n "communication": "communication"\n }\n },\n "about": {\n "seo": {\n "title": "A propos de nous"\n },\n "content": {\n "title": ["qui sommes", "nous ?"],\n "presentation": "MOSI Agency » L’Expertise IT Services 36O°.",\n "specialization": "MOSI AGENCY est une entreprise spécialisée dans la communication globale offrant une vaste gamme de services, notamment la production de supports visuels diversifiés et des conseils en communication.",\n "expertise": "Notre expertise s\'étend également à l\'ingénierie informatique, permettant des solutions novatrices à moindre coût. Dans ce domaine, nos services incluent :",\n "sections": [\n {\n "label": "ingénierie informatique",\n "items": [\n "Développement de logiciels sur mesure",\n "Solutions informatiques adaptées aux besoins spécifiques",\n "Intégration de systèmes"\n ]\n },\n {\n "label": "services de communication",\n "items": [\n "Production de supports visuels variés",\n "Conseil en stratégies de communication",\n "Conception de publicités",\n "Reportages",\n "Community Management",\n "Création de sites web"\n ]\n }\n ]\n }\n },\n "contact": {\n "seo": {\n "title": "Contactez-nous"\n },\n "content": {\n "address": "Meudon la forêt, 92360",\n "telephone": "+33663741976",\n "email": "contact@mosi-agency",\n "partnership": {\n "label": "En partenariat avec :",\n "name": "icell technologies"\n }\n }\n },\n "services": {\n "seo": {\n "title": "Nos Services"\n },\n "content": {\n "title": "nos services",\n "items": [\n "organisation de célébrations festives",\n "organisation d\'événements culturels",\n "mise en place d\'un réseau d\'influence",\n "coordination d\'une compagne marketing",\n "optimisation sur google (seo)",\n "services google analytics",\n "google ads",\n "publicité payante sur les réseaux sociaux (meta ads)",\n "marketing des réseaux sociaux",\n "personal branding",\n "création de contenus",\n "conception et développement de sites web",\n "développement d\'applications mobiles",\n "hébergement web & nom de domaine",\n "adresses e-mails personnalisées",\n "design graphique (infographie)",\n "design de documents d\'entreprises (en-tête, factures,..)",\n "publicités & reportages",\n "infogérance (maintenance matérielle & logicielle)"\n ]\n }\n },\n "notFound": {\n "seo": {\n "title": "Page non trouvée"\n }\n },\n "menu": [\n { "key": "home", "link": "/", "text": "accueil" },\n { "key": "about", "link": "/a-propos-de-nous", "text": "a propos de nous" },\n { "key": "contact", "link": "/contact", "text": "contact" },\n { "key": "services", "link": "/services", "text": "nos services" },\n {\n "key": "pricing",\n "link": "/nos-tarifs",\n "text": "tarifs",\n "children": [\n {\n "key": "event-planning",\n "link": "/event-planning",\n "text": "service évènementiel"\n },\n {\n "key": "marketing-campaign",\n "link": "/marketing-campaign",\n "text": "campagnes marketing"\n },\n {\n "key": "design-and-multimedia",\n "link": "/design-and-multimedia",\n "text": "design & multimedia"\n },\n {\n "key": "social-media-management",\n "link": "/social-media-management",\n "text": "gestion social media"\n },\n {\n "key": "website-conception",\n "link": "/website-conception",\n "text": "création sites web"\n }\n ]\n }\n ],\n "footer": {\n "copyright": "Tout droits réservés",\n "navigation": [\n { "key": "sitemap", "link": "/sitemap", "text": "Plan du site" },\n {\n "key": "legal-notice",\n "link": "/legal-notice",\n "text": "Mentions légales"\n },\n {\n "key": "terms-conditions",\n "link": "/terms-conditions",\n "text": "Conditions générales"\n },\n {\n "key": "data-protection",\n "link": "/data-protection",\n "text": "Protection des données"\n },\n {\n "key": "cookie-management",\n "link": "/cookie-management",\n "text": "Paramètres des cookies"\n },\n {\n "key": "faq",\n "link": "/faq",\n "text": "FAQ"\n }\n ],\n "socialNetworks": [\n { "key": "twitter", "link": "#", "icon": "twitter-x-fill" },\n { "key": "instagram", "link": "#", "icon": "instagram-fill" },\n { "key": "linkedin", "link": "#", "icon": "linkedin-box-fill" }\n ]\n }\n}\n', +}; diff --git a/tests/expected-e2e-output.js b/tests/expected-e2e-output.js index a9db2d1..ecfdf53 100644 --- a/tests/expected-e2e-output.js +++ b/tests/expected-e2e-output.js @@ -3,10 +3,12 @@ export const expectedE2EOutput = `Usage: blk [options] Transform json file into jsdoc or typescript definition Options: - -v, --version display the version number - -i, --input input JSON file - -o, --output output file - --name name of the type - --format output format (jsdoc, ts or schema) (default: "jsdoc") - --watch watch for changes - -h, --help display help for command`; \ No newline at end of file + -v, --version display the version number + -i, --input input JSON file + -I, --input-dir input directory containing directories and json files + -o, --output output file + --name name of the type + --format output format (jsdoc, ts or schema) (default: + "jsdoc") + --watch watch for changes + -h, --help display help for command`; \ No newline at end of file diff --git a/tests/expected-js-doc-output.js b/tests/expected-js-doc-output.js index 866c49e..13b2fa8 100644 --- a/tests/expected-js-doc-output.js +++ b/tests/expected-js-doc-output.js @@ -66,4 +66,18 @@ export const expectedJSDocOutput2 = `/** * @property {string} footer.socialNetworks[].link * @property {string} footer.socialNetworks[].icon */ +`; + +export const expectedDirectoryToJSDoc = `/** + * @typedef {object} MyType + * @property {object} emptyDirectory + * @property {string} emptyObject + * @property {string} mock1 + * @property {string} mock2 + * @property {string} mock3 + * @property {object} mocks + * @property {string} mocks.mockThree + * @property {string} mocks.mockOne + * @property {string} mocks.mock_two + */ `; \ No newline at end of file diff --git a/tests/expected-ts-output.js b/tests/expected-ts-output.js index c3e9c02..b6f17be 100644 --- a/tests/expected-ts-output.js +++ b/tests/expected-ts-output.js @@ -136,4 +136,40 @@ export interface HomeSeo { title: string; } -`; \ No newline at end of file +`; + +export const expectedDirectoryToTs = `export interface MyDirectoryType { + emptyDirectory: EmptyDirectory; + emptyObject: string; + mock1: string; + mock2: string; + mock3: string; + mocks: Mocks; +} + +export interface Mocks { + mockThree: string; + mockOne: string; + mock_two: string; +} + +export interface EmptyDirectory { +}`; + +export const expectedDirectoryOverFileToTs = `export interface MyDirectoryOverFileType { + emptyDirectory: EmptyDirectory; + emptyObject: string; + mock1: string; + mock2: string; + mock3: string; + mocks: Mocks; +} + +export interface Mocks { + mockThree: string; + mock_two: string; + mockOne: string; +} + +export interface EmptyDirectory { +}`; \ No newline at end of file diff --git a/tests/inputs/empty-object.json b/tests/inputs/empty-object.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/tests/inputs/empty-object.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/inputs/mocks/mock-three.json b/tests/inputs/mocks/mock-three.json new file mode 100644 index 0000000..55a395e --- /dev/null +++ b/tests/inputs/mocks/mock-three.json @@ -0,0 +1,166 @@ +{ + "home": { + "seo": { + "title": "Agence Mosi" + }, + "content": { + "activities": ["évènementiel", "marketing", "communication digitale"], + "motto": ["être meilleur", "pour les autres"], + "communication": "communication" + } + }, + "about": { + "seo": { + "title": "A propos de nous" + }, + "content": { + "title": ["qui sommes", "nous ?"], + "presentation": "MOSI Agency » L'Expertise IT Services 36O°.", + "specialization": "MOSI AGENCY est une entreprise spécialisée dans la communication globale offrant une vaste gamme de services, notamment la production de supports visuels diversifiés et des conseils en communication.", + "expertise": "Notre expertise s'étend également à l'ingénierie informatique, permettant des solutions novatrices à moindre coût. Dans ce domaine, nos services incluent :", + "sections": [ + { + "label": "ingénierie informatique", + "items": [ + "Développement de logiciels sur mesure", + "Solutions informatiques adaptées aux besoins spécifiques", + "Intégration de systèmes" + ] + }, + { + "label": "services de communication", + "items": [ + "Production de supports visuels variés", + "Conseil en stratégies de communication", + "Conception de publicités", + "Reportages", + "Community Management", + "Création de sites web" + ] + } + ] + } + }, + "contact": { + "seo": { + "title": "Contactez-nous" + }, + "content": { + "address": "Meudon la forêt, 92360", + "telephone": "+33663741976", + "email": "contact@mosi-agency", + "partnership": { + "label": "En partenariat avec :", + "name": "icell technologies" + } + } + }, + "services": { + "seo": { + "title": "Nos Services" + }, + "content": { + "title": "nos services", + "items": [ + "organisation de célébrations festives", + "organisation d'événements culturels", + "mise en place d'un réseau d'influence", + "coordination d'une compagne marketing", + "optimisation sur google (seo)", + "services google analytics", + "google ads", + "publicité payante sur les réseaux sociaux (meta ads)", + "marketing des réseaux sociaux", + "personal branding", + "création de contenus", + "conception et développement de sites web", + "développement d'applications mobiles", + "hébergement web & nom de domaine", + "adresses e-mails personnalisées", + "design graphique (infographie)", + "design de documents d'entreprises (en-tête, factures,..)", + "publicités & reportages", + "infogérance (maintenance matérielle & logicielle)" + ] + } + }, + "notFound": { + "seo": { + "title": "Page non trouvée" + } + }, + "menu": [ + { "key": "home", "link": "/", "text": "accueil" }, + { "key": "about", "link": "/a-propos-de-nous", "text": "a propos de nous" }, + { "key": "contact", "link": "/contact", "text": "contact" }, + { "key": "services", "link": "/services", "text": "nos services" }, + { + "key": "pricing", + "link": "/nos-tarifs", + "text": "tarifs", + "children": [ + { + "key": "event-planning", + "link": "/event-planning", + "text": "service évènementiel" + }, + { + "key": "marketing-campaign", + "link": "/marketing-campaign", + "text": "campagnes marketing" + }, + { + "key": "design-and-multimedia", + "link": "/design-and-multimedia", + "text": "design & multimedia" + }, + { + "key": "social-media-management", + "link": "/social-media-management", + "text": "gestion social media" + }, + { + "key": "website-conception", + "link": "/website-conception", + "text": "création sites web" + } + ] + } + ], + "footer": { + "copyright": "Tout droits réservés", + "navigation": [ + { "key": "sitemap", "link": "/sitemap", "text": "Plan du site" }, + { + "key": "legal-notice", + "link": "/legal-notice", + "text": "Mentions légales" + }, + { + "key": "terms-conditions", + "link": "/terms-conditions", + "text": "Conditions générales" + }, + { + "key": "data-protection", + "link": "/data-protection", + "text": "Protection des données" + }, + { + "key": "cookie-management", + "link": "/cookie-management", + "text": "Paramètres des cookies" + }, + { + "key": "faq", + "link": "/faq", + "text": "FAQ" + } + ], + "socialNetworks": [ + { "key": "twitter", "link": "#", "icon": "twitter-x-fill" }, + { "key": "instagram", "link": "#", "icon": "instagram-fill" }, + { "key": "linkedin", "link": "#", "icon": "linkedin-box-fill" } + ] + } +} diff --git a/tests/inputs/mocks/mock.one.json b/tests/inputs/mocks/mock.one.json new file mode 100644 index 0000000..dd0f142 --- /dev/null +++ b/tests/inputs/mocks/mock.one.json @@ -0,0 +1,6 @@ +{ + "userId": 1, + "id": 1, + "title": "delectus aut autem", + "completed": false +} diff --git a/tests/inputs/mocks/mock_two.json b/tests/inputs/mocks/mock_two.json new file mode 100644 index 0000000..144435a --- /dev/null +++ b/tests/inputs/mocks/mock_two.json @@ -0,0 +1,6 @@ +{ + "home": { + "seo": { "title": "Test" }, + "content": { "activities": ["a", "b"] } + } +} diff --git a/tests/outputs/mock-directory-over-file.ts b/tests/outputs/mock-directory-over-file.ts new file mode 100644 index 0000000..95bea8e --- /dev/null +++ b/tests/outputs/mock-directory-over-file.ts @@ -0,0 +1,18 @@ +export interface MyDirectoryOverFileType { + emptyDirectory: EmptyDirectory; + emptyObject: string; + mock1: string; + mock2: string; + mock3: string; + mocks: Mocks; +} + +export interface Mocks { + mockThree: string; + mockOne: string; + mock_two: string; +} + +export interface EmptyDirectory { +} + diff --git a/tests/outputs/mock-directory.ts b/tests/outputs/mock-directory.ts new file mode 100644 index 0000000..5840465 --- /dev/null +++ b/tests/outputs/mock-directory.ts @@ -0,0 +1,18 @@ +export interface MyDirectoryType { + emptyDirectory: EmptyDirectory; + emptyObject: string; + mock1: string; + mock2: string; + mock3: string; + mocks: Mocks; +} + +export interface Mocks { + mockThree: string; + mockOne: string; + mock_two: string; +} + +export interface EmptyDirectory { +} + diff --git a/tests/outputs/mock2.js b/tests/outputs/mock2.js index 015e112..6a254fe 100644 --- a/tests/outputs/mock2.js +++ b/tests/outputs/mock2.js @@ -1,8 +1,12 @@ /** * @typedef {object} MyType - * @property {object} home - * @property {object} home.seo - * @property {string} home.seo.title - * @property {object} home.content - * @property {string[]} home.content.activities + * @property {object} emptyDirectory + * @property {string} emptyObject + * @property {string} mock1 + * @property {string} mock2 + * @property {string} mock3 + * @property {object} mocks + * @property {string} mocks.mockThree + * @property {string} mocks.mockOne + * @property {string} mocks.mock_two */ diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 0000000..8230c56 --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,28 @@ +import { organizeToJSON } from "$lib/utils"; +import { describe, expect, it, beforeAll } from "bun:test"; +import { existsSync, mkdirSync } from "fs"; +import path from "path"; +import { expectedDirectoryToJsonOutput } from "tests/expected-directory-to-json-output"; + +beforeAll(async () => { + const emptyDir = path.join(__dirname, "./inputs/empty-directory"); + const isDirExists = await existsSync(emptyDir); + if (!isDirExists) { + await mkdirSync(emptyDir); + } +}); + +describe("DirectoryToJSON", () => { + it("should return a json object", async () => { + const inputDir = path.join(__dirname, "./inputs"); + const directoryJSON = await organizeToJSON(inputDir); + expect(directoryJSON).toEqual(expectedDirectoryToJsonOutput); + }); + + it("should return empty object when directory is empty", async () => { + const inputDir = path.join(__dirname, "./inputs/empty-directory"); + const directoryJSON = await organizeToJSON(inputDir); + const expectedOutput = {}; + expect(directoryJSON).toStrictEqual(expectedOutput); + }); +});