···11+#!/usr/bin/env node
22+"use strict";
33+44+const crypto = require('crypto');
55+const fs = require("fs");
66+const https = require("https");
77+const path = require("path");
88+const util = require("util");
99+1010+const lockfile = require("@yarnpkg/lockfile")
1111+const docopt = require("docopt").docopt;
1212+1313+////////////////////////////////////////////////////////////////////////////////
1414+1515+const USAGE = `
1616+Usage: yarn2nix [options]
1717+1818+Options:
1919+ -h --help Shows this help.
2020+ --no-nix Hide the nix output
2121+ --no-patch Don't patch the lockfile if hashes are missing
2222+ --lockfile=FILE Specify path to the lockfile [default: ./yarn.lock].
2323+`
2424+2525+const HEAD = `
2626+{fetchurl, linkFarm}: rec {
2727+ offline_cache = linkFarm "offline" packages;
2828+ packages = [
2929+`.trim();
3030+3131+////////////////////////////////////////////////////////////////////////////////
3232+3333+function generateNix(lockedDependencies) {
3434+ let found = {};
3535+3636+ console.log(HEAD)
3737+3838+ for (var depRange in lockedDependencies) {
3939+ let dep = lockedDependencies[depRange];
4040+4141+ let depRangeParts = depRange.split('@');
4242+ let [url, sha1] = dep["resolved"].split("#");
4343+ let file_name = path.basename(url)
4444+4545+ if (found.hasOwnProperty(file_name)) {
4646+ continue;
4747+ } else {
4848+ found[file_name] = null;
4949+ }
5050+5151+5252+ console.log(`
5353+ {
5454+ name = "${file_name}";
5555+ path = fetchurl {
5656+ name = "${file_name}";
5757+ url = "${url}";
5858+ sha1 = "${sha1}";
5959+ };
6060+ }`)
6161+ }
6262+6363+ console.log(" ];")
6464+ console.log("}")
6565+}
6666+6767+6868+function getSha1(url) {
6969+ return new Promise((resolve, reject) => {
7070+ https.get(url, (res) => {
7171+ const { statusCode } = res;
7272+ const hash = crypto.createHash('sha1');
7373+ if (statusCode !== 200) {
7474+ const err = new Error('Request Failed.\n' +
7575+ `Status Code: ${statusCode}`);
7676+ // consume response data to free up memory
7777+ res.resume();
7878+ reject(err);
7979+ }
8080+8181+ res.on('data', (chunk) => { hash.update(chunk); });
8282+ res.on('end', () => { resolve(hash.digest('hex')) });
8383+ res.on('error', reject);
8484+ });
8585+ });
8686+};
8787+8888+function updateResolvedSha1(pkg) {
8989+ // local dependency
9090+ if (!pkg.resolved) { return Promise.resolve(); }
9191+ let [url, sha1] = pkg.resolved.split("#", 2)
9292+ if (!sha1) {
9393+ return new Promise((resolve, reject) => {
9494+ getSha1(url).then(sha1 => {
9595+ pkg.resolved = `${url}#${sha1}`;
9696+ resolve();
9797+ }).catch(reject);
9898+ });
9999+ } else {
100100+ // nothing to do
101101+ return Promise.resolve();
102102+ };
103103+}
104104+105105+function values(obj) {
106106+ var entries = [];
107107+ for (let key in obj) {
108108+ entries.push(obj[key]);
109109+ }
110110+ return entries;
111111+}
112112+113113+////////////////////////////////////////////////////////////////////////////////
114114+// Main
115115+////////////////////////////////////////////////////////////////////////////////
116116+117117+var options = docopt(USAGE);
118118+119119+let data = fs.readFileSync(options['--lockfile'], 'utf8')
120120+let json = lockfile.parse(data)
121121+if (json.type != "success") {
122122+ throw new Error("yarn.lock parse error")
123123+}
124124+125125+// Check fore missing hashes in the yarn.lock and patch if necessary
126126+var pkgs = values(json.object);
127127+Promise.all(pkgs.map(updateResolvedSha1)).then(() => {
128128+ let newData = lockfile.stringify(json.object);
129129+130130+ if (newData != data) {
131131+ console.error("found changes in the lockfile", options["--lockfile"]);
132132+133133+ if (options["--no-patch"]) {
134134+ console.error("...aborting");
135135+ process.exit(1);
136136+ }
137137+138138+ fs.writeFileSync(options['--lockfile'], newData);
139139+ }
140140+141141+ if (!options['--no-nix']) {
142142+ generateNix(json.object);
143143+ }
144144+})
+199
pkgs/development/tools/yarn2nix/default.nix
···11+{ stdenv, lib, fetchurl, linkFarm, runCommand, nodejs, yarn }:
22+33+let
44+ unlessNull = item: alt:
55+ if item == null then alt else item;
66+77+ yarn2nix = mkYarnPackage {
88+ src = ./.;
99+ yarnNix = ./yarn.nix;
1010+1111+ passthru = {
1212+ inherit
1313+ defaultYarnFlags
1414+ linkNodeModulesHook
1515+ mkYarnModules
1616+ mkYarnNix
1717+ mkYarnPackage
1818+ # Export yarn again to make it easier to find out which yarn was used.
1919+ yarn
2020+ ;
2121+ };
2222+2323+ meta = with lib; {
2424+ description = "generate nix expressions from a yarn.lock file";
2525+ homepage = "https://github.com/moretea/yarn2nix";
2626+ license = licenses.gpl3;
2727+ maintainers = with maintainers; [ manveru zimbatm ];
2828+ };
2929+ };
3030+3131+ # Generates the yarn.nix from the yarn.lock file
3232+ mkYarnNix = yarnLock:
3333+ runCommand "yarn.nix" {}
3434+ "${yarn2nix}/bin/yarn2nix --lockfile ${yarnLock} --no-patch > $out";
3535+3636+ # Loads the generated offline cache. This will be used by yarn as
3737+ # the package source.
3838+ importOfflineCache = yarnNix:
3939+ let
4040+ pkg = import yarnNix { inherit fetchurl linkFarm; };
4141+ in
4242+ pkg.offline_cache;
4343+4444+ defaultYarnFlags = [
4545+ "--offline"
4646+ "--frozen-lockfile"
4747+ "--ignore-engines"
4848+ "--ignore-scripts"
4949+ ];
5050+5151+ mkYarnModules = {
5252+ name,
5353+ packageJSON,
5454+ yarnLock,
5555+ yarnNix ? mkYarnNix yarnLock,
5656+ yarnFlags ? defaultYarnFlags,
5757+ pkgConfig ? {},
5858+ preBuild ? "",
5959+ }:
6060+ let
6161+ offlineCache = importOfflineCache yarnNix;
6262+ extraBuildInputs = (lib.flatten (builtins.map (key:
6363+ pkgConfig.${key} . buildInputs or []
6464+ ) (builtins.attrNames pkgConfig)));
6565+ postInstall = (builtins.map (key:
6666+ if (pkgConfig.${key} ? postInstall) then
6767+ ''
6868+ for f in $(find -L -path '*/node_modules/${key}' -type d); do
6969+ (cd "$f" && (${pkgConfig.${key}.postInstall}))
7070+ done
7171+ ''
7272+ else
7373+ ""
7474+ ) (builtins.attrNames pkgConfig));
7575+ in
7676+ stdenv.mkDerivation {
7777+ inherit name preBuild;
7878+ phases = ["configurePhase" "buildPhase"];
7979+ buildInputs = [ yarn nodejs ] ++ extraBuildInputs;
8080+8181+ configurePhase = ''
8282+ # Yarn writes cache directories etc to $HOME.
8383+ export HOME=$PWD/yarn_home
8484+ '';
8585+8686+ buildPhase = ''
8787+ runHook preBuild
8888+8989+ cp ${packageJSON} ./package.json
9090+ cp ${yarnLock} ./yarn.lock
9191+ chmod +w ./yarn.lock
9292+9393+ yarn config --offline set yarn-offline-mirror ${offlineCache}
9494+9595+ # Do not look up in the registry, but in the offline cache.
9696+ # TODO: Ask upstream to fix this mess.
9797+ sed -i -E 's|^(\s*resolved\s*")https?://.*/|\1|' yarn.lock
9898+ yarn install ${lib.escapeShellArgs yarnFlags}
9999+100100+ ${lib.concatStringsSep "\n" postInstall}
101101+102102+ mkdir $out
103103+ mv node_modules $out/
104104+ patchShebangs $out
105105+ '';
106106+ };
107107+108108+ # This can be used as a shellHook in mkYarnPackage. It brings the built node_modules into
109109+ # the shell-hook environment.
110110+ linkNodeModulesHook = ''
111111+ if [[ -d node_modules || -L node_modules ]]; then
112112+ echo "./node_modules is present. Replacing."
113113+ rm -rf node_modules
114114+ fi
115115+116116+ ln -s "$node_modules" node_modules
117117+ '';
118118+119119+ mkYarnPackage = {
120120+ name ? null,
121121+ src,
122122+ packageJSON ? src + "/package.json",
123123+ yarnLock ? src + "/yarn.lock",
124124+ yarnNix ? mkYarnNix yarnLock,
125125+ yarnFlags ? defaultYarnFlags,
126126+ yarnPreBuild ? "",
127127+ pkgConfig ? {},
128128+ extraBuildInputs ? [],
129129+ publishBinsFor ? null,
130130+ ...
131131+ }@attrs:
132132+ let
133133+ package = lib.importJSON packageJSON;
134134+ pname = package.name;
135135+ version = package.version;
136136+ deps = mkYarnModules {
137137+ name = "${pname}-modules-${version}";
138138+ preBuild = yarnPreBuild;
139139+ inherit packageJSON yarnLock yarnNix yarnFlags pkgConfig;
140140+ };
141141+ publishBinsFor_ = unlessNull publishBinsFor [pname];
142142+ in stdenv.mkDerivation (builtins.removeAttrs attrs ["pkgConfig"] // {
143143+ inherit src;
144144+145145+ name = unlessNull name "${pname}-${version}";
146146+147147+ buildInputs = [ yarn nodejs ] ++ extraBuildInputs;
148148+149149+ node_modules = deps + "/node_modules";
150150+151151+ configurePhase = attrs.configurePhase or ''
152152+ runHook preConfigure
153153+154154+ if [ -d npm-packages-offline-cache ]; then
155155+ echo "npm-pacakges-offline-cache dir present. Removing."
156156+ rm -rf npm-packages-offline-cache
157157+ fi
158158+159159+ if [[ -d node_modules || -L node_modules ]]; then
160160+ echo "./node_modules is present. Removing."
161161+ rm -rf node_modules
162162+ fi
163163+164164+ mkdir -p node_modules
165165+ ln -s $node_modules/* node_modules/
166166+ ln -s $node_modules/.bin node_modules/
167167+168168+ if [ -d node_modules/${pname} ]; then
169169+ echo "Error! There is already an ${pname} package in the top level node_modules dir!"
170170+ exit 1
171171+ fi
172172+173173+ runHook postConfigure
174174+ '';
175175+176176+ # Replace this phase on frontend packages where only the generated
177177+ # files are an interesting output.
178178+ installPhase = attrs.installPhase or ''
179179+ runHook preInstall
180180+181181+ mkdir -p $out
182182+ cp -r node_modules $out/node_modules
183183+ cp -r . $out/node_modules/${pname}
184184+ rm -rf $out/node_modules/${pname}/node_modules
185185+186186+ mkdir $out/bin
187187+ node ${./fixup_bin.js} $out ${lib.concatStringsSep " " publishBinsFor_}
188188+189189+ runHook postInstall
190190+ '';
191191+192192+ passthru = {
193193+ inherit package deps;
194194+ } // (attrs.passthru or {});
195195+196196+ # TODO: populate meta automatically
197197+ });
198198+in
199199+ yarn2nix
+45
pkgs/development/tools/yarn2nix/fixup_bin.js
···11+#!/usr/bin/env node
22+"use strict";
33+44+/* Usage:
55+ * node fixup_bin.js <output_dir> [<bin_pkg_1>, <bin_pkg_2> ... ]
66+ */
77+88+const fs = require("fs");
99+const path = require("path");
1010+1111+const output = process.argv[2];
1212+const packages_to_publish_bin = process.argv.slice(3);
1313+const derivation_bin_path = output + "/bin";
1414+1515+function processPackage(name) {
1616+ console.log("Processing ", name);
1717+ const package_path = output + "/node_modules/" + name;
1818+ const package_json_path = package_path + "/package.json";
1919+ const package_json = JSON.parse(fs.readFileSync(package_json_path));
2020+2121+ if (!package_json.bin) {
2222+ console.log("No binaries provided");
2323+ return;
2424+ }
2525+2626+ // There are two alternative syntaxes for `bin`
2727+ // a) just a plain string, in which case the name of the package is the name of the binary.
2828+ // b) an object, where key is the name of the eventual binary, and the value the path to that binary.
2929+ if (typeof package_json.bin == "string") {
3030+ let bin_name = package_json.bin;
3131+ package_json.bin = { };
3232+ package_json.bin[package_json.name] = bin_name;
3333+ }
3434+3535+ for (let binName in package_json.bin) {
3636+ const bin_path = package_json.bin[binName];
3737+ const full_bin_path = path.normalize(package_path + "/" + bin_path);
3838+ fs.symlinkSync(full_bin_path, derivation_bin_path + "/"+ binName);
3939+ console.log("Linked", binName);
4040+ }
4141+}
4242+4343+packages_to_publish_bin.forEach((pkg) => {
4444+ processPackage(pkg);
4545+});
···11+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
22+# yarn lockfile v1
33+44+55+"@yarnpkg/lockfile@^1.0.0":
66+ version "1.0.0"
77+ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.0.0.tgz#33d1dbb659a23b81f87f048762b35a446172add3"
88+99+docopt@^0.6.2:
1010+ version "0.6.2"
1111+ resolved "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz#b28e9e2220da5ec49f7ea5bb24a47787405eeb11"