Skip to content

Commit 748ae70

Browse files
authored
fix: improve 404 logics (#20)
* 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
1 parent 9a5dc4b commit 748ae70

File tree

6 files changed

+141
-24
lines changed

6 files changed

+141
-24
lines changed

Diff for: lib/can-npm-publish.js

+99-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require("path");
44
const spawn = require("cross-spawn");
55
const readPkg = require("read-pkg");
66
const validatePkgName = require("validate-npm-package-name");
7+
const { extractJSONObject } = require("extract-first-json");
78
/**
89
* @param {string} [filePathOrDirPath]
910
* @returns {Promise<readPkg.NormalizedPackageJson>}
@@ -63,43 +64,122 @@ const checkPrivateField = (packagePath) => {
6364
* or rejects if anything failed
6465
* @param packageName
6566
* @param registry
67+
* @param {{verbose : boolean}} options
6668
* @returns {Promise}
6769
*/
68-
const viewPackage = (packageName, registry) => {
70+
const viewPackage = (packageName, registry, options) => {
6971
return new Promise((resolve, reject) => {
7072
const registryArgs = registry ? ["--registry", registry] : [];
7173
const view = spawn("npm", ["view", packageName, "versions", "--json"].concat(registryArgs));
72-
let result = "";
73-
let errorResult = "";
74+
let _stdoutResult = "";
75+
let _stderrResult = "";
7476

77+
/**
78+
* @param stdout
79+
* @param stderr
80+
* @returns {{stdoutJSON: null | {}, stderrJSON: null | {}}}
81+
*/
82+
const getJsonOutputs = ({ stdout, stderr }) => {
83+
let stdoutJSON = null;
84+
let stderrJSON = null;
85+
if (stdout) {
86+
try {
87+
stdoutJSON = JSON.parse(stdout);
88+
} catch (error) {
89+
// nope
90+
if (options.verbose) {
91+
console.error("stdoutJSON parse error", stdout);
92+
}
93+
}
94+
}
95+
if (stderr) {
96+
try {
97+
stderrJSON = JSON.parse(stderr);
98+
} catch (error) {
99+
// nope
100+
if (options.verbose) {
101+
console.error("stderrJSON parse error", stdout);
102+
}
103+
}
104+
}
105+
return {
106+
stdoutJSON,
107+
stderrJSON
108+
};
109+
};
110+
const isError = (json) => {
111+
return json && "error" in json;
112+
};
113+
const is404Error = (json) => {
114+
return isError(json) && json.error.code === "E404";
115+
};
75116
view.stdout.on("data", (data) => {
76-
result += data.toString();
117+
_stdoutResult += data.toString();
77118
});
78119

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

83134
view.on("close", (code) => {
135+
// Note:
136+
// npm 6 output JSON in stdout
137+
// npm 7(7.18.1) output JSON in stderr
138+
const { stdoutJSON, stderrJSON } = getJsonOutputs({
139+
stdout: _stdoutResult,
140+
stderr: _stderrResult
141+
});
142+
if (options.verbose) {
143+
console.log("`npm view` command's exit code:", code);
144+
console.log("`npm view` stdoutJSON", stdoutJSON);
145+
console.log("`npm view` stderrJSON", stderrJSON);
146+
}
147+
// npm6 view --json output to stdout if the package is 404 → can publish
148+
if (is404Error(stdoutJSON)) {
149+
return resolve([]);
150+
}
151+
// npm7 view --json output to stderr if the package is 404 → can publish
152+
if (is404Error(stderrJSON)) {
153+
return resolve([]);
154+
}
155+
// in other error, can not publish → reject
156+
if (isError(stdoutJSON)) {
157+
return reject(new Error(_stdoutResult));
158+
}
159+
if (isError(stderrJSON)) {
160+
return reject(new Error(_stderrResult));
161+
}
162+
// if command is failed by other reasons(no json output), treat it as actual error
84163
if (code !== 0) {
85-
return reject(new Error(errorResult));
164+
return reject(new Error(_stderrResult));
86165
}
87-
const resultJSON = JSON.parse(result);
88-
if (resultJSON && resultJSON.error) {
89-
// the package is not in the npm registry => can publish
90-
if (resultJSON.error.code === "E404") {
91-
return resolve([]); // resolve as empty version
92-
} else {
93-
// other error => can not publish
94-
return reject(new Error(errorResult));
95-
}
166+
if (stdoutJSON) {
167+
// if success to get, resolve with versions json
168+
return resolve(stdoutJSON);
169+
} else {
170+
return reject(_stderrResult);
96171
}
97-
resolve(resultJSON);
98172
});
99173
});
100174
};
101175

102-
const checkAlreadyPublish = (packagePath) => {
176+
/**
177+
*
178+
* @param {string} packagePath
179+
* @param {{verbose : boolean}} options
180+
* @returns {Promise<readPkg.NormalizedPackageJson>}
181+
*/
182+
const checkAlreadyPublish = (packagePath, options) => {
103183
return readPkgWithPath(packagePath).then((pkg) => {
104184
const name = pkg["name"];
105185
const version = pkg["version"];
@@ -111,7 +191,7 @@ const checkAlreadyPublish = (packagePath) => {
111191
if (version === undefined) {
112192
return Promise.reject(new Error("This package has no `version`."));
113193
}
114-
return viewPackage(name, registry).then((versions) => {
194+
return viewPackage(name, registry, options).then((versions) => {
115195
if (versions.includes(version)) {
116196
return Promise.reject(new Error(`${name}@${version} is already published`));
117197
}
@@ -128,7 +208,7 @@ const checkAlreadyPublish = (packagePath) => {
128208
const canNpmPublish = (packagePath, options = { verbose: false }) => {
129209
return Promise.all([
130210
checkPkgName(packagePath, options),
131-
checkAlreadyPublish(packagePath),
211+
checkAlreadyPublish(packagePath, options),
132212
checkPrivateField(packagePath)
133213
]);
134214
};

Diff for: package.json

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
},
4040
"dependencies": {
4141
"cross-spawn": "^7.0.3",
42+
"extract-first-json": "^1.0.1",
4243
"meow": "^9.0.0",
4344
"read-pkg": "^5.0.0",
4445
"validate-npm-package-name": "^3.0.0"
@@ -58,5 +59,9 @@
5859
"*.{js,jsx,ts,tsx,css}": [
5960
"prettier --write"
6061
]
62+
},
63+
"volta": {
64+
"node": "16.4.0",
65+
"npm": "7.18.1"
6166
}
6267
}

Diff for: test/can-npm-publish-bin-test.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ const path = require("path");
55
// Path to the executable script
66
const binPath = path.join(__dirname, "../bin/cmd.js");
77

8-
const shouldNotCalled = () => {
9-
throw new Error("SHOULD NOT CALLED");
10-
};
11-
128
describe("can-npm-publish bin", () => {
139
it("should return 0, it can publish", (done) => {
1410
const bin = spawn("node", [binPath, path.join(__dirname, "fixtures/not-published-yet.json")]);
1511

1612
// Finish the test when the executable finishes and returns 0
1713
bin.on("close", (exit_code) => {
1814
assert.ok(exit_code === 0);
15+
});
16+
bin.on("close", () => {
1917
done();
2018
});
2119
});
@@ -30,11 +28,12 @@ describe("can-npm-publish bin", () => {
3028
});
3129
it("should send errors to stderr when verbose, it can't publish", (done) => {
3230
const bin = spawn("node", [binPath, path.join(__dirname, "fixtures/already-published.json"), "--verbose"]);
33-
3431
// Finish the test and stop the executable when it outputs to stderr
3532
bin.stderr.on("data", (data) => {
3633
assert.ok(/almin@0.15.2 is already published/.test(data));
3734
bin.kill();
35+
});
36+
bin.on("close", () => {
3837
done();
3938
});
4039
});

Diff for: test/can-npm-publish-test.js

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ describe("can-npm-publish", () => {
3737
it("should be resolve, it is not published yet", () => {
3838
return canNpmPublish(path.join(__dirname, "fixtures/not-published-yet.json"));
3939
});
40+
it("should be resolve, it is not 404 package", () => {
41+
return canNpmPublish(path.join(__dirname, "fixtures/404-package.json"), { verbose: true });
42+
});
4043
it("should be resolve, it is not published yet to yarnpkg registry", () => {
4144
return canNpmPublish(path.join(__dirname, "fixtures/not-published-yet-registry.json"));
4245
});

Diff for: test/fixtures/404-package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"private": false,
3+
"name": "asasaiqjpjopmsonopajrpqwkmxzoj22",
4+
"version": "1.0.0"
5+
}

Diff for: yarn.lock

+25
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,14 @@ execa@^5.0.0:
373373
signal-exit "^3.0.3"
374374
strip-final-newline "^2.0.0"
375375

376+
extract-first-json@^1.0.1:
377+
version "1.0.1"
378+
resolved "https://registry.yarnpkg.com/extract-first-json/-/extract-first-json-1.0.1.tgz#be828615ac0c83c982f22d8c93063e4f9dc2a356"
379+
integrity sha512-L4XmAoZaHyXawEy8fJuLfIjRH9rG9D3bLz6fiMepVMiw/0Mpu+OSYH3/XmJ6sCPjui8a8ayz6c46fWlQFhdLvA==
380+
dependencies:
381+
parse-json-object "^2.0.0"
382+
reduce-first "^1.0.1"
383+
376384
fill-range@^7.0.1:
377385
version "7.0.1"
378386
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -912,6 +920,13 @@ parent-module@^1.0.0:
912920
dependencies:
913921
callsites "^3.0.0"
914922

923+
parse-json-object@^2.0.0:
924+
version "2.0.1"
925+
resolved "https://registry.yarnpkg.com/parse-json-object/-/parse-json-object-2.0.1.tgz#a441bd8c36d2c33a69516286e7e4138a23607ee0"
926+
integrity sha512-/oF7PUUBjCqHmMEE6xIQeX5ZokQ9+miudACzPt4KBU2qi6CxZYPdisPXx4ad7wpZJYi2ZpcW2PacLTU3De3ebw==
927+
dependencies:
928+
types-json "^1.2.0"
929+
915930
parse-json@^5.0.0:
916931
version "5.2.0"
917932
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@@ -1010,6 +1025,11 @@ redent@^3.0.0:
10101025
indent-string "^4.0.0"
10111026
strip-indent "^3.0.0"
10121027

1028+
reduce-first@^1.0.1:
1029+
version "1.0.1"
1030+
resolved "https://registry.yarnpkg.com/reduce-first/-/reduce-first-1.0.1.tgz#ef934f0dd4e010fdcaec2c51c9027722ee810c1c"
1031+
integrity sha512-/jBjEiF5Oe1xsa7CeCscbOIxSlFJcn4h1gj3OvUHPtxnThCbZ1Wh72uqO/o1zHNSGU4EgFclvCdc5TLJyt1hOQ==
1032+
10131033
require-directory@^2.1.1:
10141034
version "2.1.1"
10151035
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -1258,6 +1278,11 @@ type-fest@^0.8.1:
12581278
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
12591279
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
12601280

1281+
types-json@^1.2.0:
1282+
version "1.2.2"
1283+
resolved "https://registry.yarnpkg.com/types-json/-/types-json-1.2.2.tgz#91ebe6de59e741ab38a98b071708a29494cedfe6"
1284+
integrity sha512-VfVLISHypS7ayIHvhacOESOTib4Sm4mAhnsgR8fzQdGp89YoBwMqvGmqENjtYehUQzgclT+7NafpEXkK/MHKwA==
1285+
12611286
validate-npm-package-license@^3.0.1:
12621287
version "3.0.4"
12631288
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"

0 commit comments

Comments
 (0)