@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 +4 -2
- package/dist/cli/index.js +66 -0
- package/dist/cli/lib/color.js +31 -0
- package/dist/cli/lib/compile.js +91 -0
- package/dist/cli/lib/data.js +30 -0
- package/dist/cli/lib/init.js +49 -0
- package/dist/cli/lib/jsdelivr.js +10 -0
- package/dist/cli/lib/language.js +106 -0
- package/dist/cli/lib/log.js +174 -0
- package/dist/cli/lib/pkg.js +78 -0
- package/dist/cli/lib/settings.js +158 -0
- package/dist/cli/lib/string.js +3 -0
- package/dist/data/messages.json +14 -4
- package/dist/data/package-data.json +1 -1
- package/dist/scss-to-css.js +235 -0
- package/docs/README.md +4 -2
- package/package.json +7 -4
- package/dist/cli/index.min.js +0 -9
- package/dist/cli/lib/compile.min.js +0 -8
- package/dist/cli/lib/data.min.js +0 -6
- package/dist/cli/lib/init.min.js +0 -6
- package/dist/cli/lib/jsdelivr.min.js +0 -6
- package/dist/cli/lib/language.min.js +0 -7
- package/dist/cli/lib/log.min.js +0 -23
- package/dist/cli/lib/pkg.min.js +0 -6
- package/dist/cli/lib/settings.min.js +0 -7
- package/dist/scss-to-css.min.js +0 -9
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.
|
|
37
|
-
<img height=31 src="https://img.shields.io/badge/Latest_Build-2.
|
|
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
|
+
}
|