Skip to content

Commit

Permalink
fix: improve 404 logics (#20)
Browse files Browse the repository at this point in the history
* fix: improve 404 logics

* chore: add pre check

* chore:  if not output, make null

* chore: rename

* chore: add --verbose support

* chore: update

* fix: use extract-first-json
  • Loading branch information
azu authored Jun 24, 2021
1 parent 9a5dc4b commit 748ae70
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 24 deletions.
118 changes: 99 additions & 19 deletions lib/can-npm-publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require("path");
const spawn = require("cross-spawn");
const readPkg = require("read-pkg");
const validatePkgName = require("validate-npm-package-name");
const { extractJSONObject } = require("extract-first-json");
/**
* @param {string} [filePathOrDirPath]
* @returns {Promise<readPkg.NormalizedPackageJson>}
Expand Down Expand Up @@ -63,43 +64,122 @@ const checkPrivateField = (packagePath) => {
* or rejects if anything failed
* @param packageName
* @param registry
* @param {{verbose : boolean}} options
* @returns {Promise}
*/
const viewPackage = (packageName, registry) => {
const viewPackage = (packageName, registry, options) => {
return new Promise((resolve, reject) => {
const registryArgs = registry ? ["--registry", registry] : [];
const view = spawn("npm", ["view", packageName, "versions", "--json"].concat(registryArgs));
let result = "";
let errorResult = "";
let _stdoutResult = "";
let _stderrResult = "";

/**
* @param stdout
* @param stderr
* @returns {{stdoutJSON: null | {}, stderrJSON: null | {}}}
*/
const getJsonOutputs = ({ stdout, stderr }) => {
let stdoutJSON = null;
let stderrJSON = null;
if (stdout) {
try {
stdoutJSON = JSON.parse(stdout);
} catch (error) {
// nope
if (options.verbose) {
console.error("stdoutJSON parse error", stdout);
}
}
}
if (stderr) {
try {
stderrJSON = JSON.parse(stderr);
} catch (error) {
// nope
if (options.verbose) {
console.error("stderrJSON parse error", stdout);
}
}
}
return {
stdoutJSON,
stderrJSON
};
};
const isError = (json) => {
return json && "error" in json;
};
const is404Error = (json) => {
return isError(json) && json.error.code === "E404";
};
view.stdout.on("data", (data) => {
result += data.toString();
_stdoutResult += data.toString();
});

view.stderr.on("data", (err) => {
errorResult += err.toString();
const stdErrorStr = err.toString();
// Workaround for npm 7
// npm 7 --json option is broken
// It aim to remove non json output.
// FIXME: However,This logics will break json chunk(chunk may be invalid json)
// https://github.com/azu/can-npm-publish/issues/19
// https://github.com/npm/cli/issues/2740
const jsonObject = extractJSONObject(stdErrorStr);
if (jsonObject) {
_stderrResult = JSON.stringify(jsonObject, null, 4);
}
});

view.on("close", (code) => {
// Note:
// npm 6 output JSON in stdout
// npm 7(7.18.1) output JSON in stderr
const { stdoutJSON, stderrJSON } = getJsonOutputs({
stdout: _stdoutResult,
stderr: _stderrResult
});
if (options.verbose) {
console.log("`npm view` command's exit code:", code);
console.log("`npm view` stdoutJSON", stdoutJSON);
console.log("`npm view` stderrJSON", stderrJSON);
}
// npm6 view --json output to stdout if the package is 404 → can publish
if (is404Error(stdoutJSON)) {
return resolve([]);
}
// npm7 view --json output to stderr if the package is 404 → can publish
if (is404Error(stderrJSON)) {
return resolve([]);
}
// in other error, can not publish → reject
if (isError(stdoutJSON)) {
return reject(new Error(_stdoutResult));
}
if (isError(stderrJSON)) {
return reject(new Error(_stderrResult));
}
// if command is failed by other reasons(no json output), treat it as actual error
if (code !== 0) {
return reject(new Error(errorResult));
return reject(new Error(_stderrResult));
}
const resultJSON = JSON.parse(result);
if (resultJSON && resultJSON.error) {
// the package is not in the npm registry => can publish
if (resultJSON.error.code === "E404") {
return resolve([]); // resolve as empty version
} else {
// other error => can not publish
return reject(new Error(errorResult));
}
if (stdoutJSON) {
// if success to get, resolve with versions json
return resolve(stdoutJSON);
} else {
return reject(_stderrResult);
}
resolve(resultJSON);
});
});
};

const checkAlreadyPublish = (packagePath) => {
/**
*
* @param {string} packagePath
* @param {{verbose : boolean}} options
* @returns {Promise<readPkg.NormalizedPackageJson>}
*/
const checkAlreadyPublish = (packagePath, options) => {
return readPkgWithPath(packagePath).then((pkg) => {
const name = pkg["name"];
const version = pkg["version"];
Expand All @@ -111,7 +191,7 @@ const checkAlreadyPublish = (packagePath) => {
if (version === undefined) {
return Promise.reject(new Error("This package has no `version`."));
}
return viewPackage(name, registry).then((versions) => {
return viewPackage(name, registry, options).then((versions) => {
if (versions.includes(version)) {
return Promise.reject(new Error(`${name}@${version} is already published`));
}
Expand All @@ -128,7 +208,7 @@ const checkAlreadyPublish = (packagePath) => {
const canNpmPublish = (packagePath, options = { verbose: false }) => {
return Promise.all([
checkPkgName(packagePath, options),
checkAlreadyPublish(packagePath),
checkAlreadyPublish(packagePath, options),
checkPrivateField(packagePath)
]);
};
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
},
"dependencies": {
"cross-spawn": "^7.0.3",
"extract-first-json": "^1.0.1",
"meow": "^9.0.0",
"read-pkg": "^5.0.0",
"validate-npm-package-name": "^3.0.0"
Expand All @@ -58,5 +59,9 @@
"*.{js,jsx,ts,tsx,css}": [
"prettier --write"
]
},
"volta": {
"node": "16.4.0",
"npm": "7.18.1"
}
}
9 changes: 4 additions & 5 deletions test/can-npm-publish-bin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ const path = require("path");
// Path to the executable script
const binPath = path.join(__dirname, "../bin/cmd.js");

const shouldNotCalled = () => {
throw new Error("SHOULD NOT CALLED");
};

describe("can-npm-publish bin", () => {
it("should return 0, it can publish", (done) => {
const bin = spawn("node", [binPath, path.join(__dirname, "fixtures/not-published-yet.json")]);

// Finish the test when the executable finishes and returns 0
bin.on("close", (exit_code) => {
assert.ok(exit_code === 0);
});
bin.on("close", () => {
done();
});
});
Expand All @@ -30,11 +28,12 @@ describe("can-npm-publish bin", () => {
});
it("should send errors to stderr when verbose, it can't publish", (done) => {
const bin = spawn("node", [binPath, path.join(__dirname, "fixtures/already-published.json"), "--verbose"]);

// Finish the test and stop the executable when it outputs to stderr
bin.stderr.on("data", (data) => {
assert.ok(/almin@0.15.2 is already published/.test(data));
bin.kill();
});
bin.on("close", () => {
done();
});
});
Expand Down
3 changes: 3 additions & 0 deletions test/can-npm-publish-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ describe("can-npm-publish", () => {
it("should be resolve, it is not published yet", () => {
return canNpmPublish(path.join(__dirname, "fixtures/not-published-yet.json"));
});
it("should be resolve, it is not 404 package", () => {
return canNpmPublish(path.join(__dirname, "fixtures/404-package.json"), { verbose: true });
});
it("should be resolve, it is not published yet to yarnpkg registry", () => {
return canNpmPublish(path.join(__dirname, "fixtures/not-published-yet-registry.json"));
});
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/404-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"private": false,
"name": "asasaiqjpjopmsonopajrpqwkmxzoj22",
"version": "1.0.0"
}
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ execa@^5.0.0:
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"

extract-first-json@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/extract-first-json/-/extract-first-json-1.0.1.tgz#be828615ac0c83c982f22d8c93063e4f9dc2a356"
integrity sha512-L4XmAoZaHyXawEy8fJuLfIjRH9rG9D3bLz6fiMepVMiw/0Mpu+OSYH3/XmJ6sCPjui8a8ayz6c46fWlQFhdLvA==
dependencies:
parse-json-object "^2.0.0"
reduce-first "^1.0.1"

fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
Expand Down Expand Up @@ -912,6 +920,13 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"

parse-json-object@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/parse-json-object/-/parse-json-object-2.0.1.tgz#a441bd8c36d2c33a69516286e7e4138a23607ee0"
integrity sha512-/oF7PUUBjCqHmMEE6xIQeX5ZokQ9+miudACzPt4KBU2qi6CxZYPdisPXx4ad7wpZJYi2ZpcW2PacLTU3De3ebw==
dependencies:
types-json "^1.2.0"

parse-json@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
Expand Down Expand Up @@ -1010,6 +1025,11 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"

reduce-first@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/reduce-first/-/reduce-first-1.0.1.tgz#ef934f0dd4e010fdcaec2c51c9027722ee810c1c"
integrity sha512-/jBjEiF5Oe1xsa7CeCscbOIxSlFJcn4h1gj3OvUHPtxnThCbZ1Wh72uqO/o1zHNSGU4EgFclvCdc5TLJyt1hOQ==

require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
Expand Down Expand Up @@ -1258,6 +1278,11 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==

types-json@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/types-json/-/types-json-1.2.2.tgz#91ebe6de59e741ab38a98b071708a29494cedfe6"
integrity sha512-VfVLISHypS7ayIHvhacOESOTib4Sm4mAhnsgR8fzQdGp89YoBwMqvGmqENjtYehUQzgclT+7NafpEXkK/MHKwA==

validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
Expand Down

0 comments on commit 748ae70

Please sign in to comment.