@adamlui/scss-to-css 2.2.1 → 2.3.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.
package/README.md CHANGED
@@ -33,8 +33,8 @@
33
33
  <img height=31 src="https://img.shields.io/npm/dm/%40adamlui%2Fscss-to-css?logo=npm&color=af68ff&logoColor=white&labelColor=464646&style=for-the-badge"></a>
34
34
  <a href="#%EF%B8%8F-mit-license">
35
35
  <img height=31 src="https://img.shields.io/badge/License-MIT-orange.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
36
- <a href="https://github.com/adamlui/js-utils/releases/tag/scss-to-css-2.2.1">
37
- <img height=31 src="https://img.shields.io/badge/Latest_Build-2.2.1-44cc11.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
36
+ <a href="https://github.com/adamlui/js-utils/releases/tag/scss-to-css-2.3.0">
37
+ <img height=31 src="https://img.shields.io/badge/Latest_Build-2.3.0-44cc11.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
38
38
  <a href="https://www.npmjs.com/package/@adamlui/scss-to-css?activeTab=code">
39
39
  <img height=31 src="https://img.shields.io/npm/unpacked-size/%40adamlui%2Fscss-to-css?style=for-the-badge&logo=ebox&logoColor=white&color=blue&labelColor=464646"></a>
40
40
  <a href="https://sonarcloud.io/component_measures?metric=new_vulnerabilities&id=adamlui_scss-to-css:src/scss-to-css.js">
@@ -170,6 +170,8 @@ Commands:
170
170
  -i, --init Create config file (in project root).
171
171
  -h, --help Display help screen.
172
172
  -v, --version Show version number.
