Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: npm install -g ttyper #97

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
23 changes: 23 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,26 @@ jobs:
files: |
ttyper-*/ttyper-*
LICENSE.md

npm-release:
name: NPM Publish
needs: github-release
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout
uses: actions/checkout@v4
- name: Setup | Node
uses: actions/setup-node@v3
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup | Version
run: npm version ${{ github.ref_name }}
working-directory: ./npm
- name: NPM Publish
run: |
cp ../README.md .
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
working-directory: ./npm
129 changes: 129 additions & 0 deletions npm/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import fs from "fs";
import https from "https";
import path from "path";
import { fileURLToPath } from "url";

// ESM doesn't support __dirname and __filename by default
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf8"));
// The version number is expected to be in parity with the ttyper release version
const { version, releasesUrl, name, binDir: directory } = packageJson;

// All the binary files will be stored in the /bin directory
const binDir = path.join(__dirname, directory);
console.log(`Installing ${name} v${version}`);

try {
void install();
} catch (error) {
console.error("Installation failed:", error.message);
}

async function install() {
if (fs.existsSync(binDir)) {
fs.rmSync(binDir, { recursive: true });
}
fs.mkdirSync(binDir, {
mode: 0o777,
});
await getBinary();

// Remove the node_modules as we'll only need the binary
fs.rmSync(path.join(__dirname, "node_modules"), { recursive: true });
}

function getBinaryDownloadURL() {
let os, arch;

// Possible values are : 'aix' | 'android' | 'darwin' | 'freebsd' | 'haiku' | 'linux' | 'openbsd' | 'sunos' | 'win32' | 'cygwin' | 'netbsd'
switch (process.platform) {
case "win32":
case "cygwin":
os = "pc-windows-msvc";
break;
case "darwin":
os = "apple-darwin";
break;
case "linux":
os = "unknown-linux-gnu";
break;
default:
throw new Error(`Unsupported OS: ${process.platform}`);
}

// Possible values are: 'arm' | 'arm64' | 'ia32' | 'mips' | 'mipsel' | 'ppc' | 'ppc64' | 's390' | 's390x' | 'x64'
switch (process.arch) {
case "x64":
arch = "x86_64";
break;
case "arm64":
arch = "aarch64";
break;
case "ia32":
arch = "i686";
break;
default:
throw new Error(`Unsupported architecture: ${process.arch}`);
}

const extension = os === "pc-windows-msvc" ? "zip" : "tar.gz";

return `${releasesUrl}/download/v${version}/${name}-${arch}-${os}.${extension}`;
}

function downloadPackage(url, outputPath) {
// We use https.get instead of fetch to get a readable stream from the response without additional dependencies
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
// If the response is a redirect, we download the package from the new location
if (response.statusCode === 302) {
resolve(downloadPackage(response.headers.location, outputPath));
} else if (response.statusCode === 200) {
const file = fs.createWriteStream(outputPath);
response.pipe(file);
file.on("finish", () => {
file.close(resolve);
});
} else {
reject(
new Error(
`Failed to download ${name}. Status code: ${response.statusCode}`
)
);
}
})
.on("error", reject);
});
}

async function extractPackage(inputPath, outputPath) {
if (path.extname(inputPath) === ".gz") {
const tar = await import("tar");
await tar.x({
file: inputPath,
cwd: outputPath,
});
} else if (path.extname(inputPath) === ".zip") {
const AdmZip = (await import("adm-zip")).default;
const zip = new AdmZip(inputPath);
zip.extractAllTo(outputPath, true, true);
}
}

async function getBinary() {
const downloadURL = getBinaryDownloadURL();
console.log(`Downloading ${name} from ${downloadURL}`);

const pkgName = ["win32", "cygwin"].includes(process.platform)
? `package.zip`
: `package.tar.gz`;
const packagePath = path.join(binDir, pkgName);

await downloadPackage(downloadURL, packagePath);
await extractPackage(packagePath, binDir);

fs.rmSync(packagePath);
}
122 changes: 122 additions & 0 deletions npm/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ttyper",
"version": "0.0.0",
"description": "ttyper is a terminal-based typing test built with Rust and tui-rs.",
"repository": {
"type": "git",
"url": "git+https://github.com/Anush008/ttyper.git"
},
"releasesUrl": "https://github.com/Anush008/ttyper/releases",
Comment on lines +5 to +9
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Update these URLs to https://github.com/max-niederman/ttyper

"license": "MIT",
"bin": {
"ttyper": "runner.js"
},
"type": "module",
"scripts": {
"install": "node install.js"
},
"binDir": "bin",
"dependencies": {
"adm-zip": "^0.5.10",
"tar": "^6.1.15"
}
}
23 changes: 23 additions & 0 deletions npm/runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node

import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

// ESM doesn't support __dirname and __filename by default
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const packageJsonPath = path.join(__dirname, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const { name, binDir } = packageJson;

const commandArgs = process.argv.slice(2);
const exeName = ["win32", "cygwin"].includes(process.platform)
? `${name}.exe`
: name;
const cwd = path.join(__dirname, binDir);
const binPath = path.join(cwd, exeName);

const child = spawn(binPath, commandArgs, { stdio: "inherit", cwd });
Loading