vnu-jar 26.5.19 → 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.
Binary file
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name": "vnu-jar", "version": "26.5.19", "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"]}}
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(1);
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
  }
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { spawnSync, execSync } = require('child_process');
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 fetch = globalThis.fetch || require('node-fetch');
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
- const majorVersion = parseInt(match[1], 10);
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
- async function downloadJava() {
75
- mkdirSync(CACHE_DIR, { recursive: true });
76
- const archiveName = getPlatformArchiveName();
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
- console.log('Extracting ZIP archive...');
93
- const extractRes = spawnSync('powershell', [
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 (extractRes.status !== 0) {
103
- throw new Error('Failed to extract Java archive.');
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 extractRes = spawnSync('tar', ['-xzf', archivePath, '-C', CACHE_DIR], { stdio: 'inherit' });
108
- if (extractRes.status !== 0) {
109
- throw new Error('Failed to extract Java archive.');
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 ensureLocalJava() {
129
- if (!existsSync(CACHE_DIR)) {
130
- await downloadJava();
131
- }
132
- return resolveLocalJavaExecutable();
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 javaPath = findSystemJava();
137
- if (javaPath) return javaPath;
138
- try {
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
- return ensureLocalJava();
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
- void resolveJava().catch((err) => {
148
- console.error('Failed to resolve Java runtime:', err && err.stack ? err.stack : String(err));
149
- process.exit(1);
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 = { resolveJava };
307
+ module.exports = {
308
+ resolveJava,
309
+ describeError,
310
+ getProxyFromEnv,
311
+ getPlatformArchiveName,
312
+ getLocalJavaExecutablePath,
313
+ findCachedJava,
314
+ };