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 +}