Skip to content

Commit

Permalink
fix data item order
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious committed Apr 29, 2024
1 parent 0f49ee9 commit 7c5df2f
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 75 deletions.
9 changes: 9 additions & 0 deletions .changeset/wild-cameras-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'mdxts': minor
---

Fixes data source ordering to use strings instead of `parseInt` to ensure that the items are always ordered correctly.

### Breaking Changes

The `order` property for a data source item is now a padded string instead of a number.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ exports[`getAllData should initialize correctly with basic input 1`] = `
"frontMatter": {},
"label": "Button",
"mdxPath": "/components/Button.mdx",
"order": 1,
"order": "01",
"pathname": "/button",
"sourcePath": "",
"title": "Button",
Expand Down
16 changes: 8 additions & 8 deletions packages/mdxts/src/utils/get-all-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,19 @@ describe('getAllData', () => {
baseDirectory: 'package',
})

expect(allData['/components/button'].order).toBe(1.1)
expect(allData['/components/card'].order).toBe(1.2)
expect(allData['/hooks/use-focus'].order).toBe(2.1)
expect(allData['/hooks/use-pressable'].order).toBe(2.2)
expect(allData['/components/button'].order).toBe('01.01')
expect(allData['/components/card'].order).toBe('01.02')
expect(allData['/hooks/use-focus'].order).toBe('02.01')
expect(allData['/hooks/use-pressable'].order).toBe('02.02')
})

it('parses order from file path', () => {
const allData = getDocsData()

expect(allData['/docs/getting-started'].order).toBe(1)
expect(allData['/docs/routing'].order).toBe(2)
expect(allData['/docs/examples/authoring'].order).toBe(3.1)
expect(allData['/docs/examples/rendering'].order).toBe(3.2)
expect(allData['/docs/getting-started'].order).toBe('01')
expect(allData['/docs/routing'].order).toBe('02')
expect(allData['/docs/examples/authoring'].order).toBe('03.01')
expect(allData['/docs/examples/rendering'].order).toBe('03.02')
})

it('adds previous and next pathnames', () => {
Expand Down
80 changes: 14 additions & 66 deletions packages/mdxts/src/utils/get-all-data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import parseTitle from 'title'
import { dirname, join, sep } from 'node:path'
import type { ExportedDeclarations, Project } from 'ts-morph'
import { Directory, SourceFile } from 'ts-morph'
import { SourceFile } from 'ts-morph'
import { getSymbolDescription, resolveExpression } from '@tsxmod/utils'
import matter from 'gray-matter'

Expand All @@ -13,6 +13,7 @@ import { getExportedTypes } from './get-exported-types'
import { getGitMetadata } from './get-git-metadata'
import { getMainExportDeclaration } from './get-main-export-declaration'
import { getNameFromDeclaration } from './get-name-from-declaration'
import { getSourceFilesOrderMap as getSourceFilesSortOrder } from './get-source-files-sort-order'
import { getSourcePath } from './get-source-path'
import { getSharedDirectoryPath } from './get-shared-directory-path'
import { getPackageMetadata } from './get-package-metadata'
Expand All @@ -27,7 +28,7 @@ export type ModuleData<Type extends { frontMatter: Record<string, any> }> = {
title: string
label: string
description?: string
order: number
order: string
depth: number
mdxPath?: string
tsPath?: string
Expand Down Expand Up @@ -281,7 +282,7 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({

// Add order, this must be done after all data has been collected and added to the project above
const sharedDirectory = project.addDirectoryAtPath(sharedDirectoryPath)
const sourceFilesSortOrder = getDirectorySourceFilesOrder(
const sourceFilesSortOrder = getSourceFilesSortOrder(
sharedDirectory,
allPublicPaths
)
Expand All @@ -291,7 +292,7 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
const isIndexOrReadme = /index|readme/i.test(sourcePath)

if (value.isMainExport) {
value.order = 0
value.order = '0'
} else if (isIndexOrReadme) {
const directoryPath = dirname(sourcePath)
if (directoryPath in sourceFilesSortOrder) {
Expand All @@ -305,6 +306,10 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
})

const sortedAndFilteredData = Object.entries(allData)
.filter(
// Filter out TypeScript modules that have no MDX content or exported types
([, data]) => data.mdxPath || data.exportedTypes.length > 0
)
.sort((a, b) => {
// Give the main export the highest priority
if (a[1].isMainExport) {
Expand All @@ -314,9 +319,11 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
return 1
}

// Sort by order if available
// Sort by order next if available
if (a[1].order && b[1].order) {
return a[1].order - b[1].order
return a[1].order.localeCompare(b[1].order, undefined, {
numeric: true,
})
}
if (a[1].order) {
return -1
Expand All @@ -328,9 +335,8 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
// Fallback to alphabetical order
return a[0].localeCompare(b[0])
})
.filter(([, data]) => data.mdxPath || data.exportedTypes.length > 0)

// Add previous/next data to each module
// Add previous/next data to each module now that they're sorted
sortedAndFilteredData.forEach(([, data], index) => {
const previousData = sortedAndFilteredData[index - 1]
const nextData = sortedAndFilteredData[index + 1]
Expand Down Expand Up @@ -446,61 +452,3 @@ function getMetadata(sourceFile: SourceFile) {
}
return null
}

/** Returns a map of source file paths to their sort order. */
function getDirectorySourceFilesOrder(
directory: Directory,
allPublicPaths: string[]
): Record<string, number> {
const orderMap: Record<string, number> = {}
traverseDirectory(directory, '', orderMap, new Set(), allPublicPaths)
return orderMap
}

/** Recursively traverses a directory, adding each file to the order map. */
function traverseDirectory(
directory: Directory,
prefix: string,
orderMap: Record<string, number>,
seenBaseNames: Set<string>,
allPublicPaths: string[]
) {
const isRoot = prefix === ''
let index = 1

if (!isRoot) {
orderMap[directory.getPath()] = parseFloat(prefix.slice(0, -1)) // Remove trailing dot from prefix and convert to float
}

const directories = directory
.getDirectories()
.sort((a, b) => a.getBaseName().localeCompare(b.getBaseName()))
const files = directory
.getSourceFiles()
.filter((file) => allPublicPaths.includes(file.getFilePath()))

// Iterate through all files in the current directory
for (const file of files) {
// Extract the base part of the file name up to the first period e.g. `Button` from `Button.test.tsx`
const baseName = file.getBaseName().split('.').at(0)
if (baseName && !seenBaseNames.has(baseName)) {
orderMap[file.getFilePath()] = parseFloat(`${prefix}${index}`)
seenBaseNames.add(baseName)
index++
} else {
orderMap[file.getFilePath()] = parseFloat(`${prefix}${index - 1}`)
}
}

// Iterate through subdirectories
for (const subdirectory of directories) {
traverseDirectory(
subdirectory,
`${prefix}${index}.`,
orderMap,
new Set(),
allPublicPaths
)
index++
}
}
52 changes: 52 additions & 0 deletions packages/mdxts/src/utils/get-source-files-sort-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Project, type Directory } from 'ts-morph'

import { getSourceFilesOrderMap } from './get-source-files-sort-order'

describe('getSourceFilesOrderMap', () => {
let project: Project
let rootDirectory: Directory

beforeEach(() => {
project = new Project({
useInMemoryFileSystem: true,
})

rootDirectory = project.createDirectory('/src')

const componentsDirectory = rootDirectory.createDirectory('components')

componentsDirectory.createSourceFile('Button.tsx', '')
componentsDirectory.createSourceFile('index.ts', '')

const codeBlockDirectory = componentsDirectory.createDirectory('CodeBlock')
codeBlockDirectory.createSourceFile('CodeBlock.tsx', '')

rootDirectory.createDirectory('utils').createSourceFile('helpers.ts', '')
})

test('should return an empty object when no files or directories are present', () => {
const emptyDirectory = project.createDirectory('/empty')
const orderMap = getSourceFilesOrderMap(emptyDirectory, [])
expect(orderMap).toEqual({})
})

test('should return the correct order for files and directories', () => {
const allPublicPaths = [
'/src/components/Button.tsx',
'/src/components/CodeBlock/CodeBlock.tsx',
'/src/utils/helpers.ts',
]

const orderMap = getSourceFilesOrderMap(rootDirectory, allPublicPaths)

const expectedOrderMap = {
'/src/components': '01',
'/src/components/Button.tsx': '01.01',
'/src/components/CodeBlock': '01.02',
'/src/components/CodeBlock/CodeBlock.tsx': '01.02.01',
'/src/utils': '02',
'/src/utils/helpers.ts': '02.01',
}
expect(orderMap).toEqual(expectedOrderMap)
})
})
73 changes: 73 additions & 0 deletions packages/mdxts/src/utils/get-source-files-sort-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Directory } from 'ts-morph'

/** Returns a map of source file paths to their sort order. */
export function getSourceFilesOrderMap(
directory: Directory,
allPublicPaths: string[]
): Record<string, string> {
const orderMap: Record<string, string> = {}
traverseDirectory(directory, '', orderMap, allPublicPaths)
return orderMap
}

/** Recursively traverses a directory adding each file and directory to path order map. */
function traverseDirectory(
directory: Directory,
prefix: string,
orderMap: Record<string, string>,
allPublicPaths: string[],
level: number = 1,
index: number = 1
) {
const entries: {
name: string
path: string
directory?: Directory
}[] = []
const directories = directory.getDirectories()

directories.forEach((sourceDirectory) => {
entries.push({
name: sourceDirectory.getBaseName(),
path: sourceDirectory.getPath(),
directory: sourceDirectory,
})
})

const files = directory
.getSourceFiles()
.filter((file) => allPublicPaths.includes(file.getFilePath()))

files.forEach((file) => {
entries.push({
name: file.getBaseName(),
path: file.getFilePath(),
})
})

// Sort alphabetically by name
entries.sort((a, b) => a.name.localeCompare(b.name))

// Iterate through each entry and assign an order
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
const entry = entries[entryIndex]
const orderString = `${prefix}${String(index).padStart(2, '0')}`

if (entry.directory) {
orderMap[entry.path] = orderString

traverseDirectory(
entry.directory,
`${orderString}.`,
orderMap,
allPublicPaths,
level + 1,
1
)
} else {
orderMap[entry.path] = orderString
}

index++
}
}

0 comments on commit 7c5df2f

Please sign in to comment.