@wiremock/npm-jar-wrapper-maker 1.0.0

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.
@@ -0,0 +1,711 @@
1
+ /**
2
+ * NPM Wrapper Generator
3
+ *
4
+ * Generates NPM wrapper projects for Java JARs.
5
+ */
6
+
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+
10
+ /**
11
+ * Default configuration
12
+ */
13
+ const defaults = {
14
+ targetJavaVersion: 17,
15
+ javaOpts: [],
16
+ enableNativeAccess: false,
17
+ };
18
+
19
+ /**
20
+ * Generate package.json for the wrapper
21
+ */
22
+ function generatePackageJson(config) {
23
+ const {
24
+ packageName,
25
+ version = '1.0.0',
26
+ description = '',
27
+ author = '',
28
+ license = 'Apache-2.0',
29
+ cliName,
30
+ jarFileName,
31
+ repository = {},
32
+ keywords = [],
33
+ homepage = '',
34
+ bugs = {},
35
+ enablePostinstall = false,
36
+ postinstallCommand = '',
37
+ } = config;
38
+
39
+ const pkg = {
40
+ name: packageName,
41
+ version,
42
+ description,
43
+ main: 'index.js',
44
+ bin: {},
45
+ files: ['index.js', 'cli.js', jarFileName],
46
+ scripts: {
47
+ test: 'echo "No tests specified"',
48
+ },
49
+ keywords: ['java', 'cli', ...keywords],
50
+ author,
51
+ license,
52
+ dependencies: {
53
+ decompress: '^4.2.1',
54
+ },
55
+ engines: {
56
+ node: '>=18.0.0',
57
+ },
58
+ };
59
+
60
+ // Set bin entry
61
+ pkg.bin[cliName] = 'index.js';
62
+
63
+ // Add postinstall if enabled
64
+ if (enablePostinstall) {
65
+ pkg.scripts.postinstall = 'node ./postinstall.js';
66
+ pkg.files.push('postinstall.js');
67
+ }
68
+
69
+ // Add optional fields if provided
70
+ if (Object.keys(repository).length > 0) {
71
+ pkg.repository = repository;
72
+ }
73
+ if (homepage) {
74
+ pkg.homepage = homepage;
75
+ }
76
+ if (Object.keys(bugs).length > 0) {
77
+ pkg.bugs = bugs;
78
+ }
79
+
80
+ return JSON.stringify(pkg, null, 2);
81
+ }
82
+
83
+ /**
84
+ * Generate the CLI entry point (index.js)
85
+ */
86
+ function generateIndexJs(config) {
87
+ const {
88
+ cliName,
89
+ jarFileName,
90
+ targetJavaVersion = defaults.targetJavaVersion,
91
+ javaOpts = defaults.javaOpts,
92
+ enableNativeAccess = defaults.enableNativeAccess,
93
+ configDirName,
94
+ } = config;
95
+
96
+ // Use the CLI name as config dir name if not specified
97
+ const configDir = configDirName || cliName;
98
+
99
+ return `#!/usr/bin/env node
100
+
101
+ const path = require('node:path');
102
+ const os = require('node:os');
103
+ const { runCli } = require('./cli');
104
+
105
+ const targetJavaVersion = ${targetJavaVersion};
106
+ const jarPath = path.join(__dirname, '${jarFileName}');
107
+ const jreDir = path.join(userConfigDir(), '${configDir}', 'jre');
108
+
109
+ const config = {
110
+ env: process.env,
111
+ targetJavaVersion,
112
+ jreDir,
113
+ jarPath,
114
+ cmdArgs: process.argv.slice(2),
115
+ platform: process.platform,
116
+ arch: process.arch,
117
+ spawnOptions: { stdio: 'inherit' },
118
+ javaOpts: ${JSON.stringify(javaOpts)},
119
+ enableNativeAccess: ${enableNativeAccess},
120
+ };
121
+
122
+ runCli(config).then((ret) => {
123
+ if (ret.error) {
124
+ console.error(\`Failed to run: \${ret.error.message}\`);
125
+ }
126
+ process.exit(ret.status ?? 1);
127
+ });
128
+
129
+ function userConfigDir() {
130
+ switch (process.platform) {
131
+ case 'darwin':
132
+ return path.join(os.homedir(), '.config');
133
+ case 'win32':
134
+ return process.env['LOCALAPPDATA'] || path.join(os.homedir(), 'AppData', 'Local');
135
+ default:
136
+ return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
137
+ }
138
+ }
139
+ `;
140
+ }
141
+
142
+ /**
143
+ * Generate the CLI runtime (cli.js)
144
+ */
145
+ function generateCliJs() {
146
+ return `/**
147
+ * CLI Runtime
148
+ *
149
+ * Manages Java JRE discovery/download and JAR execution.
150
+ */
151
+
152
+ const { spawnSync } = require('node:child_process');
153
+ const fs = require('node:fs');
154
+ const path = require('node:path');
155
+ const https = require('node:https');
156
+ const crypto = require('node:crypto');
157
+
158
+ /**
159
+ * Map Node.js platform to JRE OS name
160
+ */
161
+ function mapPlatformToOs(platform) {
162
+ switch (platform) {
163
+ case 'darwin':
164
+ return 'mac';
165
+ case 'win32':
166
+ return 'windows';
167
+ default:
168
+ return 'linux';
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Map Node.js architecture to JRE architecture name
174
+ */
175
+ function mapArchToJreArch(arch) {
176
+ switch (arch) {
177
+ case 'arm64':
178
+ return 'aarch64';
179
+ case 'x64':
180
+ return 'x64';
181
+ case 'ia32':
182
+ return 'x32';
183
+ default:
184
+ return arch;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Check if a path is valid (no path traversal attacks)
190
+ */
191
+ function isValidArchivePath(filePath) {
192
+ if (path.isAbsolute(filePath)) {
193
+ return false;
194
+ }
195
+ const segments = filePath.split(path.sep);
196
+ return !segments.some((segment) => segment === '..');
197
+ }
198
+
199
+ /**
200
+ * Verify file checksum
201
+ */
202
+ function verifyChecksum(filePath, expectedChecksum, algorithm = 'sha256') {
203
+ const fileBuffer = fs.readFileSync(filePath);
204
+ const hash = crypto.createHash(algorithm);
205
+ hash.update(fileBuffer);
206
+ const actualChecksum = hash.digest('hex');
207
+ return actualChecksum === expectedChecksum;
208
+ }
209
+
210
+ /**
211
+ * Download a file from URL to destination
212
+ */
213
+ function downloadFile(url, dest) {
214
+ return new Promise((resolve, reject) => {
215
+ const file = fs.createWriteStream(dest);
216
+
217
+ const request = (url) => {
218
+ https.get(url, (response) => {
219
+ if (response.statusCode === 301 || response.statusCode === 302) {
220
+ request(response.headers.location);
221
+ return;
222
+ }
223
+
224
+ if (response.statusCode !== 200) {
225
+ reject(new Error(\`Failed to download: HTTP \${response.statusCode}\`));
226
+ return;
227
+ }
228
+
229
+ response.pipe(file);
230
+ file.on('finish', () => {
231
+ file.close(resolve);
232
+ });
233
+ }).on('error', (err) => {
234
+ fs.unlink(dest, () => {});
235
+ reject(err);
236
+ });
237
+ };
238
+
239
+ request(url);
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Fetch JSON from URL
245
+ */
246
+ function fetchJson(url) {
247
+ return new Promise((resolve, reject) => {
248
+ const request = (url) => {
249
+ https.get(url, (response) => {
250
+ if (response.statusCode === 301 || response.statusCode === 302) {
251
+ request(response.headers.location);
252
+ return;
253
+ }
254
+
255
+ if (response.statusCode !== 200) {
256
+ reject(new Error(\`Failed to fetch: HTTP \${response.statusCode}\`));
257
+ return;
258
+ }
259
+
260
+ let data = '';
261
+ response.on('data', (chunk) => (data += chunk));
262
+ response.on('end', () => {
263
+ try {
264
+ resolve(JSON.parse(data));
265
+ } catch (e) {
266
+ reject(new Error(\`Failed to parse JSON: \${e.message}\`));
267
+ }
268
+ });
269
+ }).on('error', reject);
270
+ };
271
+
272
+ request(url);
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Check if Java is available at JAVA_HOME
278
+ */
279
+ function checkJavaHome(env) {
280
+ const javaHome = env.JAVA_HOME;
281
+ if (!javaHome) {
282
+ return null;
283
+ }
284
+
285
+ const javaBin = path.join(javaHome, 'bin', 'java');
286
+ try {
287
+ if (fs.existsSync(javaBin) || fs.existsSync(javaBin + '.exe')) {
288
+ return javaHome;
289
+ }
290
+ } catch (e) {
291
+ // Ignore errors
292
+ }
293
+ return null;
294
+ }
295
+
296
+ /**
297
+ * Check if Java is available on PATH
298
+ */
299
+ function checkSystemJava() {
300
+ try {
301
+ const result = spawnSync('java', ['-version'], {
302
+ stdio: ['ignore', 'pipe', 'pipe'],
303
+ timeout: 5000,
304
+ });
305
+
306
+ if (result.status === 0) {
307
+ const whichResult = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['java'], {
308
+ stdio: ['ignore', 'pipe', 'pipe'],
309
+ });
310
+
311
+ if (whichResult.status === 0) {
312
+ const javaPath = whichResult.stdout.toString().trim().split('\\n')[0];
313
+ return path.dirname(path.dirname(javaPath));
314
+ }
315
+ }
316
+ } catch (e) {
317
+ // Ignore errors
318
+ }
319
+ return null;
320
+ }
321
+
322
+ /**
323
+ * Check if JRE exists in managed directory
324
+ */
325
+ function checkManagedJre(jreDir) {
326
+ if (!fs.existsSync(jreDir)) {
327
+ return null;
328
+ }
329
+
330
+ const entries = fs.readdirSync(jreDir);
331
+ for (const entry of entries) {
332
+ const entryPath = path.join(jreDir, entry);
333
+ const stat = fs.statSync(entryPath);
334
+
335
+ if (stat.isDirectory()) {
336
+ const possiblePaths = [
337
+ path.join(entryPath, 'bin', 'java'),
338
+ path.join(entryPath, 'Contents', 'Home', 'bin', 'java'),
339
+ ];
340
+
341
+ for (const javaBin of possiblePaths) {
342
+ if (fs.existsSync(javaBin) || fs.existsSync(javaBin + '.exe')) {
343
+ return javaBin.includes('Contents/Home')
344
+ ? path.join(entryPath, 'Contents', 'Home')
345
+ : entryPath;
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ return null;
352
+ }
353
+
354
+ /**
355
+ * Download JRE from Adoptium
356
+ */
357
+ async function downloadJre(targetVersion, jreDir, platform, arch) {
358
+ const jreOs = mapPlatformToOs(platform);
359
+ const jreArch = mapArchToJreArch(arch);
360
+
361
+ const apiUrl = \`https://api.adoptium.net/v3/assets/latest/\${targetVersion}/hotspot?architecture=\${jreArch}&image_type=jre&os=\${jreOs}\`;
362
+
363
+ console.log('Fetching JRE metadata from Adoptium...');
364
+ const assets = await fetchJson(apiUrl);
365
+
366
+ if (!assets || assets.length === 0) {
367
+ throw new Error(\`No JRE found for Java \${targetVersion} on \${jreOs}/\${jreArch}\`);
368
+ }
369
+
370
+ const asset = assets[0];
371
+ const binary = asset.binary;
372
+ const downloadUrl = binary.package.link;
373
+ const checksum = binary.package.checksum;
374
+ const filename = binary.package.name;
375
+
376
+ fs.mkdirSync(jreDir, { recursive: true });
377
+
378
+ const downloadPath = path.join(jreDir, filename);
379
+
380
+ console.log(\`Downloading JRE from Adoptium (\${binary.package.size / 1024 / 1024 | 0} MB)...\`);
381
+ await downloadFile(downloadUrl, downloadPath);
382
+
383
+ console.log('Verifying checksum...');
384
+ if (!verifyChecksum(downloadPath, checksum)) {
385
+ fs.unlinkSync(downloadPath);
386
+ throw new Error('Checksum verification failed');
387
+ }
388
+
389
+ console.log('Extracting JRE...');
390
+ const decompress = require('decompress');
391
+ await decompress(downloadPath, jreDir, {
392
+ filter: (file) => isValidArchivePath(file.path),
393
+ });
394
+
395
+ fs.unlinkSync(downloadPath);
396
+ console.log('JRE installation complete.');
397
+
398
+ return checkManagedJre(jreDir);
399
+ }
400
+
401
+ /**
402
+ * Find or download Java JRE
403
+ */
404
+ async function findOrDownloadJava(targetVersion, jreDir, platform, arch, env) {
405
+ let javaHome = checkJavaHome(env);
406
+ if (javaHome) {
407
+ return javaHome;
408
+ }
409
+
410
+ javaHome = checkSystemJava();
411
+ if (javaHome) {
412
+ return javaHome;
413
+ }
414
+
415
+ javaHome = checkManagedJre(jreDir);
416
+ if (javaHome) {
417
+ return javaHome;
418
+ }
419
+
420
+ return downloadJre(targetVersion, jreDir, platform, arch);
421
+ }
422
+
423
+ /**
424
+ * Run the CLI
425
+ */
426
+ async function runCli(config) {
427
+ const {
428
+ env,
429
+ targetJavaVersion,
430
+ jreDir,
431
+ jarPath,
432
+ cmdArgs,
433
+ platform,
434
+ arch,
435
+ spawnOptions,
436
+ javaOpts = [],
437
+ enableNativeAccess = false,
438
+ } = config;
439
+
440
+ try {
441
+ const javaHome = await findOrDownloadJava(targetJavaVersion, jreDir, platform, arch, env);
442
+
443
+ if (!javaHome) {
444
+ return { error: new Error('Failed to find or download Java'), status: 1 };
445
+ }
446
+
447
+ const javaBin = path.join(javaHome, 'bin', 'java');
448
+
449
+ const javaArgs = [
450
+ '-Dsun.misc.URLClassPath.disableJarChecking=true',
451
+ ...javaOpts,
452
+ ];
453
+
454
+ if (enableNativeAccess) {
455
+ javaArgs.push('--enable-native-access=ALL-UNNAMED');
456
+ }
457
+
458
+ if (env.JAVA_OPTS) {
459
+ const extraOpts = env.JAVA_OPTS.split(' ').filter(Boolean);
460
+ javaArgs.push(...extraOpts);
461
+ }
462
+
463
+ javaArgs.push('-jar', jarPath, ...cmdArgs);
464
+
465
+ const result = spawnSync(javaBin, javaArgs, {
466
+ ...spawnOptions,
467
+ env,
468
+ });
469
+
470
+ return { status: result.status, error: result.error };
471
+ } catch (error) {
472
+ return { error, status: 1 };
473
+ }
474
+ }
475
+
476
+ module.exports = {
477
+ runCli,
478
+ findOrDownloadJava,
479
+ mapPlatformToOs,
480
+ mapArchToJreArch,
481
+ isValidArchivePath,
482
+ };
483
+ `;
484
+ }
485
+
486
+ /**
487
+ * Generate postinstall.js
488
+ */
489
+ function generatePostinstallJs(config) {
490
+ const { postinstallCommand = '', cliName } = config;
491
+
492
+ if (!postinstallCommand) {
493
+ return `/**
494
+ * Post-install hook
495
+ *
496
+ * Runs after npm install.
497
+ */
498
+
499
+ // Add custom post-install logic here
500
+ console.log('${cliName} installed successfully.');
501
+ `;
502
+ }
503
+
504
+ return `/**
505
+ * Post-install hook
506
+ *
507
+ * Runs after npm install.
508
+ */
509
+
510
+ const { execSync } = require('node:child_process');
511
+ const path = require('node:path');
512
+
513
+ try {
514
+ // Check if installed globally
515
+ const globalPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
516
+ const isGlobal = __dirname.startsWith(globalPrefix);
517
+
518
+ if (isGlobal) {
519
+ ${postinstallCommand}
520
+ }
521
+ } catch (e) {
522
+ // Ignore errors during postinstall
523
+ }
524
+ `;
525
+ }
526
+
527
+ /**
528
+ * Generate .gitignore
529
+ */
530
+ function generateGitignore(config) {
531
+ const { jarFileName } = config;
532
+
533
+ return `# Dependencies
534
+ node_modules/
535
+
536
+ # Build artifacts
537
+ ${jarFileName}
538
+
539
+ # IDE
540
+ .idea/
541
+ .vscode/
542
+ *.iml
543
+
544
+ # OS
545
+ .DS_Store
546
+ Thumbs.db
547
+
548
+ # Logs
549
+ *.log
550
+ npm-debug.log*
551
+
552
+ # Test coverage
553
+ coverage/
554
+ `;
555
+ }
556
+
557
+ /**
558
+ * Generate .npmignore
559
+ */
560
+ function generateNpmignore() {
561
+ return `# Development files
562
+ .gitignore
563
+ .prettierrc
564
+ .editorconfig
565
+
566
+ # Test files
567
+ test/
568
+ *.test.js
569
+
570
+ # IDE
571
+ .idea/
572
+ .vscode/
573
+ *.iml
574
+
575
+ # CI/CD
576
+ .github/
577
+ .gitlab-ci.yml
578
+ Jenkinsfile
579
+
580
+ # Documentation source
581
+ docs/
582
+ `;
583
+ }
584
+
585
+ /**
586
+ * Generate README.md
587
+ */
588
+ function generateReadme(config) {
589
+ const {
590
+ packageName,
591
+ description = '',
592
+ cliName,
593
+ jarFileName,
594
+ targetJavaVersion = defaults.targetJavaVersion,
595
+ } = config;
596
+
597
+ return `# ${packageName}
598
+
599
+ ${description}
600
+
601
+ ## Installation
602
+
603
+ \`\`\`bash
604
+ npm install -g ${packageName}
605
+ \`\`\`
606
+
607
+ ## Usage
608
+
609
+ \`\`\`bash
610
+ ${cliName} [options]
611
+ \`\`\`
612
+
613
+ ## Requirements
614
+
615
+ - Node.js >= 18.0.0
616
+ - Java ${targetJavaVersion}+ (will be downloaded automatically if not found)
617
+
618
+ ## How it works
619
+
620
+ This package wraps a Java JAR file (\`${jarFileName}\`) and provides a convenient CLI interface.
621
+ When you run the CLI, it will:
622
+
623
+ 1. Check for Java in \`JAVA_HOME\`
624
+ 2. Check for Java on your system PATH
625
+ 3. If no Java is found, automatically download and cache a JRE from Adoptium
626
+
627
+ ## License
628
+
629
+ Apache-2.0
630
+ `;
631
+ }
632
+
633
+ /**
634
+ * Generate the wrapper project
635
+ */
636
+ function generateWrapper(config, outputDir) {
637
+ const files = {
638
+ 'package.json': generatePackageJson(config),
639
+ 'index.js': generateIndexJs(config),
640
+ 'cli.js': generateCliJs(),
641
+ '.gitignore': generateGitignore(config),
642
+ '.npmignore': generateNpmignore(),
643
+ 'README.md': generateReadme(config),
644
+ };
645
+
646
+ if (config.enablePostinstall) {
647
+ files['postinstall.js'] = generatePostinstallJs(config);
648
+ }
649
+
650
+ // Create output directory if it doesn't exist
651
+ fs.mkdirSync(outputDir, { recursive: true });
652
+
653
+ // Write all files
654
+ const writtenFiles = [];
655
+ for (const [filename, content] of Object.entries(files)) {
656
+ const filePath = path.join(outputDir, filename);
657
+ fs.writeFileSync(filePath, content, 'utf-8');
658
+ writtenFiles.push(filePath);
659
+ }
660
+
661
+ return {
662
+ outputDir,
663
+ files: writtenFiles,
664
+ jarPlaceholder: path.join(outputDir, config.jarFileName),
665
+ };
666
+ }
667
+
668
+ /**
669
+ * Validate configuration
670
+ */
671
+ function validateConfig(config) {
672
+ const errors = [];
673
+
674
+ if (!config.packageName) {
675
+ errors.push('packageName is required');
676
+ } else if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(config.packageName)) {
677
+ errors.push('packageName must be a valid npm package name');
678
+ }
679
+
680
+ if (!config.cliName) {
681
+ errors.push('cliName is required');
682
+ } else if (!/^[a-z0-9-]+$/.test(config.cliName)) {
683
+ errors.push('cliName must contain only lowercase letters, numbers, and hyphens');
684
+ }
685
+
686
+ if (!config.jarFileName) {
687
+ errors.push('jarFileName is required');
688
+ }
689
+
690
+ if (config.targetJavaVersion && (typeof config.targetJavaVersion !== 'number' || config.targetJavaVersion < 8)) {
691
+ errors.push('targetJavaVersion must be a number >= 8');
692
+ }
693
+
694
+ return {
695
+ valid: errors.length === 0,
696
+ errors,
697
+ };
698
+ }
699
+
700
+ module.exports = {
701
+ generateWrapper,
702
+ generatePackageJson,
703
+ generateIndexJs,
704
+ generateCliJs,
705
+ generatePostinstallJs,
706
+ generateGitignore,
707
+ generateNpmignore,
708
+ generateReadme,
709
+ validateConfig,
710
+ defaults,
711
+ };