vnu-jar 26.5.21 → 26.5.22
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/build/dist/vnu.jar +0 -0
- package/package.json +1 -1
- package/vnu-jar.js +4 -1
- package/vnu-java-downloader.js +220 -59
package/build/dist/vnu.jar
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "vnu-jar", "version": "26.5.
|
|
1
|
+
{"name": "vnu-jar", "version": "26.5.22", "description": "Provides the Nu Html Checker \u00abvnu.jar\u00bb file", "main": "vnu-jar.js", "author": "sideshowbarker@gmail.com", "engines": {"node": ">=0.10"}, "repository": {"type": "git", "url": "git+https://github.com/validator/validator.git"}, "license": "MIT", "bugs": {"url": "https://github.com/validator/validator/issues"}, "homepage": "https://github.com/validator/validator#readme", "keywords": ["checker", "html", "lint", "linter", "jar", "nu", "validator", "vnu", "w3c"], "files": ["build/dist/vnu.jar", "vnu-java-downloader.js", "vnu-jar.js"], "bin": {"vnu": "./vnu-jar.js"}, "scripts": {"postinstall": "node vnu-java-downloader.js", "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test"}, "devDependencies": {"@playwright/test": "^1.57.0", "@types/node": "^25.0.3", "vitest": "^4.0.16"}, "pnpm": {"onlyBuiltDependencies": ["esbuild"]}}
|
package/vnu-jar.js
CHANGED
|
@@ -38,7 +38,10 @@ if (require.main === module) {
|
|
|
38
38
|
child.on('close', (code) => process.exit(code || 0));
|
|
39
39
|
} catch (err) {
|
|
40
40
|
console.error(err.message.trim());
|
|
41
|
-
process.exit(
|
|
41
|
+
// Set the exit code rather than calling process.exit(): exiting
|
|
42
|
+
// while the sockets from a failed download are still closing
|
|
43
|
+
// crashes Node with a libuv assertion on Windows.
|
|
44
|
+
process.exitCode = 1;
|
|
42
45
|
}
|
|
43
46
|
})();
|
|
44
47
|
}
|
package/vnu-java-downloader.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { spawnSync
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
5
|
const { mkdirSync, existsSync, createWriteStream } = require('fs');
|
|
6
6
|
const { join, dirname, parse } = require('path');
|
|
7
7
|
const { Readable } = require('stream');
|
|
8
|
-
const
|
|
8
|
+
const { pipeline } = require('stream/promises');
|
|
9
|
+
|
|
10
|
+
const fetchImpl = typeof globalThis.fetch === 'function'
|
|
11
|
+
? globalThis.fetch
|
|
12
|
+
: null;
|
|
9
13
|
|
|
10
14
|
function findNearestNodeModules(startDir) {
|
|
11
15
|
let dir = startDir;
|
|
@@ -26,6 +30,46 @@ const NODE_MODULES_DIR = findNearestNodeModules(__dirname);
|
|
|
26
30
|
const CACHE_DIR = join(NODE_MODULES_DIR, '.cache', 'vnu-jar', 'java');
|
|
27
31
|
const TEMURIN_VERSION = '17.0.17+10';
|
|
28
32
|
const TEMURIN_BASE_URL = `https://github.com/adoptium/temurin17-binaries/releases/download/jdk-${TEMURIN_VERSION}/`;
|
|
33
|
+
const MAX_FETCH_ATTEMPTS = 3;
|
|
34
|
+
|
|
35
|
+
// Builds a readable description of an error, walking the chain of cause
|
|
36
|
+
// errors that the fetch API attaches. The real reason for a failed fetch
|
|
37
|
+
// (a DNS failure, a refused connection, a TLS or proxy error) is carried
|
|
38
|
+
// on err.cause, not in the generic top-level message.
|
|
39
|
+
function describeError(err) {
|
|
40
|
+
if (!err) {
|
|
41
|
+
return 'unknown error';
|
|
42
|
+
}
|
|
43
|
+
let description = err.message || String(err);
|
|
44
|
+
const seen = new Set([err]);
|
|
45
|
+
let cause = err.cause;
|
|
46
|
+
while (cause && !seen.has(cause)) {
|
|
47
|
+
seen.add(cause);
|
|
48
|
+
description += ` (cause: ${cause.code || cause.message || cause})`;
|
|
49
|
+
cause = cause.cause;
|
|
50
|
+
}
|
|
51
|
+
return description;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Returns a proxy URL from the npm or shell environment, or null. The
|
|
55
|
+
// built-in fetch ignores these variables; curl honors them, so the curl
|
|
56
|
+
// fallback forwards whatever this finds as an explicit --proxy argument.
|
|
57
|
+
function getProxyFromEnv() {
|
|
58
|
+
const env = process.env;
|
|
59
|
+
return env.npm_config_https_proxy
|
|
60
|
+
|| env.npm_config_proxy
|
|
61
|
+
|| env.HTTPS_PROXY
|
|
62
|
+
|| env.https_proxy
|
|
63
|
+
|| env.HTTP_PROXY
|
|
64
|
+
|| env.http_proxy
|
|
65
|
+
|| env.ALL_PROXY
|
|
66
|
+
|| env.all_proxy
|
|
67
|
+
|| null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function delay(ms) {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
29
73
|
|
|
30
74
|
function getJavaVersion(javaPath) {
|
|
31
75
|
try {
|
|
@@ -33,8 +77,7 @@ function getJavaVersion(javaPath) {
|
|
|
33
77
|
const versionOutput = result.stderr || result.stdout || '';
|
|
34
78
|
const match = versionOutput.match(/version "(\d+)(?:\.(\d+))?/);
|
|
35
79
|
if (match) {
|
|
36
|
-
|
|
37
|
-
return majorVersion;
|
|
80
|
+
return parseInt(match[1], 10);
|
|
38
81
|
}
|
|
39
82
|
} catch (err) {
|
|
40
83
|
return null;
|
|
@@ -71,26 +114,118 @@ function getPlatformArchiveName() {
|
|
|
71
114
|
return `OpenJDK17U-jre_${arch}_${plat}_hotspot_${version}.${ext}`;
|
|
72
115
|
}
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const url = TEMURIN_BASE_URL + archiveName;
|
|
78
|
-
const archivePath = join(CACHE_DIR, archiveName);
|
|
79
|
-
console.log(`Downloading Java 17 runtime from ${url}...`);
|
|
80
|
-
const res = await fetch(url);
|
|
81
|
-
if (!res.ok) {
|
|
82
|
-
throw new Error(`Failed to download Java: ${res.status} ${res.statusText}`);
|
|
83
|
-
}
|
|
84
|
-
await new Promise((resolve, reject) => {
|
|
85
|
-
const file = createWriteStream(archivePath);
|
|
86
|
-
Readable.fromWeb(res.body)
|
|
87
|
-
.pipe(file)
|
|
88
|
-
.on('finish', resolve)
|
|
89
|
-
.on('error', reject);
|
|
90
|
-
});
|
|
117
|
+
// Path of the java executable inside an extracted Temurin runtime.
|
|
118
|
+
function getLocalJavaExecutablePath() {
|
|
119
|
+
const home = join(CACHE_DIR, `jdk-${TEMURIN_VERSION}-jre`);
|
|
91
120
|
if (process.platform === 'win32') {
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
return join(home, 'bin', 'java.exe');
|
|
122
|
+
}
|
|
123
|
+
if (process.platform === 'darwin') {
|
|
124
|
+
return join(home, 'Contents', 'Home', 'bin', 'java');
|
|
125
|
+
}
|
|
126
|
+
return join(home, 'bin', 'java');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Returns the cached java executable path, or null if none is present.
|
|
130
|
+
// The check targets the executable itself, not the cache directory: an
|
|
131
|
+
// interrupted download can leave the directory in place with no runtime
|
|
132
|
+
// inside it, and that must count as not installed.
|
|
133
|
+
function findCachedJava() {
|
|
134
|
+
const javaPath = getLocalJavaExecutablePath();
|
|
135
|
+
return existsSync(javaPath) ? javaPath : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Downloads url to destPath with the built-in fetch, retrying network
|
|
139
|
+
// failures a few times. A non-OK HTTP response is not retried, since a
|
|
140
|
+
// bad status will not change between attempts.
|
|
141
|
+
async function downloadWithFetch(url, destPath) {
|
|
142
|
+
let lastError;
|
|
143
|
+
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetchImpl(url);
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`server responded ${res.status} ${res.statusText}`);
|
|
149
|
+
}
|
|
150
|
+
await pipeline(Readable.fromWeb(res.body),
|
|
151
|
+
createWriteStream(destPath));
|
|
152
|
+
return;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
lastError = err;
|
|
155
|
+
// Network failures from fetch carry a cause; the non-OK HTTP
|
|
156
|
+
// error thrown above does not. Only network failures retry.
|
|
157
|
+
const transient = err.cause !== undefined;
|
|
158
|
+
if (!transient || attempt === MAX_FETCH_ATTEMPTS) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
const waitMs = 1000 * attempt;
|
|
162
|
+
console.log(`Download attempt ${attempt} failed `
|
|
163
|
+
+ `(${describeError(err)}); retrying in ${waitMs} ms ...`);
|
|
164
|
+
await delay(waitMs);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw lastError;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Downloads url to destPath by spawning curl. curl honors proxy
|
|
171
|
+
// environment variables and the OS trust store, both of which the
|
|
172
|
+
// built-in fetch ignores, so it can succeed where fetch cannot.
|
|
173
|
+
function downloadWithCurl(url, destPath, proxy) {
|
|
174
|
+
const args = ['--fail', '--show-error', '--location',
|
|
175
|
+
'--retry', '3', '--retry-delay', '2',
|
|
176
|
+
'--output', destPath, url];
|
|
177
|
+
if (proxy) {
|
|
178
|
+
args.push('--proxy', proxy);
|
|
179
|
+
}
|
|
180
|
+
const result = spawnSync('curl', args, { stdio: 'inherit' });
|
|
181
|
+
if (result.error) {
|
|
182
|
+
const err = new Error(`could not run curl: ${result.error.message}`);
|
|
183
|
+
err.cause = result.error;
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
if (result.status !== 0) {
|
|
187
|
+
throw new Error(`curl exited with status ${result.status}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Downloads url to destPath, preferring the built-in fetch and falling
|
|
192
|
+
// back to curl. When a proxy is configured, curl is used directly,
|
|
193
|
+
// because the built-in fetch cannot reach the network through a proxy.
|
|
194
|
+
async function downloadArchive(url, destPath) {
|
|
195
|
+
const proxy = getProxyFromEnv();
|
|
196
|
+
if (fetchImpl && !proxy) {
|
|
197
|
+
try {
|
|
198
|
+
await downloadWithFetch(url, destPath);
|
|
199
|
+
return;
|
|
200
|
+
} catch (fetchErr) {
|
|
201
|
+
console.log('Built-in download failed '
|
|
202
|
+
+ `(${describeError(fetchErr)}); retrying with curl ...`);
|
|
203
|
+
try {
|
|
204
|
+
downloadWithCurl(url, destPath, proxy);
|
|
205
|
+
return;
|
|
206
|
+
} catch (curlErr) {
|
|
207
|
+
// The message below already states both failure reasons in
|
|
208
|
+
// full, so no cause is attached: that would only make
|
|
209
|
+
// describeError repeat them.
|
|
210
|
+
throw new Error('Could not download the Java runtime. '
|
|
211
|
+
+ `Built-in download: ${describeError(fetchErr)}. `
|
|
212
|
+
+ `curl fallback: ${describeError(curlErr)}.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (proxy) {
|
|
217
|
+
console.log(`Using curl to download (proxy configured: ${proxy}).`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log('Using curl to download (the built-in fetch is '
|
|
220
|
+
+ 'unavailable).');
|
|
221
|
+
}
|
|
222
|
+
downloadWithCurl(url, destPath, proxy);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractArchive(archivePath) {
|
|
226
|
+
if (process.platform === 'win32') {
|
|
227
|
+
console.log('Extracting ZIP archive ...');
|
|
228
|
+
const result = spawnSync('powershell', [
|
|
94
229
|
'-NoProfile',
|
|
95
230
|
'-NonInteractive',
|
|
96
231
|
'-Command',
|
|
@@ -99,55 +234,81 @@ async function downloadJava() {
|
|
|
99
234
|
archivePath,
|
|
100
235
|
CACHE_DIR
|
|
101
236
|
], { stdio: 'inherit' });
|
|
102
|
-
if (
|
|
103
|
-
throw new Error(
|
|
237
|
+
if (result.error) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`could not run PowerShell: ${result.error.message}`);
|
|
240
|
+
}
|
|
241
|
+
if (result.status !== 0) {
|
|
242
|
+
throw new Error('Failed to extract the Java archive.');
|
|
104
243
|
}
|
|
105
244
|
} else {
|
|
106
|
-
console.log('Extracting tar.gz archive...');
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
245
|
+
console.log('Extracting tar.gz archive ...');
|
|
246
|
+
const result = spawnSync('tar',
|
|
247
|
+
['-xzf', archivePath, '-C', CACHE_DIR], { stdio: 'inherit' });
|
|
248
|
+
if (result.error) {
|
|
249
|
+
throw new Error(`could not run tar: ${result.error.message}`);
|
|
250
|
+
}
|
|
251
|
+
if (result.status !== 0) {
|
|
252
|
+
throw new Error('Failed to extract the Java archive.');
|
|
110
253
|
}
|
|
111
254
|
}
|
|
112
|
-
console.log('Local Java runtime now installed.');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resolveLocalJavaExecutable() {
|
|
116
|
-
let javaPath = join(CACHE_DIR, `jdk-${TEMURIN_VERSION}-jre`, 'bin', 'java');
|
|
117
|
-
if (process.platform === 'win32') {
|
|
118
|
-
javaPath = join(CACHE_DIR, `jdk-${TEMURIN_VERSION}-jre`, 'bin', 'java.exe');
|
|
119
|
-
} else if (process.platform === 'darwin') {
|
|
120
|
-
javaPath = join(CACHE_DIR, `jdk-${TEMURIN_VERSION}-jre`, 'Contents', 'Home', 'bin', 'java');
|
|
121
|
-
}
|
|
122
|
-
if (!existsSync(javaPath)) {
|
|
123
|
-
throw new Error('Local Java runtime not found after installation.');
|
|
124
|
-
}
|
|
125
|
-
return javaPath;
|
|
126
255
|
}
|
|
127
256
|
|
|
128
|
-
async function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
257
|
+
async function downloadJava() {
|
|
258
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
259
|
+
const archiveName = getPlatformArchiveName();
|
|
260
|
+
const url = TEMURIN_BASE_URL + archiveName;
|
|
261
|
+
const archivePath = join(CACHE_DIR, archiveName);
|
|
262
|
+
console.log(`Downloading a Java runtime from ${url} ...`);
|
|
263
|
+
await downloadArchive(url, archivePath);
|
|
264
|
+
extractArchive(archivePath);
|
|
265
|
+
console.log('Local Java runtime now installed.');
|
|
133
266
|
}
|
|
134
267
|
|
|
268
|
+
// Resolves a usable java executable: a system Java 11 or later if one is
|
|
269
|
+
// present, otherwise a runtime from an earlier download, otherwise a
|
|
270
|
+
// freshly downloaded one.
|
|
135
271
|
async function resolveJava() {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
return resolveLocalJavaExecutable();
|
|
140
|
-
} catch (err) {
|
|
141
|
-
console.error('Failed to resolve local Java runtime, falling back to ensureLocalJava():', err);
|
|
272
|
+
const systemJava = findSystemJava();
|
|
273
|
+
if (systemJava) {
|
|
274
|
+
return systemJava;
|
|
142
275
|
}
|
|
143
|
-
|
|
276
|
+
const cachedJava = findCachedJava();
|
|
277
|
+
if (cachedJava) {
|
|
278
|
+
return cachedJava;
|
|
279
|
+
}
|
|
280
|
+
await downloadJava();
|
|
281
|
+
const javaPath = findCachedJava();
|
|
282
|
+
if (!javaPath) {
|
|
283
|
+
throw new Error('The Java runtime was not found after download and '
|
|
284
|
+
+ `extraction; expected it at: ${getLocalJavaExecutablePath()}`);
|
|
285
|
+
}
|
|
286
|
+
return javaPath;
|
|
144
287
|
}
|
|
145
288
|
|
|
146
289
|
if (require.main === module) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
290
|
+
// Invoked as the npm postinstall script. A failed download must not
|
|
291
|
+
// abort npm install: vnu.jar itself is already in place, and
|
|
292
|
+
// resolveJava() downloads Java on first use when it is needed.
|
|
293
|
+
resolveJava().catch((err) => {
|
|
294
|
+
console.warn(
|
|
295
|
+
'vnu-jar: could not install a Java runtime during postinstall.');
|
|
296
|
+
console.warn(`vnu-jar: reason: ${describeError(err)}`);
|
|
297
|
+
console.warn('vnu-jar: this is not fatal. A Java runtime will be '
|
|
298
|
+
+ 'downloaded the first time you run vnu, or you can install '
|
|
299
|
+
+ 'Java 11 or later yourself and put it on your PATH.');
|
|
300
|
+
// process.exit() is intentionally not called here. Calling it while
|
|
301
|
+
// fetch sockets are still closing aborts Node with a libuv
|
|
302
|
+
// assertion on Windows; letting this callback return lets the event
|
|
303
|
+
// loop close those handles and then exit with status 0.
|
|
150
304
|
});
|
|
151
305
|
}
|
|
152
306
|
|
|
153
|
-
module.exports = {
|
|
307
|
+
module.exports = {
|
|
308
|
+
resolveJava,
|
|
309
|
+
describeError,
|
|
310
|
+
getProxyFromEnv,
|
|
311
|
+
getPlatformArchiveName,
|
|
312
|
+
getLocalJavaExecutablePath,
|
|
313
|
+
findCachedJava,
|
|
314
|
+
};
|