From f904559ac53232d562c1fe0eee9bcf8c6635f5b6 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 27 Apr 2025 20:11:06 +0100 Subject: [PATCH 01/20] feat: create template --- .github/workflows/tests.yml | 33 ++++++++++ .gitignore | 1 + cli.test.js | 51 +++++++++++++--- src/cli.js | 117 ++++++++++++++++++++++++++++++++++-- src/errors.js | 2 +- 5 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..55414a2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Node.js Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix['node-version'] }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index d4ca257..9a8b391 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules drafts +output/* \ No newline at end of file diff --git a/cli.test.js b/cli.test.js index 186f6be..4fdaf34 100644 --- a/cli.test.js +++ b/cli.test.js @@ -1,19 +1,50 @@ -import test from "node:test"; -import assert from "node:assert"; -import { exec } from "node:child_process"; +// Assertion +import { beforeEach, afterEach, describe, it } from "node:test"; +import assert from "node:assert/strict"; +// Built-in modules +import { readFile, rm } from "node:fs/promises"; +import { resolve } from "node:path"; import { promisify } from "node:util"; -import { resolve } from "path"; +import { exec } from "node:child_process"; +// Local modules import { ERR_MISSING_TEMPLATE } from "./src/errors.js"; +import { writeFileSync } from "fs"; const execAsync = promisify(exec); - const CLI_PATH = resolve("./src/cli.js"); -test("Exit if missing template name", async () => { - const command = `node ${CLI_PATH}`; +function clearOutputDir() { + const outputDir = resolve("output"); + // Clean up the output directory before each test + rm(outputDir, { recursive: true, force: true }).catch(() => {}); +} + +describe("YNDAP CLI", () => { + beforeEach(clearOutputDir); + afterEach(clearOutputDir); + + // ============ + // Successful test case + // ============ + it("Create a file passing correct params, creating .js by default", async () => { + const templateName = "sum"; + const outputDir = resolve("output/sum"); + const targetFile = resolve(outputDir, "sum.js"); + await execAsync(`node ${CLI_PATH} -t ${templateName} -o ${outputDir}`); + const content = await readFile(targetFile, "utf8"); + + assert.ok(content.includes("export default function sum")); + }); + + // ============ + // Fail test case + // ============ + it("Exit if missing template name", async () => { + const command = `node ${CLI_PATH}`; - await assert.rejects(execAsync(command), { - code: 1, - stderr: new RegExp(ERR_MISSING_TEMPLATE), + await assert.rejects(execAsync(command), { + code: 1, + stderr: new RegExp(ERR_MISSING_TEMPLATE), + }); }); }); diff --git a/src/cli.js b/src/cli.js index db02818..a974615 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,8 +1,21 @@ #!/usr/bin/env node import { parseArgs } from "node:util"; -import { ERR_MISSING_TEMPLATE } from "./errors.js"; +import { ERR_MISSING_TEMPLATE, ERR_FETCH_TEMPLATE } from "./errors.js"; +import { resolve, extname, dirname } from "node:path"; +import { mkdirSync } from "node:fs"; +import { writeFile } from "fs/promises"; +import { Writable } from "node:stream"; + +// =========== +// Initialization +// =========== const options = { + verbose: { + type: "boolean", + short: "v", + default: false, + }, extension: { type: "string", short: "e", @@ -13,18 +26,112 @@ const options = { type: "string", short: "t", }, + user: { + type: "string", + short: "u", + }, + output: { + type: "string", + short: "o", + }, }; +// =========== +// Helpers +// =========== + +const logStream = new Writable({ + write(chunk, encoding, callback) { + process.stdout.write(chunk, encoding, callback); + }, +}); + +function log(...args) { + if (values.verbose) { + logStream.write(`YNDAP: ${args.join(" ")}\n`); + } +} + +/* @function buildTemplate + * @description Build the template URL based on the provided options. + * Official ydnap templates are hosted on GitHub + * https://raw.githubusercontent.com/:user/:repo/:branch/:type/index.js + * 3th party templates + * https://raw.githubusercontent.com/:user/:repo/:branch/:path + */ +function buildTemplate() { + const branch = "main"; + const repo = "guildadev/ydnap-templates"; + const type = values.extension; + const template = values.template; + const path = `src/${type}/${template}/index.${type}`; + + if (values.user) { + path = `${type}/${values.user}/src/${template}/index.${type}`; + log(`Building template URL: ${path}`); + } + + const url = `https://raw.githubusercontent.com/${repo}/${branch}/${path}`; + log(`Template URL: ${url}`); + return { + url, + }; +} + +async function resolveOutput() { + const { output, template, extension } = values; + + if (output) { + const outputPath = resolve(process.cwd(), output); + const outputExt = extname(output); + log("Output", outputPath); + + if (outputExt) { + // When output is a file + // Ensure its parent directory exists + const parentDir = dirname(outputPath); + mkdirSync(parentDir, { recursive: true }); + log("Output is a file, parentDir:", parentDir); + return outputPath; + } else { + // When output is a directory + mkdirSync(outputPath, { recursive: true }); + return resolve(outputPath, `${template}.${extension}`); + } + } + + // No output provided → use template name in cwd + return resolve(process.cwd(), `${template}.${extension}`); +} + const { values } = parseArgs({ options, tokens: true }); +// =========== +// Validation +// =========== + if (!values.template) { - console.error( + log( "Usage: ydnap -t