diff --git a/.bitmap b/.bitmap index 4a8b00f31dea..3e4c1507cad7 100644 --- a/.bitmap +++ b/.bitmap @@ -1549,6 +1549,13 @@ "mainFile": "index.ts", "rootDir": "components/ui/code-tab-page" }, + "ui/code-view": { + "name": "ui/code-view", + "scope": "teambit.code", + "version": "0.0.519", + "mainFile": "index.ts", + "rootDir": "components/ui/code-view" + }, "ui/compare/lane-compare": { "name": "ui/compare/lane-compare", "scope": "teambit.lanes", @@ -1759,6 +1766,20 @@ "mainFile": "index.ts", "rootDir": "components/ui/hooks/scope-context" }, + "ui/hooks/use-core-aspects": { + "name": "ui/hooks/use-core-aspects", + "scope": "", + "version": "", + "defaultScope": "teambit.harmony", + "mainFile": "index.ts", + "rootDir": "components/ui/hooks/use-core-aspects", + "config": { + "teambit.react/react": {}, + "teambit.envs/envs": { + "env": "teambit.react/react" + } + } + }, "ui/inputs/lane-selector": { "name": "ui/inputs/lane-selector", "scope": "teambit.lanes", diff --git a/components/ui/code-tab-page/code-tab-page.tsx b/components/ui/code-tab-page/code-tab-page.tsx index 00e937d223e3..1a805cdafd4a 100644 --- a/components/ui/code-tab-page/code-tab-page.tsx +++ b/components/ui/code-tab-page/code-tab-page.tsx @@ -40,13 +40,60 @@ export function CodePage({ className, fileIconSlot, host, codeViewClassName }: C const [searchParams] = useSearchParams(); const scopeFromQueryParams = searchParams.get('scope'); const component = useContext(ComponentContext); - const { mainFile, fileTree = [], dependencies, devFiles } = useCode(component.id); + const [fileParam, setFileParam] = useState<{ + current?: string; + prev?: string; + }>({ current: urlParams.file }); + + React.useEffect(() => { + if (urlParams.file !== fileParam.current) { + setFileParam((prev) => ({ current: urlParams.file, prev: prev.current })); + } + }, [urlParams.file, fileParam.current]); + + const { mainFile, fileTree = [], dependencies, devFiles, loading: loadingCode } = useCode(component.id); const { data: artifacts = [] } = useComponentArtifacts(host, component.id.toString()); + const currentFile = loadingCode + ? undefined + : (() => { + if (urlParams.file && fileTree.includes(urlParams.file)) { + return urlParams.file; + } + if (!urlParams.file) return mainFile; + + const extractNameAndExtension = (filename) => { + const match = filename.match(/^(.*?)(\.[^.]+)?$/); + return [match[1], match[2]]; + }; + + const [currentBase] = extractNameAndExtension(fileParam.current || ''); + const mainFileExt = extractNameAndExtension(mainFile)[1]; + const [, prevExt] = fileParam.prev ? extractNameAndExtension(fileParam.prev) : [null, null]; + + const matchingFiles = fileTree.filter((file) => { + const [fileBase] = extractNameAndExtension(file); + return fileBase === currentBase || fileBase === fileParam.current; + }); + + if (matchingFiles.length === 1) { + return matchingFiles[0]; + } + + const preferredExt = prevExt || mainFileExt; + if (preferredExt) { + const exactExtensionMatch = matchingFiles.find((file) => { + const [, fileExt] = extractNameAndExtension(file); + return fileExt === preferredExt; + }); + if (exactExtensionMatch) return exactExtensionMatch; + } + + return matchingFiles[0] || mainFile; + })(); - const currentFile = urlParams.file || mainFile; const currentArtifact = getArtifactFileDetailsFromUrl(artifacts, currentFile); const currentArtifactFile = currentArtifact?.artifactFile; - const { data: currentArtifactFileData, loading } = useComponentArtifactFileContent( + const { data: currentArtifactFileData, loading: loadingArtifact } = useComponentArtifactFileContent( host, { componentId: component.id.toString(), @@ -64,7 +111,8 @@ export function CodePage({ className, fileIconSlot, host, codeViewClassName }: C const sidebarOpenness = isSidebarOpen ? Layout.row : Layout.left; const fileIconMatchers: FileIconMatch[] = useMemo(() => flatten(fileIconSlot?.values()), [fileIconSlot]); const icon = getFileIcon(fileIconMatchers, currentFile); - const loadingArtifactFileContent = loading !== undefined ? loading : !!currentFile && !currentArtifact; + const loadingArtifactFileContent = + loadingArtifact !== undefined ? loadingArtifact : !!currentFile && !currentArtifact; const getHref = React.useCallback( (node) => { const queryParams = new URLSearchParams(); @@ -90,8 +138,9 @@ export function CodePage({ className, fileIconSlot, host, codeViewClassName }: C currentFile={currentFile} icon={icon} currentFileContent={currentArtifactFileContent} - loading={loadingArtifactFileContent} + loading={loadingArtifactFileContent || loadingCode} codeSnippetClassName={codeViewClassName} + dependencies={dependencies} /> diff --git a/components/ui/code-view/code-view.module.scss b/components/ui/code-view/code-view.module.scss new file mode 100644 index 000000000000..ea5f38dc4f63 --- /dev/null +++ b/components/ui/code-view/code-view.module.scss @@ -0,0 +1,66 @@ +.codeView { + padding: 24px 40px; + width: 100%; + overflow: hidden; + box-sizing: border-box; + + .codeSnippetWrapper { + width: 100%; + max-width: 100%; + .codeSnippet { + display: block; + overflow: auto; + height: calc(100vh - 200px); + > code { + > code { + // this is to design the line numbers culumn + border-right: 1px solid #323232; + padding-right: 16px !important; + margin-right: 16px; + } + } + } + // TODO - fix this in code snippet component. it breaks when the component renders in different places + > span { + top: 13px !important; + } + } +} + +.codeView ::selection { + background-color: rgba(200, 200, 200, 0.25); +} + +.img { + width: 20px; + margin-right: 10px; +} + +.fileName { + display: flex; + align-items: baseline; +} + +.emptyCodeView { + margin: auto; + text-align: center; + font-size: 24px; +} + +.highlightedRow, +.highlightedRow * { + background-color: rgb(97, 82, 207, 0.2) !important; // Ensure that this color is applied to all children as well +} + +:global { + // after we set the line number to be inline we need to override the styles to make it look like it used to + .linenumber { + color: rgb(212, 212, 212) !important; + border-right: 1px solid #323232; + text-align: center !important; + padding-right: 0 !important; + margin-right: 1em !important; + user-select: none; + cursor: pointer; + } +} diff --git a/components/ui/code-view/code-view.tsx b/components/ui/code-view/code-view.tsx new file mode 100644 index 000000000000..a2cf200f6ebc --- /dev/null +++ b/components/ui/code-view/code-view.tsx @@ -0,0 +1,295 @@ +import { H1 } from '@teambit/documenter.ui.heading'; +import classNames from 'classnames'; +import React, { HTMLAttributes, useMemo } from 'react'; +import { CodeSnippet } from '@teambit/documenter.ui.code-snippet'; +import { createElement } from 'react-syntax-highlighter'; +import { useFileContent } from '@teambit/code.ui.queries.get-file-content'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light'; +import markDownSyntax from 'react-syntax-highlighter/dist/esm/languages/prism/markdown'; +import { staticStorageUrl } from '@teambit/base-ui.constants.storage'; +import { useLocation, useNavigate } from '@teambit/base-react.navigation.link'; +import { ComponentUrl } from '@teambit/component.modules.component-url'; +import { DependencyType } from '@teambit/code.ui.queries.get-component-code'; +import { ComponentID } from '@teambit/component'; +import { useCoreAspects } from '@teambit/harmony.ui.hooks.use-core-aspects'; +import styles from './code-view.module.scss'; + +export type CodeViewProps = { + componentId: ComponentID; + currentFile?: string; + currentFileContent?: string; + icon?: string; + loading?: boolean; + codeSnippetClassName?: string; + dependencies?: DependencyType[]; +} & HTMLAttributes; + +SyntaxHighlighter.registerLanguage('md', markDownSyntax); + +const extractLineNumber = (hash?: string) => { + if (!hash) return null; + const match = hash.match(/#l(\d+)/); + return match ? parseInt(match[1], 10) : null; +}; + +const extractSearchKeyword = (hash?: string) => { + if (!hash) return null; + const match = hash.match(/#search=(.*)/); + return match ? decodeURIComponent(match[1]) : null; +}; + +const extractLineRange = (hash) => { + if (!hash) return null; + + // Regex to match the range pattern (e.g., #l5-10) + const match = hash.match(/#l(\d+)-(\d+)/); + if (match && match.length === 3) { + const start = parseInt(match[1], 10); + const end = parseInt(match[2], 10); + return { start, end }; + } + + return null; +}; + +function joinPaths(base: string, relative: string) { + // Ensure we start with a base directory path + const baseParts = base.endsWith('/') ? base.split('/') : base.split('/').slice(0, -1); + const relativeParts = relative.split('/'); + + for (const part of relativeParts) { + if (part === '.' || part === '') { + // No operation needed for current directory marker or empty parts. + } else if (part === '..') { + // Navigate up one directory level + baseParts.pop(); + } else { + // Navigate into a new directory level + baseParts.push(part); + } + } + + // Reconstruct the path from the parts + // Also, prevent creating a leading '/' to keep the path relative to the base + const newPath = baseParts.join('/'); + return newPath.startsWith('/') ? newPath : `/${newPath}`; +} + +export function CodeView({ + className, + componentId, + currentFile, + icon, + currentFileContent, + codeSnippetClassName, + loading: loadingFromProps, + dependencies, +}: CodeViewProps) { + const depsByPackageName = new Map( + (dependencies || []).map((dep) => [(dep.packageName || dep.id).toString(), dep]) + ); + const coreAspects = useCoreAspects(); + const { fileContent: downloadedFileContent, loading: loadingFileContent } = useFileContent( + componentId, + currentFile, + !!currentFileContent + ); + const loading = loadingFromProps || loadingFileContent; + const location = useLocation(); + const navigate = useNavigate(); + const [scrollBlock, setScrollBlock] = React.useState<'nearest' | 'center'>('center'); + const highlightedLineRef = React.useRef(null); + const fileContent = currentFileContent || downloadedFileContent; + const title = useMemo(() => currentFile?.split('/').pop(), [currentFile]); + const lang = useMemo(() => { + const langFromFileEnding = currentFile?.split('.').pop(); + + // for some reason, SyntaxHighlighter doesnt support scss or sass highlighting, only css. I need to check how to fix this properly + if (langFromFileEnding === 'scss' || langFromFileEnding === 'sass') return 'css'; + if (langFromFileEnding === 'mdx') return 'md'; + return langFromFileEnding; + }, [fileContent]); + + const lineNumber = extractLineNumber(location?.hash); + const searchKeyword = extractSearchKeyword(location?.hash); + const lineRange = extractLineRange(location?.hash); + + React.useEffect(() => { + if (highlightedLineRef?.current) { + highlightedLineRef?.current.scrollIntoView({ behavior: 'smooth', block: scrollBlock }); + } + }, [highlightedLineRef?.current, scrollBlock]); + + const onLineClicked = React.useCallback( + (_lineNumber: number) => () => { + setScrollBlock('nearest'); + // If the line number is already highlighted, remove the hash + if ( + lineNumber === _lineNumber || + (lineRange && _lineNumber >= lineRange?.start && _lineNumber <= lineRange?.end) + ) { + navigate(`${location?.pathname}${location?.search}`, { replace: true }); + } else { + navigate(`${location?.pathname}${location?.search}#l${_lineNumber}`, { replace: true }); + } + }, + [lineNumber, lineRange, location?.pathname, location?.search] + ); + + const customRenderer = React.useCallback( + ({ rows, stylesheet, useInlineStyles }) => { + let isKeywordHighlighted = false; + + return rows.map((node, index) => { + // console.log('🚀 ~ returnrows.map ~ node:', node); + const lineText = node.children + .map((child) => child.children?.[0]?.value ?? '') + .join('') + .trim(); + + node.children.forEach((child) => { + if (child.properties?.className?.includes('string') && child.children[0]?.value?.length > 3) { + const packageNameOrPath = child.children[0].value.replace(/['"]/g, ''); + const dep = depsByPackageName.get(packageNameOrPath); + const isRelativePath = packageNameOrPath.startsWith('.'); + const filePath = isRelativePath && currentFile ? joinPaths(currentFile, packageNameOrPath) : undefined; + if (filePath) { + child.properties = { + ...child.properties, + onClick: (e) => { + e.stopPropagation(); + navigate(`${filePath.substring(1)}${location?.search}`); + }, + style: { cursor: 'pointer', textDecoration: 'underline' }, + }; + } + + if (dep || coreAspects[packageNameOrPath]) { + const id = dep + ? (dep?.__typename === 'ComponentDependency' && ComponentID.fromString(dep.id)) || undefined + : ComponentID.fromString(coreAspects[packageNameOrPath]); + const compUrl = id && ComponentUrl.toUrl(id, { includeVersion: true }); + const [compIdUrl, version] = compUrl ? compUrl.split('?') : [undefined, undefined]; + const link = compIdUrl + ? `${compIdUrl}/~code?${version}` + : `https://www.npmjs.com/package/${packageNameOrPath}`; + + child.tagName = 'a'; + child.properties = { ...child.properties, href: link, target: '_blank', rel: 'noopener noreferrer' }; + } + } + }); + + const lineNum = index + 1; + const matchesSearchWord = !isKeywordHighlighted && searchKeyword && lineText.includes(searchKeyword); + const isInRange = lineRange && lineNum >= lineRange.start && lineNum <= lineRange.end; + const isHighlighted = lineNum === lineNumber || matchesSearchWord || isInRange; + if (isHighlighted && searchKeyword) isKeywordHighlighted = true; + + let lineNumberNode; + + if (node.children.length > 0 && node.children[0].properties.className.includes('linenumber')) { + lineNumberNode = node.children[0]; + node = { + ...node, + children: node.children.slice(1), + }; + } + + const lineNumberElement = + lineNumberNode && + createElement({ + node: lineNumberNode, + stylesheet, + useInlineStyles, + key: `line-number-${index}`, + }); + + const highlightedRef = !isInRange + ? (isHighlighted && highlightedLineRef) || null + : (lineNum === lineRange.start && highlightedLineRef) || null; + + return ( +
+ {lineNumberElement} + {createElement({ + key: index, + node, + stylesheet, + useInlineStyles, + })} +
+ ); + }); + }, + [onLineClicked, searchKeyword, lineNumber, lineRange, coreAspects, depsByPackageName.size, currentFile] + ); + + const getSelectedLineRange = () => { + const selection = window.getSelection(); + if (!selection || !selection.rangeCount) return null; + + const startNode = selection.getRangeAt(0).startContainer; + const endNode = selection.getRangeAt(0).endContainer; + + // Traverse up to find the parent divs that contain the line numbers + const findLineNumberElement = (node) => { + while (node && node.nodeName !== 'DIV') { + node = node.parentNode; + } + return node.querySelector('.linenumber'); + }; + + const startLineNumberElement = findLineNumberElement(startNode); + const endLineNumberElement = findLineNumberElement(endNode); + + const startLineNumber = startLineNumberElement ? parseInt(startLineNumberElement.textContent, 10) : null; + const endLineNumber = endLineNumberElement ? parseInt(endLineNumberElement.textContent, 10) : null; + + return startLineNumber && endLineNumber ? { start: startLineNumber, end: endLineNumber } : null; + }; + + const handleMouseUp = React.useCallback(() => { + const selectedRange = getSelectedLineRange(); + if (!selectedRange || selectedRange.end === selectedRange.start) return; + navigate(`${location?.pathname}${location?.search}#l${selectedRange.start}-${selectedRange.end}`, { + replace: true, + }); + }, [location?.pathname, location?.search]); + + if (!fileContent && !loading && currentFile) return ; + + return ( +
+

+ {currentFile && } + {title} +

+ + {fileContent || ''} + +
+ ); +} + +function EmptyCodeView() { + return ( +
+ +
Nothing to show
+
+ ); +} diff --git a/components/ui/code-view/index.ts b/components/ui/code-view/index.ts new file mode 100644 index 000000000000..3dd57f347c74 --- /dev/null +++ b/components/ui/code-view/index.ts @@ -0,0 +1,2 @@ +export { CodeView } from './code-view'; +export type { CodeViewProps } from './code-view'; diff --git a/components/ui/hooks/use-core-aspects/index.ts b/components/ui/hooks/use-core-aspects/index.ts new file mode 100644 index 000000000000..ed5a0692eca2 --- /dev/null +++ b/components/ui/hooks/use-core-aspects/index.ts @@ -0,0 +1 @@ +export { useCoreAspects } from './use-core-aspects.js'; diff --git a/components/ui/hooks/use-core-aspects/use-core-aspects.tsx b/components/ui/hooks/use-core-aspects/use-core-aspects.tsx new file mode 100644 index 000000000000..6c4610fcc5fa --- /dev/null +++ b/components/ui/hooks/use-core-aspects/use-core-aspects.tsx @@ -0,0 +1,17 @@ +import { useDataQuery } from '@teambit/ui-foundation.ui.hooks.use-data-query'; +import { gql } from '@apollo/client'; + +export type CoreAspectIdByPackageName = { + [packageName: string]: string; +}; + +const GET_CORE_ASPECTS = gql` + query CoreAspects { + coreAspects + } +`; + +export function useCoreAspects(): CoreAspectIdByPackageName { + const { data } = useDataQuery(GET_CORE_ASPECTS); + return data?.coreAspects; +} diff --git a/scopes/component/graph/index.ts b/scopes/component/graph/index.ts index 6efa85c1c07d..ef923e1b8c5a 100644 --- a/scopes/component/graph/index.ts +++ b/scopes/component/graph/index.ts @@ -1,5 +1,5 @@ export { DependenciesCompare } from './ui/dependencies-compare'; -export { Dependency } from './model/dependency'; +export { Dependency, DependencyType } from './model/dependency'; export { DuplicateDependency } from './duplicate-dependency'; export { GraphAspect as default, GraphAspect } from './graph.aspect'; export { IdGraph, objectListToGraph, bitObjectListToGraph } from './object-list-to-graph'; diff --git a/scopes/dependencies/dependencies/dependencies-loader/auto-detect-deps.ts b/scopes/dependencies/dependencies/dependencies-loader/auto-detect-deps.ts index 18c371451ab9..f6eecf40a309 100644 --- a/scopes/dependencies/dependencies/dependencies-loader/auto-detect-deps.ts +++ b/scopes/dependencies/dependencies/dependencies-loader/auto-detect-deps.ts @@ -24,7 +24,7 @@ import { } from '@teambit/legacy/dist/consumer/component/dependencies/files-dependency-builder/types/dependency-tree-type'; import { DevFilesMain } from '@teambit/dev-files'; import { Workspace } from '@teambit/workspace'; -import { AspectLoaderMain, getCoreAspectPackageName } from '@teambit/aspect-loader'; +import { AspectLoaderMain } from '@teambit/aspect-loader'; import { ResolvedPackageData } from '@teambit/legacy/dist/utils/packages'; import { DependencyDetector } from '@teambit/legacy/dist/consumer/component/dependencies/files-dependency-builder/detector-hook'; import { packageToDefinetlyTyped } from './package-to-definetly-typed'; @@ -507,18 +507,6 @@ export class AutoDetectDeps { } else this.issues.getOrCreate(IssuesClasses.ResolveErrors).data[originFile] = error.message; } - private getCoreAspectsPackagesAndIds(): Record { - const allCoreAspectsIds = this.aspectLoader.getCoreAspectIds(); - const coreAspectsPackagesAndIds = {}; - - allCoreAspectsIds.forEach((id) => { - const packageName = getCoreAspectPackageName(id); - coreAspectsPackagesAndIds[packageName] = id; - }); - - return coreAspectsPackagesAndIds; - } - /** * when a user uses core-extensions these core-extensions should not be dependencies. * here, we filter them out from all places they could entered as dependencies. @@ -526,7 +514,7 @@ export class AutoDetectDeps { * which case we recognizes that the current originFile is a core-extension and avoid filtering. */ private processCoreAspects(originFile: PathLinuxRelative) { - const coreAspects = this.getCoreAspectsPackagesAndIds(); + const coreAspects = this.aspectLoader.getCoreAspectsPackagesAndIds(); // const scopes = coreAspects.map((id) => { // const id = id.split() diff --git a/scopes/harmony/aspect-loader/aspect-loader.graphql.ts b/scopes/harmony/aspect-loader/aspect-loader.graphql.ts new file mode 100644 index 000000000000..30458dcb99dd --- /dev/null +++ b/scopes/harmony/aspect-loader/aspect-loader.graphql.ts @@ -0,0 +1,23 @@ +import { Schema } from '@teambit/graphql'; +import gql from 'graphql-tag'; + +import { AspectLoaderMain } from './aspect-loader.main.runtime'; + +export function aspectLoaderSchema(aspectLoaderMain: AspectLoaderMain): Schema { + return { + typeDefs: gql` + scalar JSONObject + + type Query { + coreAspects: JSONObject + } + `, + resolvers: { + Query: { + coreAspects: async () => { + return aspectLoaderMain.getCoreAspectsPackagesAndIds(); + }, + }, + }, + }; +} diff --git a/scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts b/scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts index 0552d559c4b1..3231ec4269eb 100644 --- a/scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts +++ b/scopes/harmony/aspect-loader/aspect-loader.main.runtime.ts @@ -18,6 +18,7 @@ import { replaceFileExtToJs } from '@teambit/compilation.modules.babel-compiler' import { EnvsAspect, EnvsMain } from '@teambit/envs'; import { loadBit } from '@teambit/bit'; import { ScopeAspect, ScopeMain } from '@teambit/scope'; +import { GraphqlAspect, GraphqlMain } from '@teambit/graphql'; import mapSeries from 'p-map-series'; import { difference, compact, flatten, intersection, uniqBy, some, isEmpty, isObject } from 'lodash'; import { AspectDefinition, AspectDefinitionProps } from './aspect-definition'; @@ -26,8 +27,9 @@ import { AspectLoaderAspect } from './aspect-loader.aspect'; import { UNABLE_TO_LOAD_EXTENSION, UNABLE_TO_LOAD_EXTENSION_FROM_LIST } from './constants'; import { isEsmModule } from './is-esm-module'; import { CannotLoadExtension } from './exceptions'; -import { getAspectDef } from './core-aspects'; +import { getAspectDef, getCoreAspectPackageName } from './core-aspects'; import { Plugins } from './plugins'; +import { aspectLoaderSchema } from './aspect-loader.graphql'; export type PluginDefinitionSlot = SlotRegistry; @@ -127,6 +129,18 @@ export class AspectLoaderMain { private pluginSlot: PluginDefinitionSlot ) {} + getCoreAspectsPackagesAndIds(): Record { + const allCoreAspectsIds = this.getCoreAspectIds(); + const coreAspectsPackagesAndIds = {}; + + allCoreAspectsIds.forEach((id) => { + const packageName = getCoreAspectPackageName(id); + coreAspectsPackagesAndIds[packageName] = id; + }); + + return coreAspectsPackagesAndIds; + } + private getCompiler(component: Component) { const env = this.envs.getEnv(component)?.env; return env?.getCompiler(); @@ -820,7 +834,7 @@ export class AspectLoaderMain { } static runtime = MainRuntime; - static dependencies = [LoggerAspect, EnvsAspect]; + static dependencies = [LoggerAspect, EnvsAspect, GraphqlAspect]; static slots = [ Slot.withType(), Slot.withType(), @@ -828,7 +842,7 @@ export class AspectLoaderMain { ]; static async provider( - [loggerExt, envs]: [LoggerMain, EnvsMain], + [loggerExt, envs, graphql]: [LoggerMain, EnvsMain, GraphqlMain], config, [onAspectLoadErrorSlot, onLoadRequireableExtensionSlot, pluginSlot]: [ OnAspectLoadErrorSlot, @@ -847,6 +861,7 @@ export class AspectLoaderMain { pluginSlot ); + graphql.register(aspectLoaderSchema(aspectLoader)); aspectLoader.registerPlugins([envs.getEnvPlugin()]); return aspectLoader; diff --git a/workspace.jsonc b/workspace.jsonc index 547c58544243..2e87f4ed46c6 100644 --- a/workspace.jsonc +++ b/workspace.jsonc @@ -97,7 +97,6 @@ "@teambit/code.ui.code-compare-section": "0.0.5", "@teambit/code.ui.code-editor": "^0.0.8", "@teambit/code.ui.code-tab-tree": "^0.0.613", - "@teambit/code.ui.code-view": "^0.0.519", "@teambit/code.ui.dependency-tree": "0.0.546", "@teambit/code.ui.hooks.use-code-params": "0.0.496", "@teambit/code.ui.object-formatter": "0.0.1",