diff --git a/integration/__fixtures__/lerna-generated-build-tool/.gitignore b/integration/__fixtures__/lerna-generated-build-tool/.gitignore new file mode 100644 index 0000000000..4ba2db8253 --- /dev/null +++ b/integration/__fixtures__/lerna-generated-build-tool/.gitignore @@ -0,0 +1 @@ +a-build-tool.js diff --git a/integration/__fixtures__/lerna-generated-build-tool/lerna.json b/integration/__fixtures__/lerna-generated-build-tool/lerna.json new file mode 100644 index 0000000000..1587a66968 --- /dev/null +++ b/integration/__fixtures__/lerna-generated-build-tool/lerna.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} diff --git a/integration/__fixtures__/lerna-generated-build-tool/package.json b/integration/__fixtures__/lerna-generated-build-tool/package.json new file mode 100644 index 0000000000..3b59021096 --- /dev/null +++ b/integration/__fixtures__/lerna-generated-build-tool/package.json @@ -0,0 +1,7 @@ +{ + "name": "basic", + "version": "monorepo", + "private": true, + "dependencies": { + } +} diff --git a/integration/__fixtures__/lerna-generated-build-tool/packages/build-tool/package.json b/integration/__fixtures__/lerna-generated-build-tool/packages/build-tool/package.json new file mode 100644 index 0000000000..5f8710f055 --- /dev/null +++ b/integration/__fixtures__/lerna-generated-build-tool/packages/build-tool/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/build-tool", + "version": "1.0.0", + "bin": { + "a-build-tool": "a-build-tool.js" + }, + "scripts": { + "build": "echo \"#!/usr/bin/env node\nconsole.log('build tool executed')\" > a-build-tool.js" + } +} diff --git a/integration/__fixtures__/lerna-generated-build-tool/packages/buildable/package.json b/integration/__fixtures__/lerna-generated-build-tool/packages/buildable/package.json new file mode 100644 index 0000000000..2de1dc04d9 --- /dev/null +++ b/integration/__fixtures__/lerna-generated-build-tool/packages/buildable/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/buildable", + "version": "2.0.0", + "devDependencies": { + "@test/build-tool": "*" + }, + "scripts": { + "build": "a-build-tool" + } +} diff --git a/integration/lerna-link-sibling-bins.test.js b/integration/lerna-link-sibling-bins.test.js new file mode 100644 index 0000000000..20e6c86e30 --- /dev/null +++ b/integration/lerna-link-sibling-bins.test.js @@ -0,0 +1,16 @@ +"use strict"; + +const cliRunner = require("@lerna-test/cli-runner"); +const initFixture = require("@lerna-test/init-fixture")(__dirname); + +test("lerna link symlinks generated binaries of sibling packages", async () => { + const cwd = await initFixture("lerna-generated-build-tool"); + const lerna = cliRunner(cwd); + + // First bootstrap, I expect this to succeed but don't are about the output + await lerna("bootstrap"); + + const { stdout } = await lerna("run", "build"); + + expect(stdout).toMatch("build tool executed"); +}); diff --git a/utils/create-symlink/__tests__/create-symlink.test.js b/utils/create-symlink/__tests__/create-symlink.test.js index ab6f1bfe0c..79663ad327 100644 --- a/utils/create-symlink/__tests__/create-symlink.test.js +++ b/utils/create-symlink/__tests__/create-symlink.test.js @@ -15,6 +15,7 @@ describe("create-symlink", () => { fs.lstat.mockImplementation(() => Promise.reject(new Error("MOCK"))); fs.unlink.mockResolvedValue(); fs.symlink.mockResolvedValue(); + fs.pathExists.mockResolvedValue(true); // cmdShim is a traditional errback cmdShim.mockImplementation(callsBack()); diff --git a/utils/create-symlink/create-symlink.js b/utils/create-symlink/create-symlink.js index 11659c88b7..77ff2fe8cf 100644 --- a/utils/create-symlink/create-symlink.js +++ b/utils/create-symlink/create-symlink.js @@ -31,9 +31,20 @@ function createSymbolicLink(src, dest, type) { function createPosixSymlink(origin, dest, _type) { const type = _type === "exec" ? "file" : _type; - const src = path.relative(path.dirname(dest), origin); + const relativeSymlink = path.relative(path.dirname(dest), origin); - return createSymbolicLink(src, dest, type); + if (_type === "exec") { + // If the target exists, create real symlink. If the target doesn't exist yet, + // create a shim shell script. + return fs.pathExists(origin).then(exists => { + if (exists) { + return createSymbolicLink(relativeSymlink, dest, type).then(() => fs.chmod(origin, "755")); + } + return shShim(origin, dest, type).then(() => fs.chmod(dest, "755")); + }); + } + + return createSymbolicLink(relativeSymlink, dest, type); } function createWindowsSymlink(src, dest, type) { @@ -51,3 +62,13 @@ function createWindowsSymlink(src, dest, type) { return createSymbolicLink(src, dest, type); } + +function shShim(target, script, type) { + log.silly("shShim", [target, script, type]); + + const absTarget = path.resolve(path.dirname(script), target); + + const scriptLines = ["#!/bin/sh", `chmod +x ${absTarget} && exec ${absTarget} "$@"`]; + + return fs.writeFile(script, scriptLines.join("\n")); +} diff --git a/utils/symlink-binary/__tests__/symlink-binary.test.js b/utils/symlink-binary/__tests__/symlink-binary.test.js index 7e7cc1144a..252ee75e65 100644 --- a/utils/symlink-binary/__tests__/symlink-binary.test.js +++ b/utils/symlink-binary/__tests__/symlink-binary.test.js @@ -42,7 +42,7 @@ describe("symlink-binary", () => { expect(dstPath).not.toHaveBinaryLinks(); }); - it("should skip missing bin files", async () => { + it("should create shims for all declared binaries", async () => { const testDir = await initFixture("links"); const srcPath = path.join(testDir, "packages/package-3"); const dstPath = path.join(testDir, "packages/package-4"); @@ -50,7 +50,7 @@ describe("symlink-binary", () => { await symlinkBinary(srcPath, dstPath); expect(srcPath).toHaveExecutables("cli1.js", "cli2.js"); - expect(dstPath).toHaveBinaryLinks("links3cli1", "links3cli2"); + expect(dstPath).toHaveBinaryLinks("links3cli1", "links3cli2", "links3cli3"); }); it("should preserve previous bin entries", async () => { @@ -62,6 +62,6 @@ describe("symlink-binary", () => { await symlinkBinary(pkg2Path, destPath); await symlinkBinary(pkg3Path, destPath); - expect(destPath).toHaveBinaryLinks("links-2", "links3cli1", "links3cli2"); + expect(destPath).toHaveBinaryLinks("links-2", "links3cli1", "links3cli2", "links3cli3"); }); }); diff --git a/utils/symlink-binary/symlink-binary.js b/utils/symlink-binary/symlink-binary.js index 31a1633ef4..d6b12bd6e2 100644 --- a/utils/symlink-binary/symlink-binary.js +++ b/utils/symlink-binary/symlink-binary.js @@ -22,11 +22,11 @@ function symlinkBinary(srcPackageRef, destPackageRef) { const src = path.join(srcPackage.location, srcPackage.bin[name]); const dst = path.join(destPackage.binLocation, name); - return fs.pathExists(src).then(exists => { - if (exists) { - return { src, dst }; - } - }); + // Symlink all declared binaries, even if they don't exist (yet). We will + // assume the package author knows what they're doing and that the binaries + // will be generated during a later build phase (potentially source compiled from + // another language). + return { src, dst }; }); if (actions.length === 0) { @@ -36,7 +36,7 @@ function symlinkBinary(srcPackageRef, destPackageRef) { return fs.mkdirp(destPackage.binLocation).then(() => pMap(actions, meta => { if (meta) { - return createSymlink(meta.src, meta.dst, "exec").then(() => fs.chmod(meta.src, "755")); + return createSymlink(meta.src, meta.dst, "exec"); } }) );