Skip to content

Commit 564001a

Browse files
committed
feat: implement parseArgs and replace fetch with request to support node 14
Release-As: 0.3.2
1 parent 70adde9 commit 564001a

7 files changed

Lines changed: 207 additions & 35 deletions

File tree

.github/workflows/release-please.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@ permissions: {}
77
jobs:
88
release-please:
99
permissions:
10-
contents: write # to create release commit (google-github-actions/release-please-action)
11-
pull-requests: write # to create release PR (google-github-actions/release-please-action)
10+
contents: write # to create release commit (googleapis/release-please-action)
11+
pull-requests: write # to create release PR (googleapis/release-please-action)
1212
id-token: write # (JS-DevTools/npm-publish)
1313

1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: google-github-actions/release-please-action@v4
16+
- uses: googleapis/release-please-action@v4
1717
id: release
1818
with:
1919
token: ${{ secrets.GITHUB_TOKEN }}
2020
release-type: node
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v5
2222
- uses: actions/setup-node@v4
2323
with:
2424
node-version: 22
2525

2626
- run: npm test
2727
if: ${{ steps.release.outputs.release_created }}
2828

29-
- uses: JS-DevTools/npm-publish@v3
29+
- uses: JS-DevTools/npm-publish@v4
3030
if: ${{ steps.release.outputs.release_created }}
3131
with:
3232
token: ${{secrets.NPM_TOKEN}}

benchmark.test.mjs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function time(...args) {
1111
lines.map((line) => {
1212
const [type, spent] = line.split('\t')
1313
return [type, spent]
14-
}),
14+
})
1515
)
1616
return { ...data, spent: await spent(...args) }
1717
}
@@ -34,11 +34,15 @@ const __filename = fileURLToPath(import.meta.url)
3434
const __dirname = dirname(__filename)
3535
const script = join(__dirname, 'cli.mjs')
3636

37-
console.log('Deno')
38-
console.log(await time('deno', 'run', '-A', script))
37+
try {
38+
console.log('Deno')
39+
console.log(await time('deno', 'run', '-A', script))
3940

40-
console.log('Node')
41-
console.log(await time('node', script))
41+
console.log('Node')
42+
console.log(await time('node', script))
43+
} catch (e) {
44+
console.error('Error running benchmark:', e)
45+
}
4246

4347
// TODO: running this benchmark shows that Node is faster than Deno,
4448
// but when directly running the cli, Deno is faster than Node. (which is the real case)

cli.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env node
22
import process from 'node:process'
3-
import { parseArgs } from 'node:util'
43
import { readFileSync } from 'node:fs'
54
import { fileURLToPath } from 'node:url'
65
import { dirname, join } from 'node:path'
@@ -12,7 +11,12 @@ import {
1211
getAllRegistries,
1312
} from './config.mjs'
1413
import { speedTest } from './registry.mjs'
15-
import { styleText, execFileAsync, printRegistries } from './utils.mjs'
14+
import {
15+
parseArgs,
16+
styleText,
17+
execFileAsync,
18+
printRegistries,
19+
} from './utils.mjs'
1620
import { appendNrmrc, readNrmrc, writeNrmrc } from './nrmrc.mjs'
1721

1822
// https://nodejs.org/api/util.html#utilparseargsconfig

config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function getConfigPath(local) {
5252
if (local || detectLocal) {
5353
return rc
5454
}
55-
return join(normalize(homedir()), rc).replaceAll('\\', '/')
55+
return join(normalize(homedir()), rc).replace(/\\/g, '/')
5656
}
5757

