@spark-ui/cli-utils 2.14.0 → 2.15.1
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/CHANGELOG.md +26 -0
- package/bin/spark-generate.mjs +1 -1
- package/bin/spark-scan.mjs +9 -3
- package/package.json +2 -2
- package/src/core/Logger.mjs +39 -9
- package/src/scan/config.cjs +37 -0
- package/src/scan/index.mjs +40 -40
- package/src/scan/index.test.ts +157 -0
- package/src/scan/loadConfig.mjs +41 -0
- package/src/scan/utils/index.mjs +0 -1
- package/test/spark-generate.test.ts +24 -31
- package/test/stubs/scanConfigWithCustomExtensions.js +10 -0
- package/test/stubs/scanConfigWithCustomImports.js +10 -0
- package/test/stubs/scanConfigWithCustomSorting.js +10 -0
- package/test/stubs/scanConfigWithDetailedOutput.js +10 -0
- package/test/utils/cmd.js +37 -17
- package/src/scan/utils/logger.mjs +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.15.1](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.15.0...@spark-ui/cli-utils@2.15.1) (2024-07-03)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @spark-ui/cli-utils
|
|
9
|
+
|
|
10
|
+
# [2.15.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.14.0...@spark-ui/cli-utils@2.15.0) (2024-07-03)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **cli-utils:** add some feedback when not loading a given config file ([1231fe6](https://github.com/adevinta/spark/commit/1231fe6cb94c15643a11278c3210836e81059364))
|
|
15
|
+
- **cli-utils:** fix config merge method to allow proper option override ([8b6a124](https://github.com/adevinta/spark/commit/8b6a124c6e70f05769305bf6ca84137c7a05510c))
|
|
16
|
+
- **cli-utils:** fix logger issue on generate script after changes ([083c9b0](https://github.com/adevinta/spark/commit/083c9b043d1837a807b159a8143fd16bf8d7087e))
|
|
17
|
+
- **cli-utils:** merge values ([a7e8129](https://github.com/adevinta/spark/commit/a7e81292d452d4ee026f3d4705198ac4ecc6363e))
|
|
18
|
+
- **cli-utils:** merging configs ([aeb6251](https://github.com/adevinta/spark/commit/aeb6251d23bc506674e4545ebad928081292f520))
|
|
19
|
+
- **cli-utils:** missalignment on the default config values nesting ([79729f7](https://github.com/adevinta/spark/commit/79729f722f4ecf6f0c671e2c1c1d5929a87d98c9))
|
|
20
|
+
- **cli-utils:** output file creates new content ([973ffa8](https://github.com/adevinta/spark/commit/973ffa85e6987dbd87a119e9253db57556a705d4))
|
|
21
|
+
- **cli-utils:** remove betta tagging ([96cafae](https://github.com/adevinta/spark/commit/96cafae45e4a4759ef9ff4a763963f26cb898cb6))
|
|
22
|
+
- **cli-utils:** remove extra logs ([f588649](https://github.com/adevinta/spark/commit/f588649eb299fc42ef9d6dbf8f8f4eebd0cfd6a9))
|
|
23
|
+
- **cli-utils:** remove unnecesary log ([614d834](https://github.com/adevinta/spark/commit/614d834d5d7a0f3084b3097e2dee5436b369fbb6))
|
|
24
|
+
- log config setted ([190a058](https://github.com/adevinta/spark/commit/190a0581ab6219591c5d3a3ecde2ee46d504d7ca))
|
|
25
|
+
- priority of commands ([ed178a5](https://github.com/adevinta/spark/commit/ed178a51a8cea0255c96467b0d8760daf8f851dd))
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- **cli-utils:** exoposes all configuration parameters as a build argument ([3e88a99](https://github.com/adevinta/spark/commit/3e88a998c05da026cd196b3dd54a2ad50506266c))
|
|
30
|
+
- mixing parameters ([da1bf1d](https://github.com/adevinta/spark/commit/da1bf1d838df2c577755eb3c651cd0bba8d07203))
|
|
31
|
+
|
|
6
32
|
# [2.14.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.13.8...@spark-ui/cli-utils@2.14.0) (2024-06-14)
|
|
7
33
|
|
|
8
34
|
### Features
|
package/bin/spark-generate.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { TemplateGenerator } from '../src/generate/generators/index.mjs'
|
|
|
6
6
|
import { Logger, System } from '../src/core/index.mjs'
|
|
7
7
|
import { DescriptionValidator, NameValidator } from '../src/generate/validators/index.mjs'
|
|
8
8
|
|
|
9
|
-
const logger = new Logger()
|
|
9
|
+
const logger = new Logger({ verbose: true })
|
|
10
10
|
const system = new System({ logger })
|
|
11
11
|
const generator = new TemplateGenerator({ system })
|
|
12
12
|
|
package/bin/spark-scan.mjs
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander'
|
|
4
|
-
import { adoption } from '../src/scan/index.mjs'
|
|
4
|
+
import { adoption, config } from '../src/scan/index.mjs'
|
|
5
5
|
|
|
6
6
|
const program = new Command()
|
|
7
7
|
|
|
8
8
|
program
|
|
9
9
|
.command('adoption')
|
|
10
10
|
.description('Scan @spark-ui adoption for .tsx files with given imports')
|
|
11
|
-
.option('-c, --configuration <
|
|
12
|
-
.option('-o, --output <
|
|
11
|
+
.option('-c, --configuration <config>', 'configuration file route')
|
|
12
|
+
.option('-o, --output <output>', 'output file route')
|
|
13
|
+
.option('-v, --verbose', 'output log information', false)
|
|
14
|
+
.option('-d, --details', 'output information about each match', config.details)
|
|
15
|
+
.option('-s, --sort <sort>', 'sort results (count or alphabetical)', config.sort)
|
|
16
|
+
.option('-dir, --directory <directory>', 'directory to parse', config.directory)
|
|
17
|
+
.option('-ext, --extensions <extensions...>', 'file extensions to parse', config.extensions)
|
|
18
|
+
.option('-i, --imports <imports...>', 'import patterns to identify', config.imports)
|
|
13
19
|
.action(adoption)
|
|
14
20
|
|
|
15
21
|
program.parse(process.argv)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spark-ui/cli-utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"description": "Spark CLI utils",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/fs-extra": "11.0.4"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "89a5654b5e5b02d911e40ead40c608617b5bd44b"
|
|
49
49
|
}
|
package/src/core/Logger.mjs
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import chalk from 'chalk'
|
|
3
2
|
|
|
4
3
|
export class Logger {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
#force = false
|
|
5
|
+
|
|
6
|
+
constructor({ verbose } = {}) {
|
|
7
|
+
this.verbose = verbose
|
|
8
|
+
|
|
9
|
+
if (typeof Logger.instance === 'object') {
|
|
10
|
+
return Logger.instance
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Logger.instance = this
|
|
14
|
+
|
|
15
|
+
return this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#log({ type = v => v, force, verbose }, ...args) {
|
|
19
|
+
if (force || verbose) {
|
|
20
|
+
console.log(type(...args)) // eslint-disable-line no-console
|
|
21
|
+
}
|
|
22
|
+
this.#force = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
force() {
|
|
26
|
+
this.#force = true
|
|
27
|
+
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
error(...args) {
|
|
32
|
+
this.#log({ type: chalk.red, force: this.#force, verbose: this.verbose }, ...args)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
warning(...args) {
|
|
36
|
+
this.#log({ type: chalk.yellow, force: this.#force, verbose: this.verbose }, ...args)
|
|
7
37
|
}
|
|
8
38
|
|
|
9
|
-
|
|
10
|
-
|
|
39
|
+
info(...args) {
|
|
40
|
+
this.#log({ type: chalk.cyan, force: this.#force, verbose: this.verbose }, ...args)
|
|
11
41
|
}
|
|
12
42
|
|
|
13
|
-
|
|
14
|
-
|
|
43
|
+
success(...args) {
|
|
44
|
+
this.#log({ type: chalk.green, force: this.#force, verbose: this.verbose }, ...args)
|
|
15
45
|
}
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
break() {
|
|
48
|
+
this.#log({ type: chalk.green, force: this.#force, verbose: this.verbose }, '')
|
|
19
49
|
}
|
|
20
50
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
adoption: {
|
|
3
|
+
/**
|
|
4
|
+
* Shows (or not) details about each package adoption
|
|
5
|
+
* @param {boolean}
|
|
6
|
+
* @default false
|
|
7
|
+
*/
|
|
8
|
+
details: false,
|
|
9
|
+
/**
|
|
10
|
+
* Sorts packages list, based on count or alphabetical order
|
|
11
|
+
* @param {'count'|'alphabetical'}
|
|
12
|
+
* @default 'count'
|
|
13
|
+
*/
|
|
14
|
+
sort: 'count',
|
|
15
|
+
/**
|
|
16
|
+
* Packages pattern to be scanned
|
|
17
|
+
* @params {string[]}
|
|
18
|
+
*/
|
|
19
|
+
imports: ['@spark-ui'],
|
|
20
|
+
/**
|
|
21
|
+
* File extensions to be scanned
|
|
22
|
+
* @params {string[]}
|
|
23
|
+
*/
|
|
24
|
+
extensions: ['.tsx', '.ts'],
|
|
25
|
+
/**
|
|
26
|
+
* Directory to be scanned
|
|
27
|
+
* @param {string}
|
|
28
|
+
* @default '.''
|
|
29
|
+
*/
|
|
30
|
+
directory: '.',
|
|
31
|
+
/**
|
|
32
|
+
* Output file path to export the results
|
|
33
|
+
* @param {string}
|
|
34
|
+
*/
|
|
35
|
+
output: null,
|
|
36
|
+
},
|
|
37
|
+
}
|
package/src/scan/index.mjs
CHANGED
|
@@ -1,49 +1,41 @@
|
|
|
1
|
+
/* eslint-disable complexity */
|
|
2
|
+
/* eslint-disable max-lines-per-function */
|
|
1
3
|
import * as process from 'node:process'
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
4
6
|
import path from 'path'
|
|
5
7
|
|
|
8
|
+
import { Logger } from '../core/index.mjs'
|
|
9
|
+
import defaultConfig from './config.cjs'
|
|
10
|
+
import { loadConfig } from './loadConfig.mjs'
|
|
6
11
|
import { scanCallback } from './scanCallback.mjs'
|
|
7
|
-
import {
|
|
12
|
+
import { scanDirectories } from './utils/index.mjs'
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
details: false,
|
|
12
|
-
sort: 'count',
|
|
13
|
-
imports: ['@spark-ui'],
|
|
14
|
-
extensions: ['.tsx', '.ts'],
|
|
15
|
-
directory: '.',
|
|
16
|
-
},
|
|
17
|
-
}
|
|
14
|
+
export async function adoption(options = {}) {
|
|
15
|
+
const { configuration, ...optionsConfig } = options
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
let config =
|
|
17
|
+
const logger = new Logger({ verbose: optionsConfig.verbose })
|
|
18
|
+
let config = await loadConfig(configuration, { logger })
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
config = structuredClone(customConfig, DEFAULT_CONFIG)
|
|
30
|
-
} else {
|
|
31
|
-
logger.warn('⚠️ No custom configuration file found')
|
|
32
|
-
logger.info('ℹ️ Loading default configuration')
|
|
33
|
-
}
|
|
34
|
-
} catch (error) {
|
|
35
|
-
logger.error('💥 Something went wrong loading the custom configuration file')
|
|
20
|
+
config = {
|
|
21
|
+
adoption: {
|
|
22
|
+
...config.adoption,
|
|
23
|
+
...optionsConfig,
|
|
24
|
+
imports: optionsConfig.imports || config.adoption.imports,
|
|
25
|
+
extensions: optionsConfig.extensions || config.adoption.extensions,
|
|
26
|
+
},
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
const extensions = config.adoption.extensions
|
|
39
|
-
|
|
40
29
|
let importCount = 0
|
|
41
30
|
const importResults = {}
|
|
42
31
|
let importsUsed = {}
|
|
43
32
|
let importsCount = {}
|
|
44
|
-
|
|
33
|
+
|
|
34
|
+
const { details, directory, extensions, imports, sort, output } = config.adoption
|
|
35
|
+
|
|
36
|
+
imports.forEach(moduleName => {
|
|
45
37
|
logger.info(`ℹ️ Scanning adoption for ${moduleName}`)
|
|
46
|
-
const directoryPath = path.join(process.cwd(),
|
|
38
|
+
const directoryPath = path.join(process.cwd(), directory)
|
|
47
39
|
|
|
48
40
|
const response = scanDirectories(directoryPath, moduleName, extensions, scanCallback, {
|
|
49
41
|
importCount,
|
|
@@ -56,7 +48,7 @@ export async function adoption(options) {
|
|
|
56
48
|
`🎉 Found ${response.importCount - importCount} imports with "${moduleName}" modules across directory ${directoryPath}.`
|
|
57
49
|
)
|
|
58
50
|
} else {
|
|
59
|
-
logger.
|
|
51
|
+
logger.warning(
|
|
60
52
|
`⚠️ No files found with "${moduleName}" imports across directory ${directoryPath}.`
|
|
61
53
|
)
|
|
62
54
|
}
|
|
@@ -64,7 +56,7 @@ export async function adoption(options) {
|
|
|
64
56
|
})
|
|
65
57
|
|
|
66
58
|
// Sort importsUsed by alphabet
|
|
67
|
-
if (
|
|
59
|
+
if (sort === 'alphabetical') {
|
|
68
60
|
importsUsed = Object.fromEntries(
|
|
69
61
|
Object.entries(importsUsed)
|
|
70
62
|
.sort(([pkgNameA], [pkgNameB]) => pkgNameA.localeCompare(pkgNameB))
|
|
@@ -83,7 +75,7 @@ export async function adoption(options) {
|
|
|
83
75
|
]
|
|
84
76
|
})
|
|
85
77
|
)
|
|
86
|
-
} else if (
|
|
78
|
+
} else if (sort === 'count') {
|
|
87
79
|
// Sort importsUsed by most used
|
|
88
80
|
importsUsed = Object.fromEntries(
|
|
89
81
|
Object.entries(importsUsed)
|
|
@@ -110,17 +102,25 @@ export async function adoption(options) {
|
|
|
110
102
|
const result = Object.fromEntries(
|
|
111
103
|
Object.entries(importsUsed).map(([pkgName, value]) => [
|
|
112
104
|
pkgName,
|
|
113
|
-
{ ...value, ...(
|
|
105
|
+
{ ...value, ...(details && { results: importResults[pkgName] }) },
|
|
114
106
|
])
|
|
115
107
|
)
|
|
116
108
|
|
|
117
|
-
if (
|
|
109
|
+
if (output) {
|
|
118
110
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
const { dir } = path.parse(path.join(process.cwd(), output))
|
|
112
|
+
if (!existsSync(dir)) {
|
|
113
|
+
mkdirSync(dir, { recursive: true })
|
|
114
|
+
}
|
|
115
|
+
writeFileSync(`${path.join(process.cwd(), output)}`, JSON.stringify(result, null, 2))
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error(`💥 Error writing file: ${error}`)
|
|
118
|
+
process.exit(1)
|
|
122
119
|
}
|
|
123
120
|
} else {
|
|
124
|
-
logger.info(JSON.stringify(result, null, 2))
|
|
121
|
+
logger.force().info(JSON.stringify(result, null, 2))
|
|
122
|
+
process.exit(0)
|
|
125
123
|
}
|
|
126
124
|
}
|
|
125
|
+
|
|
126
|
+
export const config = { ...defaultConfig }
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import cmd, { ENTER } from '../../test/utils/cmd'
|
|
8
|
+
|
|
9
|
+
const __dirname = fileURLToPath(import.meta.url)
|
|
10
|
+
const cliPath = path.join(__dirname, '../../../bin/spark.mjs')
|
|
11
|
+
|
|
12
|
+
const cliProcess = cmd.create(cliPath)
|
|
13
|
+
|
|
14
|
+
describe('CLI `spark scan`', () => {
|
|
15
|
+
const OUTPUT_FILE = 'report.json'
|
|
16
|
+
|
|
17
|
+
const EXT_CONFIG_FILE_PATH = 'packages/utils/cli/test/stubs/scanConfigWithCustomExtensions.js'
|
|
18
|
+
const SORT_CONFIG_FILE_PATH = 'packages/utils/cli/test/stubs/scanConfigWithCustomSorting.js'
|
|
19
|
+
const DETAILED_OUTPUT_CONFIG_FILE_PATH =
|
|
20
|
+
'packages/utils/cli/test/stubs/scanConfigWithDetailedOutput.js'
|
|
21
|
+
const IMPORTS_CONFIG_FILE_PATH = 'packages/utils/cli/test/stubs/scanConfigWithCustomImports.js'
|
|
22
|
+
|
|
23
|
+
const adoptionCommand = async (args: string[] = []) => {
|
|
24
|
+
/**
|
|
25
|
+
* We define here some common options such as custom directory (for performance reasons).
|
|
26
|
+
*/
|
|
27
|
+
return await cliProcess.execute(
|
|
28
|
+
['scan', 'adoption', '-dir', './packages/components', ...args],
|
|
29
|
+
[ENTER]
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
if (fse.existsSync(OUTPUT_FILE)) fse.removeSync(OUTPUT_FILE)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
if (fse.existsSync(OUTPUT_FILE)) fse.removeSync(OUTPUT_FILE)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('Adoption (components adoption)', () => {
|
|
42
|
+
it('should execute command with default config', async () => {
|
|
43
|
+
const response = await adoptionCommand(['-v'])
|
|
44
|
+
|
|
45
|
+
expect(response).toMatch(/Loading default configuration/i)
|
|
46
|
+
expect(response).toMatch(/Scanning adoption for @spark-ui/i)
|
|
47
|
+
|
|
48
|
+
expect(response).toMatch(/Found/i)
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* With no output option the adoption report will be simply logged
|
|
52
|
+
*/
|
|
53
|
+
expect(
|
|
54
|
+
response.filter(
|
|
55
|
+
(entry: string | Record<string, unknown>) =>
|
|
56
|
+
entry['@spark-ui/button' as keyof typeof entry]
|
|
57
|
+
)
|
|
58
|
+
).toBeDefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('Results details with output', () => {
|
|
62
|
+
it('should output results with packages details from command option', async () => {
|
|
63
|
+
await adoptionCommand(['-d', '-o', OUTPUT_FILE])
|
|
64
|
+
|
|
65
|
+
expect(fse.pathExistsSync(OUTPUT_FILE)).toBe(true)
|
|
66
|
+
|
|
67
|
+
const outputContent = JSON.parse(fse.readFileSync(OUTPUT_FILE, 'utf-8'))
|
|
68
|
+
|
|
69
|
+
Object.keys(outputContent).forEach(outputItem => {
|
|
70
|
+
expect(Object.keys(outputContent[outputItem]).includes('results')).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should output results with packages details from configuration file', async () => {
|
|
75
|
+
await adoptionCommand(['-c', DETAILED_OUTPUT_CONFIG_FILE_PATH])
|
|
76
|
+
|
|
77
|
+
expect(fse.pathExistsSync(OUTPUT_FILE)).toBe(true)
|
|
78
|
+
|
|
79
|
+
const outputContent = JSON.parse(fse.readFileSync(OUTPUT_FILE, 'utf-8'))
|
|
80
|
+
|
|
81
|
+
Object.keys(outputContent).forEach(outputItem => {
|
|
82
|
+
expect(Object.keys(outputContent[outputItem]).includes('results')).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('Results sorting', () => {
|
|
88
|
+
it('should sort results alphabetically from command option', async () => {
|
|
89
|
+
const response = await adoptionCommand(['-v', '-s', 'alphabetical'])
|
|
90
|
+
|
|
91
|
+
expect(response).toMatch(/Loading default configuration/i)
|
|
92
|
+
expect(response).toMatch(/Scanning adoption for @spark-ui/i)
|
|
93
|
+
|
|
94
|
+
const pkgList = Object.keys(
|
|
95
|
+
response.filter((entry: string | Record<string, unknown>) => typeof entry !== 'string')[0]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const sparkInputIndex = pkgList.indexOf('@spark-ui/input')
|
|
99
|
+
const sparkFormFieldIndex = pkgList.indexOf('@spark-ui/form-field')
|
|
100
|
+
|
|
101
|
+
expect(sparkInputIndex).toBeGreaterThan(sparkFormFieldIndex)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should sort results alphabetically from configuration file', async () => {
|
|
105
|
+
const response = await adoptionCommand(['-v', '-c', SORT_CONFIG_FILE_PATH])
|
|
106
|
+
|
|
107
|
+
expect(response).toMatch(/Loading spark-ui custom configuration file/i)
|
|
108
|
+
expect(response).toMatch(/Scanning adoption for @spark-ui/i)
|
|
109
|
+
|
|
110
|
+
const pkgList = Object.keys(
|
|
111
|
+
response.filter((entry: string | Record<string, unknown>) => typeof entry !== 'string')[0]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const sparkInputIndex = pkgList.indexOf('@spark-ui/input')
|
|
115
|
+
const sparkFormFieldIndex = pkgList.indexOf('@spark-ui/form-field')
|
|
116
|
+
|
|
117
|
+
expect(sparkInputIndex).toBeGreaterThan(sparkFormFieldIndex)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('File extensions', () => {
|
|
122
|
+
it('should execute command with custom extensions from command option', async () => {
|
|
123
|
+
const response = await adoptionCommand(['-v', '-ext', '.css'])
|
|
124
|
+
|
|
125
|
+
expect(response).toMatch(/Loading default configuration/i)
|
|
126
|
+
expect(response).toMatch(/Scanning adoption for @spark-ui/i)
|
|
127
|
+
|
|
128
|
+
expect(response).toMatch(/No files found with "@spark-ui" imports across directory/i)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should execute command with custom extensions from configuration file', async () => {
|
|
132
|
+
const response = await adoptionCommand(['-v', '-c', EXT_CONFIG_FILE_PATH])
|
|
133
|
+
|
|
134
|
+
expect(response).toMatch(/Loading spark-ui custom configuration file/i)
|
|
135
|
+
expect(response).toMatch(/Scanning adoption for @spark-ui/i)
|
|
136
|
+
|
|
137
|
+
expect(response).toMatch(/No files found with "@spark-ui" imports across directory/i)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('Imports patterns', () => {
|
|
142
|
+
it('should execute command for custom import patterns from command option', async () => {
|
|
143
|
+
const response = await adoptionCommand(['-v', '-i', '@react-aria', '@react-stately'])
|
|
144
|
+
|
|
145
|
+
expect(response).toMatch(/Scanning adoption for @react-aria/i)
|
|
146
|
+
expect(response).toMatch(/Scanning adoption for @react-stately/i)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should execute command for custom import patterns from configuration file', async () => {
|
|
150
|
+
const response = await adoptionCommand(['-v', '-c', IMPORTS_CONFIG_FILE_PATH])
|
|
151
|
+
|
|
152
|
+
expect(response).toMatch(/Scanning adoption for @react-aria/i)
|
|
153
|
+
expect(response).toMatch(/Scanning adoption for @react-stately/i)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
import defaultConfig from './config.cjs'
|
|
7
|
+
|
|
8
|
+
export async function loadConfig(configuration, { logger }) {
|
|
9
|
+
try {
|
|
10
|
+
const configFileRoute = path.join(process.cwd(), configuration || '.spark-ui.cjs')
|
|
11
|
+
|
|
12
|
+
if (existsSync(configFileRoute)) {
|
|
13
|
+
logger.info('ℹ️ Loading spark-ui custom configuration file')
|
|
14
|
+
const { default: customConfig } = await import(configFileRoute)
|
|
15
|
+
|
|
16
|
+
const config = {
|
|
17
|
+
adoption: {
|
|
18
|
+
...defaultConfig.adoption,
|
|
19
|
+
...customConfig.adoption,
|
|
20
|
+
imports: customConfig.adoption.imports || defaultConfig.adoption.imports,
|
|
21
|
+
extensions: customConfig.adoption.extensions || defaultConfig.adoption.extensions,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return config
|
|
26
|
+
} else {
|
|
27
|
+
if (configuration) {
|
|
28
|
+
logger.error('⚠️ No custom configuration file found:', configFileRoute)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
logger.warning('⚠️ No custom configuration file found')
|
|
32
|
+
logger.info('ℹ️ Loading default configuration')
|
|
33
|
+
|
|
34
|
+
return { ...defaultConfig }
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error('💥 Something went wrong loading the custom configuration file', error)
|
|
38
|
+
|
|
39
|
+
return { ...defaultConfig }
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/scan/utils/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { extractImports } from './extract-imports.mjs'
|
|
2
2
|
export { fileContainsImport } from './file-contains-import.mjs'
|
|
3
3
|
export { getFormatedTimestamp } from './get-formated-timestamp.mjs'
|
|
4
|
-
export { logger } from './logger.mjs'
|
|
5
4
|
export { scanDirectories } from './scan-directories.mjs'
|
|
@@ -12,21 +12,28 @@ const cliPath = path.join(__dirname, '../../bin/spark.mjs')
|
|
|
12
12
|
const cliProcess = cmd.create(cliPath)
|
|
13
13
|
|
|
14
14
|
describe('CLI `spark generate` (component package)', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const packageType = 'component'
|
|
16
|
+
|
|
17
|
+
const contextPath = TemplateGenerator.CONTEXTS[packageType] as string
|
|
18
|
+
|
|
19
|
+
const packagePath = path.join(process.cwd(), 'packages', contextPath, 'bar')
|
|
20
|
+
const invalidPackagePath = path.join(process.cwd(), 'packages', contextPath, '123')
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
if (fse.existsSync(packagePath)) fse.removeSync(packagePath)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
if (fse.existsSync(packagePath)) fse.removeSync(packagePath)
|
|
28
|
+
})
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
it('should properly generate package from CLI when arguments are valid', async () => {
|
|
21
31
|
const response = await cliProcess.execute(
|
|
22
32
|
['generate'],
|
|
23
|
-
[
|
|
33
|
+
['bar', ENTER, packageType, ENTER, 'This is my foo component', ENTER]
|
|
24
34
|
)
|
|
25
|
-
const contextPath = TemplateGenerator.CONTEXTS[packageType] as string
|
|
26
|
-
const packagePath = path.join(process.cwd(), 'packages', contextPath, packageName)
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
const expectedFiles = [
|
|
36
|
+
;[
|
|
30
37
|
'/.npmignore',
|
|
31
38
|
'/package.json',
|
|
32
39
|
'/src/index.ts',
|
|
@@ -37,33 +44,19 @@ describe('CLI `spark generate` (component package)', () => {
|
|
|
37
44
|
'/src/Bar.test.tsx',
|
|
38
45
|
'/src/Bar.stories.tsx',
|
|
39
46
|
'/tsconfig.json',
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
const assertExpectedFiles = (filePath: string) => {
|
|
43
|
-
expect(response).toContain(`Created ${packagePath}${filePath}`)
|
|
47
|
+
].forEach((filePath: string) => {
|
|
48
|
+
expect(response.toString()).toContain(`Created ${packagePath}${filePath}`)
|
|
44
49
|
expect(fse.pathExistsSync(`${packagePath}${filePath}`)).toBe(true)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
expectedFiles.forEach(assertExpectedFiles)
|
|
48
|
-
|
|
49
|
-
// Finally we clean up the generated component package
|
|
50
|
-
fse.removeSync(packagePath)
|
|
50
|
+
})
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('should prevent generating package when argument are invalid', async () => {
|
|
54
|
-
|
|
55
|
-
const packageName = '123'
|
|
56
|
-
const packageType = 'component'
|
|
57
|
-
|
|
58
|
-
// WHEN we try to create it
|
|
59
|
-
const response = await cliProcess.execute(['generate'], [packageName, ENTER])
|
|
60
|
-
const contextPath = TemplateGenerator.CONTEXTS[packageType] as string
|
|
61
|
-
const packagePath = path.join(process.cwd(), 'packages', contextPath, packageName)
|
|
54
|
+
const response = await cliProcess.execute(['generate'], ['123', ENTER])
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
expect(response).toContain(
|
|
56
|
+
expect(response.toString()).toContain(
|
|
65
57
|
'Name name must contain letters and dash symbols only (ex: "my-package")'
|
|
66
58
|
)
|
|
67
|
-
|
|
59
|
+
|
|
60
|
+
expect(fse.pathExistsSync(invalidPackagePath)).toBe(false)
|
|
68
61
|
})
|
|
69
62
|
})
|
package/test/utils/cmd.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
/**
|
|
2
3
|
* CLI Integration Test Helper
|
|
3
4
|
* @author Andrés Zorro <zorrodg@gmail.com>
|
|
@@ -11,11 +12,30 @@ import { constants } from 'os'
|
|
|
11
12
|
|
|
12
13
|
const PATH = process.env.PATH
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Format CMD response as a JSON object
|
|
17
|
+
*/
|
|
18
|
+
function getReportFromData(data) {
|
|
19
|
+
const regex = /({[\s\S]*})/ // Regular expression to match object type
|
|
20
|
+
const match = data.match(regex)
|
|
21
|
+
|
|
22
|
+
const messages = data
|
|
23
|
+
.slice(0, match?.index ?? undefined)
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map(line => line.trim())
|
|
27
|
+
.filter(line => line)
|
|
28
|
+
|
|
29
|
+
const json = match ? JSON.parse(match[0]) : undefined
|
|
30
|
+
|
|
31
|
+
return [...messages, json]
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
/**
|
|
15
35
|
* Creates a child process with script path
|
|
16
|
-
* @param processPath Path of the process to execute
|
|
17
|
-
* @param args Arguments to the command
|
|
18
|
-
* @param env (optional) Environment variables
|
|
36
|
+
* @param {string} processPath Path of the process to execute
|
|
37
|
+
* @param {Array} args Arguments to the command
|
|
38
|
+
* @param {Object} env (optional) Environment variables
|
|
19
39
|
*/
|
|
20
40
|
function createProcess(processPath, args = [], env = null) {
|
|
21
41
|
// Ensure that path exists
|
|
@@ -44,18 +64,18 @@ function createProcess(processPath, args = [], env = null) {
|
|
|
44
64
|
* Creates a command and executes inputs (user responses) to the stdin
|
|
45
65
|
* Returns a promise that resolves when all inputs are sent
|
|
46
66
|
* Rejects the promise if any error
|
|
47
|
-
* @param processPath Path of the process to execute
|
|
48
|
-
* @param args Arguments to the command
|
|
49
|
-
* @param inputs (Optional) Array of inputs (user responses)
|
|
50
|
-
* @param opts (optional) Environment variables
|
|
67
|
+
* @param {string} processPath Path of the process to execute
|
|
68
|
+
* @param {Array} args Arguments to the command
|
|
69
|
+
* @param {Array} inputs (Optional) Array of inputs (user responses)
|
|
70
|
+
* @param {Object} opts (optional) Environment variables
|
|
51
71
|
*/
|
|
52
|
-
function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
72
|
+
function executeWithInput(processPath, args = [], inputs = [], opts = {}) {
|
|
53
73
|
if (!Array.isArray(inputs)) {
|
|
54
74
|
opts = inputs
|
|
55
75
|
inputs = []
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
const { env = null, timeout =
|
|
78
|
+
const { env = null, timeout = 250, maxTimeout = 10000 } = opts
|
|
59
79
|
const childProcess = createProcess(processPath, args, env)
|
|
60
80
|
childProcess.stdin.setEncoding('utf-8')
|
|
61
81
|
|
|
@@ -64,12 +84,12 @@ function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
|
64
84
|
// Creates a loop to feed user inputs to the child process in order to get results from the tool
|
|
65
85
|
// This code is heavily inspired (if not blantantly copied) from inquirer-test:
|
|
66
86
|
// https://github.com/ewnd9/inquirer-test/blob/6e2c40bbd39a061d3e52a8b1ee52cdac88f8d7f7/index.js#L14
|
|
67
|
-
const loop =
|
|
87
|
+
const loop = inputs => {
|
|
68
88
|
if (killIOTimeout) {
|
|
69
89
|
clearTimeout(killIOTimeout)
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
if (!
|
|
92
|
+
if (!inputs.length) {
|
|
73
93
|
childProcess.stdin.end()
|
|
74
94
|
|
|
75
95
|
// Set a timeout to wait for CLI response. If CLI takes longer than
|
|
@@ -83,12 +103,12 @@ function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
currentInputTimeout = setTimeout(() => {
|
|
86
|
-
childProcess.stdin.write(
|
|
106
|
+
childProcess.stdin.write(inputs[0])
|
|
87
107
|
// Log debug I/O statements on tests
|
|
88
108
|
if (env && env.DEBUG) {
|
|
89
|
-
console.log('input:',
|
|
109
|
+
console.log('input:', inputs[0])
|
|
90
110
|
}
|
|
91
|
-
loop(
|
|
111
|
+
loop(inputs.slice(1))
|
|
92
112
|
}, timeout)
|
|
93
113
|
}
|
|
94
114
|
|
|
@@ -97,7 +117,7 @@ function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
|
97
117
|
childProcess.stderr.on('data', data => {
|
|
98
118
|
// Log debug I/O statements on tests
|
|
99
119
|
if (env && env.DEBUG) {
|
|
100
|
-
console.log('error:', data.toString())
|
|
120
|
+
console.log('error:', data.toString())
|
|
101
121
|
}
|
|
102
122
|
})
|
|
103
123
|
|
|
@@ -105,7 +125,7 @@ function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
|
105
125
|
childProcess.stdout.on('data', data => {
|
|
106
126
|
// Log debug I/O statements on tests
|
|
107
127
|
if (env && env.DEBUG) {
|
|
108
|
-
console.log('output:', data.toString())
|
|
128
|
+
console.log('output:', data.toString())
|
|
109
129
|
}
|
|
110
130
|
})
|
|
111
131
|
|
|
@@ -130,7 +150,7 @@ function executeWithInput(processPath, args, inputs, opts = {}) {
|
|
|
130
150
|
clearTimeout(killIOTimeout)
|
|
131
151
|
}
|
|
132
152
|
|
|
133
|
-
resolve(result.toString())
|
|
153
|
+
resolve(getReportFromData(result.toString()))
|
|
134
154
|
})
|
|
135
155
|
)
|
|
136
156
|
})
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
|
|
3
|
-
export const logger = {
|
|
4
|
-
error(...args) {
|
|
5
|
-
console.log(chalk.red(...args))
|
|
6
|
-
},
|
|
7
|
-
warn(...args) {
|
|
8
|
-
console.log(chalk.yellow(...args))
|
|
9
|
-
},
|
|
10
|
-
info(...args) {
|
|
11
|
-
console.log(chalk.cyan(...args))
|
|
12
|
-
},
|
|
13
|
-
success(...args) {
|
|
14
|
-
console.log(chalk.green(...args))
|
|
15
|
-
},
|
|
16
|
-
break() {
|
|
17
|
-
console.log('')
|
|
18
|
-
},
|
|
19
|
-
}
|