Skip to content

Commit

Permalink
normalize entry source files (#105)
Browse files Browse the repository at this point in the history
* normalize entry source files

* infer source and output options for loader

* fix tests

* add changeset

* filter internal components
  • Loading branch information
souporserious committed Apr 28, 2024
1 parent c57b51f commit f05b552
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 137 deletions.
39 changes: 39 additions & 0 deletions .changeset/hip-eagles-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'mdxts': minor
---

Normalizes the internal `getEntrySourceFiles` utility that is responsible for determining what TypeScript data sources are public based on `package.json` exports, index files, and top-level directories.

To determine what source files should be considered public when dealing with package exports, `createSource` gets two new options used to remap `package.json` exports to their original source files:

```ts
import { createSource } from 'mdxts'

const allPackages = createSource('../packages/mdxts/src/**/*.{ts,tsx}', {
sourceDirectory: 'src',
outputDirectory: 'dist',
})
```

Using a subset of the `mdxts` `package.json` exports as an example:

```json
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"require": "./dist/cjs/index.js"
},
"./components": {
"types": "./dist/src/components/index.d.ts",
"import": "./dist/src/components/index.js",
"require": "./dist/cjs/components/index.js"
},
},
```

These would be remapped to their original source files, filtering out any paths gathered from the `createSource` pattern not explicitly exported:

```json
["../packages/mdxts/src/index.ts", "../packages/mdxts/src/components/index.ts"]
```
2 changes: 1 addition & 1 deletion packages/mdxts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"hast-util-to-string": "^3.0.0",
"mdast-util-to-string": "^4.0.0",
"minimatch": "^9.0.4",
"read-package-up": "^11.0.0",
"read-pkg-up": "^7.0.1",
"rehype-infer-reading-time-meta": "^2.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^3.0.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/mdxts/src/components/ContentRefresh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { useRouter } from 'next/navigation'

const ws = new WebSocket(`ws://localhost:${process.env.MDXTS_REFRESH_PORT}/ws`)

/**
* Refreshes the Next.js development server when MDX or TypeScript source files change.
* @internal
*/
export function ContentRefresh({
mdxPath,
tsPath,
Expand Down
4 changes: 4 additions & 0 deletions packages/mdxts/src/components/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createContext } from '../utils/context'

/**
* Manages passing the current tree's `workingDirectory` to descendant Server Components.
* @internal
*/
export const Context = createContext<{
workingDirectory?: string
}>({
Expand Down
4 changes: 4 additions & 0 deletions packages/mdxts/src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use client'
import * as React from 'react'

/**
* Copies a value to the user's clipboard.
* @internal
*/
export function CopyButton({
value,
style,
Expand Down
5 changes: 4 additions & 1 deletion packages/mdxts/src/components/PackageInstall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const packageManagers = {
yarn: 'yarn add',
}

/** Renders a package install command with a variant for each package manager. */
/**
* Renders a package install command with a variant for each package manager.
* @internal
*/
export async function PackageInstall({
packages,
style,
Expand Down
4 changes: 4 additions & 0 deletions packages/mdxts/src/components/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use server'
import { readFile, writeFile } from 'node:fs/promises'

/**
* Modify the MDX code block source to allow errors.
* @internal
*/
export async function showErrors({
sourcePath,
sourcePathLine,
Expand Down
21 changes: 20 additions & 1 deletion packages/mdxts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ export function createSource<Type extends { frontMatter: Record<string, any> }>(
* the hostname (e.g. `/docs` in `https://mdxts.com/docs`).
*/
basePathname?: string

/**
* The source directory used to calculate package export paths. This is useful when the source is
* located in a different workspace than the project rendering it.
*/
sourceDirectory?: string

/**
* The output directory for built files used to calculate package export paths. This is useful
* when the source is located in a different workspace than the project rendering it.
*/
outputDirectory?: string | string[]
} = {}
) {
let allModules = arguments[2] as AllModules
Expand All @@ -111,13 +123,20 @@ export function createSource<Type extends { frontMatter: Record<string, any> }>(
])
)

const { baseDirectory = '', basePathname = '' } = options || {}
const {
baseDirectory = '',
basePathname = '',
sourceDirectory,
outputDirectory,
} = options || {}
const allData = getAllData<Type>({
allModules,
globPattern,
project,
baseDirectory,
basePathname,
sourceDirectory,
outputDirectory,
})
const filteredDataKeys = Object.keys(allData).filter((pathname) => {
const moduleData = allData[pathname]
Expand Down
31 changes: 16 additions & 15 deletions packages/mdxts/src/loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { dirname, join, relative, resolve, sep } from 'node:path'
import { glob } from 'fast-glob'
import globParent from 'glob-parent'
import { Node, Project, SyntaxKind } from 'ts-morph'
import { addComputedTypes } from '@tsxmod/utils'
import { addComputedTypes, resolveObject } from '@tsxmod/utils'

import { project } from '../components/project'
import { getEntrySourceFiles } from '../utils/get-entry-source-files'
import { getExportedSourceFiles } from '../utils/get-exported-source-files'
import { getSharedDirectoryPath } from '../utils/get-shared-directory-path'

/**
* A Webpack loader that exports front matter data for MDX files and augments `createSource` call sites to add an additional
Expand Down Expand Up @@ -92,7 +92,6 @@ export default async function loader(

/** Search for MDX files named the same as the source files (e.g. `Button.mdx` for `Button.tsx`) */
if (!isMdxPattern) {
const { readPackageUp } = await import('read-package-up')
const allSourceFilePaths = await glob(globPattern, {
cwd: workingDirectory,
ignore: ['**/*.examples.(ts|tsx)'],
Expand All @@ -108,18 +107,20 @@ export default async function loader(
)
}

const sharedDirectoryPath = getSharedDirectoryPath(...allPaths)
const packageJson = (
await readPackageUp({ cwd: sharedDirectoryPath })
)?.packageJson
const entrySourceFiles = project.addSourceFilesAtPaths(
packageJson?.exports
? /** If package.json exports found use that for calculating public paths. */
Object.keys(packageJson.exports).map((key) =>
join(resolve(sharedDirectoryPath, key), 'index.(ts|tsx)')
)
: /** Otherwise default to a root index file. */
resolve(sharedDirectoryPath, '**/index.(ts|tsx)')
const optionsArgument = createSourceCall.getArguments()[1]
const { sourceDirectory, outputDirectory } = (
Node.isObjectLiteralExpression(optionsArgument)
? resolveObject(optionsArgument)
: {}
) as {
sourceDirectory?: string
outputDirectory?: string
}
const entrySourceFiles = getEntrySourceFiles(
project,
allPaths,
sourceDirectory,
outputDirectory
)
const exportedSourceFiles = getExportedSourceFiles(entrySourceFiles)
const exportedSourceFilePaths = entrySourceFiles
Expand Down
4 changes: 2 additions & 2 deletions packages/mdxts/src/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextConfig } from 'next'
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants'
import { dirname, resolve } from 'node:path'
import createMdxPlugin from '@next/mdx'
import { sync as readPackageUpSync } from 'read-pkg-up'
import type { bundledThemes } from 'shiki'

import { getMdxPlugins } from '../mdx-plugins'
Expand Down Expand Up @@ -88,8 +89,7 @@ export function createMdxtsPlugin(pluginOptions: PluginOptions) {
nextConfig.env.MDXTS_SITE_URL = siteUrl

if (!theme.endsWith('.json')) {
const { readPackageUp } = await import('read-package-up')
const mdxtsPackageJson = await readPackageUp({ cwd: __dirname })
const mdxtsPackageJson = readPackageUpSync({ cwd: __dirname })
themePath = resolve(
dirname(mdxtsPackageJson!.path),
`node_modules/tm-themes/themes/${theme}.json`
Expand Down
4 changes: 2 additions & 2 deletions packages/mdxts/src/utils/get-all-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ describe('getAllData', () => {

it('includes only public files based on package.json exports', (done) => {
jest.isolateModules(() => {
jest.mock('read-package-up', () => ({
readPackageUpSync: () => ({
jest.mock('read-pkg-up', () => ({
sync: () => ({
packageJson: {
name: 'mdxts',
exports: {
Expand Down
44 changes: 14 additions & 30 deletions packages/mdxts/src/utils/get-all-data.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import parseTitle from 'title'
import { dirname, join, sep } from 'node:path'
import { readPackageUpSync } from 'read-package-up'
import type { ExportedDeclarations, Project } from 'ts-morph'
import { Directory, SourceFile } from 'ts-morph'
import { getSymbolDescription, resolveExpression } from '@tsxmod/utils'
import matter from 'gray-matter'

import { filePathToPathname } from './file-path-to-pathname'
import { getExamplesFromSourceFile } from './get-examples'
import { getEntrySourceFiles } from './get-entry-source-files'
import { getExportedSourceFiles } from './get-exported-source-files'
import { getExportedTypes } from './get-exported-types'
import { getGitMetadata } from './get-git-metadata'
Expand Down Expand Up @@ -56,9 +56,10 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
allModules,
globPattern,
project,
sourceDirectory = 'src',
baseDirectory,
basePathname = '',
sourceDirectory = 'src',
outputDirectory = 'dist',
}: {
/** A map of all MDX modules keyed by their pathname. */
allModules: AllModules
Expand All @@ -69,14 +70,17 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({
/** The ts-morph project to use for parsing source files. */
project: Project

/** The source directory used to calculate package export paths. */
sourceDirectory?: string

/** The base directory to use when calculating source paths. */
baseDirectory?: string

/** The base path to use when calculating navigation paths. */
basePathname?: string

/** The source directory used to calculate package export paths. */
sourceDirectory?: string

/** The output directory or directories for built files used to calculate package export paths. */
outputDirectory?: string | string[]
}) {
const typeScriptSourceFiles = /ts(x)?/.test(globPattern)
? project.addSourceFilesAtPaths(globPattern)
Expand All @@ -94,32 +98,12 @@ export function getAllData<Type extends { frontMatter: Record<string, any> }>({

const sharedDirectoryPath = getSharedDirectoryPath(...allPaths)
const packageMetadata = getPackageMetadata(...allPaths)
let entrySourceFiles = project.addSourceFilesAtPaths(
packageMetadata?.exports
? /** If package.json exports found use that for calculating public paths. */
Object.keys(packageMetadata.exports).map((key) =>
join(
packageMetadata.directory,
sourceDirectory,
key,
'index.{js,jsx,ts,tsx}'
)
)
: /** Otherwise default to a common root index file. */
join(sharedDirectoryPath, 'index.{js,jsx,ts,tsx}')
const entrySourceFiles = getEntrySourceFiles(
project,
allPaths,
sourceDirectory,
outputDirectory
)

/** If no root index files exist, assume the top-level directory files are all public exports. */
if (
typeScriptSourceFiles &&
!packageMetadata?.exports &&
entrySourceFiles.length === 0
) {
entrySourceFiles = project.addSourceFilesAtPaths(
join(sharedDirectoryPath, '*.{js,jsx,ts,tsx}')
)
}

const exportedSourceFiles = getExportedSourceFiles(entrySourceFiles)
const allPublicPaths = entrySourceFiles
.concat(exportedSourceFiles)
Expand Down
81 changes: 81 additions & 0 deletions packages/mdxts/src/utils/get-entry-source-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { join, resolve } from 'path'
import { Project, SourceFile } from 'ts-morph'

import { getPackageMetadata } from './get-package-metadata'
import { getSharedDirectoryPath } from './get-shared-directory-path'

const extensionPatterns = [
'.{js,jsx,ts,tsx}',
'.{examples,test}.{js,jsx,ts,tsx}',
]

/**
* Filters paths and returns TypeScript source files based on the following entry points:
* - Package.json exports
* - Root index file
* - Top-level directory files
*/
export function getEntrySourceFiles(
project: Project,
allPaths: string[],
sourceDirectory: string = 'src',
outputDirectory: string | string[] = 'dist'
): SourceFile[] {
if (typeof outputDirectory === 'string') {
outputDirectory = [outputDirectory]
}

const sharedDirectoryPath = getSharedDirectoryPath(...allPaths)
const packageMetadata = getPackageMetadata(...allPaths)
let entrySourceFiles: SourceFile[] = []

// Use package.json exports for calculating public paths if they exist.
if (packageMetadata?.exports) {
for (const exportKey in packageMetadata.exports) {
const exportValue = packageMetadata.exports[exportKey]
let exportPath = exportValue

if (typeof exportValue === 'object') {
exportPath = exportValue.import
}

const sourceFilePaths = extensionPatterns
.flatMap((pattern, index) =>
(outputDirectory as string[]).map((directory) => {
if (!exportPath.includes(directory)) {
return
}
const exportPattern = exportPath
.replace(directory, sourceDirectory)
.replace(/\.js$/, pattern)
const sourcePathPattern = resolve(
packageMetadata.directory,
exportPattern
)
// Include the first pattern and exclude examples and tests.
return index === 0 ? sourcePathPattern : `!${sourcePathPattern}`
})
)
.filter(Boolean) as string[]

entrySourceFiles.push(...project.addSourceFilesAtPaths(sourceFilePaths))
}
} else {
// Otherwise default to a common root index file.
const defaultSourcePath = join(sharedDirectoryPath, 'index.{js,jsx,ts,tsx}')

entrySourceFiles.push(...project.addSourceFilesAtPaths(defaultSourcePath))

// If no root index files exist, assume the top-level directory files are all public exports.
if (entrySourceFiles.length === 0) {
entrySourceFiles = project.addSourceFilesAtPaths(
extensionPatterns.map((pattern, index) => {
const sourcePathPattern = join(sharedDirectoryPath, `*${pattern}`)
return index === 0 ? sourcePathPattern : `!${sourcePathPattern}`
})
)
}
}

return entrySourceFiles
}

0 comments on commit f05b552

Please sign in to comment.