Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Publish Package to npmjs
on:
release:
types: [published]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
cache: yarn
- run: yarn install --frozen-lockfile
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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: "yarn"

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run tests
run: yarn test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
drafts
output/*
71 changes: 61 additions & 10 deletions cli.test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,70 @@
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";

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"));
});

it("Create a file passing correct params, creating .ts by change extension", async () => {
const templateName = "sum";
const outputDir = resolve("output/sum");
const targetFile = resolve(outputDir, "sum.ts");
await execAsync(
`node ${CLI_PATH} -t ${templateName} -o ${outputDir} -e ts`,
);
const content = await readFile(targetFile, "utf8");

assert.ok(content.includes("export default function sum"));
});

it("Create a file passing an another github user", async () => {
const targetFile = resolve("output", "is-even.js");
await execAsync(
`node ${CLI_PATH} -t even -o ${targetFile} -r alexcastrodev/ydnap-example`,
);
const content = await readFile(targetFile, "utf8");
assert.ok(content.includes("export default function isEven"));
});

// ============
// 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),
});
});
});
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "ydnap",
"version": "1.0.0",
"name": "@guildadev/ydnap",
"version": "0.1.0",
"description": "A CLI that offers you templates for things you don't need a package for.",
"bin": {
"ydnap": "./src/cli.js"
"ydnap": "src/cli.js"
},
"scripts": {
"test": "node --test './test/**'",
"test": "node --test",
"format": "prettier --write ."
},
"type": "module",
Expand Down
49 changes: 46 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,59 @@ It is designed to be lightweight and easy to use, easy to collaborate, without t

Sick of installing packages just to archive a simple task? YDNAP is here to help!

In the end of the day, you maybe don't need ramda, lodash, date-fns, or any other package to do simple tasks.
In the end of the day, you maybe don't need Ramda, Lodash, date-fns, or any other package to do simple tasks.

### CLI Options

| Option | Type | Short | Default | Choices | Description |
| ------------- | ------- | ----- | ------- | ---------- | ------------------------------------------------------------------------------- |
| `--verbose` | boolean | `-v` | `false` | N/A | Enables verbose mode for detailed logging. |
| `--extension` | string | `-e` | `js` | `js`, `ts` | Specifies the file extension to use (`js` for JavaScript, `ts` for TypeScript). |
| `--template` | string | `-t` | N/A | N/A | Specifies the template folder to use. |
| `--repo` | string | `-r` | N/A | N/A | (Optional) Specifies the repository URL to fetch templates from. |
| `--output` | string | `-o` | N/A | N/A | (Optional) Specifies the output directory for generated files. |

## Installation

You can install YDNAP globally using npm:

```bash
npm install -g ydnap
```

Or you can use it without installing it by using npx:

```bash
npx ydnap
```

## Usage

### Using with npx
You can use YDNAP and create files using our [templates](https://github.com/GuildaDev/ydnap-templates), or you can create your own templates (or share them with your friiiiends).

To use our template, you can run:

```bash
ydnap -t sum # or npx ydnap -t sum
```

by default, we will always find the javascript file.

you can also specify the typescript file:

```bash
npx ydnap -t sum
ydnap -t sum -l ts # or npx ydnap -t sum -l ts
```

Using you repository (eg https://github.com/alexcastrodev/ydnap-example/tree/main/src/even)

```bash
ydnap -u alexcastrodev/ydnap-example -t even
```

> **Note**
> It's mandatory that the `-t` (template) argument points to a folder, and the file inside the folder should be named `index.ts` or `index.js`.

## Drawbacks

YDNAP is designed to solve small tasks, like navigating through objects with JavaScript or TypeScript (without needing the full weight of libraries like Ramda or Lodash), or creating a useDebounceCallback for React without installing an entire hooks library.
Expand Down
146 changes: 141 additions & 5 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -13,18 +26,141 @@ const options = {
type: "string",
short: "t",
},
repo: {
type: "string",
short: "r",
},
output: {
type: "string",
short: "o",
},
help: {
type: "boolean",
short: "h",
},
};

const { values } = parseArgs({ options, tokens: true });

// ===========
// 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`);
}
}

// ===========
// Help
// ===========

if (values.help) {
console.log(`
Usage: ydnap [options]

Options:
-t, --template <template> Specify the template to use (required)
-e, --extension <js|ts> Specify the file extension (default: js)
-o, --output <output> Specify the output file or directory
-r, --repo <user/repo> Specify a custom repository for templates
-v, --verbose Enable verbose logging
-h, --help Display this help message

Examples:
ydnap -t sum -e ts
ydnap -t sum -o ./output-dir
ydnap -t sum -r customuser/customrepo
`);

process.exit(0);
}

/* @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 type = values.extension;
const template = values.template;
let repo = values.repo || "guildadev/ydnap-templates";
let path = `src/${type}/${template}/index.${type}`;

if (values.repo) {
path = `src/${template}/index.${type}`;
log(`Building template URL: ${path}`);
}

const url = `https://raw.githubusercontent.com/${repo}/refs/heads/${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}`);
}

// ===========
// Validation
// ===========

if (!values.template) {
console.error(
log(
"Usage: ydnap -t <js|ts> <template> [-u githubuser | --user githubuser | -r user/repo | --repo user/repo]",
);
throw new TypeError(ERR_MISSING_TEMPLATE);
}

if (!values.filename) {
console.error("Error: Filename is required.");
throw new TypeError(ERR_MISSING_FILENAME);
// ===========
// Main
// ===========
const { url } = buildTemplate();
log("Template URL", url);
const filepath = await resolveOutput();
log("Filepath", filepath);

const response = await fetch(url);
const responseText = await response.text();

if (!response.ok) {
log("Response", responseText);
throw new TypeError(ERR_FETCH_TEMPLATE);
}

await writeFile(filepath, responseText, { encoding: "utf-8" });
log(`Template written to ${filepath}`);
2 changes: 1 addition & 1 deletion src/errors.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const ERR_MISSING_TEMPLATE = "Missing template name";
export const ERR_MISSING_FILENAME = "Missing filename";
export const ERR_FETCH_TEMPLATE = "Missing filename";