5858
/**

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"benchmark": "node benchmark.mjs"
1313
},
1414
"engines": {
15-
"node": ">16"
15+
"node": ">=14"
1616
},
1717
"files": [
1818
"*.js",
@@ -23,8 +23,8 @@
2323
"nrml": "cli.mjs"
2424
},
2525
"devDependencies": {
26-
"@types/node": "^22.16.0",
27-
"typescript": "^5.8.3"
26+
"@types/node": "^22.18.11",
27+
"typescript": "^5.9.3"
2828
},
2929
"publishConfig": {
3030
"registry": "https://registry.npmjs.org"

registry.mjs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { parse } from 'node:url'
12
import { createInterface } from 'node:readline'
3+
import { request } from './utils.mjs'
24

35
/**
46
* @type {Record<string, string>}
@@ -67,18 +69,22 @@ export async function setRegistryFromStream(stream, registryUrl) {
6769
* @param {number} timeoutLimit - in milliseconds
6870
*/
6971
export async function speedTest(url, timeoutLimit) {
70-
try {
72+
return new Promise((resolve) => {
7173
const beginTime = Date.now()
72-
await fetch(url, {
73-
method: 'HEAD',
74-
signal: AbortSignal.timeout(timeoutLimit),
75-
})
76-
const timeSpent = Date.now() - beginTime
77-
return timeSpent > timeoutLimit ? Infinity : timeSpent
78-
} catch (e) {
79-
if (e instanceof DOMException) {
80-
return Infinity
81-
}
82-
return null // Network Error
83-
}
74+
request(
75+
{ method: 'HEAD', ...parse(url), timeout: timeoutLimit },
76+
(res) => {
77+
res.destroy()
78+
const timeSpent = Date.now() - beginTime
79+
resolve(timeSpent > timeoutLimit ? Infinity : timeSpent) // Normal response
80+
},
81+
)
82+
.on('timeout', () => {
83+
resolve(Infinity) // Timeout
84+
})
85+
.on('error', () => {
86+
resolve(null) // Network Error
87+
})
88+
.end()
89+
})
8490
}

utils.mjs

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,162 @@
1+
import utils, { promisify } from 'node:util'
12
import { stat } from 'node:fs/promises'
23
import { execFile } from 'node:child_process'
3-
import { promisify } from 'node:util'
44
import { platform } from 'node:os'
5+
import http from 'node:http'
6+
import https from 'node:https'
7+
import { parse, format } from 'node:url'
58
import { getAllRegistries } from './config.mjs'
69

