@jsenv/package-publish 1.11.52 → 1.11.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/internal/publish.js +22 -91
- package/src/internal/staged_version.js +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/package-publish",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Publish package to one or many registry.",
|
|
6
6
|
"repository": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"/src/"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jsenv/filesystem": "4.15.
|
|
27
|
-
"@jsenv/humanize": "1.8.
|
|
26
|
+
"@jsenv/filesystem": "4.15.22",
|
|
27
|
+
"@jsenv/humanize": "1.8.1",
|
|
28
28
|
"semver": "7.8.5"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/internal/publish.js
CHANGED
|
@@ -4,6 +4,7 @@ import { exec } from "node:child_process";
|
|
|
4
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { setNpmConfig } from "./set_npm_config.js";
|
|
7
|
+
import { waitForStagedVersionToLand } from "./staged_version.js";
|
|
7
8
|
|
|
8
9
|
export const publish = async ({
|
|
9
10
|
logger,
|
|
@@ -63,8 +64,9 @@ export const publish = async ({
|
|
|
63
64
|
[computeRegistryKey(packageObject.name)]: registryUrl,
|
|
64
65
|
}),
|
|
65
66
|
);
|
|
67
|
+
let publishResult;
|
|
66
68
|
try {
|
|
67
|
-
|
|
69
|
+
publishResult = await new Promise((resolve, reject) => {
|
|
68
70
|
const command = exec(
|
|
69
71
|
"npm publish --no-workspaces",
|
|
70
72
|
{
|
|
@@ -106,23 +108,10 @@ export const publish = async ({
|
|
|
106
108
|
reason: "already-published",
|
|
107
109
|
});
|
|
108
110
|
} else if (error.message.includes("previously staged version")) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// through, so wait for the version to land.
|
|
114
|
-
resolve(
|
|
115
|
-
waitForStagedVersionToLand({
|
|
116
|
-
logger,
|
|
117
|
-
registryUrl,
|
|
118
|
-
packageName: packageObject.name,
|
|
119
|
-
packageVersion: packageObject.version,
|
|
120
|
-
token,
|
|
121
|
-
}).then(() => ({
|
|
122
|
-
success: true,
|
|
123
|
-
reason: "already-published",
|
|
124
|
-
})),
|
|
125
|
-
);
|
|
111
|
+
resolve({
|
|
112
|
+
success: true,
|
|
113
|
+
reason: "staged",
|
|
114
|
+
});
|
|
126
115
|
}
|
|
127
116
|
// github publish conflict
|
|
128
117
|
else if (
|
|
@@ -158,14 +147,28 @@ export const publish = async ({
|
|
|
158
147
|
});
|
|
159
148
|
if (publishResult.reason === "already-published") {
|
|
160
149
|
publishTask.setRightText(`(already published)`);
|
|
150
|
+
} else if (publishResult.reason === "staged") {
|
|
151
|
+
publishTask.setRightText(`(staged)`);
|
|
161
152
|
}
|
|
162
153
|
publishTask.done();
|
|
163
|
-
return publishResult;
|
|
164
154
|
} finally {
|
|
165
155
|
restoreProcessEnv();
|
|
166
156
|
restorePackageFile();
|
|
167
157
|
restoreNpmConfigFile();
|
|
168
158
|
}
|
|
159
|
+
if (publishResult.reason === "staged") {
|
|
160
|
+
await waitForStagedVersionToLand({
|
|
161
|
+
registryUrl,
|
|
162
|
+
packageName: packageObject.name,
|
|
163
|
+
packageVersion: packageObject.version,
|
|
164
|
+
token,
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
reason: "already-published",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return publishResult;
|
|
169
172
|
} catch (e) {
|
|
170
173
|
publishTask.fail();
|
|
171
174
|
console.error(e.stack);
|
|
@@ -198,75 +201,3 @@ const computeRegistryKey = (packageName) => {
|
|
|
198
201
|
}
|
|
199
202
|
return `registry`;
|
|
200
203
|
};
|
|
201
|
-
|
|
202
|
-
// A version becomes available in two steps: npm stages it, then the registry
|
|
203
|
-
// makes it visible. While it is staged the registry answers 409 to a publish of
|
|
204
|
-
// that same version, and once staged that version can never be published again,
|
|
205
|
-
// so waiting for it is the only way through.
|
|
206
|
-
const STAGED_VERSION_TIMEOUT_MS = 120_000;
|
|
207
|
-
const STAGED_VERSION_POLL_INTERVAL_MS = 5_000;
|
|
208
|
-
|
|
209
|
-
const waitForStagedVersionToLand = async ({
|
|
210
|
-
logger,
|
|
211
|
-
registryUrl,
|
|
212
|
-
packageName,
|
|
213
|
-
packageVersion,
|
|
214
|
-
token,
|
|
215
|
-
}) => {
|
|
216
|
-
logger.info(
|
|
217
|
-
`${packageName}@${packageVersion} is staged on ${registryUrl}, waiting for the registry to publish it`,
|
|
218
|
-
);
|
|
219
|
-
const msBeforeTimeout = Date.now() + STAGED_VERSION_TIMEOUT_MS;
|
|
220
|
-
while (true) {
|
|
221
|
-
const versionIsInRegistry = await checkVersionInRegistry({
|
|
222
|
-
registryUrl,
|
|
223
|
-
packageName,
|
|
224
|
-
packageVersion,
|
|
225
|
-
token,
|
|
226
|
-
});
|
|
227
|
-
if (versionIsInRegistry) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (Date.now() > msBeforeTimeout) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`${packageName}@${packageVersion} is staged on ${registryUrl} but did not get published. Bump the version, a staged version cannot be published again.`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
await new Promise((resolve) => {
|
|
236
|
-
setTimeout(resolve, STAGED_VERSION_POLL_INTERVAL_MS);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const checkVersionInRegistry = async ({
|
|
242
|
-
registryUrl,
|
|
243
|
-
packageName,
|
|
244
|
-
packageVersion,
|
|
245
|
-
token,
|
|
246
|
-
}) => {
|
|
247
|
-
let response;
|
|
248
|
-
try {
|
|
249
|
-
response = await fetch(`${registryUrl}/${packageName}`, {
|
|
250
|
-
headers: {
|
|
251
|
-
"accept":
|
|
252
|
-
"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
|
|
253
|
-
// the registry is served by a cache; without this the version can stay
|
|
254
|
-
// invisible long after it landed
|
|
255
|
-
"cache-control": "no-cache",
|
|
256
|
-
...(token
|
|
257
|
-
? {
|
|
258
|
-
authorization: `token ${token}`,
|
|
259
|
-
}
|
|
260
|
-
: {}),
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
} catch {
|
|
264
|
-
// a network hiccup is one more reason for the version not to be there yet
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
if (response.status !== 200) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
const packageObject = await response.json();
|
|
271
|
-
return Boolean(packageObject.versions[packageVersion]);
|
|
272
|
-
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A version lands on the registry in two steps: the registry takes the tarball
|
|
3
|
+
* ("staged"), then it exposes the version. While a version is staged the
|
|
4
|
+
* registry answers 409 'Cannot publish over previously staged version "x.y.z"'
|
|
5
|
+
* to any publish of it, so waiting for it to be exposed is the only way through.
|
|
6
|
+
*
|
|
7
|
+
* The packument says nothing about a staged version, but its tarball is already
|
|
8
|
+
* served: that is what tells a staged version apart from one that was never
|
|
9
|
+
* published.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createTaskLog } from "@jsenv/humanize";
|
|
13
|
+
|
|
14
|
+
export const VERSION_STATUS = {
|
|
15
|
+
ABSENT: "absent",
|
|
16
|
+
STAGED: "staged",
|
|
17
|
+
PUBLISHED: "published",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const STAGED_VERSION_TIMEOUT_MS = 300_000;
|
|
21
|
+
const STAGED_VERSION_POLL_INTERVAL_MS = 5_000;
|
|
22
|
+
|
|
23
|
+
export const checkVersionStatusInRegistry = async ({
|
|
24
|
+
registryUrl,
|
|
25
|
+
packageName,
|
|
26
|
+
packageVersion,
|
|
27
|
+
token,
|
|
28
|
+
}) => {
|
|
29
|
+
const versionIsInRegistry = await checkVersionIsInRegistry({
|
|
30
|
+
registryUrl,
|
|
31
|
+
packageName,
|
|
32
|
+
packageVersion,
|
|
33
|
+
token,
|
|
34
|
+
});
|
|
35
|
+
if (versionIsInRegistry) {
|
|
36
|
+
return VERSION_STATUS.PUBLISHED;
|
|
37
|
+
}
|
|
38
|
+
const tarballIsInRegistry = await checkTarballIsInRegistry({
|
|
39
|
+
registryUrl,
|
|
40
|
+
packageName,
|
|
41
|
+
packageVersion,
|
|
42
|
+
token,
|
|
43
|
+
});
|
|
44
|
+
if (tarballIsInRegistry) {
|
|
45
|
+
return VERSION_STATUS.STAGED;
|
|
46
|
+
}
|
|
47
|
+
return VERSION_STATUS.ABSENT;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const waitForStagedVersionToLand = async ({
|
|
51
|
+
registryUrl,
|
|
52
|
+
packageName,
|
|
53
|
+
packageVersion,
|
|
54
|
+
token,
|
|
55
|
+
}) => {
|
|
56
|
+
const waitTask = createTaskLog(
|
|
57
|
+
`wait for ${packageName}@${packageVersion} to be published by ${registryUrl}`,
|
|
58
|
+
);
|
|
59
|
+
const msBeforeTimeout = Date.now() + STAGED_VERSION_TIMEOUT_MS;
|
|
60
|
+
try {
|
|
61
|
+
while (true) {
|
|
62
|
+
const versionIsInRegistry = await checkVersionIsInRegistry({
|
|
63
|
+
registryUrl,
|
|
64
|
+
packageName,
|
|
65
|
+
packageVersion,
|
|
66
|
+
token,
|
|
67
|
+
});
|
|
68
|
+
if (versionIsInRegistry) {
|
|
69
|
+
waitTask.done();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() > msBeforeTimeout) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${packageName}@${packageVersion} is staged on ${registryUrl} but did not get published. Run the publish again to keep waiting; a staged version cannot be published again.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
await new Promise((resolve) => {
|
|
78
|
+
setTimeout(resolve, STAGED_VERSION_POLL_INTERVAL_MS);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
waitTask.fail();
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const checkVersionIsInRegistry = async ({
|
|
88
|
+
registryUrl,
|
|
89
|
+
packageName,
|
|
90
|
+
packageVersion,
|
|
91
|
+
token,
|
|
92
|
+
}) => {
|
|
93
|
+
const response = await fetchRegistry(`${registryUrl}/${packageName}`, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
token,
|
|
96
|
+
});
|
|
97
|
+
if (!response || response.status !== 200) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const packageObject = await response.json();
|
|
101
|
+
return Boolean(packageObject.versions[packageVersion]);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const checkTarballIsInRegistry = async ({
|
|
105
|
+
registryUrl,
|
|
106
|
+
packageName,
|
|
107
|
+
packageVersion,
|
|
108
|
+
token,
|
|
109
|
+
}) => {
|
|
110
|
+
const response = await fetchRegistry(
|
|
111
|
+
computeTarballUrl({ registryUrl, packageName, packageVersion }),
|
|
112
|
+
{ method: "HEAD", token },
|
|
113
|
+
);
|
|
114
|
+
return Boolean(response) && response.status === 200;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const computeTarballUrl = ({ registryUrl, packageName, packageVersion }) => {
|
|
118
|
+
const packageBasename =
|
|
119
|
+
packageName[0] === "@"
|
|
120
|
+
? packageName.slice(packageName.indexOf("/") + 1)
|
|
121
|
+
: packageName;
|
|
122
|
+
return `${registryUrl}/${packageName}/-/${packageBasename}-${packageVersion}.tgz`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const fetchRegistry = async (url, { method, token }) => {
|
|
126
|
+
try {
|
|
127
|
+
return await fetch(url, {
|
|
128
|
+
method,
|
|
129
|
+
headers: {
|
|
130
|
+
"accept":
|
|
131
|
+
"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
|
|
132
|
+
// the registry is served by a cache; without this a version can stay
|
|
133
|
+
// invisible long after it landed
|
|
134
|
+
"cache-control": "no-cache",
|
|
135
|
+
...(token ? { authorization: `token ${token}` } : {}),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
// a network hiccup is one more reason for the version not to be there yet
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|