diff --git a/package-lock.json b/package-lock.json
index 31cb9db0b..2170b4c23 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -87,7 +87,7 @@
"nx": "^16.5.0",
"postcss": "8.4.38",
"prettier": "3.2.5",
- "prettier-plugin-tailwindcss": "0.5.12",
+ "prettier-plugin-tailwindcss": "0.5.14",
"react": "18.2.0",
"react-dom": "18.3.1",
"react-live": "3.2.0",
@@ -18065,9 +18065,9 @@
"dev": true
},
"node_modules/ejs": {
- "version": "3.1.9",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
- "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"dependencies": {
"jake": "^10.8.5"
@@ -29320,9 +29320,9 @@
}
},
"node_modules/prettier-plugin-tailwindcss": {
- "version": "0.5.12",
- "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.12.tgz",
- "integrity": "sha512-o74kiDBVE73oHW+pdkFSluHBL3cYEvru5YgEqNkBMFF7Cjv+w1vI565lTlfoJT4VLWDe0FMtZ7FkE/7a4pMXSQ==",
+ "version": "0.5.14",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.14.tgz",
+ "integrity": "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==",
"dev": true,
"engines": {
"node": ">=14.21.3"
@@ -29332,6 +29332,7 @@
"@prettier/plugin-pug": "*",
"@shopify/prettier-plugin-liquid": "*",
"@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig-melody": "*",
"prettier": "^3.0",
"prettier-plugin-astro": "*",
"prettier-plugin-css-order": "*",
@@ -29357,6 +29358,9 @@
"@trivago/prettier-plugin-sort-imports": {
"optional": true
},
+ "@zackad/prettier-plugin-twig-melody": {
+ "optional": true
+ },
"prettier-plugin-astro": {
"optional": true
},
@@ -29386,9 +29390,6 @@
},
"prettier-plugin-svelte": {
"optional": true
- },
- "prettier-plugin-twig-melody": {
- "optional": true
}
}
},
@@ -35223,7 +35224,7 @@
},
"packages/components/combobox": {
"name": "@spark-ui/combobox",
- "version": "0.12.0",
+ "version": "0.12.2",
"license": "MIT",
"dependencies": {
"@spark-ui/form-field": "^1.5.2",
@@ -35810,7 +35811,7 @@
},
"packages/utils/cli": {
"name": "@spark-ui/cli-utils",
- "version": "2.12.10",
+ "version": "2.13.1",
"license": "MIT",
"dependencies": {
"@clack/prompts": "0.7.0",
@@ -35824,7 +35825,8 @@
},
"bin": {
"spark": "bin/spark.mjs",
- "spark-generate": "bin/spark-generate.mjs"
+ "spark-generate": "bin/spark-generate.mjs",
+ "spark-scan": "bin/spark-scan.mjs"
},
"devDependencies": {
"@types/fs-extra": "11.0.4"
diff --git a/package.json b/package.json
index 2321a386b..ffec9dd59 100644
--- a/package.json
+++ b/package.json
@@ -108,7 +108,7 @@
"nx": "^16.5.0",
"postcss": "8.4.38",
"prettier": "3.2.5",
- "prettier-plugin-tailwindcss": "0.5.12",
+ "prettier-plugin-tailwindcss": "0.5.14",
"react": "18.2.0",
"react-dom": "18.3.1",
"react-live": "3.2.0",
diff --git a/packages/components/combobox/CHANGELOG.md b/packages/components/combobox/CHANGELOG.md
index 5d659e95e..5095c38fb 100644
--- a/packages/components/combobox/CHANGELOG.md
+++ b/packages/components/combobox/CHANGELOG.md
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.12.2](https://github.com/adevinta/spark/compare/@spark-ui/combobox@0.12.1...@spark-ui/combobox@0.12.2) (2024-05-06)
+
+### Bug Fixes
+
+- **combobox:** clear internal input value on escape ([3bb5c69](https://github.com/adevinta/spark/commit/3bb5c6915669b3eacbe2627900862b025d2c7ddb))
+- **combobox:** combobox content overflow on smaller screens ([9e1d55b](https://github.com/adevinta/spark/commit/9e1d55b5b3b73074d233c92aef72ef21236c1de4))
+- **combobox:** missing padding in combobox empty view ([1d76dfc](https://github.com/adevinta/spark/commit/1d76dfc742bc1c9b35414cadc02fd4d6cfb90ba2))
+- **combobox:** preserve combobox cursor position upon change ([4ba34da](https://github.com/adevinta/spark/commit/4ba34da2a5a8dbec9d48f735b624783d4df5f2fe))
+
+## [0.12.1](https://github.com/adevinta/spark/compare/@spark-ui/combobox@0.12.0...@spark-ui/combobox@0.12.1) (2024-05-03)
+
+### Bug Fixes
+
+- **combobox:** fix combobox highligh index while typing ([adf4c8b](https://github.com/adevinta/spark/commit/adf4c8b137d89ca3d6c5b6ace85a14bc37b64e43))
+
# [0.12.0](https://github.com/adevinta/spark/compare/@spark-ui/combobox@0.11.12...@spark-ui/combobox@0.12.0) (2024-05-02)
### Features
diff --git a/packages/components/combobox/package.json b/packages/components/combobox/package.json
index f2fb31fd5..c1601f1be 100644
--- a/packages/components/combobox/package.json
+++ b/packages/components/combobox/package.json
@@ -1,6 +1,6 @@
{
"name": "@spark-ui/combobox",
- "version": "0.12.0",
+ "version": "0.12.2",
"description": "An input that behaves similarly to a select, with the addition of a free text input to filter options.",
"publishConfig": {
"access": "public"
diff --git a/packages/components/combobox/src/ComboboxContext.tsx b/packages/components/combobox/src/ComboboxContext.tsx
index 22c7731d4..f3e86e323 100644
--- a/packages/components/combobox/src/ComboboxContext.tsx
+++ b/packages/components/combobox/src/ComboboxContext.tsx
@@ -346,6 +346,7 @@ export const ComboboxProvider = ({
allowCustomValue,
setSelectedItems: onInternalSelectedItemsChange,
triggerAreaRef,
+ items: itemsMap,
})
: singleSelectionReducer({
allowCustomValue,
diff --git a/packages/components/combobox/src/ComboboxEmpty.tsx b/packages/components/combobox/src/ComboboxEmpty.tsx
index ddaa8e736..59ab9560c 100644
--- a/packages/components/combobox/src/ComboboxEmpty.tsx
+++ b/packages/components/combobox/src/ComboboxEmpty.tsx
@@ -1,3 +1,4 @@
+import { cx } from 'class-variance-authority'
import { forwardRef, type ReactNode, type Ref } from 'react'
import { useComboboxContext } from './ComboboxContext'
@@ -13,7 +14,7 @@ export const Empty = forwardRef(
const hasNoItemVisible = ctx.filteredItemsMap.size === 0
return hasNoItemVisible ? (
-
+
{children}
) : null
diff --git a/packages/components/combobox/src/ComboboxInput.tsx b/packages/components/combobox/src/ComboboxInput.tsx
index e3b30f443..4adbaf065 100644
--- a/packages/components/combobox/src/ComboboxInput.tsx
+++ b/packages/components/combobox/src/ComboboxInput.tsx
@@ -63,6 +63,15 @@ export const Input = forwardRef(
multiselectInputProps.onKeyDown?.(event)
ctx.setLastInteractionType('keyboard')
},
+ /**
+ *
+ * Important:
+ * - without this, the input cursor is moved to the end after every change.
+ * @see https://github.com/downshift-js/downshift/issues/1108#issuecomment-674180157
+ */
+ onChange: (e: React.ChangeEvent
) => {
+ ctx.setInputValue(e.target.value)
+ },
ref: inputRef,
})
@@ -79,7 +88,8 @@ export const Input = forwardRef(
type="text"
placeholder={placeholder}
className={cx(
- 'h-sz-28 shrink-0 flex-grow basis-[80px] text-ellipsis bg-surface px-sm text-body-1 outline-none',
+ 'max-w-full shrink-0 grow basis-[80px]',
+ 'h-sz-28 text-ellipsis bg-surface px-sm text-body-1 outline-none',
'disabled:cursor-not-allowed disabled:bg-transparent disabled:text-on-surface/dim-3',
'read-only:cursor-default read-only:bg-transparent read-only:text-on-surface',
className
diff --git a/packages/components/combobox/src/ComboboxItem.tsx b/packages/components/combobox/src/ComboboxItem.tsx
index 6b98b9b59..16f75b658 100644
--- a/packages/components/combobox/src/ComboboxItem.tsx
+++ b/packages/components/combobox/src/ComboboxItem.tsx
@@ -70,6 +70,7 @@ const ItemContent = forwardRef(
item: itemCtx.itemData,
index: itemCtx.index,
})
+
const ref = useMergeRefs(forwardedRef, downshiftRef)
if (!isVisible) return null
diff --git a/packages/components/combobox/src/ComboboxTrigger.tsx b/packages/components/combobox/src/ComboboxTrigger.tsx
index 2f17d466d..15e00b3ee 100644
--- a/packages/components/combobox/src/ComboboxTrigger.tsx
+++ b/packages/components/combobox/src/ComboboxTrigger.tsx
@@ -92,14 +92,16 @@ export const Trigger = forwardRef(
{selectedItems}
{input}
+
{hasClearButton && clearButton}
+
{disclosure}
diff --git a/packages/components/combobox/src/useCombobox/multipleSelectionReducer.ts b/packages/components/combobox/src/useCombobox/multipleSelectionReducer.ts
index 3161d3f5b..dfb88d4f1 100644
--- a/packages/components/combobox/src/useCombobox/multipleSelectionReducer.ts
+++ b/packages/components/combobox/src/useCombobox/multipleSelectionReducer.ts
@@ -1,10 +1,12 @@
import { useCombobox, UseComboboxProps, UseMultipleSelectionReturnValue } from 'downshift'
import React from 'react'
-import { ComboboxItem } from '../types'
+import { ComboboxItem, ItemsMap } from '../types'
+import { getIndexByKey } from '../utils'
interface Props {
allowCustomValue?: boolean
+ items: ItemsMap
selectedItems: ComboboxItem[]
multiselect: UseMultipleSelectionReturnValue
setSelectedItems: (items: ComboboxItem[]) => void
@@ -17,8 +19,9 @@ export const multipleSelectionReducer = ({
allowCustomValue = false,
setSelectedItems,
triggerAreaRef,
+ items,
}: Props) => {
- const reducer: UseComboboxProps['stateReducer'] = (state, { changes, type }) => {
+ const reducer: UseComboboxProps['stateReducer'] = (_, { changes, type }) => {
const isFocusInsideTriggerArea = triggerAreaRef.current?.contains?.(document.activeElement)
switch (type) {
@@ -34,7 +37,10 @@ export const multipleSelectionReducer = ({
if (changes.selectedItem != null) {
newState.inputValue = '' // keep input value after selection
newState.isOpen = true // keep menu opened after selection
- newState.highlightedIndex = state.highlightedIndex // preserve highlighted item index after selection
+
+ const highlightedIndex = getIndexByKey(items, changes.selectedItem.value)
+
+ newState.highlightedIndex = highlightedIndex // preserve highlighted item index after selection
const isAlreadySelected = multiselect.selectedItems.some(
selectedItem => selectedItem.value === changes.selectedItem?.value
@@ -55,6 +61,11 @@ export const multipleSelectionReducer = ({
...changes,
inputValue: allowCustomValue ? changes.inputValue : '',
}
+ case useCombobox.stateChangeTypes.InputChange:
+ return {
+ ...changes,
+ selectedItem: changes.highlightedIndex === -1 ? null : changes.selectedItem,
+ }
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
diff --git a/packages/components/combobox/src/useCombobox/singleSelectionReducer.ts b/packages/components/combobox/src/useCombobox/singleSelectionReducer.ts
index 9aec4c33f..9a60fcd31 100644
--- a/packages/components/combobox/src/useCombobox/singleSelectionReducer.ts
+++ b/packages/components/combobox/src/useCombobox/singleSelectionReducer.ts
@@ -19,6 +19,12 @@ export const singleSelectionReducer = ({
)
switch (type) {
+ case useCombobox.stateChangeTypes.InputKeyDownEscape:
+ if (!changes.selectedItem) {
+ setSelectedItem(null)
+ }
+
+ return changes
case useCombobox.stateChangeTypes.ItemClick:
case useCombobox.stateChangeTypes.InputKeyDownEnter:
if (changes.selectedItem) {
diff --git a/packages/utils/cli/CHANGELOG.md b/packages/utils/cli/CHANGELOG.md
index 2c32d8105..3feff0603 100644
--- a/packages/utils/cli/CHANGELOG.md
+++ b/packages/utils/cli/CHANGELOG.md
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [2.13.1](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@2.13.0...@spark-ui/cli-utils@2.13.1) (2024-05-06)
+
+### Bug Fixes
+
+- wrong chmod on new ci script ([7b47919](https://github.com/adevinta/spark/commit/7b479196b0fcf5be21f2f067fdcb63e1d77c3496))
+
+# [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)
+
+### Features
+
+- **cli-utils:** add scan adoption script ([203e05e](https://github.com/adevinta/spark/commit/203e05e02285be18e5d0c6211f3ec04e4322837d))
+
## [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)
**Note:** Version bump only for package @spark-ui/cli-utils
diff --git a/packages/utils/cli/bin/spark-scan.mjs b/packages/utils/cli/bin/spark-scan.mjs
new file mode 100755
index 000000000..ec4d24a73
--- /dev/null
+++ b/packages/utils/cli/bin/spark-scan.mjs
@@ -0,0 +1,15 @@
+#! /usr/bin/env node
+
+import { Command } from 'commander'
+import { adoption } from '../src/scan/index.mjs'
+
+const program = new Command()
+
+program
+ .command('adoption')
+ .description('Scan @spark-ui adoption for .tsx files with given imports')
+ .option('-c, --configuration ', 'configuration file route', '.spark-ui.cjs')
+ .option('-o, --output ', 'output file route')
+ .action(adoption)
+
+program.parse(process.argv)
diff --git a/packages/utils/cli/bin/spark.mjs b/packages/utils/cli/bin/spark.mjs
index c3adbc0b0..946d1d6b6 100755
--- a/packages/utils/cli/bin/spark.mjs
+++ b/packages/utils/cli/bin/spark.mjs
@@ -9,6 +9,6 @@ const { version } = require('../package.json')
program.version(version, '--version')
program.command('generate', 'Generate a component scaffolding').alias('g')
-program.command('setup-themes', 'Set up Spark theming configuration')
+program.command('scan', 'Scan a directory for components').alias('s')
program.parse(process.argv)
diff --git a/packages/utils/cli/package.json b/packages/utils/cli/package.json
index 48f311cd7..b889ce3da 100644
--- a/packages/utils/cli/package.json
+++ b/packages/utils/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@spark-ui/cli-utils",
- "version": "2.12.10",
+ "version": "2.13.1",
"description": "Spark CLI utils",
"publishConfig": {
"access": "public"
@@ -12,7 +12,8 @@
],
"bin": {
"spark": "./bin/spark.mjs",
- "spark-generate": "./bin/spark-generate.mjs"
+ "spark-generate": "./bin/spark-generate.mjs",
+ "spark-scan": "./bin/spark-scan.mjs"
},
"type": "module",
"repository": {
diff --git a/packages/utils/cli/src/index.doc.mdx b/packages/utils/cli/src/index.doc.mdx
index 404babd39..e6b3cd7dc 100644
--- a/packages/utils/cli/src/index.doc.mdx
+++ b/packages/utils/cli/src/index.doc.mdx
@@ -40,3 +40,73 @@ Then, a command prompt will guide you through the process by asking you for:
- the package name (required),
- the template used (required, only `Component` template available right now)
- and the package description (optional).
+
+## Scanning directory adoption
+
+For viewing the adoption of packages in a project directory, the following command can be executed:
+
+```bash
+$ spark scan adoption
+```
+
+### Options
+
+#### Configuration
+
+```bash
+$ spark scan adoption --configuration
+```
+
+alias
+
+```bash
+$ spark scan adoption -c
+```
+
+example
+
+```bash
+spark scan adoption -c "./spark-ui.cjs"
+````
+
+
+##### configuration filename structure
+
+ ```js
+ // .spark-ui.cjs
+module.exports = {
+ adoption: {
+ details: true,
+ sort: 'count', // 'count' or 'alphabetical'
+ imports: ['@spark-ui'],
+ extensions: ['.tsx', '.ts'],
+ directory: './packages',
+ },
+}
+
+/***
+ - `details` (boolean) - whether to show the details of the adoption or not. Default: false
+- `sort` ('count' | 'alphabetical') - packages are sorted alphabetically. Default: false means sorted by adoption number
+- `imports` (array) - the imports to be scanned.
+- `extensions` (array) - the extensions to be scanned
+- `directory` (string) - the directory to be scanned. Default: '.' means the current directory
+***/
+```
+
+#### Output
+The output option is used to save the adoption data to a file. It is optional
+
+```bash
+$ spark scan adoption --output
+```
+ alias
+
+```bash
+$ spark scan adoption -o
+```
+
+example
+
+```bash
+spark scan adoption -o "./adoption.$(date +"%Y%m%d_%H:%M:%S").json"
+````
\ No newline at end of file
diff --git a/packages/utils/cli/src/scan/index.mjs b/packages/utils/cli/src/scan/index.mjs
new file mode 100644
index 000000000..db1d0a59c
--- /dev/null
+++ b/packages/utils/cli/src/scan/index.mjs
@@ -0,0 +1,122 @@
+import * as process from 'node:process'
+
+import { appendFileSync, existsSync } from 'fs'
+import path from 'path'
+
+import { scanCallback } from './scanCallback.mjs'
+import { logger } from './utils/logger.mjs'
+import { scanDirectories } from './utils/scan-directories.mjs'
+
+const DEFAULT_CONFIG = {
+ adoption: {
+ details: false,
+ sort: 'count',
+ imports: ['@spark-ui'],
+ extensions: ['.tsx', '.ts'],
+ directory: '.',
+ },
+}
+
+export async function adoption(options) {
+ let config = DEFAULT_CONFIG
+
+ const configFileRoute = path.join(process.cwd(), options.configuration || '.spark-ui.cjs')
+ try {
+ if (existsSync(configFileRoute)) {
+ console.log('✨✨✨ loading spark-ui custom configuration file ✨✨✨')
+ const { default: customConfig } = await import(
+ path.join(process.cwd(), options.configuration)
+ )
+ config = structuredClone(customConfig, DEFAULT_CONFIG)
+ }
+ } catch (error) {
+ logger.info('ℹ️ Loading default configuration')
+ }
+
+ const extensions = config.adoption.extensions
+
+ let importCount = 0
+ const importResults = {}
+ let importsUsed = {}
+ let importsCount = {}
+ config.adoption.imports.forEach(moduleName => {
+ console.log(`scanning adoption for ${moduleName}`)
+ const directoryPath = path.join(process.cwd(), config.adoption.directory)
+
+ const response = scanDirectories(directoryPath, moduleName, extensions, scanCallback, {
+ importCount,
+ importResults,
+ importsUsed,
+ importsCount,
+ })
+ if (importCount !== response.importCount) {
+ logger.success(
+ `Found ${response.importCount - importCount} imports with "${moduleName}" modules across directory ${directoryPath}.`
+ )
+ } else {
+ logger.warn(`No files found with "${moduleName}" imports across directory ${directoryPath}.`)
+ }
+ importCount = response.importCount
+ })
+
+ // Sort importsUsed by alphabet
+ if (config.adoption.sort === 'alphabetical') {
+ importsUsed = Object.fromEntries(
+ Object.entries(importsUsed)
+ .sort(([pkgNameA], [pkgNameB]) => pkgNameA.localeCompare(pkgNameB))
+ .map(([pkgName, content]) => {
+ return [
+ pkgName,
+ {
+ default: Object.fromEntries(
+ Object.entries(content.default).sort(([a], [b]) => a.localeCompare(b))
+ ),
+ named: Object.fromEntries(
+ Object.entries(content.named).sort(([a], [b]) => a.localeCompare(b))
+ ),
+ importsCount: content.importsCount,
+ },
+ ]
+ })
+ )
+ } else if (config.adoption.sort === 'count') {
+ // Sort importsUsed by most used
+ importsUsed = Object.fromEntries(
+ Object.entries(importsUsed)
+ .sort(([, contentA], [, contentB]) => contentB.importsCount - contentA.importsCount)
+ .map(([pkgName, content]) => {
+ return [
+ pkgName,
+ {
+ default: Object.fromEntries(
+ Object.entries(content.default).sort(([, a], [, b]) => b - a)
+ ),
+ named: Object.fromEntries(
+ Object.entries(content.named).sort(([, a], [, b]) => b - a)
+ ),
+ importsCount: content.importsCount,
+ },
+ ]
+ })
+ )
+
+ importsCount = Object.fromEntries(Object.entries(importsCount).sort(([, a], [, b]) => b - a))
+ }
+
+ const result = Object.fromEntries(
+ Object.entries(importsUsed).map(([pkgName, value]) => [
+ pkgName,
+ { ...value, ...(config.adoption.details && { results: importResults[pkgName] }) },
+ ])
+ )
+
+ if (options.output) {
+ try {
+ appendFileSync(`${options.output}`, JSON.stringify(result, null, 2))
+ } catch (err) {
+ logger.error(`Error writing file: ${err}`)
+ }
+ } else {
+ logger.info(JSON.stringify(result, null, 2))
+ }
+}
diff --git a/packages/utils/cli/src/scan/scanCallback.mjs b/packages/utils/cli/src/scan/scanCallback.mjs
new file mode 100644
index 000000000..ef3882fb0
--- /dev/null
+++ b/packages/utils/cli/src/scan/scanCallback.mjs
@@ -0,0 +1,62 @@
+import extractImports from './utils/extract-imports.mjs'
+
+export function scanCallback(
+ f,
+ moduleName,
+ { importCount, importResults, importsUsed, importsCount }
+) {
+ const response = { importCount, importResults, importsUsed, importsCount }
+ if (!f.fileContent) return response
+
+ const imports = extractImports(f.filePath, moduleName)
+
+ Object.entries(imports).forEach(([key, importDeclarations]) => {
+ const moduleName = key.split('/').splice(0, 2).join('/')
+ importDeclarations.forEach(importDeclaration => {
+ const statement = importDeclaration.getText()
+ const defaultImport = importDeclaration.getDefaultImport()?.getText() || null
+ const namedImports = importDeclaration.getNamedImports().map(n => n.getText())
+
+ if (!importResults[moduleName]) {
+ importResults[moduleName] = []
+ }
+
+ importResults[moduleName].push({
+ path: f.filePath,
+ statement,
+ hasDefault: !!defaultImport,
+ hasNamed: !!namedImports.length,
+ defaultImport,
+ namedImports,
+ })
+
+ if (!importsUsed[moduleName]) {
+ importsUsed[moduleName] = {
+ default: {},
+ named: {},
+ importsCount: 0,
+ }
+ }
+
+ if (defaultImport) {
+ importsUsed[moduleName].default[defaultImport] =
+ importsUsed[moduleName].default[defaultImport] + 1 || 1
+ importsUsed.importsCount = importsCount[defaultImport] + 1
+
+ importsCount[defaultImport] = importsCount[defaultImport] + 1 || 1
+ response.importCount++
+ }
+
+ if (namedImports.length) {
+ namedImports.forEach(n => {
+ importsUsed[moduleName].named[n] = importsUsed[moduleName].named[n] + 1 || 1
+ importsUsed[moduleName].importsCount = importsUsed[moduleName].importsCount + 1
+ importsCount[n] = importsCount[n] + 1 || 1
+ response.importCount++
+ })
+ }
+ })
+ })
+
+ return response
+}
diff --git a/packages/utils/cli/src/scan/utils/extract-imports.mjs b/packages/utils/cli/src/scan/utils/extract-imports.mjs
new file mode 100644
index 000000000..294589898
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/extract-imports.mjs
@@ -0,0 +1,28 @@
+import { Project } from 'ts-morph'
+
+export function extractImports(filePath, requestedModuleName) {
+ const project = new Project()
+ const sourceFile = project.addSourceFileAtPath(filePath)
+
+ const importStatements = {}
+
+ const importNodes = sourceFile.getImportDeclarations()
+
+ importNodes
+ .filter(node => {
+ const moduleName = node.getModuleSpecifierValue()
+
+ return moduleName.includes(requestedModuleName)
+ })
+ .forEach(node => {
+ const moduleName = node.getModuleSpecifierValue()
+ if (!importStatements[moduleName]) {
+ importStatements[moduleName] = []
+ }
+ importStatements[moduleName].push(node)
+ })
+
+ return importStatements
+}
+
+export default extractImports
diff --git a/packages/utils/cli/src/scan/utils/file-contains-import.mjs b/packages/utils/cli/src/scan/utils/file-contains-import.mjs
new file mode 100644
index 000000000..c42887ca2
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/file-contains-import.mjs
@@ -0,0 +1,17 @@
+import fs from 'fs'
+
+/**
+ * Check if a file contains an import from a given import name.
+ * @param filePath The path to the file to check.
+ * @param importName The name of the import to check for.
+ * @returns Whether the file contains an import from the given import name.
+ */
+export function fileContainsImport(filePath, importName) {
+ const fileContent = fs.readFileSync(filePath, 'utf8')
+
+ if (new RegExp(`import.*from\\s+["']${importName}.*["']`, 'm').test(fileContent)) {
+ return { filePath, fileContent }
+ }
+
+ return { filePath }
+}
diff --git a/packages/utils/cli/src/scan/utils/get-csv.mjs b/packages/utils/cli/src/scan/utils/get-csv.mjs
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/utils/cli/src/scan/utils/get-formated-timestamp.mjs b/packages/utils/cli/src/scan/utils/get-formated-timestamp.mjs
new file mode 100644
index 000000000..e58250d19
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/get-formated-timestamp.mjs
@@ -0,0 +1,7 @@
+export function getFormatedTimestamp() {
+ const d = new Date()
+ const date = d.toISOString().split('T')[0]
+ const time = d.toTimeString().split(' ')[0].replace(/:/g, '-')
+
+ return `${date} ${time}`
+}
diff --git a/packages/utils/cli/src/scan/utils/index.mjs b/packages/utils/cli/src/scan/utils/index.mjs
new file mode 100644
index 000000000..0de2a35f9
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/index.mjs
@@ -0,0 +1,5 @@
+export { extractImports } from './extract-imports.mjs'
+export { fileContainsImport } from './file-contains-import.mjs'
+export { getFormatedTimestamp } from './get-formated-timestamp.mjs'
+export { logger } from './logger.mjs'
+export { scanDirectories } from './scan-directories.mjs'
diff --git a/packages/utils/cli/src/scan/utils/logger.mjs b/packages/utils/cli/src/scan/utils/logger.mjs
new file mode 100644
index 000000000..8ba30c614
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/logger.mjs
@@ -0,0 +1,19 @@
+import chalk from 'chalk'
+
+export const logger = {
+ error(...args) {
+ console.log(chalk.red(...args))
+ },
+ warn(...args) {
+ console.log(chalk.yellow(...args))
+ },
+ info(...args) {
+ console.log(chalk.cyan(...args))
+ },
+ success(...args) {
+ console.log(chalk.green(...args))
+ },
+ break() {
+ console.log('')
+ },
+}
diff --git a/packages/utils/cli/src/scan/utils/scan-directories.mjs b/packages/utils/cli/src/scan/utils/scan-directories.mjs
new file mode 100644
index 000000000..cb33b877d
--- /dev/null
+++ b/packages/utils/cli/src/scan/utils/scan-directories.mjs
@@ -0,0 +1,45 @@
+import fs from 'fs'
+import path from 'path'
+
+import { fileContainsImport } from './file-contains-import.mjs'
+
+export function scanDirectories(
+ directoryPath,
+ importName,
+ extensions,
+ scanningCallback,
+ { importCount, importResults, importsUsed, importsCount }
+) {
+ const files = fs.readdirSync(directoryPath)
+
+ let response = {
+ importCount,
+ importResults,
+ importsUsed,
+ importsCount,
+ }
+
+ for (const file of files) {
+ const filePath = path.join(directoryPath, file)
+ const stats = fs.statSync(filePath)
+
+ if (stats.isDirectory()) {
+ response = scanDirectories(filePath, importName, extensions, scanningCallback, response)
+ } else if (stats.isFile() && extensions.includes(path.extname(filePath))) {
+ const f = fileContainsImport(filePath, importName)
+
+ if (f) {
+ response = scanningCallback(
+ {
+ filePath: f.filePath,
+ fileContent: f.fileContent,
+ },
+ importName,
+ response
+ )
+ }
+ }
+ }
+
+ return response
+}