@@ -1,15 +0,0 @@ | |||
import {FC, ReactNode} from 'react'; | |||
export interface WrapperProps { | |||
children: ReactNode; | |||
} | |||
export const Wrapper: FC<WrapperProps> = ({ | |||
children, | |||
}) => { | |||
return ( | |||
<div className="amanuensis-wrapper"> | |||
{children} | |||
</div> | |||
) | |||
}; |
@@ -1,8 +1,18 @@ | |||
import { readFile, stat, writeFile } from 'fs/promises'; | |||
import { stat, writeFile } from 'fs/promises'; | |||
import { resolve } from 'path'; | |||
import { Argv } from 'yargs'; | |||
import { Stats } from 'fs'; | |||
import { getPackages, TypedocData } from '../utils/data'; | |||
import { getPackages } from '../utils/data'; | |||
import { useBasePath } from '../mixins/base-path'; | |||
import { CommandError } from '../utils/error'; | |||
import { useInternalPath } from '../mixins/internal-path'; | |||
export enum GenerateReturnCode { | |||
SUCCESS = 0, | |||
NO_TYPEDOC_JSON = -2, | |||
COULD_NOT_GENERATE_TYPEDOC_DATA = -3, | |||
COULD_NOT_GENERATE_PACKAGE_DATA = -4, | |||
} | |||
const ensureTypedocJson = async (typedocPath: string) => { | |||
const trueTypedocPath = resolve(typedocPath); | |||
@@ -15,62 +25,59 @@ const ensureTypedocJson = async (typedocPath: string) => { | |||
const err = errRaw as NodeJS.ErrnoException; | |||
if (err.code === 'ENOENT') { | |||
process.stdout.write('no\n'); | |||
process.stderr.write('Could not find typedoc.json\n'); | |||
throw new Error('Could not find typedoc.json'); | |||
throw new CommandError(GenerateReturnCode.NO_TYPEDOC_JSON, 'Could not find typedoc.json'); | |||
} | |||
process.stdout.write('maybe?\n'); | |||
process.stderr.write('Could not ensure typedoc.json\n'); | |||
throw err; | |||
throw new CommandError(GenerateReturnCode.NO_TYPEDOC_JSON, 'Could not ensure typedoc.json', err); | |||
} | |||
if (statResult.isDirectory()) { | |||
process.stdout.write('no\n'); | |||
process.stderr.write('typedoc.json is a directory\n'); | |||
throw new Error('typedoc.json is a directory'); | |||
throw new CommandError(GenerateReturnCode.NO_TYPEDOC_JSON, 'typedoc.json is a directory'); | |||
} | |||
process.stdout.write('yes\n'); | |||
}; | |||
const generateTypedocData = async () => { | |||
const generateTypedocData = async (outPath: string, basePath: string) => { | |||
process.stdout.write('Generating typedoc data...\n'); | |||
const outPath = resolve(__dirname, '..', '..', '..', '.amanuensis', 'data.json'); | |||
const typedocBinPath = resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'typedoc'); | |||
const typedocBinPath = await useInternalPath('node_modules', '.bin', 'typedoc'); | |||
const { execa } = await import('execa'); | |||
await execa(typedocBinPath, ['--json', outPath], { | |||
const result = await execa(typedocBinPath, ['--json', outPath, '--pretty', 'false'], { | |||
stdout: 'inherit', | |||
stderr: 'inherit', | |||
cwd: basePath, | |||
}); | |||
if (result.exitCode !== 0) { | |||
process.stdout.write('failed\n'); | |||
throw new CommandError(GenerateReturnCode.COULD_NOT_GENERATE_TYPEDOC_DATA, 'Could not generate typedoc data'); | |||
} | |||
process.stdout.write('done\n'); | |||
}; | |||
const produceGroupings = async () => { | |||
const generatePackageData = async ( | |||
typedocDataJsonPath: string, | |||
configPath: string, | |||
basePath: string, | |||
) => { | |||
process.stdout.write('Grouping typedoc data...\n'); | |||
const typedocDataJsonPath = resolve(__dirname, '..', '..', '..', '.amanuensis', 'data.json'); | |||
const typedocDataJson = await readFile(typedocDataJsonPath, 'utf-8'); | |||
const typedocData = JSON.parse(typedocDataJson) as TypedocData; | |||
const packages = await getPackages(process.cwd()); | |||
const groupings = { | |||
packages, | |||
typedocData, | |||
}; | |||
await writeFile(typedocDataJsonPath, JSON.stringify(groupings, null, 2)); | |||
const packages = await getPackages(configPath, basePath); | |||
process.stdout.write(`File written to ${typedocDataJsonPath}\n`); | |||
try { | |||
await writeFile(typedocDataJsonPath, JSON.stringify(packages)); | |||
process.stdout.write(`File written to ${typedocDataJsonPath}\n`); | |||
} catch (errRaw) { | |||
const err = errRaw as NodeJS.ErrnoException; | |||
process.stderr.write(`Could not write to ${typedocDataJsonPath}: ${err.message}\n`); | |||
throw new CommandError(GenerateReturnCode.COULD_NOT_GENERATE_PACKAGE_DATA, 'Could not generate package data', err); | |||
} | |||
}; | |||
export const description = 'Generate documentation from typedoc.json' as const; | |||
export enum GenerateReturnCode { | |||
SUCCESS = 0, | |||
NO_TYPEDOC_JSON = -1, | |||
COULD_NOT_GENERATE_TYPEDOC_DATA = -2, | |||
COULD_NOT_PRODUCE_GROUPINGS = -3, | |||
} | |||
export const description = 'Analyze project for fetching documentation data' as const; | |||
export interface AnalyzeArgs { | |||
typedocJsonPath?: string; | |||
@@ -84,26 +91,20 @@ export const builder = (yargs: Argv) => yargs | |||
}); | |||
const analyze = async (args: AnalyzeArgs) => { | |||
const { | |||
typedocJsonPath = resolve(process.cwd(), 'typedoc.json'), | |||
} = args; | |||
const basePath = await useBasePath(); | |||
const configPath = await useBasePath('amanuensis.config.json'); | |||
const typedocJsonPath = args.typedocJsonPath ?? await useBasePath('typedoc.json'); | |||
const typedocOutPath = await useInternalPath('.amanuensis', 'types.json'); | |||
const packagesPath = await useInternalPath('.amanuensis', 'packages.json'); | |||
try { | |||
await ensureTypedocJson(typedocJsonPath); | |||
} catch { | |||
return GenerateReturnCode.NO_TYPEDOC_JSON; | |||
} | |||
try { | |||
await generateTypedocData(); | |||
} catch { | |||
return GenerateReturnCode.COULD_NOT_GENERATE_TYPEDOC_DATA; | |||
} | |||
try { | |||
await produceGroupings(); | |||
} catch { | |||
return GenerateReturnCode.COULD_NOT_PRODUCE_GROUPINGS; | |||
await generateTypedocData(typedocOutPath, basePath); | |||
await generatePackageData(packagesPath, configPath, basePath); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
return err.exitCode; | |||
} | |||
return GenerateReturnCode.SUCCESS; | |||
@@ -1,11 +1,26 @@ | |||
import { cp } from 'fs/promises'; | |||
import { resolve } from 'path'; | |||
import { Argv } from 'yargs'; | |||
import { useInternalPath } from '../mixins/internal-path'; | |||
import { CommandError } from '../utils/error'; | |||
export enum InitReturnCode { | |||
SUCCESS = 0, | |||
COULD_NOT_COPY_FILES = -2, | |||
COULD_NOT_INSTALL_DEPENDENCIES = -3, | |||
} | |||
const copyFiles = async () => { | |||
const srcPath = resolve(__dirname, '..', '..', '..', 'src', 'next'); | |||
const destPath = resolve(__dirname, '..', '..', '..', '.amanuensis', 'next'); | |||
await cp(srcPath, destPath, { recursive: true }); | |||
try { | |||
const srcPath = await useInternalPath('src', 'next'); | |||
const destPath = await useInternalPath('.amanuensis', 'next'); | |||
await cp(srcPath, destPath, { recursive: true }); | |||
} catch (errRaw) { | |||
const err = errRaw as NodeJS.ErrnoException; | |||
throw new CommandError( | |||
InitReturnCode.COULD_NOT_COPY_FILES, | |||
`Could not copy files: ${err.message}`, | |||
); | |||
} | |||
}; | |||
interface PackageManager { | |||
@@ -54,14 +69,18 @@ const installDependencies = async () => { | |||
} | |||
const { [selectedPackageManagerIndex]: selectedPackageManager } = packageManagers; | |||
const cwd = resolve(__dirname, '..', '..', '..', '.amanuensis', 'next'); | |||
const cwd = await useInternalPath('.amanuensis', 'next'); | |||
process.stdout.write(`In path: ${cwd}\n`); | |||
process.stdout.write(`Installing dependencies with ${selectedPackageManager.name}\n`); | |||
await execa( | |||
const result = await execa( | |||
selectedPackageManager.installCmd[0], | |||
selectedPackageManager.installCmd[1], | |||
{ cwd }, | |||
); | |||
if (result.exitCode !== 0) { | |||
throw new CommandError(InitReturnCode.COULD_NOT_INSTALL_DEPENDENCIES, 'Could not install dependencies'); | |||
} | |||
}; | |||
export const description = 'Initialize a new Amanuensis project' as const; | |||
@@ -69,9 +88,15 @@ export const description = 'Initialize a new Amanuensis project' as const; | |||
export const builder = (yargsBuilder: Argv) => yargsBuilder; | |||
const init = async () => { | |||
await copyFiles(); | |||
await installDependencies(); | |||
return 0; | |||
try { | |||
await copyFiles(); | |||
await installDependencies(); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
return err.exitCode; | |||
} | |||
return InitReturnCode.SUCCESS; | |||
}; | |||
export default init; |
@@ -1,17 +1,27 @@ | |||
import { | |||
cp, readFile, rm, stat, writeFile, | |||
} from 'fs/promises'; | |||
import { dirname, resolve, basename, extname, join } from 'path'; | |||
import { | |||
dirname, resolve, basename, extname, join, | |||
} from 'path'; | |||
import { Argv } from 'yargs'; | |||
import { mkdirp } from 'mkdirp'; | |||
import { TypedocData } from '../utils/data'; | |||
import { PackageData } from '../utils/data'; | |||
import { CommandError } from '../utils/error'; | |||
import { useBasePath } from '../mixins/base-path'; | |||
import { useInternalPath } from '../mixins/internal-path'; | |||
export enum GenerateReturnCode { | |||
SUCCESS = 0, | |||
COULD_NOT_GENERATE_PAGES = -2, | |||
} | |||
const linkComponents = async () => { | |||
const linkComponents = async (cwd: string) => { | |||
process.stdout.write('Linking components...\n'); | |||
const projectCwd = resolve(process.cwd(), '.amanuensis'); | |||
const defaultCwd = resolve(__dirname, '..', '..', '..', 'src', 'next'); | |||
const destCwd = resolve(__dirname, '..', '..', '..', '.amanuensis', 'next'); | |||
const projectCwd = resolve(cwd, '.amanuensis'); | |||
const defaultCwd = await useInternalPath('src', 'next'); | |||
const destCwd = await useInternalPath('.amanuensis', 'next'); | |||
const componentsList = [ | |||
'components/Wrapper.tsx', | |||
]; | |||
@@ -21,6 +31,7 @@ const linkComponents = async () => { | |||
} catch { | |||
// noop | |||
} | |||
await Promise.all(componentsList.map(async (componentPath) => { | |||
const destPath = resolve(destCwd, componentPath); | |||
let baseCwd = projectCwd; | |||
@@ -42,23 +53,24 @@ const linkComponents = async () => { | |||
process.stdout.write(`Linked ${componentPath}\n`); | |||
})); | |||
const typedocDataJsonPath = resolve(__dirname, '..', '..', '..', '.amanuensis', 'data.json'); | |||
const typedocDataJson = await readFile(typedocDataJsonPath, 'utf-8'); | |||
const typedocData = JSON.parse(typedocDataJson) as TypedocData; | |||
await Promise.all( | |||
typedocData.packages.map(async (pkg: any) => { | |||
await mkdirp(resolve(destCwd, 'content', pkg.basePath)); | |||
await mkdirp(resolve(destCwd, 'pages', pkg.basePath)); | |||
await Promise.all( | |||
pkg.markdown.map(async (m: any) => { | |||
const srcPath = resolve(process.cwd(), pkg.basePath, m.filePath); | |||
const destPath = resolve(destCwd, 'content', pkg.basePath, m.name); | |||
const pageDestPath = resolve(destCwd, 'pages', pkg.basePath, `${basename(m.name, extname(m.name))}.tsx`); | |||
await cp(srcPath, destPath); | |||
await writeFile( | |||
pageDestPath, | |||
`import {NextPage} from 'next'; | |||
const packagesPath = await useInternalPath('.amanuensis', 'packages.json'); | |||
const typedocDataJson = await readFile(packagesPath, 'utf-8'); | |||
const typedocData = JSON.parse(typedocDataJson) as PackageData[]; | |||
try { | |||
await Promise.all( | |||
typedocData.map(async (pkg) => { | |||
await mkdirp(resolve(destCwd, 'content', pkg.basePath)); | |||
await mkdirp(resolve(destCwd, 'pages', pkg.basePath)); | |||
await Promise.all( | |||
pkg.markdown.map(async (m) => { | |||
const srcPath = resolve(cwd, pkg.basePath, m.filePath); | |||
const destPath = resolve(destCwd, 'content', pkg.basePath, m.name); | |||
const pageDestPath = resolve(destCwd, 'pages', pkg.basePath, `${basename(m.name, extname(m.name))}.tsx`); | |||
await cp(srcPath, destPath); | |||
await writeFile( | |||
pageDestPath, | |||
`import {NextPage} from 'next'; | |||
import {Wrapper} from '@/components/Wrapper'; | |||
import Content from '@/${join('content', pkg.basePath, m.name)}'; | |||
@@ -72,29 +84,32 @@ const IndexPage: NextPage = () => { | |||
export default IndexPage; | |||
`, | |||
// todo fix problem when building with import aliases | |||
// todo find a way to build with tailwind | |||
// todo link components to next project | |||
); | |||
}), | |||
); | |||
}), | |||
); | |||
const srcPath = resolve(process.cwd(), 'README.md'); | |||
const destPath = resolve(destCwd, 'content', 'index.md'); | |||
await cp(srcPath, destPath); | |||
// todo fix problem when building with import aliases | |||
// todo find a way to build with tailwind | |||
// todo link components to next project | |||
); | |||
}), | |||
); | |||
}), | |||
); | |||
} catch (errRaw) { | |||
console.log(errRaw); | |||
throw new CommandError(GenerateReturnCode.COULD_NOT_GENERATE_PAGES, 'Could not write inner page file', errRaw as Error); | |||
} | |||
try { | |||
const srcPath = resolve(cwd, 'README.md'); | |||
const destPath = resolve(destCwd, 'content', 'index.md'); | |||
await cp(srcPath, destPath); | |||
} catch (errRaw) { | |||
throw new CommandError(GenerateReturnCode.COULD_NOT_GENERATE_PAGES, 'Could not write index file', errRaw as Error); | |||
} | |||
process.stdout.write('done\n'); | |||
}; | |||
export const description = 'Generate documentation from typedoc.json' as const; | |||
export enum GenerateReturnCode { | |||
SUCCESS = 0, | |||
COULD_NOT_GENERATE_PAGES = -1, | |||
} | |||
export interface RefreshArgs { | |||
subcommands?: string[]; | |||
} | |||
@@ -107,9 +122,12 @@ export const builder = (yargs: Argv) => yargs | |||
const refresh = async (args: RefreshArgs) => { | |||
try { | |||
await linkComponents(); | |||
} catch { | |||
return GenerateReturnCode.COULD_NOT_GENERATE_PAGES; | |||
const basePath = await useBasePath(); | |||
await linkComponents(basePath); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
return err.exitCode; | |||
} | |||
return GenerateReturnCode.SUCCESS; | |||
@@ -1,5 +1,6 @@ | |||
import { Argv } from 'yargs'; | |||
import { resolve } from 'path'; | |||
import {CommandError} from '../utils/error'; | |||
const DEFAULT_PORT = 3000 as const; | |||
@@ -53,12 +54,17 @@ export const builder = (yargs: Argv) => yargs | |||
}); | |||
const serve = async (args: ServeArgs) => { | |||
const { | |||
port = DEFAULT_PORT, | |||
} = args; | |||
const { port = DEFAULT_PORT } = args; | |||
try { | |||
await buildApp(); | |||
await serveApp(port); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
return err.exitCode; | |||
} | |||
await buildApp(); | |||
await serveApp(port); | |||
return ServeReturnCode.SUCCESS; | |||
}; | |||
@@ -0,0 +1,22 @@ | |||
import { resolve } from 'path'; | |||
import { searchForConfigFile } from '../utils/paths'; | |||
import { CommandError } from '../utils/error'; | |||
export enum UseBasePathReturnCode { | |||
COULD_NOT_FIND_CONFIG_FILE = -1, | |||
} | |||
export const useBasePath = async (...args: string[]) => { | |||
const basePathPrefix = await searchForConfigFile(); | |||
if (typeof basePathPrefix !== 'string') { | |||
throw new CommandError( | |||
UseBasePathReturnCode.COULD_NOT_FIND_CONFIG_FILE, | |||
'Could not find config file', | |||
); | |||
} | |||
const basePath = resolve(basePathPrefix, ...args); | |||
process.stdout.write(`Using base path: ${basePath}\n`); | |||
return basePath; | |||
}; |
@@ -0,0 +1,7 @@ | |||
import { resolve } from 'path'; | |||
export const useInternalPath = (...args: string[]) => { | |||
return Promise.resolve( | |||
resolve(__dirname, '..', '..', '..', ...args), | |||
); | |||
}; |
@@ -31,15 +31,19 @@ interface SymbolIdMapEntry { | |||
type TypedocDataNode = TypedocDataTextNode | TypedocDataInlineTagNode; | |||
export interface TypedocData { | |||
packages: any[]; | |||
typedocData: { | |||
readme: TypedocDataNode[]; | |||
symbolIdMap: Record<string, SymbolIdMapEntry>; | |||
}; | |||
readme: TypedocDataNode[]; | |||
symbolIdMap: Record<string, SymbolIdMapEntry>; | |||
} | |||
export const getPackages = async (cwd = process.cwd()) => { | |||
const configPath = resolve(cwd, '.amanuensis', 'config.json'); | |||
export interface PackageData { | |||
name: string; | |||
packageJson: Record<string, unknown>; | |||
basePath: string; | |||
markdown: { name: string; filePath: string; content: string }[]; | |||
classifications: Record<string, string | undefined>; | |||
} | |||
export const getPackages = async (configPath: string, cwd: string): Promise<PackageData[]> => { | |||
const configString = await readFile(configPath, 'utf-8'); | |||
const config = JSON.parse(configString) as AmanuensisConfig; | |||
const searchPatternsRaw = config.package.searchPatterns; | |||
@@ -50,21 +54,26 @@ export const getPackages = async (cwd = process.cwd()) => { | |||
? searchPattern | |||
: `${searchPattern}/package.json`, | |||
{ | |||
cwd, | |||
ignore: ['**/node_modules/**'], | |||
}, | |||
)), | |||
); | |||
const packagePaths = patternPackagePaths.flat(); | |||
const markdownFilePaths = await glob( | |||
const markdownFilePathsRaw = await glob( | |||
'**/*.{md,mdx}', | |||
{ | |||
cwd, | |||
ignore: ['**/node_modules/**'], | |||
}, | |||
); | |||
return Promise.all( | |||
packagePaths.map(async (packagePath) => { | |||
const packageString = await readFile(packagePath, 'utf-8'); | |||
const basePath = dirname(packagePath); | |||
const markdownFilePaths = markdownFilePathsRaw.map((p) => resolve(cwd, p)); | |||
const readPackages = await Promise.all( | |||
packagePaths.map(async (packagePathRaw) => { | |||
const absolutePackagePath = resolve(cwd, packagePathRaw); | |||
const packageString = await readFile(absolutePackagePath, 'utf-8'); | |||
const basePath = dirname(absolutePackagePath); | |||
const packageJson = JSON.parse(packageString) as { name: string }; | |||
const classifications = Object.fromEntries( | |||
Object.entries(config.package.classifications) | |||
@@ -100,23 +109,24 @@ export const getPackages = async (cwd = process.cwd()) => { | |||
return { | |||
name: packageJson.name, | |||
packageJson, | |||
basePath, | |||
basePath: basePath.slice(cwd.length + 1), | |||
markdown, | |||
classifications, | |||
}; | |||
}), | |||
); | |||
return readPackages.sort((a, b) => a.name.localeCompare(b.name)); | |||
}; | |||
export const getFileSources = async (cwd = process.cwd()) => { | |||
const typedocDataJsonPath = resolve(cwd, '.amanuensis', 'data.json'); | |||
const typedocDataJson = await readFile(typedocDataJsonPath, 'utf-8'); | |||
const typedocData = JSON.parse(typedocDataJson) as TypedocData; | |||
const symbolIdMapEntries = Object.values(typedocData.typedocData.symbolIdMap); | |||
const symbolIdMapEntries = Object.values(typedocData.symbolIdMap); | |||
const firstPartySources = symbolIdMapEntries.filter( | |||
({ sourceFileName }) => !sourceFileName.startsWith('node_modules'), | |||
); | |||
const firstPartySourceFiles = firstPartySources.map(({ sourceFileName }) => sourceFileName); | |||
const uniqueFirstPartySourceFiles = Array.from(new Set(firstPartySourceFiles)); | |||
return uniqueFirstPartySourceFiles; | |||
return Array.from(new Set(firstPartySourceFiles)); | |||
}; |
@@ -0,0 +1,7 @@ | |||
export class CommandError extends Error { | |||
constructor(readonly exitCode: number, message: string, cause?: Error) { | |||
super(message); | |||
this.name = 'CommandError'; | |||
this.cause = cause; | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
import * as fs from 'fs/promises'; | |||
import * as path from 'path'; | |||
export const searchForConfigFile = async () => { | |||
const filePath: string = path.resolve(process.cwd(), 'amanuensis.config.json'); | |||
async function searchInDir(dirPath: string): Promise<string | null> { | |||
const configFile: string = path.join(dirPath, 'amanuensis.config.json'); | |||
try { | |||
await fs.access(configFile, fs.constants.F_OK); | |||
return dirPath; | |||
} catch (err) { | |||
if (dirPath === path.dirname(dirPath)) { | |||
// Reached the root directory, config file not found | |||
return null; | |||
} | |||
const parentDir: string = path.dirname(dirPath); | |||
return searchInDir(parentDir); | |||
} | |||
} | |||
return searchInDir(filePath); | |||
}; |