diff --git a/lib/src/formatters/frameworks/base.ts b/lib/src/formatters/frameworks/base.ts deleted file mode 100644 index c8c13ce..0000000 --- a/lib/src/formatters/frameworks/base.ts +++ /dev/null @@ -1,13 +0,0 @@ -import OutputFile from "../shared/fileTypes/OutputFile"; - -export default class BaseFramework { - protected format: string; - - constructor(format: string) { - this.format = format; - } - - process(...args: any[]): OutputFile[] { - throw new Error("Not implemented"); - } -} diff --git a/lib/src/formatters/frameworks/index.ts b/lib/src/formatters/frameworks/index.ts deleted file mode 100644 index d85bad8..0000000 --- a/lib/src/formatters/frameworks/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import I18NextFramework from "./i18next"; - -export function getFrameworkProcessor(framework: string) { - switch (framework) { - case "i18next": - return new I18NextFramework(framework); - default: - throw new Error(`Unsupported framework: ${framework}`); - } -} diff --git a/lib/src/formatters/frameworks/json/base.ts b/lib/src/formatters/frameworks/json/base.ts new file mode 100644 index 0000000..4430b56 --- /dev/null +++ b/lib/src/formatters/frameworks/json/base.ts @@ -0,0 +1,21 @@ +import { Output } from "../../../outputs"; +import appContext from "../../../utils/appContext"; +import OutputFile from "../../shared/fileTypes/OutputFile"; + +export default class BaseFramework { + protected output: Output; + protected outDir: string; + + constructor(output: Output) { + this.output = output; + this.outDir = output.outDir ?? appContext.projectConfigDir; + } + + get framework() { + return this.output.framework; + } + + process(...args: any[]): OutputFile[] { + throw new Error("Not implemented"); + } +} diff --git a/lib/src/formatters/frameworks/i18next.ts b/lib/src/formatters/frameworks/json/i18next.ts similarity index 84% rename from lib/src/formatters/frameworks/i18next.ts rename to lib/src/formatters/frameworks/json/i18next.ts index 749ae6a..54c7c57 100644 --- a/lib/src/formatters/frameworks/i18next.ts +++ b/lib/src/formatters/frameworks/json/i18next.ts @@ -1,9 +1,8 @@ -import appContext from "../../utils/appContext"; -import JavascriptOutputFile from "../shared/fileTypes/JavascriptOutputFile"; -import OutputFile from "../shared/fileTypes/OutputFile"; -import { applyMixins } from "../shared"; -import javascriptCodegenMixin from "../mixins/javascriptCodegenMixin"; -import JSONOutputFile from "../shared/fileTypes/JSONOutputFile"; +import JavascriptOutputFile from "../../shared/fileTypes/JavascriptOutputFile"; +import OutputFile from "../../shared/fileTypes/OutputFile"; +import { applyMixins } from "../../shared"; +import javascriptCodegenMixin from "../../mixins/javascriptCodegenMixin"; +import JSONOutputFile from "../../shared/fileTypes/JSONOutputFile"; import BaseFramework from "./base"; export default class I18NextFramework extends applyMixins( @@ -13,12 +12,9 @@ export default class I18NextFramework extends applyMixins( process( outputJsonFiles: Record> ) { - const outputDir = appContext.projectConfigDir; - // Generate Driver file - const driverFile = new JavascriptOutputFile({ filename: "index", - path: outputDir, + path: this.outDir, }); const filesGroupedByVariantId = Object.values(outputJsonFiles).reduce( diff --git a/lib/src/formatters/frameworks/json/index.ts b/lib/src/formatters/frameworks/json/index.ts new file mode 100644 index 0000000..7e99d0d --- /dev/null +++ b/lib/src/formatters/frameworks/json/index.ts @@ -0,0 +1,15 @@ +import I18NextFramework from "./i18next"; +import { Output } from "../../../outputs"; + +export function getFrameworkProcessor(output: Output) { + if (!output.framework) { + throw new Error("Only call this function with a framework output"); + } + let frameworkType = output.framework; + switch (frameworkType) { + case "i18next": + return new I18NextFramework(output); + default: + throw new Error(`Unsupported JSON framework: ${frameworkType}`); + } +} diff --git a/lib/src/formatters/index.ts b/lib/src/formatters/index.ts index 72e8438..a2536fb 100644 --- a/lib/src/formatters/index.ts +++ b/lib/src/formatters/index.ts @@ -8,10 +8,7 @@ export default function handleOutput( ) { switch (output.format) { case "json": - return new JSONFormatter(output, projectConfig).format( - output, - projectConfig - ); + return new JSONFormatter(output, projectConfig).format(); default: throw new Error(`Unsupported output format: ${output}`); } diff --git a/lib/src/formatters/json.ts b/lib/src/formatters/json.ts index aa8f38b..14b3bfd 100644 --- a/lib/src/formatters/json.ts +++ b/lib/src/formatters/json.ts @@ -5,7 +5,7 @@ import OutputFile from "./shared/fileTypes/OutputFile"; import JSONOutputFile from "./shared/fileTypes/JSONOutputFile"; import appContext from "../utils/appContext"; import { applyMixins } from "./shared"; -import { getFrameworkProcessor } from "./frameworks"; +import { getFrameworkProcessor } from "./frameworks/json"; type JSONAPIData = { textItems: TextItemsResponse; @@ -29,7 +29,6 @@ export default class JSONFormatter extends applyMixins( } protected async transformAPIData(data: JSONAPIData) { - const outputDir = appContext.projectConfigDir; let outputJsonFiles: Record< string, @@ -38,7 +37,7 @@ export default class JSONFormatter extends applyMixins( const variablesOutputFile = new JSONOutputFile({ filename: "variables", - path: appContext.projectConfigDir, + path: this.outputDir, }); for (let i = 0; i < data.textItems.length; i++) { @@ -48,7 +47,7 @@ export default class JSONFormatter extends applyMixins( outputJsonFiles[fileName] ??= new JSONOutputFile({ filename: fileName, - path: outputDir, + path: this.outputDir, metadata: { variantId: textItem.variantId || "base" }, }); @@ -67,7 +66,7 @@ export default class JSONFormatter extends applyMixins( if (this.output.framework) { // process framework - results.push(...getFrameworkProcessor(this.output.framework).process(outputJsonFiles)); + results.push(...getFrameworkProcessor(this.output).process(outputJsonFiles)); } return results; diff --git a/lib/src/formatters/shared/base.ts b/lib/src/formatters/shared/base.ts index 264a75b..abb9cc1 100644 --- a/lib/src/formatters/shared/base.ts +++ b/lib/src/formatters/shared/base.ts @@ -3,14 +3,17 @@ import { writeFile } from "../../utils/fileSystem"; import logger from "../../utils/logger"; import { ProjectConfigYAML } from "../../services/projectConfig"; import OutputFile from "./fileTypes/OutputFile"; +import appContext from "../../utils/appContext"; export default class BaseFormatter { protected output: Output; protected projectConfig: ProjectConfigYAML; + protected outputDir: string; constructor(output: Output, projectConfig: ProjectConfigYAML) { this.output = output; this.projectConfig = projectConfig; + this.outputDir = output.outDir ?? appContext.outDir; } protected async fetchAPIData(): Promise { @@ -21,10 +24,7 @@ export default class BaseFormatter { return []; } - async format( - output: Output, - projectConfig: ProjectConfigYAML - ): Promise { + async format(): Promise { const data = await this.fetchAPIData(); const files = await this.transformAPIData(data); await this.writeFiles(files); diff --git a/lib/src/index.ts b/lib/src/index.ts index f556eb3..4a03718 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -9,6 +9,7 @@ import logger from "./utils/logger"; import { initAPIToken } from "./services/apiToken"; import { initProjectConfig } from "./services/projectConfig"; import appContext from "./utils/appContext"; +import type commander from "commander"; type Command = "pull"; @@ -24,7 +25,13 @@ interface CommandConfig { const COMMANDS: CommandConfig[] = [ { name: "pull", - description: "Sync copy from Ditto into the current working directory", + description: "Sync copy from Ditto", + flags: { + "-c, --config [value]": { + description: + "Relative path to the project config file. Defaults to `./ditto/config.yml`. Alternatively, you can set the DITTO_PROJECT_CONFIG_FILE environment variable.", + }, + }, }, ]; @@ -59,22 +66,23 @@ const setupOptions = () => { }; const executeCommand = async ( - command: Command | "none", - options: any + commandName: Command | "none", + command: commander.Command ): Promise => { try { + const options = command.opts(); const token = await initAPIToken(); appContext.setApiToken(token); - await initProjectConfig(); + await initProjectConfig(options); - switch (command) { + switch (commandName) { case "none": case "pull": { return await pull(); } default: { - await quit(`Invalid command: ${command}. Exiting Ditto CLI...`); + await quit(`Invalid command: ${commandName}. Exiting Ditto CLI...`); return; } } @@ -98,11 +106,6 @@ const appEntry = async () => { setupCommands(); setupOptions(); - if (process.argv.length <= 2 && process.argv[1].includes("ditto-cli")) { - await executeCommand("none", []); - return; - } - program.parse(process.argv); }; diff --git a/lib/src/outputs/shared.ts b/lib/src/outputs/shared.ts index 3c00cf1..2ba0b05 100644 --- a/lib/src/outputs/shared.ts +++ b/lib/src/outputs/shared.ts @@ -7,4 +7,5 @@ import { z } from "zod"; export const ZBaseOutputFilters = z.object({ projects: z.array(z.object({ id: z.string() })).optional(), variants: z.array(z.object({ id: z.string() })).optional(), + outDir: z.string().optional(), }); diff --git a/lib/src/services/projectConfig.ts b/lib/src/services/projectConfig.ts index b788c38..713b35b 100644 --- a/lib/src/services/projectConfig.ts +++ b/lib/src/services/projectConfig.ts @@ -18,12 +18,13 @@ export const DEFAULT_PROJECT_CONFIG_JSON: ProjectConfigYAML = { outputs: [ { format: "json", + framework: "i18next", }, ], }; -export async function initProjectConfig() { - const projectConfig = readProjectConfigData(); +export async function initProjectConfig(options: { config?: string }) { + const projectConfig = readProjectConfigData(options.config); appContext.setProjectConfig(projectConfig); } diff --git a/lib/src/utils/appContext.ts b/lib/src/utils/appContext.ts index 0536e8e..4093fef 100644 --- a/lib/src/utils/appContext.ts +++ b/lib/src/utils/appContext.ts @@ -18,7 +18,7 @@ class AppContext { #projectConfigFile: string; #clientId: string; #projectConfig: ProjectConfigYAML; - + #outDir: string; constructor() { this.#apiHost = process.env.DITTO_API_HOST || "https://api.dittowords.com"; this.#apiToken = process.env.DITTO_TOKEN; @@ -32,6 +32,7 @@ class AppContext { ); this.#clientId = crypto.randomUUID(); this.#projectConfig = DEFAULT_PROJECT_CONFIG_JSON; + this.#outDir = process.env.DITTO_OUT_DIR || this.projectConfigDir; } get apiHost() { @@ -85,6 +86,10 @@ class AppContext { get projectConfigDir() { return this.#projectConfigDir; } + + get outDir() { + return this.projectConfig.outDir || this.#outDir; + } } const appContext = new AppContext(); diff --git a/lib/src/utils/fileSystem.ts b/lib/src/utils/fileSystem.ts index be5843d..21e0e92 100644 --- a/lib/src/utils/fileSystem.ts +++ b/lib/src/utils/fileSystem.ts @@ -1,6 +1,7 @@ import path from "path"; import fs from "fs"; import fsPromises from "fs/promises"; + /** * Creates a file with the given filename if it doesn't already exist. * @param filename The path to the file to create. @@ -52,10 +53,16 @@ export async function createFileIfMissing( } export function writeFileSync(filename: string, content: string) { + if (!fs.existsSync(path.dirname(filename))) { + fs.mkdirSync(path.dirname(filename), { recursive: true }); + } fs.writeFileSync(filename, content, "utf-8"); } export async function writeFile(filename: string, content: string) { + if (!fs.existsSync(path.dirname(filename))) { + await fsPromises.mkdir(path.dirname(filename), { recursive: true }); + } await fsPromises.writeFile(filename, content, "utf-8"); }