10+
/**
11+
* @param {import('http').RequestOptions | import('https').RequestOptions | string | URL} options
12+
* @param {(res: import('http').IncomingMessage) => void} callback
13+
* @returns {import('http').ClientRequest}
14+
*/
15+
export function request(options, callback) {
16+
const url = parse(format(options), false, true)
17+
const module = url.protocol === 'https:' ? https : http
18+
return module.request(options, callback)
19+
}
20+
21+
export const parseArgs = utils.parseArgs || _parseArgs
22+
23+
/**
24+
* Polyfill for Node.js util.parseArgs
25+
* @param {import('util').ParseArgsConfig} config
26+
* @returns {ReturnType<import('util').parseArgs>}
27+
*/
28+
function _parseArgs(config = {}) {
29+
const {
30+
options = {},
31+
strict = false,
32+
allowPositionals = false,
33+
args = process.argv.slice(2),
34+
} = config
35+
36+
/** @type {{ [longOption: string]: undefined | string | boolean | Array<string | boolean> }} */
37+
const values = {}
38+
/** @type {string[]} */
39+
const positionals = []
40+
41+
// Initialize default values
42+
for (const [name, opt] of Object.entries(options)) {
43+
if ('default' in opt) {
44+
values[name] = opt.default
45+
} else if (opt.multiple) {
46+
values[name] = []
47+
}
48+
}
49+
50+
for (let i = 0; i < args.length; i++) {
51+
const arg = args[i]
52+
53+
// Handle positional arguments
54+
if (!arg.startsWith('-')) {
55+
if (!allowPositionals && strict) {
56+
throw new Error(`Unexpected positional argument: ${arg}`)
57+
}
58+
positionals.push(arg)
59+
continue
60+
}
61+
62+
// Handle long options (--option)
63+
if (arg.startsWith('--')) {
64+
const optName = arg.slice(2)
65+
const option = options[optName]
66+
67+
if (!option) {
68+
if (strict) {
69+
throw new Error(`Unknown option: ${arg}`)
70+
}
71+
continue
72+
}
73+
74+
if (option.type === 'boolean') {
75+
if (option.multiple) {
76+
if (!Array.isArray(values[optName])) values[optName] = []
77+
values[optName].push(true)
78+
} else {
79+
values[optName] = true
80+
}
81+
} else if (option.type === 'string') {
82+
const value = args[++i]
83+
if (value === undefined) {
84+
throw new Error(`Option ${arg} requires a value`)
85+
}
86+
if (option.multiple) {
87+
if (!Array.isArray(values[optName])) values[optName] = []
88+
values[optName].push(value)
89+
} else {
90+
values[optName] = value
91+
}
92+
}
93+
continue
94+
}
95+
96+
// Handle short options (-o)
97+
if (arg.startsWith('-')) {
98+
const shortOpts = arg.slice(1).split('')
99+
100+
for (let j = 0; j < shortOpts.length; j++) {
101+
const shortOpt = shortOpts[j]
102+
let optName = null
103+
104+
// Find option by short name
105+
for (const [name, opt] of Object.entries(options)) {
106+
if (opt.short === shortOpt) {
107+
optName = name
108+
break
109+
}
110+
}
111+
112+
if (!optName) {
113+
if (strict) {
114+
throw new Error(`Unknown option: -${shortOpt}`)
115+
}
116+
continue
117+
}
118+
119+
const option = options[optName]
120+
121+
if (option.type === 'boolean') {
122+
if (option.multiple) {
123+
if (!Array.isArray(values[optName]))
124+
values[optName] = []
125+
//@ts-ignore
126+
values[optName].push(true)
127+
} else {
128+
values[optName] = true
129+
}
130+
} else if (option.type === 'string') {
131+
let value
132+
// If not last char in group, remaining chars are the value
133+
if (j < shortOpts.length - 1) {
134+
value = shortOpts.slice(j + 1).join('')
135+
j = shortOpts.length // End loop
136+
} else {
137+
value = args[++i]
138+
if (value === undefined) {
139+
throw new Error(
140+
`Option -${shortOpt} requires a value`,
141+
)
142+
}
143+
}
144+
if (option.multiple) {
145+
if (!Array.isArray(values[optName]))
146+
values[optName] = []
147+
//@ts-ignore
148+
values[optName].push(value)
149+
} else {
150+
values[optName] = value
151+
}
152+
}
153+
}
154+
}
155+
}
156+
157+
return { values, positionals }
158+
}
159+
7160
/**
8161
* ANSI escape codes mapping
9162
* @typedef {keyof typeof styles} Format
@@ -51,13 +204,15 @@ const styles = {
51204
bgWhite: '\x1b[47m',
52205
}
53206

207+
export const styleText = utils.styleText || _styleText
208+
54209
/**
55210
* Basic implementation of util.styleText for formatting text with ANSI colors
56211
* @param {Format | Format[]} format - A text format or an Array of text formats
57212
* @param {string} text - The text to be formatted
58213
* @returns {string} The formatted text with ANSI escape codes
59214
*/
60-
export function styleText(format, text) {
215+
function _styleText(format, text) {
61216
const formats = Array.isArray(format) ? format : [format]
62217
// Build the opening escape sequences
63218
let openCodes = ''
@@ -103,10 +258,13 @@ export async function printRegistries(
103258
timeoutLimit,
104259
) {
105260
const registries = await getAllRegistries()
106-
registriesInfo ||= Array.from(registries.entries()).map(([name, url]) => ({
107-
name,
108-
url,
109-
}))
261+
if (!registriesInfo)
262+
registriesInfo = Array.from(registries.entries()).map(
263+
([name, url]) => ({
264+
name,
265+
url,
266+
}),
267+
)
110268

111269
const maxNameLength = Math.max(
112270
...registriesInfo.map(({ name }) => name.length),

0 commit comments

Comments
 (0)