aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--LICENSE13
-rw-r--r--README.md85
-rwxr-xr-xcmd/main.js173
-rw-r--r--lib/template.js89
-rw-r--r--package.json24
6 files changed, 389 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b3bb1aa
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+0.1.0 (2019-10-02)
+==================
+
+ * Initial version.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..456c488
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..731814e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+Npm4nix
+=======
+
+Npm4nix converts Node.js packages into a single Nix build expression
+for the [Npm.nix](https://github.com/ip1981/npm.nix) project. Npm4nix is
+inspired by [Cabal2nix](https://github.com/NixOS/cabal2nix). Unfortunately,
+[npm2nix](https://github.com/NixOS/npm2nix) is already taken :) Thus,
+`npm4nix`. It supports local directories and tarballs (e. g. `*.tgz`),
+remote tarballs, remote [Git](https://git-scm.com/) respositories.
+
+
+Requirements
+============
+
+Npm4nix is written in JavaScript with minimum number of depednecies
+and should be executed by [Node.js](https://nodejs.org).
+Npm4nix invokes [curl](https://curl.haxx.se/), Git, and
+[nix-hash](https://nixos.org/nix/manual/#sec-nix-hash), so these tools
+should be installed.
+
+
+Example
+=======
+
+
+```
+$ node cmd/main.js https://github.com/substack/node-mkdirp.git > mkdirp.nix
+executing git clone 'https://github.com/substack/node-mkdirp.git' '/tmp/npm4nix-Feo4WM'
+Cloning into '/tmp/npm4nix-Feo4WM'...
+executing git -C '/tmp/npm4nix-Feo4WM' rev-parse HEAD
+executing nix-hash --base32 --type sha256 '/tmp/npm4nix-Feo4WM'
+
+$ cat mkdirp.nix
+{ fetchgit, buildNpmPackage, minimist, mock-fs, tap }:
+
+buildNpmPackage {
+ pname = "mkdirp";
+ version = "0.5.1";
+ src = fetchgit {
+ url = "https://github.com/substack/node-mkdirp.git";
+ rev = "f2003bbcffa80f8c9744579fabab1212fc84545a";
+ sha256 = "0qc3l6571aknhlmzcyaah3plmf852cl160jihy3l4b05j25qv45a";
+ };
+
+ meta = {
+ description = "Recursively mkdir, like `mkdir -p`";
+ homepage = "";
+ license = "MIT";
+ };
+
+ npmInputs = [
+ minimist mock-fs tap
+ ];
+}
+```
+
+```
+$ node cmd/main.js https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz > async-foreach.nix
+executing curl -LsSf -o '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz' 'https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz'
+executing nix-hash --flat --base32 --type sha256 '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz'
+executing tar -xOf '/tmp/npm4nix-zwGeOX/v0.1.3.tar.gz' --wildcards '*/package.json'
+
+$ cat async-foreach.nix
+{ fetchurl, buildNpmPackage }:
+
+buildNpmPackage {
+ pname = "async-foreach";
+ version = "0.1.3";
+ src = fetchurl {
+ url = "https://github.com/cowboy/javascript-sync-async-foreach/archive/v0.1.3.tar.gz";
+ sha256 = "1b7h2fgj6rndkviyx1hl0mh72d60a2b2f1sl86ndk8vdvr6mxmj3";
+ };
+
+ meta = {
+ description = "An optionally-asynchronous forEach with an interesting interface.";
+ homepage = "http://github.com/cowboy/javascript-sync-async-foreach";
+ license = "";
+ };
+
+ npmInputs = [
+
+ ];
+}
+
+```
diff --git a/cmd/main.js b/cmd/main.js
new file mode 100755
index 0000000..b71bbed
--- /dev/null
+++ b/cmd/main.js
@@ -0,0 +1,173 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const process = require('process');
+const {
+ execSync
+} = require('child_process');
+
+const render = require('../lib/template');
+
+var url = null,
+ revision = null;
+
+function log(msg) {
+ console.error(msg);
+}
+
+function printHelp() {
+ console.log(`
+npm4nix converts NPM packages into build instructions for Nix.
+
+Usage: npm4nix [options] URL
+
+Options:
+ -r, --revision revision to fetching from Git (tag, branch, hash)
+ -h, --help show this help screen
+
+URL can be:
+ - a local directory with package.json
+ - a local archive (tarball)
+ - a remote Git URL
+ - a remote archive (tarball)
+
+Examples:
+
+ $ npm4nix -r 0.5.1 https://github.com/substack/node-mkdirp.git > mkdirp.nix
+ $ npm4nix . > mypkg.nix
+ `);
+}
+
+function exec(cmd) {
+ log(`executing ${cmd}`);
+ return execSync(cmd, {
+ encoding: 'utf-8'
+ }).trim();
+}
+
+function getSHA256(p) {
+ var flat = '';
+ if (fs.statSync(p).isFile()) {
+ flat = ' --flat';
+ }
+
+ return exec(`nix-hash${flat} --base32 --type sha256 '${p}'`);
+}
+
+function mkdtemp() {
+ const tmpdir = os.tmpdir();
+ return fs.mkdtempSync(`${tmpdir}${path.sep}npm4nix-`);
+}
+
+function rmTree(dir) {
+ fs.readdirSync(dir).forEach(function(entry, index) {
+ const p = path.join(dir, entry);
+ if (fs.lstatSync(p).isDirectory()) {
+ rmTree(p);
+ } else {
+ fs.unlinkSync(p);
+ }
+ });
+ fs.rmdirSync(dir);
+};
+
+function readPackage(p) {
+ var pkgFile = path.join(p, 'package.json');
+ return JSON.parse(fs.readFileSync(pkgFile));
+}
+
+function isLocal(p) {
+ return fs.existsSync(p);
+}
+
+function processLocal(p) {
+ console.log(render(p, readPackage(p)));
+}
+
+function isArchive(url) {
+ return url.match(new RegExp('(https?://)?.+/[^/]+\.t(ar\.)?(gz|bz2|xz)', 'i'));
+}
+
+function processArchive(url) {
+ const dir = mkdtemp();
+ const file = path.join(dir, path.basename(url));
+ var sha256, pkg;
+ try {
+ exec(`curl -LsSf -o '${file}' '${url}'`);
+ sha256 = getSHA256(file);
+ pkg = JSON.parse(exec(`tar -xOf '${file}' --wildcards '*/package.json'`));
+ } finally {
+ rmTree(dir);
+ }
+ console.log(render({
+ fetch: 'fetchurl',
+ url: url,
+ sha256: sha256
+ }, pkg));
+}
+
+function isGit(url) {
+ return url.match(new RegExp('(.*https://)?(github|gitlab)\.com/[^/]+/[^/]+(\.git)?$', 'i')) ||
+ url.match(new RegExp('(.*https://)?bitbucket\.org/[^/]+/[^/]+(\.git)?$', 'i')) ||
+ url.match(new RegExp('^git(\\+https)?://.+', 'i'));
+}
+
+function processGit(url) {
+ const dir = mkdtemp();
+ var rev, sha256, pkg;
+ try {
+ exec(`git clone '${url}' '${dir}'`);
+ if (revision !== null) {
+ exec(`git -C '${dir}' checkout '${revision}'`);
+ }
+ rev = exec(`git -C '${dir}' rev-parse HEAD`);
+ rmTree(path.join(dir, '.git'));
+ sha256 = getSHA256(dir);
+ pkg = readPackage(dir);
+ } finally {
+ rmTree(dir);
+ }
+ console.log(render({
+ fetch: 'fetchgit',
+ url: url,
+ rev: rev,
+ sha256: sha256
+ }, pkg));
+}
+
+
+const args = process.argv.slice(2);
+
+for (var i = 0; i < args.length; i++) {
+ switch (args[i]) {
+ case '-r':
+ case '--revision':
+ revision = args[++i];
+ break;
+ case '-h':
+ case '--help':
+ printHelp();
+ process.exit();
+ break;
+ default:
+ url = args[i];
+ }
+}
+
+if (url === null) {
+ log('Missing URL. Add --help for usage info');
+ process.exit(1);
+}
+
+if (isLocal(url)) {
+ processLocal(url);
+} else if (isGit(url)) {
+ processGit(url);
+} else if (isArchive(url)) {
+ processArchive(url);
+} else {
+ log(`unsupported URL: '${url}'`);
+ process.exit(1);
+}
diff --git a/lib/template.js b/lib/template.js
new file mode 100644
index 0000000..ce05473
--- /dev/null
+++ b/lib/template.js
@@ -0,0 +1,89 @@
+function normalize(name) {
+ return name.replace('/', '-').replace('@', '').replace('.', '-');
+}
+
+function fmt(words, maxLine, glue1, glue2) {
+ var line = [];
+ var lines = []
+ var lineLength = 0;
+
+ for (var i = 0; i < words.length; i++) {
+ const itemLength = glue1.length + words[i].length
+ if (itemLength + lineLength <= maxLine || line.length === 0) {
+ line.push(words[i]);
+ lineLength += itemLength;
+ } else {
+ lines.push(line);
+ line = [words[i]];
+ lineLength = itemLength;
+ }
+ }
+
+ lines.push(line);
+
+ lines = lines.map((l) => {
+ return l.join(glue1);
+ });
+
+ return lines.join(glue2);
+}
+
+
+function render(src, pkg) {
+ const deps = pkg.dependencies || {};
+ const devDeps = pkg.devDependencies || {};
+ const npmInputs = Object.keys({...deps,
+ ...devDeps
+ }).sort().map(normalize);
+ const args = ['buildNpmPackage', ...npmInputs];
+
+ var source = '';
+
+ if ('string' === typeof src) {
+ if (!src.startsWith('/') && !src.startsWith('./') && !src.startsWith('../')) {
+ src = `./${src}`;
+ }
+ source = ` src = ${src};`;
+ } else {
+ args.unshift(src.fetch);
+ switch (src.fetch) {
+ case 'fetchurl':
+ source = `\
+ src = ${src.fetch} {
+ url = "${src.url}";
+ sha256 = "${src.sha256}";
+ };`;
+ break;
+ case 'fetchgit':
+ source = `\
+ src = ${src.fetch} {
+ url = "${src.url}";
+ rev = "${src.rev}";
+ sha256 = "${src.sha256}";
+ };`;
+ break;
+ }
+ }
+
+ return `\
+{ ${fmt(args, 90, ', ', '\n, ')} }:
+
+buildNpmPackage {
+ pname = "${pkg.name}";
+ version = "${pkg.version}";
+${source}
+
+ meta = {
+ description = "${pkg.description || ''}";
+ homepage = "${pkg.homepage || ''}";
+ license = "${pkg.license || ''}";
+ };
+
+ npmInputs = [
+ ${fmt(npmInputs, 80, ' ', '\n ')}
+ ];
+}
+ `;
+}
+
+module.exports = render;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..43b9be3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "npm4nix",
+ "version": "0.1.0",
+ "description": "",
+ "scripts": {
+ },
+ "bin": {
+ "npm4nix": "cmd/main.js"
+ },
+ "files": [
+ "cmd/*.js",
+ "lib/*.js"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ip1981/npm4nix.git"
+ },
+ "author": "Igor Pashev",
+ "license": "WTFPL",
+ "bugs": {
+ "url": "https://github.com/ip1981/npm4nix/issues"
+ },
+ "homepage": "https://github.com/ip1981/npm4nix#readme"
+}