diff --git a/README.md b/README.md index 595c67c..665f2e4 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,13 @@ Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) import hugo from "hugo-extended"; import { execFile } from "child_process"; -execFile(hugo, ["version"], (error, stdout) => { - console.log(stdout); -}); +(async () => { + const binPath = await hugo(); + + execFile(binPath, ["version"], (error, stdout) => { + console.log(stdout); + }); +})(); ``` ```bash diff --git a/index.d.ts b/index.d.ts index 73bc20a..e6d09c8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,8 +1,7 @@ /// /** - * @returns {string} Absolute path to the Hugo executable (`hugo.exe` on - * Windows, simply `hugo` otherwise). + * @returns A promise of the absolute path to the Hugo executable (`hugo.exe` on + * Windows, simply `hugo` otherwise) once it's installed. */ -declare const hugo: string; -export = hugo; +export default function hugo(): Promise; diff --git a/index.js b/index.js index 7a996e7..96cf54a 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,22 @@ -import path from "path"; -import { fileURLToPath } from "url"; -import { getBinFilename } from "./lib/utils.js"; +import logSymbols from "log-symbols"; +import install from "./lib/install.js"; +import { getBinPath, doesBinExist } from "./lib/utils.js"; -// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#what-do-i-use-instead-of-__dirname-and-__filename -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const hugo = async () => { + const bin = getBinPath(); -const hugo = path.join( - __dirname, - "vendor", - getBinFilename(), -); + // A fix for fleeting ENOENT errors, where Hugo seems to disappear. For now, + // just reinstall Hugo when it's missing and then continue normally like + // nothing happened. + // See: https://github.com/jakejarvis/hugo-extended/issues/81 + if (!doesBinExist(bin)) { + // Hugo isn't there for some reason. Try re-installing. + console.info(`${logSymbols.info} Hugo is missing, reinstalling now...`); + await install(); + } + + return bin; +}; // The only thing this module really exports is the absolute path to Hugo: export default hugo; diff --git a/lib/cli.js b/lib/cli.js index a9279b6..82b8538 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -3,10 +3,13 @@ import { spawn } from "child_process"; import hugo from "../index.js"; -const args = process.argv.slice(2); +(async () => { + const args = process.argv.slice(2); + const bin = await hugo(); -spawn(hugo, args, { stdio: "inherit" }) - .on("exit", (code) => { - // forward Hugo's exit code so this module itself reports success/failure - process.exit(code); - }); + spawn(bin, args, { stdio: "inherit" }) + .on("exit", (code) => { + // forward Hugo's exit code so this module itself reports success/failure + process.exit(code); + }); +})(); diff --git a/lib/install.js b/lib/install.js index 8e00ca8..08c8c79 100644 --- a/lib/install.js +++ b/lib/install.js @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import { fileURLToPath } from "url"; import downloader from "careful-downloader"; import logSymbols from "log-symbols"; import { @@ -12,50 +13,50 @@ import { isExtended, } from "./utils.js"; -installHugo() - .then((bin) => - // try querying hugo's version via CLI - getBinVersion(bin), - ) - .then((version) => { - // print output of `hugo version` to console - console.log(`${logSymbols.success} Hugo installed successfully!`); - console.log(version); - }) - .catch((error) => { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function install() { + try { + const version = getPkgVersion(); + const releaseFile = getReleaseFilename(version); + const checksumFile = getChecksumFilename(version); + const binFile = getBinFilename(); + + // stop here if there's nothing we can download + if (!releaseFile) { + throw new Error(`Are you sure this platform is supported? See: https://github.com/gohugoio/hugo/releases/tag/v${version}`); + } + + // warn if platform doesn't support Hugo Extended, proceed with vanilla Hugo + if (!isExtended(releaseFile)) { + console.warn(`${logSymbols.info} Hugo Extended isn't supported on this platform, downloading vanilla Hugo instead.`); + } + + // download release from GitHub and verify its checksum + const download = await downloader(getReleaseUrl(version, releaseFile), { + checksumUrl: getReleaseUrl(version, checksumFile), + filename: releaseFile, + destDir: path.join(__dirname, "..", "vendor"), + algorithm: "sha256", + extract: true, + }); + + // full path to the binary + const installedToPath = path.join(download, binFile); + + // ensure hugo[.exe] is executable + fs.chmodSync(installedToPath, 0o755); + + console.info(`${logSymbols.success} Hugo installed successfully!`); + console.info(getBinVersion(installedToPath)); + + // return the full path to our Hugo binary + return installedToPath; + } catch (error) { // pass whatever error occured along the way to console console.error(`${logSymbols.error} Hugo installation failed. :(`); throw error; - }); - -async function installHugo() { - const version = getPkgVersion(); - const releaseFile = getReleaseFilename(version); - const checksumFile = getChecksumFilename(version); - const binFile = getBinFilename(); - - // stop here if there's nothing we can download - if (!releaseFile) { - throw new Error(`Are you sure this platform is supported? See: https://github.com/gohugoio/hugo/releases/tag/v${version}`); } - - // warn if platform doesn't support Hugo Extended, proceed with vanilla Hugo - if (!isExtended(releaseFile)) { - console.warn(`${logSymbols.info} Hugo Extended isn't supported on this platform, downloading vanilla Hugo instead.`); - } - - // download release from GitHub and verify its checksum - const download = await downloader(getReleaseUrl(version, releaseFile), { - checksumUrl: getReleaseUrl(version, checksumFile), - filename: releaseFile, - destDir: "vendor", - algorithm: "sha256", - extract: true, - }); - - // ensure hugo[.exe] is executable - fs.chmodSync(path.join(download, binFile), 0o755); - - // return the full path to our Hugo binary - return path.join(download, binFile); } + +export default install; diff --git a/lib/utils.js b/lib/utils.js index cf70bf8..b2c152f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,11 +1,16 @@ +import path from "path"; +import fs from "fs"; +import { fileURLToPath } from "url"; import { execFileSync } from "child_process"; import { readPackageUpSync } from "read-pkg-up"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + // This package's version number (should) always match the Hugo release we want. // We check for a `hugoVersion` field in package.json just in case it doesn't // match in the future (from pushing an emergency package update, etc.). export function getPkgVersion() { - const { packageJson } = readPackageUpSync(); + const { packageJson } = readPackageUpSync({ cwd: __dirname }); return packageJson.hugoVersion || packageJson.version; } @@ -19,6 +24,16 @@ export function getBinFilename() { return process.platform === "win32" ? "hugo.exe" : "hugo"; } +// Simple shortcut to ./vendor/hugo[.exe] from package root. +export function getBinPath() { + return path.join( + __dirname, + "..", + "vendor", + getBinFilename(), + ); +} + // Returns the output of the `hugo version` command, i.e.: // "hugo v0.88.1-5BC54738+extended darwin/arm64 BuildDate=..." export function getBinVersion(bin) { @@ -26,6 +41,22 @@ export function getBinVersion(bin) { return stdout.toString().trim(); } +// Simply detect if the given file exists. +export function doesBinExist(bin) { + try { + if (fs.existsSync(bin)) { + return true; + } + } catch (error) { + // something bad happened besides Hugo not existing + if (error.code !== "ENOENT") { + throw error; + } + + return false; + } +} + // Hugo Extended supports: macOS x64, macOS ARM64, Linux x64, Windows x64. // all other combos fall back to vanilla Hugo. There are surely much better ways // to do this but this is easy to read/update. :) diff --git a/package.json b/package.json index b07d98d..10ae8e9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "files": [ "index.js", "index.d.ts", + "postinstall.js", "lib" ], "bin": { @@ -33,11 +34,12 @@ }, "devDependencies": { "@jakejarvis/eslint-config": "*", + "del": "^6.0.0", "eslint": "^8.1.0", "mocha": "^9.1.3" }, "scripts": { - "postinstall": "node lib/install.js", + "postinstall": "node postinstall.js", "test": "eslint . && mocha" }, "engines": { diff --git a/postinstall.js b/postinstall.js new file mode 100644 index 0000000..7d6b7cd --- /dev/null +++ b/postinstall.js @@ -0,0 +1,4 @@ +import install from "./lib/install.js"; + +// Install Hugo right off the bat. +(async () => await install())(); diff --git a/test/index.spec.js b/test/index.spec.js index 5614573..2ba57ee 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,12 +1,33 @@ /* eslint-env node, mocha */ +import path from "path"; import { execFile } from "child_process"; import assert from "assert"; +import del from "del"; import hugo from "../index.js"; +import { getBinPath } from "../lib/utils.js"; it("Hugo exists and runs?", async function () { this.timeout(30000); // increase timeout to an excessive 30 seconds for CI - assert(execFile(hugo, ["env"], function (error, stdout) { + const hugoPath = await hugo(); + + assert(execFile(hugoPath, ["env"], function (error, stdout) { + if (error) { + throw error; + } + console.log(stdout); + })); +}); + +it("Hugo doesn't exist, install it instead of throwing an error", async function () { + this.timeout(30000); // increase timeout to an excessive 30 seconds for CI + + // delete binary to ensure it's auto-reinstalled + await del(path.dirname(getBinPath())); + + const hugoPath = await hugo(); + + assert(execFile(hugoPath, ["version"], function (error, stdout) { if (error) { throw error; } diff --git a/yarn.lock b/yarn.lock index d0663a9..1ba4eb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,23 +3,23 @@ "@babel/code-frame@^7.0.0": - version "7.15.8" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503" - integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg== + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" + integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== dependencies: - "@babel/highlight" "^7.14.5" + "@babel/highlight" "^7.16.0" -"@babel/helper-validator-identifier@^7.14.5": +"@babel/helper-validator-identifier@^7.15.7": version "7.15.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== -"@babel/highlight@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== +"@babel/highlight@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" + integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== dependencies: - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.15.7" chalk "^2.0.0" js-tokens "^4.0.0" @@ -113,9 +113,9 @@ "@types/node" "*" "@types/node@*": - version "16.11.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.1.tgz#2e50a649a50fc403433a14f829eface1a3443e97" - integrity sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA== + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/normalize-package-data@^2.4.1": version "2.4.1" @@ -687,9 +687,9 @@ esrecurse@^4.3.0: estraverse "^5.2.0" estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -892,9 +892,9 @@ glob@^7.1.3: path-is-absolute "^1.0.0" globals@^13.6.0, globals@^13.9.0: - version "13.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" - integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== + version "13.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" + integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== dependencies: type-fest "^0.20.2" @@ -1171,9 +1171,9 @@ jsonfile@^6.0.1: graceful-fs "^4.1.6" keyv@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" - integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + version "4.0.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.4.tgz#f040b236ea2b06ed15ed86fbef8407e1a1c8e376" + integrity sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg== dependencies: json-buffer "3.0.1" @@ -1816,9 +1816,9 @@ type-fest@^1.0.1: integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== type-fest@^2.0.0, type-fest@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.5.1.tgz#17ba4f36a6abfabf0a92005d045dca77564607b0" - integrity sha512-JDcsxbLR6Z6OcL7TnGAAAGQrY4g7Q4EEALMT4Kp6FQuIc0JLQvOF3l7ejFvx8o5GmLlfMseTWUL++sYFP+o8kw== + version "2.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.5.2.tgz#d6a5247b8019716b300d9023fa7b1b02016dd864" + integrity sha512-WMbytmAs5PUTqwGJRE+WoRrD2S0bYFtHX8k4Y/1l18CG5kqA3keJud9pPQ/r30FE9n8XRFCXF9BbccHIZzRYJw== unbzip2-stream@^1.0.9: version "1.4.3"