Skip to content

Commit

Permalink
feat: support add&del
Browse files Browse the repository at this point in the history
  • Loading branch information
YieldRay committed Aug 10, 2024
1 parent 50626b5 commit 69f4266
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 39 deletions.
113 changes: 87 additions & 26 deletions cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
import { platform } from 'node:os'
import { getRegistry, setRegistry, getConfigPath } from './config.mjs'
import { REGISTRIES, speedTest } from './registry.mjs'
import {
getRegistry,
setRegistry,
getConfigPath,
getAllRegistries,
} from './config.mjs'
import { speedTest } from './registry.mjs'
import c, { printRegistries } from './utils.mjs'
import { appendNrmrc, readNrmrc, writeNrmrc } from './nrmrc.mjs'

// https://nodejs.org/api/util.html#utilparseargsconfig
const { values, positionals } = parseArgs({
Expand Down Expand Up @@ -40,14 +46,6 @@ if (values.help) {

const command = positionals[0] || ''
const { local } = values
/**
* @type {string}
*/
let name
/**
* @type {any}
*/
let timeout

switch (command) {
case 'h':
Expand All @@ -60,19 +58,32 @@ switch (command) {
case 'ls':
ls()
break
case 'test':
timeout = positionals[1] || '2'
case 'test': {
const timeout = positionals[1] || '2'
test(Number.parseFloat(timeout) * 1000)
break
}
case 'add': {
const name = positionals[1]
const url = positionals[2]
add(name, url)
break
}
case 'del': {
const name = positionals[1]
del(name)
break
}
case 'rc':
rc()
break
case 'use':
name = positionals[1]
case 'use': {
const name = positionals[1]
use(name)
break
}
default:
console.error(`Unknown command '${command}'\n`)
console.log(`Unknown command '${command}'\n`)
process.exit(1)
}

Expand All @@ -86,17 +97,19 @@ function help(v) {
const __dirname = dirname(__filename)
const pkg = JSON.parse(readFileSync(`${__dirname}/package.json`, 'utf-8'))
if (v) {
console.error('v' + pkg.version)
console.log('v' + pkg.version)
process.exit(1)
}
console.error(`${c.green(pkg.name)} v${pkg.version}
console.log(`${c.green(pkg.name)} v${pkg.version}
${c.bold('Usage:')}
nrml ls List registries
nrml use ${c.gray('<name>')} Use registry
nrml test ${c.gray(
'[<timeout>]'
)} Test registry speed, optional timeout in second (default: 2)
nrml add ${c.gray('<name>')} ${c.gray('<url>')} Add custom registry
nrml del ${c.gray('<name>')} Delete custom registry
nrml rc Open .npmrc file
nrml help Show this help
${c.bold('Global Options:')}
Expand All @@ -106,46 +119,94 @@ ${c.bold('Global Options:')}

async function ls() {
const currentRegistry = await getRegistry(local)
printRegistries(currentRegistry)
await printRegistries(currentRegistry)
}

/**
* @param {string} name
*/
async function use(name) {
if (!name) {
console.error(`Please provide a name!`)
console.log(`Please provide a name!`)
process.exit(-1)
}

const names = Object.keys(REGISTRIES)
const registries = await getAllRegistries()
const names = Array.from(registries.keys())
if (!names.includes(name)) {
console.error(`'${name}' is not in ${c.gray(`[${names.join('|')}]`)}`)
console.log(`'${name}' is not in ${c.gray(`[${names.join('|')}]`)}`)
process.exit(-1)
}

const registryUrl = REGISTRIES[name]
/** @type {*} */
const registryUrl = registries.get(name)
await setRegistry(local, registryUrl)
printRegistries(registryUrl)
await printRegistries(registryUrl)
}

/**
* @param {number} timeoutLimit
*/
async function test(timeoutLimit) {
const registries = await getAllRegistries()
const info = await Promise.all(
Object.entries(REGISTRIES).map(async ([name, url]) => ({
Array.from(registries.entries()).map(async ([name, url]) => ({
name,
url,
timeSpent: await speedTest(url, timeoutLimit),
}))
)

const currentRegistry = await getRegistry(local)
printRegistries(currentRegistry, info, timeoutLimit)
await printRegistries(currentRegistry, info, timeoutLimit)
process.exit(0)
}

/**
* @param {string} name
* @param {string} url
*/
async function add(name, url) {
if (!name) {
console.log(`Please provide a name!`)
process.exit(-1)
}

if (!url) {
console.log(`Please provide an url!`)
process.exit(-1)
}

const registries = await getAllRegistries()
const names = Array.from(registries.keys())
if (names.includes(name)) {
console.log(`Registry name ${c.magenta(name)} already exists!`)
process.exit(-1)
} else {
await appendNrmrc(name, url)
console.log(
`Registry ${c.magenta(name)} has been added, run ${c.green(
`nrml use ${name}`
)} to use.`
)
}
}

/**
* @param {string} name
*/
async function del(name) {
if (!name) {
console.log(`Please provide a name!`)
process.exit(-1)
}

const nrmrc = await readNrmrc().catch(() => new Map())
nrmrc.delete(name)
await writeNrmrc(nrmrc)
console.log(`Registry ${c.magenta(name)} has been deleted.`)
}

async function rc() {
const filePath = await getConfigPath(local)
try {
Expand All @@ -167,7 +228,7 @@ async function rc() {
try {
if (shouldRunEditor) execSync(`editor "${filePath}"`)
} catch {
console.error(
console.log(
`Failed to open file, please open ${c.gray(filePath)} manually.`
)
process.exit(-1)
Expand Down
18 changes: 15 additions & 3 deletions config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from 'node:fs'
import { createReadStream } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import {
Expand All @@ -7,15 +7,17 @@ import {
setRegistryFromStream,
} from './registry.mjs'
import { isFile } from './utils.mjs'
import { readNrmrc } from './nrmrc.mjs'

/**
* Set current registry
* @param {boolean|undefined} local
* @param {string} registryUrl
*/
export async function setRegistry(local, registryUrl) {
const filePath = await getConfigPath(local)
try {
const fileStream = fs.createReadStream(filePath)
const fileStream = createReadStream(filePath)
const result = await setRegistryFromStream(fileStream, registryUrl)
return writeFile(filePath, result)
} catch {
Expand All @@ -24,12 +26,13 @@ export async function setRegistry(local, registryUrl) {
}

/**
* Get current registry
* @param {boolean=} local
*/
export async function getRegistry(local) {
const filePath = await getConfigPath(local)
try {
const fileStream = fs.createReadStream(filePath) // the file may not exists
const fileStream = createReadStream(filePath) // the file may not exists
return (await getRegistryFromStream(fileStream)) || REGISTRIES['npm']
} catch {
// when rc file not found, fallback registry to default
Expand All @@ -50,3 +53,12 @@ export async function getConfigPath(local) {
}
return `${homedir().replaceAll('\\', '/')}/${rc}`
}

/**
* Returns Map to keep order
*/
export async function getAllRegistries() {
const all = new Map(Object.entries(REGISTRIES))
for (const [k, v] of await readNrmrc().catch(() => [])) all.set(k, v)
return all
}
100 changes: 100 additions & 0 deletions nrmrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { homedir } from 'node:os'
import { resolve } from 'node:path'
import { createReadStream } from 'node:fs'
import { createInterface } from 'node:readline'
import { appendFile, writeFile } from 'node:fs/promises'

const nrmrcPath = resolve(homedir(), '.nrmrc')

/**
* Read config line by line, stop when invalid.
* Warn that this function may throws, the caller SHOULD handle any error
*/
export async function readNrmrc() {
const rl = createInterface(createReadStream(nrmrcPath))
/**
* false: [name]
* true : registry=https://registry.url
*/
let state = false
let name = ''
let url = ''
/** @type {Array<[string, string]>} */
const entries = []

for await (let line of rl) {
line = line.trim()
if (line.length === 0) continue
if (state) {
const prefix = 'registry='
if (!line.startsWith(prefix)) continue
url = line.slice(prefix.length).trim() // remove prefix
entries.push([name, url])
} else {
if (!(line.startsWith('[') && line.endsWith(']'))) break
name = line.slice(1, -1)
if (name.length === 0) break
name = decodeName(name)
}
state = !state
}
return new Map(entries)
}

// name is `name`, not `[name]`
// the '[' and ']' should be removed first

/**
* @param {string} name
*/
function encodeName(name) {
const s = JSON.stringify(name).slice(1, -1)
if (name === s) return name
return `"${s}"`
}

/**
* @param {string} name
*/
function decodeName(name) {
if (name.startsWith('"') && name.endsWith('"')) {
return JSON.parse(name)
} else {
return name
}
}

/**
* @param {string} url
*/
function resolveUrl(url) {
if (!url.endsWith('/')) url = url + '/'
if (url.startsWith('/') || url.startsWith('\\')) return resolve(url)
return url
}

/**
* @param {string} name
* @param {string} url
*/
export async function appendNrmrc(name, url) {
return await appendFile(
nrmrcPath,
`[${encodeName(name)}]\nregistry=${resolveUrl(url)}\n\n`
)
}

/**
* @param {Map<string, string>} registries
*/
export async function writeNrmrc(registries) {
return await writeFile(
nrmrcPath,
Array.from(registries.entries())
.map(
([name, url]) =>
`[${encodeName(name)}]\nregistry=${resolveUrl(url)}`
)
.join('\n\n')
)
}
6 changes: 3 additions & 3 deletions registry.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as readline from 'node:readline'
import { createInterface } from 'node:readline'

/**
* @type {Record<string,string>}
Expand Down Expand Up @@ -31,7 +31,7 @@ function checkLine(line) {
* @see https://docs.npmjs.com/cli/configuring-npm/npmrc
*/
export async function getRegistryFromStream(stream) {
const rl = readline.createInterface(stream)
const rl = createInterface(stream)
for await (const line of rl) {
const r = checkLine(line)
if (r) return r
Expand All @@ -45,7 +45,7 @@ export async function getRegistryFromStream(stream) {
* @returns {Promise<string>}
*/
export async function setRegistryFromStream(stream, registryUrl) {
const rl = readline.createInterface(stream)
const rl = createInterface(stream)
const lines = []
for await (const line of rl) {
const r = checkLine(line)
Expand Down
Loading

0 comments on commit 69f4266

Please sign in to comment.