173
+ --stats Show npm stats.
174
+ --debug [targetKey] Show debug logs.
173
175
  ```
174
176
 
175
177
  #
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ (async () => {
4
+ 'use strict'
5
+
6
+ globalThis.env = {
7
+ args: process.argv.slice(2),
8
+ modes: { dev: /[\\/]src(?:[\\/]|$)/i.test(__dirname) },
9
+ paths: { lib: './lib' }
10
+ }
11
+ env.modes.debug = env.args.some(arg => /^--?(?:V|debug(?:[-_]?mode)?)$/.test(arg))
12
+
13
+ // Import LIBS
14
+ globalThis.log = require(`${env.paths.lib}/log`)
15
+ const compile = require(`${env.paths.lib}/compile`),
16
+ { findSCSS } = require('../scss-to-css'),
17
+ fs = require('fs'),
18
+ init = require(`${env.paths.lib}/init`),
19
+ path = require('path')
20
+
21
+ await init.cli()
22
+
23
+ // Exec CMD arg if passed
24
+ if (cli.config.init) return init.configFile()
25
+ else if (cli.config.help) return log.help()
26
+ else if (cli.config.version) return log.version()
27
+ else if (cli.config.stats) return log.stats()
28
+
29
+ // Init I/O args
30
+ const [inputArg = '', outputArg = ''] = // default to empty strings for error-less handling
31
+ env.args // exclude executable and script paths
32
+ .filter(arg => !arg.startsWith('-')) // exclude flags
33
+ .map(arg => arg.replace(/^\/*/, '')) // clean leading slashes to avoid parsing system root
34
+
35
+ // Validate input arg (output arg can be anything)
36
+ let inputPath = path.resolve(process.cwd(), inputArg)
37
+ if (inputArg && !fs.existsSync(inputPath)) {
38
+ const scssInputPath = inputPath + '.scss' // append '.scss' in case ommitted from intended filename
39
+ if (!fs.existsSync(scssInputPath)) {
40
+ log.error(`${cli.msgs.error_firstArgNotExist}.\n${inputPath} ${cli.msgs.error_doesNotExist}.`)
41
+ log.success(`${cli.msgs.info_exampleValidCmd}: \n» scss-to-css . output.min.css`)
42
+ log.helpDocsCmdsDocsURL()
43
+ process.exit(1)
44
+ } else inputPath = scssInputPath
45
+ }
46
+
47
+ // Find all eligible source files or arg-passed file
48
+ log.break()
49
+ const srcFiles = /s[ac]ss$/.test(inputPath) && !fs.statSync(inputPath).isDirectory() ? [inputPath]
50
+ : findSCSS(inputPath, {
51
+ recursive: !cli.config.noRecursion,
52
+ verbose: !cli.config.quietMode,
53
+ ignores: cli.config.ignores
54
+ })
55
+
56
+ // Print/compile files
57
+ if (env.modes.debug || cli.config.dryRun) {
58
+ if (srcFiles.length) {
59
+ log.info(`${cli.msgs.info_scssFilesToBeCompiled}:`)
60
+ srcFiles.forEach(file => log.dim(file))
61
+ } else // no files found
62
+ log.info(`${cli.msgs.info_noSCSSfilesWillBeCompiled}.`)
63
+ } else
64
+ compile.scss({ srcFiles, inputPath, inputArg, outputArg })
65
+
66
+ })()
@@ -0,0 +1,31 @@
1
+ const color = module.exports = {
2
+ nc: '\x1b[0m',
3
+ hex: {
4
+ br: '#ff0000', by: '#ffff00', bo: '#ffa500', bg: '#00ff00',
5
+ bw: '#ffffff', gry: '#808080', blk: '#000000', tlBG: '#008080'
6
+ },
7
+ schemes: {
8
+ get default() {
9
+ return [
10
+ '#00e5bc', '#18c8ae', '#30ac9f', '#488f91', '#607383',
11
+ '#775674', '#8f3966', '#a71d57', '#bf0049', '#9a1b5e'
12
+ ].map(color.hexToANSI)
13
+ },
14
+ get rainbow() {
15
+ return [
16
+ '#e41a1c', '#ff7f00', '#ffff33', '#4daf4a', '#377eb8',
17
+ '#984ea3', '#f781bf', '#999999', '#a65628', '#d95f02'
18
+ ].map(color.hexToANSI)
19
+ }
20
+ },
21
+
22
+ hexToANSI(hex) {
23
+ const r = parseInt(hex.slice(1,3), 16),
24
+ g = parseInt(hex.slice(3,5), 16),
25
+ b = parseInt(hex.slice(5,7), 16)
26
+ return `\x1b[38;2;${r};${g};${b}m`
27
+ }
28
+ }
29
+
30
+ for (const hexKey of Object.keys(color.hex)) // add color[hexKey] getters that return ANSI
31
+ Object.defineProperty(color, hexKey, { get: () => color.hexToANSI(color.hex[hexKey]) })
@@ -0,0 +1,91 @@
1
+ module.exports = {
2
+ scss({ srcFiles, inputPath, inputArg, outputArg }) {
3
+ const { compile } = require('../../scss-to-css'),
4
+ fs = require('fs'),
5
+ path = require('path')
6
+
7
+ // Build array of compilation data
8
+ const failedPaths = [] ; let compileData = []
9
+ if (!cli.config.relativeOutput && fs.statSync(inputPath).isDirectory()) {
10
+ const compileResult = compile(inputPath, {
11
+ verbose: false,
12
+ minify: !cli.config.noMinify,
13
+ comment: cli.config.comment,
14
+ relativeOutput: false,
15
+ recursive: !cli.config.noRecursion,
16
+ dotFolders: cli.config.includeDotFolders,
17
+ sourceMaps: !cli.config.noSourceMaps,
18
+ ignores: cli.config.ignores
19
+ })
20
+ if (compileResult) {
21
+ if (compileResult.error) failedPaths.push(inputPath)
22
+ else compileData = [].concat(compileResult)
23
+ }
24
+ } else compileData = srcFiles.map(scssPath => {
25
+ const compileResult = compile(scssPath, {
26
+ verbose: !cli.config.quietMode,
27
+ minify: !cli.config.noMinify,
28
+ sourceMaps: !cli.config.noSourceMaps,
29
+ comment: cli.config.comment
30
+ })
31
+ if (compileResult.error) failedPaths.push(scssPath)
32
+ return compileResult
33
+ }).filter(compileResult => !compileResult.error)
34
+
35
+ // Print compilation summary
36
+ if (!cli.config.quietMode) {
37
+ const compiledCnt = compileData.length,
38
+ cssCntSuffix = compiledCnt == 1 ? '' : 's'
39
+ if (compiledCnt) {
40
+ log.success(`${cli.msgs.info_compilationComplete}!`)
41
+ log.data(`${compiledCnt} CSS ${cli.msgs.info_file}${cssCntSuffix}${
42
+ !cli.config.noSourceMaps ? ` + ${compiledCnt} ${cli.msgs.info_srcMap}${cssCntSuffix}`
43
+ : '' } ${cli.msgs.info_generated}.`
44
+ )
45
+ } else
46
+ console.info(`${cli.msgs.info_noSCSSfilesProcessed}.`)
47
+ if (failedPaths.length) {
48
+ log.error(`${failedPaths.length} ${cli.msgs.info_file}${ failedPaths.length == 1 ? '' : 's' }`,
49
+ `${cli.msgs.info_failedToCompile}:`)
50
+ failedPaths.forEach(path => log.ifNotQuiet(path))
51
+ }
52
+ }
53
+ if (!compileData?.length) return
54
+
55
+ // Copy single result code to clipboard if --copy passed
56
+ if (cli.config.copy && compileData?.length == 1) {
57
+ log.data(compileData[0].code)
58
+ log.ifNotQuiet(`\n${cli.msgs.info_copyingToClip}...`)
59
+ require('node-clipboardy').writeSync(compileData[0].code)
60
+
61
+ } else { // write array data to files
62
+ log.ifNotQuiet(`${cli.msgs.info_writing}${ compileData?.length > 1 ? 's' : '' }...`)
63
+ compileData?.forEach(({ code, srcMap, srcPath, relPath }) => {
64
+ let outputDir, outputFilename
65
+ if (!cli.config.relativeOutput && relPath) { // preserve folder structure
66
+ const outputPath = path.resolve(process.cwd(), outputArg || 'css'),
67
+ relativeDir = path.dirname(relPath)
68
+ outputDir = relativeDir != '.' ? path.join(outputPath, relativeDir) : outputPath
69
+ outputFilename =
70
+ `${path.basename(srcPath, path.extname(srcPath))}${ cli.config.noMinify ? '' : '.min' }.css`
71
+ } else {
72
+ outputDir = path.join(
73
+ path.dirname(srcPath), // path of file to be minified
74
+ outputArg.endsWith('.css') ? path.dirname(outputArg) // or path from file output arg
75
+ : outputArg || 'css' // or path from folder outputArg or css/ if no outputArg passed
76
+ )
77
+ outputFilename = `${
78
+ outputArg.endsWith('.css') && /s[ac]ss$/.test(inputArg)
79
+ ? path.basename(outputArg).replace(/(\.min)?\.css$/, '')
80
+ : path.basename(srcPath, path.extname(srcPath))
81
+ }.min.css`
82
+ } const outputPath = path.join(outputDir, outputFilename)
83
+ fs.mkdirSync(outputDir, { recursive: true })
84
+ fs.writeFileSync(outputPath, code, 'utf8')
85
+ log.ifNotQuiet(` ${log.colors.bg}✓${log.colors.nc} ${path.relative(process.cwd(), outputPath)}`)
86
+ if (!cli.config.noSourceMaps) fs.writeFileSync(`${outputPath}.map`, JSON.stringify(srcMap), 'utf8')
87
+ log.ifNotQuiet(` ${log.colors.bg}✓${log.colors.nc} ${path.relative(process.cwd(), outputPath)}.map`)
88
+ })
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,30 @@
1
+ module.exports = {
2
+
3
+ atomicWrite(filePath, data, encoding = 'utf8') { // to prevent TOCTOU
4
+ const fs = require('fs'),
5
+ path = require('path'),
6
+ tmpPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp`)
7
+ fs.writeFileSync(tmpPath, data, encoding) ; fs.renameSync(tmpPath, filePath)
8
+ },
9
+
10
+ fetch(url) { // to support Node.js < v21
11
+ return typeof fetch == 'undefined' ? new Promise((resolve, reject) => { // using https?.get()
12
+ const protocol = url.match(/^([^:]+):\/\//)[1]
13
+ if (!/^https?$/.test(protocol))
14
+ reject(new Error(`${cli.msgs.error_invalidURL}.`))
15
+ require(protocol).get(url, resp => {
16
+ let rawData = ''
17
+ resp.on('data', chunk => rawData += chunk)
18
+ resp.on('end', () => resolve({ json: () => JSON.parse(rawData), text: () => rawData }))
19
+ }).on('error', reject)
20
+ }) : fetch(url) // using Node.js fetch()
21
+ },
22
+
23
+ flatten(json, { key = 'message' } = {}) { // eliminate need to ref nested keys
24
+ const flatObj = {}
25
+ for (const jsonKey in json) flatObj[jsonKey] =
26
+ typeof json[jsonKey] == 'object' && key in json[jsonKey] ? json[jsonKey][key]
27
+ : json[jsonKey]
28
+ return flatObj
29
+ }
30
+ }
@@ -0,0 +1,49 @@
1
+ const language = require('./language'),
2
+ settings = require('./settings')
3
+
4
+ const dataPath = `../../${ env.modes.dev ? '../' : 'data/' }`
5
+
6
+ module.exports = {
7
+
8
+ async cli() {
9
+ Object.assign(globalThis.cli ??= {}, require(`${dataPath}package-data.json`))
10
+ cli.msgs = await language.getMsgs('en')
11
+ cli.msgs = await language.getMsgs(cli.lang = settings.load('uiLang') || (
12
+ env.modes.debug ? language.generateRandomLang({ excludes: ['en'] }) : language.getSysLang() ))
13
+ cli.urls.cliDocs = `${cli.urls.docs}/#-command-line-usage`
14
+ if (!cli.lang.startsWith('en')) { // localize cli.urls.cliDocs
15
+ cli.docLocale = cli.lang.replace('_', '-').toLowerCase()
16
+ cli.docLocales ??= await language.getDocLocales()
17
+ if (cli.docLocales?.includes(cli.docLocale))
18
+ log.debug(cli.urls.cliDocs = `${cli.urls.docs}/${cli.docLocale}#readme`)
19
+ }
20
+ settings.load() // all keys to cli.config
21
+ },
22
+
23
+ async configFile(filename = settings.configFilename) {
24
+ const fs = require('fs'),
25
+ path = require('path'),
26
+ paths = { target: path.resolve(process.cwd(), filename) }
27
+
28
+ if (fs.existsSync(paths.target)) // use existing config file
29
+ return log.warn(`${cli.msgs.warn_configFileExists}:`, paths.target)
30
+ if (fs.existsSync(paths.src = path.resolve(__dirname, `${dataPath}${filename}`)))
31
+ fs.copyFileSync(paths.src, paths.target) // use found template
32
+
33
+ else { // use jsDelivr copy
34
+ const jsdURL = `${require('./jsdelivr').pkgVerURL()}/${filename}/`
35
+ log.data(`${cli.msgs.info_fetchingRemoteConfigFrom} ${jsdURL}...`)
36
+ try {
37
+ const data = require('./data'),
38
+ resp = await data.fetch(jsdURL)
39
+ if (resp.ok) data.atomicWrite(paths.target, await resp.text())
40
+ else return log.warn(`${cli.msgs.warn_remoteConfigNotFound}: ${jsdURL} (${resp.status})`)
41
+ } catch (err) {
42
+ return log.warn(`${cli.msgs.warn_remoteConfigFailed}: ${jsdURL} ${err.message}`) }
43
+ }
44
+
45
+ log.success(`${cli.msgs.info_configFileCreated}: ${paths.target}\n`)
46
+ log.tip(`${cli.msgs.tip_editToSetDefaults}.`)
47
+ log.tip(`${cli.msgs.tip_cliArgsPrioritized}.`)
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+
3
+ pkgVerURL(version) {
4
+ version ||= cli.version ||= require('./pkg').getVer('local') || 'none'
5
+ const verTag = !/^\d+\.\d+\.\d+$/.test(version) ? 'latest' : `v${version}`
6
+ return `${cli.urls.jsdelivr}@${verTag}`
7
+ },
8
+
9
+ commitURL(hash = 'latest') { return `${cli.urls.jsdelivr}@${hash}` }
10
+ }
@@ -0,0 +1,106 @@
1
+ const data = require('./data')
2
+
3
+ module.exports = {
4
+
5
+ formatCode(langCode) { // to match locale dir name
6
+ return langCode.replace(
7
+ /([a-z]{2,8})[-_]([a-z]{2})/i, (_, lang, region) =>`${lang.toLowerCase()}_${region.toUpperCase()}`) },
8
+
9
+ generateRandomLang({ includes = [], excludes = [] } = {}) {
10
+ const fs = require('fs'),
11
+ path = require('path')
12
+
13
+ let locales = includes.length ? includes : (() => {
14
+
15
+ // Read cache if found
16
+ const cacheDir = path.join(__dirname, '..', '.cache'),
17
+ localeCache = path.join(cacheDir, 'locales.json')
18
+ if (fs.existsSync(localeCache))
19
+ try { return JSON.parse(fs.readFileSync(localeCache, 'utf8')) } catch (err) {}
20
+
21
+ // Discover pkg _locales
22
+ const localesDir = path.resolve(process.cwd(), '_locales')
23
+ if (!fs.existsSync(localesDir)) return ['en']
24
+ const locales = fs.readdirSync(localesDir, { withFileTypes: true })
25
+ .filter(entry => entry.isDirectory()).map(entry => entry.name)
26
+ .filter(name => /^\w{2}[-_]?\w{0,2}$/.test(name))
27
+
28
+ // Cache result
29
+ fs.mkdirSync(cacheDir, { recursive: true })
30
+ data.atomicWrite(localeCache, JSON.stringify(locales, null, 2))
31
+
32
+ return locales
33
+ })()
34
+
35
+ // Filter out excludes
36
+ const excludeSet = new Set(excludes)
37
+ locales = locales.filter(locale => !excludeSet.has(locale))
38
+
39
+ // Get random language
40
+ let randomLang = 'en'
41
+ if (locales.length)
42
+ randomLang = locales[Math.floor(Math.random() * locales.length)]
43
+ log.debug(`Random language: ${randomLang}`)
44
+
45
+ return randomLang
46
+ },
47
+
48
+ async getDocLocales() {
49
+ cli.version ||= require('./pkg').getVer('local') || 'none'
50
+ const jsdURL = `${require('./jsdelivr').pkgVerURL()}/docs/`,
51
+ locales = []
52
+ try {
53
+ const respText = await (await data.fetch(jsdURL)).text(),
54
+ reLocale = /href=".*\/docs\/([^/]+)\/"/g
55
+ let match ; while ((match = reLocale.exec(respText))) locales.push(match[1]) // store locale dir names
56
+ } catch (err) {
57
+ log.warn(`${cli.msgs.warn_docLocalesFetchFailed}:`, err.message)
58
+ }
59
+ return locales
60
+ },
61
+
62
+ async getMsgs(langCode = 'en') {
63
+ langCode = module.exports.formatCode(langCode)
64
+ if (env.msgs && langCode == cli.lang) return env.msgs // don't re-fetch same msgs
65
+
66
+ let msgs = data.flatten( // local ones
67
+ require(`../../${ env.modes.dev ? '../_locales/en/' : 'data/' }messages.json`))
68
+
69
+ if (!langCode.startsWith('en')) { // fetch non-English msgs from jsDelivr
70
+ const msgHostURL = `${require('./jsdelivr').commitURL(cli.commitHashes.locales)}/_locales/`
71
+ let msgHref = `${msgHostURL}${langCode}/messages.json`, msgFetchesTried = 0
72
+ while (msgFetchesTried < 3)
73
+ try { // fetch remote msgs
74
+ msgs = data.flatten(await (await data.fetch(msgHref)).json())
75
+ break
76
+ } catch (err) { // retry up to 2X (region-stripped + EN)
77
+ msgFetchesTried++ ; if (msgFetchesTried >= 3) break
78
+ log.debug(msgHref = langCode.includes('-') && msgFetchesTried == 1 ?
79
+ msgHref.replace(/([^_]*)_[^/]*(\/.*)/, '$1$2') // strip region before retrying
80
+ : `${msgHostURL}en/messages.json` // else use EN msgs
81
+ )
82
+ }
83
+ }
84
+
85
+ return msgs
86
+ },
87
+
88
+ getSysLang() {
89
+ try {
90
+ if (process.platform == 'win32')
91
+ return require('child_process').execSync(
92
+ '(Get-Culture).TwoLetterISOLanguageName', { shell: 'powershell', encoding: 'utf-8' }
93
+ ).trim()
94
+ else { // macOS/Linux
95
+ const pe = process.env
96
+ return (pe.LANG || pe.LANGUAGE || pe.LC_ALL || pe.LC_MESSAGES || pe.LC_NAME)
97
+ .split('.')[0] // strip encoding e.g. .UTF-8
98
+ }
99
+ } catch (err) {
100
+ log.error(`${cli.msgs.error_failedToFetchSysLang}:`, err.message)
101
+ return 'en'
102
+ }
103
+ },
104
+
105
+ validateLangCode(code) { return typeof code != 'string' ? false : /^[a-z]{2,8}(?:[-_][a-z]{2,3})?$/i.test(code) }
106
+ }
@@ -0,0 +1,174 @@
1
+ const colors = require('./color'),
2
+ { getDownloads, getVer } = require('./pkg'),
3
+ string = require('./string')
4
+
5
+ const nextMajVer = require('../../../package.json').version.replace(/^(\d+)\..*/, (_, major) => `${ +major +1 }.0.0`)
6
+
7
+ module.exports = {
8
+ colors,
9
+
10
+ configURL() { this.info(`${cli.msgs.info_exampleValidConfigFile}: ${cli.urls.config}`) },
11
+ configURLandExit(...args) { this.error(...args); this.configURL(); process.exit(1) },
12
+ data(msg) { console.log(`\n${colors.bw}${msg}${colors.nc}`) },
13
+ dim(msg) { console.log(`${colors.gry}${msg}${colors.nc}`) },
14
+ error(...args) { console.error(`\n${colors.br}ERROR:`, ...args, colors.nc) },
15
+ errorAndExit(...args) { this.error(...args); this.helpDocsCmdsDocsURL(); process.exit(1) },
16
+ ifNotQuiet(msg) { if (!cli.config.quietMode) this.info(msg) },
17
+ info(msg) { console.info(`\n${colors.schemes.default[0]}${msg}${colors.nc}`) },
18
+ break() { console.log() },
19
+ tip(msg) { console.info(`${colors.by}TIP: ${msg}${colors.nc}`) },
20
+ success(msg) { console.log(`\n${colors.bg}${msg}${colors.nc}`) },
21
+ warn(...args) { console.warn(`\n${colors.bo}WARNING:`, ...args, colors.nc) },
22
+
23
+ argDoesNothing(arg) {
24
+ this.warn(`${cli.msgs.warn_option} ${arg} ${cli.msgs.warn_noLongerHasAnyEffect} ${
25
+ cli.msgs.warn_andWillBeRemoved} @ v${nextMajVer}`)
26
+ },
27
+
28
+ configKeyReplacedBy(oldKey, newKey, oldVal) {
29
+ if (!this[`${oldKey}Warned`]) {
30
+ this.warn(
31
+ `${cli.msgs.info_configFile} ${cli.msgs.warn_option.toLowerCase()} '${oldKey}: ${oldVal}' ${
32
+ cli.msgs.warn_hasBeenReplacedBy} '${
33
+ newKey}: ${ isNegKey(oldKey) != isNegKey(newKey) ? !oldVal : oldVal }' ${
34
+ cli.msgs.warn_andWillBeRemoved} @ v${nextMajVer}`
35
+ )
36
+ this[`${oldKey}Warned`] = true
37
+ function isNegKey(key) { return /^(?:no|disable|exclude)[A-Z]/.test(key) }
38
+ }
39
+ },
40
+
41
+ debug(msg) {
42
+ if (!env.modes.debug) return
43
+ this.argIdx ??= env.args.findIndex(arg => /^--?(?:V|debug(?:[-_]?mode)?)$/.test(arg))
44
+ if (this.argIdx +1 < env.args.length && !env.args[this.argIdx +1].startsWith('-')) // use --debug [targetKey]
45
+ this.key ??= env.args[this.argIdx +1].replace('-', '_')
46
+ if (this.key)
47
+ this.val = cli.config[this.key] || `cli.config key "${this.key}" ${cli.msgs.warn_notFound.toLowerCase()}`
48
+ else
49
+ this.val = cli.config
50
+ msg += `\n${colors.gry}${JSON.stringify(this.val)}${colors.nc}`
51
+ console.debug(`\n${colors.by}DEBUG:`, msg, colors.nc)
52
+ },
53
+
54
+ help(includeSections = ['header', 'usage', 'pathArgs', 'flags', 'params', 'cmds']) {
55
+ cli.prefix = `${this.colors.tlBG}${this.colors.blk} ${cli.name.replace(/^@[^/]+\//, '')} ${this.colors.nc} `
56
+ const helpSections = {
57
+ header: [
58
+ `\n├ ${cli.prefix}${cli.msgs.pkg_copyright}.`,
59
+ `${cli.prefix}${cli.msgs.prefix_source}: ${cli.urls.src}`
60
+ ],
61
+ usage: [
62
+ `\n${this.colors.bw}o ${cli.msgs.helpSection_usage}:${this.colors.nc}`,
63
+ ` ${this.colors.bw}» ${this.colors.bg}${cli.cmdFormat}${this.colors.nc}`
64
+ ],
65
+ pathArgs: [
66
+ `\n${this.colors.bw}o ${cli.msgs.helpSection_pathArgs}:${this.colors.nc}`,
67
+ ` [inputPath] ${cli.msgs.inputPathDesc_main}, ${
68
+ cli.msgs.inputPathDesc_extra}.`,
69
+ ` [outputPath] ${cli.msgs.outputPathDesc_main}, ${
70
+ cli.msgs.outputPathDesc_extra}`
71
+ ],
72
+ flags: [
73
+ `\n${this.colors.bw}o ${cli.msgs.helpSection_flags}:${this.colors.nc}`,
74
+ ` -n, --dry-run ${cli.msgs.optionDesc_dryRun}.`,
75
+ ` -d, --include-dotfolders ${cli.msgs.optionDesc_dotfolders}.`,
76
+ ` -S, --no-source-maps ${cli.msgs.optionDesc_noSourceMaps}.`,
77
+ ` -M, --no-minify ${cli.msgs.optionDesc_noMinify}.`,
78
+ ` -R, --no-recursion ${cli.msgs.optionDesc_noRecursion}.`,
79
+ ` -r, --relative-output ${cli.msgs.optionDesc_relativeOutput}.`,
80
+ ` -c, --copy ${cli.msgs.optionDesc_copy}.`,
81
+ ` -q, --quiet ${cli.msgs.optionDesc_quiet}.`
82
+ ],
83
+ params: [
84
+ `\n${this.colors.bw}o ${cli.msgs.helpSection_params}:${this.colors.nc}`,
85
+ `--ignores="dir/,file1.scss,file2.sass" ${cli.msgs.optionDesc_ignores}.`,
86
+ `--comment="comment" ${cli.msgs.optionDesc_commentMain}. ${
87
+ cli.msgs.optionDesc_commentExtra}.`,
88
+ ` --ui-lang="code" ${cli.msgs.optionDesc_uiLang}.`,
89
+ ` --config="path/to/file" ${cli.msgs.optionDesc_config}.`
90
+ ],
91
+ cmds: [
92
+ `\n${this.colors.bw}o ${cli.msgs.helpSection_cmds}:${this.colors.nc}`,
93
+ ` -i, --init ${cli.msgs.optionDesc_init}.`,
94
+ ` -h, --help ${cli.msgs.optionDesc_help}.`,
95
+ ` -v, --version ${cli.msgs.optionDesc_version}.`,
96
+ ` -v, --stats ${cli.msgs.optionDesc_stats}.`,
97
+ ` -V, --debug ${cli.msgs.optionDesc_debug}.`
98
+ ]
99
+ }
100
+ includeSections.forEach(section => // print valid arg elems
101
+ helpSections[section]?.forEach(line => printHelpMsg(line, /header|usage/.test(section) ? 1 : 41)))
102
+ console.info(`\n${cli.msgs.info_moreHelp}, ${
103
+ cli.msgs.info_visit}: ${this.colors.bw}${cli.urls.cliDocs}${this.colors.nc}`)
104
+
105
+ function printHelpMsg(msg, indent) { // wrap msg + indent 2nd+ lines
106
+ const terminalWidth = process.stdout.columns || 80,
107
+ words = msg.match(/\S+|\s+/g),
108
+ lines = [], prefix = '| '
109
+
110
+ // Split msg into lines of appropriate lengths
111
+ let currentLine = ''
112
+ words.forEach(word => {
113
+ const lineLength = terminalWidth -( !lines.length ? 0 : indent )
114
+ if (currentLine.length + prefix.length + word.length > lineLength) { // cap/store it
115
+ lines.push(!lines.length ? currentLine : currentLine.trimStart())
116
+ currentLine = ''
117
+ }
118
+ currentLine += word
119
+ })
120
+ lines.push(!lines.length ? currentLine : currentLine.trimStart())
121
+
122
+ // Print formatted msg
123
+ lines.forEach((line, idx) => console.info(prefix +(
124
+ idx == 0 ? line // print 1st line unindented
125
+ : ' '.repeat(indent) + line // print subsequent lines indented
126
+ )))
127
+ }
128
+ },
129
+
130
+ helpDocsCmdsDocsURL() {
131
+ console.info(`\n${
132
+ cli.msgs.info_moreHelp}, ${cli.msgs.info_type} ${
133
+ colors.bw}${cli.name.split('/')[1]} --<docs|help>${colors.nc} ${
134
+ cli.msgs.info_or} ${cli.msgs.info_visit}\n${colors.by}${cli.urls.docs}${colors.nc}`
135
+ )
136
+ },
137
+
138
+ initCmd(invalidKey) {
139
+ if (invalidKey)
140
+ this.warn(
141
+ `${cli.msgs.error_invalidKey} '${invalidKey}' ${cli.msgs.error_foundIn}\n`
142
+ + `${log.colors.gry}${cli.configPath}`
143
+ )
144
+ if (!this.initTipped) {
145
+ this.break()
146
+ this.tip(`${
147
+ string.toTitleCase(cli.msgs.info_type)} '${cli.name} init' ${
148
+ cli.msgs.info_toCreateDefaultConfig}`
149
+ )
150
+ this.initTipped = true
151
+ }
152
+ },
153
+
154
+ invalidConfigKey(key) { if (!this[`${key}Tipped`]) { this.initCmd(key) ; this[`${key}Tipped`] = true } },
155
+
156
+ async stats(pkgName = cli.name, options = { ecosystem: 'npm', maxDays: 8, maxVers: 5, scheme: 'default' }) {
157
+ const pkgStats = await getDownloads(pkgName, options),
158
+ schemeData = colors.schemes[options.scheme]
159
+ if (!schemeData) return this.error(`Scheme '${options.scheme}' not found!`)
160
+ const colorMap = Object.fromEntries(schemeData.map((hex, idx) => [`c${idx}`, hex])),
161
+ statsTable = new (require('console-table-printer').Table)({ colorMap })
162
+ pkgStats.forEach((row, idx) => // build colored rows
163
+ statsTable.addRow(row, { color: `c${Math.floor(idx / pkgStats.length * schemeData.length)}` }))
164
+ statsTable.printTable()
165
+ },
166
+
167
+ version() {
168
+ this.info(cli.name)
169
+ this.data(`${
170
+ cli.msgs.prefix_globalVer}: ${ getVer('global') || 'none' }\n${
171
+ cli.msgs.prefix_localVer }: ${ getVer('local') || 'none' }`
172
+ )
173
+ }
174
+ }
@@ -0,0 +1,78 @@
1
+ const data = require('./data')
2
+
3
+ const endpoints = {
4
+ npmjsDLs: 'https://api.npmjs.org/downloads',
5
+ pepyProjects: 'https://pepy.tech/projects'
6
+ }
7
+
8
+ module.exports = {
9
+
10
+ async getDownloads(
11
+ pkgName, // e.g. some-npm-pkg, npm:@adamlui/minify.js, pypi:translate-messages
12
+ { ecosystem = 'npm', maxDays = 10, maxVers = 10 } = {}
13
+ ) {
14
+ if (pkgName.includes(':')) [ecosystem, pkgName] = pkgName.split(':')
15
+
16
+ if (/npm|node/i.test(ecosystem)) { // fetch from endpoints.npmjsDLs
17
+ function formatDate(date) { return date.toISOString().split('T')[0] }
18
+ const dates = { end: new Date(), start: new Date() }
19
+ dates.start.setMonth(dates.end.getMonth() -3)
20
+ const npmjsURL = `${endpoints.npmjsDLs}/range/${formatDate(dates.start)}:${formatDate(dates.end)}/${pkgName}`
21
+ log.info(`Fetching npm stats for ${pkgName}${
22
+ env.modes.debug ? ` from\n${log.colors.bw}${npmjsURL}` : '' }...\n`)
23
+ return (await (await data.fetch(npmjsURL)).json()).downloads // { downloads: [{ day, downloads }] }
24
+ .sort((a, b) => new Date(b.day) - new Date(a.day)) // new ⇅ old
25
+ .slice(0, maxDays) // cap rows
26
+ .map(({ day: date, downloads }) => ({ date, downloads }))
27
+
28
+ } else if (/^py/i.test(ecosystem)) { // fetch from endpoints.pepyProjects
29
+ let rows = []
30
+ const pepyURL = `${endpoints.pepyProjects}/${pkgName}`
31
+ log.info(`Fetching PyPI/mirror stats for ${pkgName}${
32
+ env.modes.debug ? ` from\n${log.colors.bw}${pepyURL}` : '' }...\n`)
33
+ const respText = await (await data.fetch(pepyURL)).text(),
34
+ rePush = /self\.__next_f\.push\(\[\d+,\s*"((?:\\.|[^"\\])*)"\]\)/g
35
+ let downloads = {}, match
36
+ while ((match = rePush.exec(respText))) {
37
+ try { // extract project.downloads
38
+ const inner = JSON.parse(`"${match[1]}"`),
39
+ data = JSON.parse(inner.substring(inner.indexOf(':') +1))
40
+ if (data[3]?.project) downloads = data[3].project.downloads
41
+ } catch (err) {}
42
+ }
43
+ rows = Object.entries(downloads)
44
+ .sort(([a], [b]) => new Date(b) - new Date(a)) // new ⇅ old
45
+ .slice(0, maxDays) // cap rows
46
+ .map(([date, data]) => ({ date, ...data }))
47
+ const activeVers = new Set()
48
+ rows.forEach(row => Object.keys(row).forEach(key => {
49
+ if (key != 'date' && row[key] > 0) activeVers.add(key) }))
50
+ const finalVers = [...activeVers]
51
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) // new ⇆ old
52
+ .slice(0, maxVers) // cap columns
53
+ return rows.map(row => {
54
+ const result = { date: row.date }
55
+ finalVers.forEach(ver => result[ver] = row[ver] || 0)
56
+ return result
57
+ })
58
+
59
+ } else return log.debug(`${ecosystem} not supported.`)
60
+ },
61
+
62
+ getVer(type = 'any') { // or <'global'|'local'>
63
+ let pkgVer
64
+ if (type != 'global')
65
+ try { // get local ver
66
+ const localManifestPath = require('path').resolve(
67
+ process.cwd(), 'node_modules', cli.name, 'package.json')
68
+ pkgVer = require(localManifestPath).version
69
+ } catch (err) { log.debug(`${cli.msgs.error_readingLocalPkgVer}: ${err.message}`) }
70
+ if (type == 'global' || type == 'all' && !pkgVer)
71
+ try { // get global ver
72
+ pkgVer = require('child_process').execSync(
73
+ `npm view ${JSON.stringify(cli.name)} version`
74
+ ).toString().trim()
75
+ } catch (err) { log.debug(`${cli.msgs.error_failedToFetchGlobalVer}: ${err.message}`) }
76
+ return pkgVer
77
+ }
78
+ }