diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7b5d9a..385988f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,28 @@ name: CI on: - push: - branches: - - main - pull_request: + push: branches: - main jobs: - test: + build: + runs-on: ubuntu-latest + steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - name: Setup Bun - uses: oven-sh/setup-bun@v1 + - uses: actions/checkout@v4 + name: Checkout repository + + - name: Setup Go + uses: actions/setup-go@v4 with: - bun-version: 1.0.25 + go-version: 1.21.7 - - name: Install Dependencies - run: bun i + - name: Install dependencies + run: go get . - name: Build - run: bun run build + run: go build -v . \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa4ba1a..a5879d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,35 +2,47 @@ name: Release on: push: - branches: - - main + tags: + - '*' -concurrency: ${{ github.workflow }}-${{ github.ref }} +permissions: + contents: write jobs: - release: - name: Release + goreleaser: runs-on: ubuntu-latest steps: - - name: Checkout Repo + - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.21.7 - - name: Setup Bun - uses: oven-sh/setup-bun@v1 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 with: - bun-version: 1.0.25 + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Dependencies - run: bun i + - uses: actions/setup-node@v4 + name: Setup Node + with: + node-version: 20 - - name: Install pnpm - run: bun i -g pnpm + - name: Generate NPM files + run: | + go run ./scripts/npm.go - - name: Create Release Pull Request - uses: changesets/action@v1 - with: - publish: bun run scripts/release.ts - createGithubReleases: true + - name: Publish to NPM + run: | + cd dist-npm + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc + npm publish env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 468f82a..259b207 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,6 @@ dist # Finder (MacOS) folder config .DS_Store + +dist/ +dist-npm/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..9fcbd54 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,51 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 1 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: raulfdm + name: node-versions-cli \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4c7be42 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "args": ["lts"] + }, + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c15561b..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - } -} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..c0dd08e --- /dev/null +++ b/api/api.go @@ -0,0 +1,36 @@ +package api + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "node-versions-cli/data" +) + +const nodeVersionURL = "https://nodejs.org/dist/index.json" + +func GetNodeVersions() (*data.NodeVersions, error) { + response, err := http.Get(nodeVersionURL) + + if err != nil { + return nil, err + } + + if response.StatusCode == http.StatusOK { + var nodeVersions data.NodeVersions + + bodyBi, error := io.ReadAll(response.Body) + + if error != nil { + return nil, error + } + + json.Unmarshal(bodyBi, &nodeVersions) + + return &nodeVersions, nil + } else { + return nil, errors.New("error fetching node versions") + } + +} diff --git a/bin/node-versions b/bin/node-versions deleted file mode 100755 index 752da35..0000000 --- a/bin/node-versions +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -import("../dist/node-versions.mjs"); diff --git a/biome.json b/biome.json deleted file mode 100644 index f8702e4..0000000 --- a/biome.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - } -} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index c9e0d8e..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/data/node_version.go b/data/node_version.go new file mode 100644 index 0000000..f8f8dd4 --- /dev/null +++ b/data/node_version.go @@ -0,0 +1,83 @@ +package data + +import ( + "errors" + "strconv" + + "github.com/Masterminds/semver/v3" +) + +type Lts interface{} + +type NodeVersion struct { + Version string `json:"version"` + Date string `json:"date"` + Files []string `json:"files"` + Npm string `json:"npm,omitempty"` + V8 string `json:"v8"` + Uv string `json:"uv,omitempty"` + Zlib string `json:"zlib,omitempty"` + Openssl string `json:"openssl,omitempty"` + Modules string `json:"modules,omitempty"` + Lts Lts `json:"lts"` + Security bool `json:"security"` +} + +func (n NodeVersion) IsLts() bool { + switch n.Lts.(type) { + case bool: + return false + default: + return true + } +} + +type NodeVersions []NodeVersion + +func (n NodeVersions) GetAll() []string { + var allVersions []string + + for _, version := range n { + allVersions = append(allVersions, version.Version) + } + + return allVersions +} + +func (n NodeVersions) GetLatest() string { + return n[0].Version +} + +func (n NodeVersions) GetLatestOf(majorVersionNumber string) (*string, error) { + for _, version := range n { + versionWithoutV := version.Version[1:len(version.Version)] + + nodeVersion, _ := semver.NewVersion(versionWithoutV) + majorVersionAsInt, _ := strconv.ParseUint(majorVersionNumber, 10, 64) + + if majorVersionAsInt == nodeVersion.Major() { + return &version.Version, nil + } + } + + return nil, errors.New("no version found for major version " + majorVersionNumber) +} + +func (n NodeVersions) GetCurrentLts() string { + + allLts := n.GetAllLts() + + return allLts[0] +} + +func (n NodeVersions) GetAllLts() []string { + var ltsVersions []string = []string{} + + for _, version := range n { + if version.IsLts() { + ltsVersions = append(ltsVersions, version.Version) + } + } + + return ltsVersions +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..77fb7eb --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module node-versions-cli + +go 1.21.7 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 + github.com/urfave/cli/v2 v2.27.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..207b451 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..98967ea --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "log" + "node-versions-cli/api" + "node-versions-cli/data" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + var nodeVersions *data.NodeVersions + + app := (&cli.App{ + Name: "node-versions", + Usage: "A simple CLI to check node versions", + UsageText: `node-versions all +node-versions lts +node-versions lts --all +node-versions latest +node-versions latest 14 + `, + EnableBashCompletion: true, + Before: func(ctx *cli.Context) error { + // We don't want to call the API if there's no subcommand + if ctx.Args().Len() > 0 { + versions, err := api.GetNodeVersions() + + if err != nil { + return err + } + + nodeVersions = versions + } + + return nil + }, + Commands: []*cli.Command{{ + Name: "all", + Usage: "show all versions", + Action: func(ctx *cli.Context) error { + + for _, version := range nodeVersions.GetAll() { + fmt.Println(version) + } + + return nil + }, + }, + { + Name: "lts", + Usage: "show LTS version", + Action: func(ctx *cli.Context) error { + + // If we don't validate the flag, both Actions will be executed + // and overlap the output + if !ctx.Bool("all") { + fmt.Println(nodeVersions.GetCurrentLts()) + } + + return nil + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Usage: "show all LTS versions", + Aliases: []string{"a"}, + Action: func(ctx *cli.Context, value bool) error { + + for _, version := range nodeVersions.GetAllLts() { + fmt.Println(version) + } + + return nil + }, + }, + }, + }, + { + Name: "latest", + Usage: "show latest version", + UsageText: `node-versions latest +node-versions latest [major-version]`, + Action: func(ctx *cli.Context) error { + if ctx.Args().Len() > 0 { + desiredMajorVersion := ctx.Args().First() + version, err := nodeVersions.GetLatestOf(desiredMajorVersion) + + if err != nil { + return err + } + + fmt.Println(*version) + } else { + fmt.Println(nodeVersions.GetLatest()) + } + + return nil + }, + }, + }, + }) + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/package.json b/package.json deleted file mode 100644 index 9886323..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@raulfdm/node-versions", - "private": false, - "publishConfig": { - "access": "public" - }, - "version": "1.1.0", - "type": "module", - "bin": { - "node-versions": "./bin/node-versions" - }, - "files": ["dist", "bin"], - "scripts": { - "dev": "bun run src/index.ts", - "build:standalone-test": "bun build src/index.ts --compile --outfile dist/node-versions --target bun --minify", - "build": "bun build src/index.ts --outfile dist/node-versions.mjs --target node --minify" - }, - "devDependencies": { - "@biomejs/biome": "1.5.3", - "@changesets/cli": "2.27.1", - "@types/bun": "1.0.4", - "consola": "3.2.3", - "execa": "8.0.1", - "just-group-by": "2.2.0", - "meow": "13.1.0", - "typescript": "5.3.3", - "zod": "3.22.4" - } -} diff --git a/scripts/npm.go b/scripts/npm.go new file mode 100644 index 0000000..87176c4 --- /dev/null +++ b/scripts/npm.go @@ -0,0 +1,233 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "text/template" +) + +func main() { + outDir, err := getDistFolderPath() + + if err != nil { + fmt.Println("[ERROR] [getDistFolderPath] ", err) + return + } + + meta, err := getReleaserMetaData() + + if err != nil { + fmt.Println("[ERROR] [getReleaserMetaData] ", err) + return + } + + err = createDistFolder(outDir) + + if err != nil { + fmt.Println("[ERROR] [createDistFolder] ", err) + return + } + + pkgString, err := getPackageJsonString(*meta) + + if err != nil { + fmt.Println("[ERROR] [getPackageJsonString] ", err) + return + } + + err = writePackageJson(outDir, pkgString) + + if err != nil { + fmt.Println("[ERROR] [writePackageJson] ", err) + return + } + + fmt.Println("package.json created") + + err = copyFile( + "README.md", + outDir, + "README.md", + ) + + if err != nil { + fmt.Println("[ERROR] [copy README.md] ", err) + return + } + + fmt.Println("README.md copied") + + err = copyFile( + "./scripts/templates/install-manager.mjs", + outDir, + "install-manager.mjs", + ) + + if err != nil { + fmt.Println("[ERROR] [copy install-manager.mjs] ", err) + return + } + + fmt.Println("install-manager.mjs copied") + + err = copyFile( + "./scripts/templates/bin.mjs", + outDir, + "bin.mjs", + ) + + if err != nil { + fmt.Println("[ERROR] [copy bin.mjs] ", err) + return + } + + fmt.Println("bin.mjs copied") + +} + +func getDistFolderPath() (string, error) { + fullPath, err := filepath.Abs("./") + + if err != nil { + return "", err + } + + distPath := filepath.Join(fullPath, "dist-npm") + + return distPath, nil +} + +func createDistFolder(distPath string) error { + err := os.Mkdir(distPath, 0755) + + if err != nil { + if !strings.Contains(err.Error(), "file exists") { + return err + } + } + + return nil +} + +type PkgJsonTemplate struct { + Version string + URL string +} + +func getPackageJsonString(meta ReleaserMetaData) (string, error) { + temp, err := template.ParseFiles("./scripts/templates/package.json") + + if err != nil { + return "", err + } + + var buff bytes.Buffer + + temp.Execute(&buff, PkgJsonTemplate{ + Version: meta.GetVersion(), + URL: meta.GetRemoteUrl(), + }) + + result := buff.String() + + return result, nil +} + +func writePackageJson(distPath string, pkgString string) error { + file, err := os.Create(filepath.Join(distPath, "package.json")) + + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write([]byte(pkgString)) + + if err != nil { + return err + } + + return nil +} + +type ReleaserMetaData struct { + Tag string `json:"tag"` + ProjectName string `json:"project_name"` +} + +func (r *ReleaserMetaData) GetVersion() string { + // remove v prefix + return r.Tag[1:] +} + +func (r *ReleaserMetaData) GetRemoteUrl() string { + return fmt.Sprintf("https://github.com/raulfdm/node-versions-cli/releases/download/v{{version}}/%s_{{platform}}_{{arch}}.tar.gz", r.ProjectName) +} + +func getReleaserMetaData() (*ReleaserMetaData, error) { + fullPath, err := filepath.Abs("./") + + if err != nil { + return nil, err + } + + releaserMetaPath := filepath.Join(fullPath, "dist/metadata.json") + + file, err := os.Open(releaserMetaPath) + + if err != nil { + return nil, err + } + + defer file.Close() + + var meta ReleaserMetaData + + err = json.NewDecoder(file).Decode(&meta) + + if err != nil { + return nil, err + } + + return &meta, nil +} + +func copyFile(relativeSrc string, outDir string, filename string) error { + fullPath, err := filepath.Abs("./") + + if err != nil { + return err + } + + readmeSrc := filepath.Join(fullPath, relativeSrc) + readmeDest := filepath.Join(outDir, filename) + + srcFile, err := os.Open(readmeSrc) + + if err != nil { + return err + } + + defer srcFile.Close() + + destFile, err := os.Create(readmeDest) + + if err != nil { + return err + } + + defer destFile.Close() + + _, err = io.Copy(destFile, srcFile) + + if err != nil { + return err + } + + return nil +} diff --git a/scripts/release.ts b/scripts/release.ts deleted file mode 100644 index e612400..0000000 --- a/scripts/release.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { $ } from "bun"; -import { execaCommand } from "execa"; -import consola from "consola"; - -consola.info("Starting release process"); - -try { - consola.log("Trying to publish"); - await execaCommand("pnpm publish --no-git-checks", { - shell: true, - all: true, - }); - consola.success("Published successfully"); - - consola.log("Trying to push tags..."); - await $`git push --follow-tags`; - consola.success("tags pushed successfully"); -} catch (error) { - if (error instanceof Error) { - if ( - error.message.includes( - "You cannot publish over the previously published versions", - ) - ) { - console.info("Version already published, skipping..."); - process.exit(0); - } else { - consola.error("Something went wrong", error.message); - process.exit(1); - } - } else { - consola.error("Unknown error", error); - process.exit(1); - } -} - -// const a = await $`git`.text(); - -// console.log(a); diff --git a/scripts/templates/bin.mjs b/scripts/templates/bin.mjs new file mode 100644 index 0000000..235eb9f --- /dev/null +++ b/scripts/templates/bin.mjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const fullPath = path.join(__dirname, "bin/node-versions-cli"); + +// Get the arguments passed to the script, excluding the first two elements +// The first element is the path to the node executable +// The second element is the path to your script +const args = process.argv.slice(2); + +// Join the arguments into a single string +const argsString = args.join(" "); + +execSync(`${fullPath} ${argsString}`, { stdio: "inherit" }); diff --git a/scripts/templates/install-manager.mjs b/scripts/templates/install-manager.mjs new file mode 100644 index 0000000..bc6962c --- /dev/null +++ b/scripts/templates/install-manager.mjs @@ -0,0 +1,224 @@ +#!/usr/bin/env node + +import * as path from "node:path"; +import * as fs from "node:fs"; +import https from "https"; +import tar from "tar"; +import { pipeline } from "stream/promises"; + +// Mapping from Node's `process.arch` to Golang's `$GOARCH` +const ARCH_MAPPING = { + ia32: "386", + x64: "amd64", + arm: "arm", + arm64: "arm64", +}; + +// Mapping between Node's `process.platform` to Golang's +const PLATFORM_MAPPING = { + darwin: "darwin", + linux: "linux", + win32: "windows", + freebsd: "freebsd", +}; + +const command = process.argv[2]; + +if (command === "install") { + await install(); +} else if (command === "uninstall") { + // do something +} else { + console.log( + "Invalid command. 'install' and 'uninstall' are the only supported commands", + ); + process.exit(1); +} + +async function install() { + console.log("Installing binary..."); + validateOsAndArch(); + const pkgJson = readPackageJson(); + const metaData = getMetaData(pkgJson); + createBinPath(metaData.binPath); + + try { + await downloadFile(metaData.url, metaData.binTarGz); + await tar.x({ + file: metaData.binTarGz, + cwd: metaData.binPath, + }); + + console.log("Binary installed successfully"); + } catch (error) { + console.error(`Error downloading binary: ${error}`); + process.exit(1); + } +} + +function readPackageJson() { + const packageJsonPath = path.join(".", "package.json"); + + if (!fs.existsSync(packageJsonPath)) { + console.error("package.json not found in current directory"); + process.exit(1); + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); + const err = validateConfiguration(packageJson); + + if (err) { + console.error(err); + process.exit(1); + } + + return packageJson; +} + +/** + * @typedef {Object} GoBinary + * @property {(string|undefined)} path - The path of the Go binary. + * @property {(string|undefined)} name - The name of the Go binary. + * @property {(string|undefined)} url - The URL of the Go binary. + */ + +/** + * @typedef {Object} PackageJson + * @property {(GoBinary|undefined)} goBinary - The goBinary object. + * @property {string} version - The version of the package. + */ + +/** + * @typedef {Object} MetaData + * @property {string} binName - The name of the binary. + * @property {string} binPath - The path of the binary. + * @property {string} url - The URL of the binary. + * @property {string} version - The version of the binary. + */ + +/** + * Extracts metadata from a package.json object. + * + * @param {PackageJson} packageJson - The package.json object. + * + * @returns {MetaData} An object containing the binary name, path, URL, and version. + */ +function getMetaData(packageJson) { + const binPath = packageJson.goBinary.path; + let binName = packageJson.goBinary.name; + let url = packageJson.goBinary.url; + let version = packageJson.version; + + if (version[0] === "v") { + version = version.substring(1); // strip the 'v' if necessary v0.0.1 => 0.0.1 + } + + // Binary name on Windows has .exe suffix + if (process.platform === "win32") { + binName += ".exe"; + } + + // Interpolate variables in URL, if necessary + url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]); + url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]); + url = url.replace(/{{version}}/g, version); + url = url.replace(/{{bin_name}}/g, binName); + + return { + binName, + binPath, + binFullName: path.join(process.cwd(), binPath), + get binTarGz() { + return `${this.binFullName}.tar.gz`; + }, + url, + version, + }; +} + +function validateOsAndArch() { + if (!(process.arch in ARCH_MAPPING)) { + console.error(`Invalid architecture: ${process.arch}`); + process.exit(1); + } + + if (!(process.platform in PLATFORM_MAPPING)) { + console.error(`Invalid platform: ${process.platform} `); + process.exit(1); + } +} + +/** + * Validates the package.json object. + * @param {PackageJson} packageJson - The package.json object. + * @returns {string} An error message if the package.json object is invalid. + */ +function validateConfiguration(packageJson) { + if (!packageJson.version) { + return "'version' property must be specified"; + } + + if (!packageJson.goBinary || typeof packageJson.goBinary !== "object") { + return "'goBinary' property must be defined and be an object"; + } + + if (!packageJson.goBinary.name) { + return "'name' property is necessary"; + } + + if (!packageJson.goBinary.path) { + return "'path' property is necessary"; + } + + if (!packageJson.goBinary.url) { + return "'url' property is required"; + } +} + +/** + * Creates a directory at the specified path. + * @param {string} binPath - The path of the directory to create. + */ +function createBinPath(binPath) { + if (!fs.existsSync(binPath)) { + fs.mkdirSync(binPath, { recursive: true }); + } +} + +/** + * Downloads a file from a given URL and saves it to the specified path. + * + * @param {string} url - The URL of the file to download. + * @param {string} outputPath - The path where the downloaded file should be saved. + * @returns {Promise} A promise that resolves with the path of the downloaded file. + * @throws {Error} Throws an error if the download or file writing fails. + */ +async function downloadFile(url, outputPath) { + return new Promise((resolve, reject) => { + const processResponse = (response) => { + // Check if the response is a redirect + if ( + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + https + .get(response.headers.location, processResponse) + .on("error", reject); + } else if (response.statusCode === 200) { + const fileStream = fs.createWriteStream(outputPath); + pipeline(response, fileStream) + .then(() => resolve(outputPath)) + .catch((error) => { + reject(`Error during download: ${error.message}`); + }); + } else { + reject( + `Server responded with ${response.statusCode}: ${response.statusMessage}`, + ); + } + }; + + https.get(url, processResponse).on("error", reject); + }); +} diff --git a/scripts/templates/package.json b/scripts/templates/package.json new file mode 100644 index 0000000..71f1370 --- /dev/null +++ b/scripts/templates/package.json @@ -0,0 +1,23 @@ +{ + "name": "@raulfdm/node-versions", + "private": false, + "bin": { + "node-versions": "bin.mjs" + }, + "publishConfig": { + "access": "public" + }, + "version": "{{ .Version }}", + "scripts": { + "postinstall": "node install-manager.mjs install", + "preuninstall": "node install-manager.mjs uninstall" + }, + "dependencies": { + "tar": "6.2.0" + }, + "goBinary": { + "name": "node-versions", + "path": "./bin", + "url": "{{ .URL }}" + } +} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 8583c22..0000000 --- a/src/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import meow from "meow"; -import consola from "consola"; -import groupBy from "just-group-by"; - -import { NodeVersions } from "./schema"; - -const { flags, showHelp } = meow( - `🌟 Node Versions CLI 🌟 - -Usage: -$ node-versions - -Options: ---all Show all versions ---all-lts Show all LTS versions ---latest Show latest version ---latest-of Show latest version of a specific version ---lts Show current LTS version - -Examples: -$ node-versions --all -$ node-versions --all-lts -$ node-versions --latest -$ node-versions --latest-of 20 -$ node-versions --lts -`, - { - importMeta: import.meta, - flags: { - lts: { - type: "boolean", - }, - allLts: { - type: "boolean", - }, - latest: { - type: "boolean", - }, - latestOf: { - type: "string", - }, - }, - }, -); - -const nodeVersions = await getNodeVersions(); - -if (flags.lts) { - showLts(); -} else if (flags.all) { - showAll(); -} else if (flags.allLts) { - showAllLts(); -} else if (flags.latestOf) { - showLatestOf(); -} else if (flags.latest) { - showLatest(); -} else { - showHelp(); -} - -function showAll() { - consola.log("All Versions:"); - logVersions(nodeVersions); -} - -function showLts() { - consola.info("Current LTS:"); - const [currentLTS] = nodeVersions.filter((version) => version.lts); - logVersions([currentLTS]); -} - -function showAllLts() { - consola.info("LTS Versions:"); - const ltsVersions = nodeVersions.filter((version) => version.lts); - logVersions(ltsVersions); -} - -function logVersions(nodeVersions: NodeVersions) { - const ascendingVersions = nodeVersions.sort((a, b) => - Bun.semver.order(a.version, b.version), - ); - - const result = ascendingVersions.reduce((acc, nodeVersion) => { - return `${acc}${nodeVersion.version}\n`; - }, ""); - - consola.log(result.trim()); -} - -function showLatestOf() { - const { latestOf } = flags; - const prependVersion = `v${latestOf}`; - - const groupedVersions = groupBy( - nodeVersions, - (version) => version.version.split(".")[0], - ); - - const allVersionsOf = groupedVersions[prependVersion]; - - if (!allVersionsOf) { - consola.error(`No versions found for ${prependVersion}`); - return; - } - - const [latestVersion] = allVersionsOf; - - consola.info(`Latest version of ${prependVersion}:`); - logVersions([latestVersion]); -} - -function showLatest() { - const [latestVersions] = nodeVersions; - console.log("Latest version:"); - logVersions([latestVersions]); -} - -async function getNodeVersions() { - const response = await fetch("https://nodejs.org/dist/index.json"); - - const nodeVersionsJson = await response.json(); - - return NodeVersions.parse(nodeVersionsJson); -} diff --git a/src/schema.ts b/src/schema.ts deleted file mode 100644 index a586c0f..0000000 --- a/src/schema.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { z } from "zod"; - -const NodeVersion = z.union([ - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - npm: z.string(), - v8: z.string(), - uv: z.string(), - zlib: z.string(), - openssl: z.string(), - modules: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - npm: z.string(), - v8: z.string(), - uv: z.string(), - zlib: z.string(), - openssl: z.string(), - modules: z.string(), - lts: z.string(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - v8: z.string(), - uv: z.string(), - zlib: z.string(), - openssl: z.string(), - modules: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - v8: z.string(), - uv: z.string(), - openssl: z.string(), - modules: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - v8: z.string(), - uv: z.string(), - modules: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - v8: z.string(), - modules: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), - z.object({ - version: z.string(), - date: z.string(), - files: z.array(z.string()), - v8: z.string(), - lts: z.boolean(), - security: z.boolean(), - }), -]); -export type NodeVersion = z.infer; - -export const NodeVersions = z.array(NodeVersion); -export type NodeVersions = z.infer;