@spark-ui/cli-utils 2.12.10 → 2.13.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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
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.13.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.12.10...@spark-ui/cli-utils@2.13.0) (2024-05-03)
7
+
8
+ ### Features
9
+
10
+ - **cli-utils:** add scan adoption script ([203e05e](https://github.com/adevinta/spark/commit/203e05e02285be18e5d0c6211f3ec04e4322837d))
11
+
6
12
  ## [2.12.10](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.12.9...@spark-ui/cli-utils@2.12.10) (2024-04-29)
7
13
 
8
14
  **Note:** Version bump only for package @spark-ui/cli-utils
@@ -0,0 +1,15 @@
1
+ #! /usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import { adoption } from '../src/scan/index.mjs'
5
+
6
+ const program = new Command()
7
+
8
+ program
9
+ .command('adoption')
10
+ .description('Scan @spark-ui adoption for .tsx files with given imports')
11
+ .option('-c, --configuration <path>', 'configuration file route', '.spark-ui.cjs')
12
+ .option('-o, --output <path>', 'output file route')
13
+ .action(adoption)
14
+
15
+ program.parse(process.argv)
package/bin/spark.mjs CHANGED
@@ -9,6 +9,6 @@ const { version } = require('../package.json')
9
9
  program.version(version, '--version')
10
10
 
11
11
  program.command('generate', 'Generate a component scaffolding').alias('g')
12
- program.command('setup-themes', 'Set up Spark theming configuration')
12
+ program.command('scan', 'Scan a directory for components').alias('s')
13
13
 
14
14
  program.parse(process.argv)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/cli-utils",
3
- "version": "2.12.10",
3
+ "version": "2.13.0",
4
4
  "description": "Spark CLI utils",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,7 +12,8 @@
12
12
  ],
13
13
  "bin": {
14
14
  "spark": "./bin/spark.mjs",
15
- "spark-generate": "./bin/spark-generate.mjs"
15
+ "spark-generate": "./bin/spark-generate.mjs",
16
+ "spark-scan": "./bin/spark-scan.mjs"
16
17
  },
17
18
  "type": "module",
18
19
  "repository": {
@@ -42,5 +43,5 @@
42
43
  "devDependencies": {
43
44
  "@types/fs-extra": "11.0.4"
44
45
  },
45
- "gitHead": "e2262cc062d779c7bafac1d01fe453477f0fdc16"
46
+ "gitHead": "5d945ed8b0a31bf0d42382c560388c95a92f6411"
46
47
  }
package/src/index.doc.mdx CHANGED
@@ -40,3 +40,73 @@ Then, a command prompt will guide you through the process by asking you for:
40
40
  - the package name (required),
41
41
  - the template used (required, only `Component` template available right now)
42
42
  - and the package description (optional).
43
+
44
+ ## Scanning directory adoption
45
+
46
+ For viewing the adoption of packages in a project directory, the following command can be executed:
47
+
48
+ ```bash
49
+ $ spark scan adoption
50
+ ```
51
+
52
+ ### Options
53
+
54
+ #### Configuration
55
+
56
+ ```bash
57
+ $ spark scan adoption --configuration <filename>
58
+ ```
59
+
60
+ alias
61
+
62
+ ```bash
63
+ $ spark scan adoption -c <filename>
64
+ ```
65
+
66
+ example
67
+
68
+ ```bash
69
+ spark scan adoption -c "./spark-ui.cjs"
70
+ ````
71
+
72
+
73
+ ##### configuration filename structure
74
+
75
+ ```js
76
+ // .spark-ui.cjs
77
+ module.exports = {
78
+ adoption: {
79
+ details: true,
80
+ sort: 'count', // 'count' or 'alphabetical'
81
+ imports: ['@spark-ui'],
82
+ extensions: ['.tsx', '.ts'],
83
+ directory: './packages',
84
+ },
85
+ }
86
+
87
+ /***
88
+ - `details` (boolean) - whether to show the details of the adoption or not. Default: false
89
+ - `sort` ('count' | 'alphabetical') - packages are sorted alphabetically. Default: false means sorted by adoption number
90
+ - `imports` (array) - the imports to be scanned.
91
+ - `extensions` (array) - the extensions to be scanned
92
+ - `directory` (string) - the directory to be scanned. Default: '.' means the current directory
93
+ ***/
94
+ ```
95
+
96
+ #### Output
97
+ The output option is used to save the adoption data to a file. It is optional
98
+
99
+ ```bash
100
+ $ spark scan adoption --output <filename>
101
+ ```
102
+ alias
103
+
104
+ ```bash
105
+ $ spark scan adoption -o <filename>
106
+ ```
107
+
108
+ example
109
+
110
+ ```bash
111
+ spark scan adoption -o "./adoption.$(date +"%Y%m%d_%H:%M:%S").json"
112
+ ````
@@ -0,0 +1,122 @@
1
+ import * as process from 'node:process'
2
+
3
+ import { appendFileSync, existsSync } from 'fs'
4
+ import path from 'path'
5
+
6
+ import { scanCallback } from './scanCallback.mjs'
7
+ import { logger } from './utils/logger.mjs'
8
+ import { scanDirectories } from './utils/scan-directories.mjs'
9
+
10
+ const DEFAULT_CONFIG = {
11
+ adoption: {
12
+ details: false,
13
+ sort: 'count',
14
+ imports: ['@spark-ui'],
15
+ extensions: ['.tsx', '.ts'],
16
+ directory: '.',
17
+ },
18
+ }
19
+
20
+ export async function adoption(options) {
21
+ let config = DEFAULT_CONFIG
22
+
23
+ const configFileRoute = path.join(process.cwd(), options.configuration || '.spark-ui.cjs')
24
+ try {
25
+ if (existsSync(configFileRoute)) {
26
+ console.log('✨✨✨ loading spark-ui custom configuration file ✨✨✨')
27
+ const { default: customConfig } = await import(
28
+ path.join(process.cwd(), options.configuration)
29
+ )
30
+ config = structuredClone(customConfig, DEFAULT_CONFIG)
31
+ }
32
+ } catch (error) {
33
+ logger.info('ℹ️ Loading default configuration')
34
+ }
35
+
36
+ const extensions = config.adoption.extensions
37
+
38
+ let importCount = 0
39
+ const importResults = {}
40
+ let importsUsed = {}
41
+ let importsCount = {}
42
+ config.adoption.imports.forEach(moduleName => {
43
+ console.log(`scanning adoption for ${moduleName}`)
44
+ const directoryPath = path.join(process.cwd(), config.adoption.directory)
45
+
46
+ const response = scanDirectories(directoryPath, moduleName, extensions, scanCallback, {
47
+ importCount,
48
+ importResults,
49
+ importsUsed,
50
+ importsCount,
51
+ })
52
+ if (importCount !== response.importCount) {
53
+ logger.success(
54
+ `Found ${response.importCount - importCount} imports with "${moduleName}" modules across directory ${directoryPath}.`
55
+ )
56
+ } else {
57
+ logger.warn(`No files found with "${moduleName}" imports across directory ${directoryPath}.`)
58
+ }
59
+ importCount = response.importCount
60
+ })
61
+
62
+ // Sort importsUsed by alphabet
63
+ if (config.adoption.sort === 'alphabetical') {
64
+ importsUsed = Object.fromEntries(
65
+ Object.entries(importsUsed)
66
+ .sort(([pkgNameA], [pkgNameB]) => pkgNameA.localeCompare(pkgNameB))
67
+ .map(([pkgName, content]) => {
68
+ return [
69
+ pkgName,
70
+ {
71
+ default: Object.fromEntries(
72
+ Object.entries(content.default).sort(([a], [b]) => a.localeCompare(b))
73
+ ),
74
+ named: Object.fromEntries(
75
+ Object.entries(content.named).sort(([a], [b]) => a.localeCompare(b))
76
+ ),
77
+ importsCount: content.importsCount,
78
+ },
79
+ ]
80
+ })
81
+ )
82
+ } else if (config.adoption.sort === 'count') {
83
+ // Sort importsUsed by most used
84
+ importsUsed = Object.fromEntries(
85
+ Object.entries(importsUsed)
86
+ .sort(([, contentA], [, contentB]) => contentB.importsCount - contentA.importsCount)
87
+ .map(([pkgName, content]) => {
88
+ return [
89
+ pkgName,
90
+ {
91
+ default: Object.fromEntries(
92
+ Object.entries(content.default).sort(([, a], [, b]) => b - a)
93
+ ),
94
+ named: Object.fromEntries(
95
+ Object.entries(content.named).sort(([, a], [, b]) => b - a)
96
+ ),
97
+ importsCount: content.importsCount,
98
+ },
99
+ ]
100
+ })
101
+ )
102
+
103
+ importsCount = Object.fromEntries(Object.entries(importsCount).sort(([, a], [, b]) => b - a))
104
+ }
105
+
106
+ const result = Object.fromEntries(
107
+ Object.entries(importsUsed).map(([pkgName, value]) => [
108
+ pkgName,
109
+ { ...value, ...(config.adoption.details && { results: importResults[pkgName] }) },
110
+ ])
111
+ )
112
+
113
+ if (options.output) {
114
+ try {
115
+ appendFileSync(`${options.output}`, JSON.stringify(result, null, 2))
116
+ } catch (err) {
117
+ logger.error(`Error writing file: ${err}`)
118
+ }
119
+ } else {
120
+ logger.info(JSON.stringify(result, null, 2))
121
+ }
122
+ }
@@ -0,0 +1,62 @@
1
+ import extractImports from './utils/extract-imports.mjs'
2
+
3
+ export function scanCallback(
4
+ f,
5
+ moduleName,
6
+ { importCount, importResults, importsUsed, importsCount }
7
+ ) {
8
+ const response = { importCount, importResults, importsUsed, importsCount }
9
+ if (!f.fileContent) return response
10
+
11
+ const imports = extractImports(f.filePath, moduleName)
12
+
13
+ Object.entries(imports).forEach(([key, importDeclarations]) => {
14
+ const moduleName = key.split('/').splice(0, 2).join('/')
15
+ importDeclarations.forEach(importDeclaration => {
16
+ const statement = importDeclaration.getText()
17
+ const defaultImport = importDeclaration.getDefaultImport()?.getText() || null
18
+ const namedImports = importDeclaration.getNamedImports().map(n => n.getText())
19
+
20
+ if (!importResults[moduleName]) {
21
+ importResults[moduleName] = []
22
+ }
23
+
24
+ importResults[moduleName].push({
25
+ path: f.filePath,
26
+ statement,
27
+ hasDefault: !!defaultImport,
28
+ hasNamed: !!namedImports.length,
29
+ defaultImport,
30
+ namedImports,
31
+ })
32
+
33
+ if (!importsUsed[moduleName]) {
34
+ importsUsed[moduleName] = {
35
+ default: {},
36
+ named: {},
37
+ importsCount: 0,
38
+ }
39
+ }
40
+
41
+ if (defaultImport) {
42
+ importsUsed[moduleName].default[defaultImport] =
43
+ importsUsed[moduleName].default[defaultImport] + 1 || 1
44
+ importsUsed.importsCount = importsCount[defaultImport] + 1
45
+
46
+ importsCount[defaultImport] = importsCount[defaultImport] + 1 || 1
47
+ response.importCount++
48
+ }
49
+
50
+ if (namedImports.length) {
51
+ namedImports.forEach(n => {
52
+ importsUsed[moduleName].named[n] = importsUsed[moduleName].named[n] + 1 || 1
53
+ importsUsed[moduleName].importsCount = importsUsed[moduleName].importsCount + 1
54
+ importsCount[n] = importsCount[n] + 1 || 1
55
+ response.importCount++
56
+ })
57
+ }
58
+ })
59
+ })
60
+
61
+ return response
62
+ }
@@ -0,0 +1,28 @@
1
+ import { Project } from 'ts-morph'
2
+
3
+ export function extractImports(filePath, requestedModuleName) {
4
+ const project = new Project()
5
+ const sourceFile = project.addSourceFileAtPath(filePath)
6
+
7
+ const importStatements = {}
8
+
9
+ const importNodes = sourceFile.getImportDeclarations()
10
+
11
+ importNodes
12
+ .filter(node => {
13
+ const moduleName = node.getModuleSpecifierValue()
14
+
15
+ return moduleName.includes(requestedModuleName)
16
+ })
17
+ .forEach(node => {
18
+ const moduleName = node.getModuleSpecifierValue()
19
+ if (!importStatements[moduleName]) {
20
+ importStatements[moduleName] = []
21
+ }
22
+ importStatements[moduleName].push(node)
23
+ })
24
+
25
+ return importStatements
26
+ }
27
+
28
+ export default extractImports
@@ -0,0 +1,17 @@
1
+ import fs from 'fs'
2
+
3
+ /**
4
+ * Check if a file contains an import from a given import name.
5
+ * @param filePath The path to the file to check.
6
+ * @param importName The name of the import to check for.
7
+ * @returns Whether the file contains an import from the given import name.
8
+ */
9
+ export function fileContainsImport(filePath, importName) {
10
+ const fileContent = fs.readFileSync(filePath, 'utf8')
11
+
12
+ if (new RegExp(`import.*from\\s+["']${importName}.*["']`, 'm').test(fileContent)) {
13
+ return { filePath, fileContent }
14
+ }
15
+
16
+ return { filePath }
17
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ export function getFormatedTimestamp() {
2
+ const d = new Date()
3
+ const date = d.toISOString().split('T')[0]
4
+ const time = d.toTimeString().split(' ')[0].replace(/:/g, '-')
5
+
6
+ return `${date} ${time}`
7
+ }
@@ -0,0 +1,5 @@
1
+ export { extractImports } from './extract-imports.mjs'
2
+ export { fileContainsImport } from './file-contains-import.mjs'
3
+ export { getFormatedTimestamp } from './get-formated-timestamp.mjs'
4
+ export { logger } from './logger.mjs'
5
+ export { scanDirectories } from './scan-directories.mjs'
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import { fileContainsImport } from './file-contains-import.mjs'
5
+
6
+ export function scanDirectories(
7
+ directoryPath,
8
+ importName,
9
+ extensions,
10
+ scanningCallback,
11
+ { importCount, importResults, importsUsed, importsCount }
12
+ ) {
13
+ const files = fs.readdirSync(directoryPath)
14
+
15
+ let response = {
16
+ importCount,
17
+ importResults,
18
+ importsUsed,
19
+ importsCount,
20
+ }
21
+
22
+ for (const file of files) {
23
+ const filePath = path.join(directoryPath, file)
24
+ const stats = fs.statSync(filePath)
25
+
26
+ if (stats.isDirectory()) {
27
+ response = scanDirectories(filePath, importName, extensions, scanningCallback, response)
28
+ } else if (stats.isFile() && extensions.includes(path.extname(filePath))) {
29
+ const f = fileContainsImport(filePath, importName)
30
+
31
+ if (f) {
32
+ response = scanningCallback(
33
+ {
34
+ filePath: f.filePath,
35
+ fileContent: f.fileContent,
36
+ },
37
+ importName,
38
+ response
39
+ )
40
+ }
41
+ }
42
+ }
43
+
44
+ return response
45
+ }