Use a single source of truth for derived paths in commands.master
@@ -9,9 +9,9 @@ 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, | |||
NO_TYPEDOC_JSON = -3, | |||
COULD_NOT_GENERATE_TYPEDOC_DATA = -4, | |||
COULD_NOT_GENERATE_PACKAGE_DATA = -5, | |||
} | |||
const ensureTypedocJson = async (typedocPath: string) => { | |||
@@ -38,10 +38,10 @@ const ensureTypedocJson = async (typedocPath: string) => { | |||
process.stdout.write('yes\n'); | |||
}; | |||
const generateTypedocData = async (outPath: string, basePath: string) => { | |||
const generateTypedocData = async (basePath: string, internalPath: string) => { | |||
process.stdout.write('Generating typedoc data...\n'); | |||
const typedocBinPath = await useInternalPath('node_modules', '.bin', 'typedoc'); | |||
const outPath = resolve(internalPath, '.amanuensis', 'types.json'); | |||
const typedocBinPath = resolve(internalPath, 'node_modules', '.bin', 'typedoc'); | |||
const { execa } = await import('execa'); | |||
const result = await execa(typedocBinPath, ['--json', outPath, '--pretty', 'false'], { | |||
@@ -59,11 +59,12 @@ const generateTypedocData = async (outPath: string, basePath: string) => { | |||
}; | |||
const generatePackageData = async ( | |||
typedocDataJsonPath: string, | |||
configPath: string, | |||
basePath: string, | |||
internalPath: string, | |||
) => { | |||
process.stdout.write('Grouping typedoc data...\n'); | |||
const configPath = resolve(basePath, 'amanuensis.config.json'); | |||
const typedocDataJsonPath = resolve(internalPath, '.amanuensis', 'packages.json'); | |||
const packages = await getPackages(configPath, basePath); | |||
@@ -91,16 +92,16 @@ export const builder = (yargs: Argv) => yargs | |||
}); | |||
const analyze = async (args: AnalyzeArgs) => { | |||
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 { | |||
const basePath = await useBasePath(); | |||
process.stdout.write(`Using base path: ${basePath}\n`); | |||
const typedocJsonPath = args.typedocJsonPath ?? resolve(basePath, 'typedoc.json'); | |||
await ensureTypedocJson(typedocJsonPath); | |||
await generateTypedocData(typedocOutPath, basePath); | |||
await generatePackageData(packagesPath, configPath, basePath); | |||
const internalPath = await useInternalPath(); | |||
await generateTypedocData(basePath, internalPath); | |||
await generatePackageData(basePath, internalPath); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
@@ -1,18 +1,19 @@ | |||
import { cp } from 'fs/promises'; | |||
import { Argv } from 'yargs'; | |||
import { resolve } from 'path'; | |||
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, | |||
COULD_NOT_COPY_FILES = -3, | |||
COULD_NOT_INSTALL_DEPENDENCIES = -4, | |||
} | |||
const copyFiles = async () => { | |||
const copyFiles = async (internalPath: string) => { | |||
const srcPath = resolve(internalPath, 'src', 'next'); | |||
const destPath = resolve(internalPath, '.amanuensis', 'next'); | |||
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; | |||
@@ -29,7 +30,7 @@ interface PackageManager { | |||
installCmd: [string, string[]]; | |||
} | |||
const packageManagers: PackageManager[] = [ | |||
const PACKAGE_MANAGERS: PackageManager[] = [ | |||
{ | |||
name: 'pnpm', | |||
testCmd: ['pnpm', ['--version']], | |||
@@ -47,10 +48,10 @@ const packageManagers: PackageManager[] = [ | |||
}, | |||
]; | |||
const installDependencies = async () => { | |||
const installDependencies = async (internalPath: string) => { | |||
const { execa } = await import('execa'); | |||
const selectedPackageManagerIndex = await packageManagers.reduce( | |||
const selectedPackageManagerIndex = await PACKAGE_MANAGERS.reduce( | |||
async (prevPmPromise, pkgm, i) => { | |||
const prevPm = await prevPmPromise; | |||
const { testCmd } = pkgm; | |||
@@ -68,8 +69,8 @@ const installDependencies = async () => { | |||
process.exit(-1); | |||
} | |||
const { [selectedPackageManagerIndex]: selectedPackageManager } = packageManagers; | |||
const cwd = await useInternalPath('.amanuensis', 'next'); | |||
const { [selectedPackageManagerIndex]: selectedPackageManager } = PACKAGE_MANAGERS; | |||
const cwd = resolve(internalPath, '.amanuensis', 'next'); | |||
process.stdout.write(`In path: ${cwd}\n`); | |||
process.stdout.write(`Installing dependencies with ${selectedPackageManager.name}\n`); | |||
const result = await execa( | |||
@@ -89,8 +90,10 @@ export const builder = (yargsBuilder: Argv) => yargsBuilder; | |||
const init = async () => { | |||
try { | |||
await copyFiles(); | |||
await installDependencies(); | |||
const internalPath = await useInternalPath(); | |||
await copyFiles(internalPath); | |||
await installDependencies(internalPath); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
@@ -22,93 +22,73 @@ import { useInternalPath } from '../mixins/internal-path'; | |||
export enum GenerateReturnCode { | |||
SUCCESS = 0, | |||
COULD_NOT_GENERATE_PAGES = -2, | |||
COULD_NOT_GENERATE_PAGES = -3, | |||
} | |||
const linkComponents = async (cwd: string) => { | |||
process.stdout.write('Linking components...\n'); | |||
const projectCwd = resolve(cwd, '.amanuensis'); | |||
const defaultCwd = await useInternalPath('src', 'next'); | |||
const destCwd = await useInternalPath('.amanuensis', 'next', 'src'); | |||
// todo merge package.json | |||
const componentsList = [ | |||
'next/src/components/Wrapper.tsx', | |||
'next/src/components/PageLayout.tsx', | |||
'next/src/components/theme.ts', | |||
'next/src/components/postcss.config.js', | |||
'next/src/components/tailwind.config.js', | |||
]; | |||
const cleanContent = async (internalPath: string) => { | |||
try { | |||
const destCwd = resolve(internalPath, '.amanuensis', 'next', 'src'); | |||
await rm(resolve(destCwd, 'content'), { recursive: true }); | |||
} catch { | |||
// noop | |||
} | |||
}; | |||
const resetPages = async (internalPath: string) => { | |||
try { | |||
const defaultCwd = resolve(internalPath, 'src', 'next'); | |||
const destCwd = resolve(internalPath, '.amanuensis', 'next', 'src'); | |||
await rm(resolve(destCwd, 'pages'), { recursive: true }); | |||
await cp(resolve(defaultCwd, 'pages'), resolve(destCwd, 'pages'), { recursive: true }); | |||
} catch { | |||
// noop | |||
} | |||
}; | |||
const linkComponents = async (basePath: string, internalPath: string) => { | |||
process.stdout.write('Linking components...\n'); | |||
const customComponentDir = resolve(basePath, '.amanuensis'); | |||
const defaultComponentDir = resolve(internalPath, 'src', 'next'); | |||
const destCwd = resolve(internalPath, '.amanuensis', 'next', 'src'); | |||
// todo merge package.json | |||
const componentsList = [ | |||
'next/src/components/Wrapper.tsx', | |||
'next/src/components/PageLayout.tsx', | |||
'next/src/components/theme.ts', | |||
'next/src/components/postcss.config.js', | |||
'next/src/components/tailwind.config.js', | |||
]; | |||
await Promise.all(componentsList.map(async (componentPath) => { | |||
const destPath = resolve(destCwd, componentPath); | |||
let baseCwd = projectCwd; | |||
let componentDir = customComponentDir; | |||
try { | |||
await stat(resolve(baseCwd, componentPath)); | |||
await stat(resolve(componentDir, componentPath)); | |||
} catch (errRaw) { | |||
const err = errRaw as NodeJS.ErrnoException; | |||
if (err.code === 'ENOENT') { | |||
baseCwd = defaultCwd; | |||
componentDir = defaultComponentDir; | |||
} | |||
} | |||
await mkdirp(dirname(destPath)); | |||
await cp( | |||
resolve(baseCwd, componentPath), | |||
resolve(componentDir, componentPath), | |||
destPath, | |||
); | |||
process.stdout.write(`Linked ${componentPath}\n`); | |||
})); | |||
const packagesPath = await useInternalPath('.amanuensis', 'packages.json'); | |||
const packagesDataJson = await readFile(packagesPath, 'utf-8'); | |||
const packagesData = JSON.parse(packagesDataJson) as PackageData[]; | |||
process.stdout.write('done\n'); | |||
}; | |||
try { | |||
await Promise.all( | |||
packagesData.map(async (pkg) => { | |||
const packageDir = await useBasePath(pkg.basePath); | |||
const packageLinkDir = await useInternalPath('.amanuensis', 'next', 'node_modules', pkg.name); | |||
await mkdirp(dirname(packageLinkDir)); | |||
try { | |||
await symlink(packageDir, packageLinkDir); | |||
} catch { | |||
// noop | |||
} | |||
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); | |||
await cp(srcPath, destPath); | |||
const pageDestPath = resolve(destCwd, 'pages', pkg.basePath, `${basename(m.name, extname(m.name))}.tsx`); | |||
const preambleImport = `@/${join('content', pkg.basePath, m.name)}`; | |||
await writeFile( | |||
pageDestPath, | |||
`import {NextPage} from 'next'; | |||
import {Wrapper} from '@/components/Wrapper'; | |||
const pageContent = (preambleImport: string) => `import {NextPage} from 'next'; | |||
import {ComponentContext} from '@/contexts/Component'; | |||
import {Wrapper} from '@/components/Wrapper'; | |||
import {PreambleContext} from '@/contexts/Preamble'; | |||
import Preamble from '${preambleImport}'; | |||
import {PageLayout} from '@/components/PageLayout'; | |||
@@ -124,44 +104,89 @@ const IndexPage: NextPage = () => { | |||
}; | |||
export default IndexPage; | |||
`, | |||
// todo fetch components for display to props and preamble | |||
// todo fix problem when building with import aliases | |||
// todo find a way to build with tailwind | |||
// todo link components to next project (done) | |||
// todo merge contents of .amanuensis with next project | |||
); | |||
}), | |||
); | |||
}), | |||
); | |||
`; | |||
const writeContent = async (basePath: string, internalPath: string) => { | |||
const destCwd = resolve(internalPath, '.amanuensis', 'next', 'src'); | |||
const packagesPath = resolve(internalPath, '.amanuensis', 'packages.json'); | |||
const packagesDataJson = await readFile(packagesPath, 'utf-8'); | |||
const packagesData = JSON.parse(packagesDataJson) as PackageData[]; | |||
try { | |||
await Promise.all(packagesData.map(async (pkg) => { | |||
const packageDir = resolve(basePath, pkg.basePath); | |||
const packageLinkDir = resolve(internalPath, '.amanuensis', 'next', 'node_modules', pkg.name); | |||
await mkdirp(dirname(packageLinkDir)); | |||
try { | |||
await symlink(packageDir, packageLinkDir); | |||
} catch { | |||
// noop | |||
} | |||
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(basePath, pkg.basePath, m.filePath); | |||
const destPath = resolve(destCwd, 'content', pkg.basePath, m.name); | |||
await cp(srcPath, destPath); | |||
const pageDestPath = resolve(destCwd, 'pages', pkg.basePath, `${basename(m.name, extname(m.name))}.tsx`); | |||
const preambleImport = `@/${join('content', pkg.basePath, m.name)}`; | |||
await writeFile(pageDestPath, pageContent(preambleImport)); | |||
// todo fetch components for display to props and preamble | |||
// todo fix problem when building with import aliases | |||
// todo find a way to build with tailwind | |||
// todo link components to next project (done) | |||
// todo merge contents of .amanuensis with next project | |||
}), | |||
); | |||
})); | |||
} catch (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 srcPath = resolve(basePath, '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 interface RefreshArgs { | |||
subcommands?: string[]; | |||
clean?: boolean; | |||
} | |||
export const builder = (yargs: Argv) => yargs; | |||
export const builder = (yargs: Argv) => yargs | |||
.option('clean', { | |||
alias: 'c', | |||
type: 'boolean', | |||
description: 'Reset generated content', | |||
default: true, | |||
}); | |||
const refresh = async (args: RefreshArgs) => { | |||
try { | |||
const internalPath = await useInternalPath(); | |||
const { clean = true } = args; | |||
if (clean) { | |||
await cleanContent(internalPath); | |||
await resetPages(internalPath); | |||
} | |||
const basePath = await useBasePath(); | |||
await linkComponents(basePath); | |||
await linkComponents(basePath, internalPath); | |||
await writeContent(basePath, internalPath); | |||
} catch (errRaw) { | |||
const err = errRaw as CommandError; | |||
process.stderr.write(`${err.message}\n`); | |||
@@ -3,6 +3,8 @@ | |||
import { hideBin } from 'yargs/helpers'; | |||
import yargs from 'yargs'; | |||
const UNKNOWN_COMMAND_EXIT_CODE = -1 as const; | |||
const main = async (args: string[]) => { | |||
const COMMANDS = { | |||
init: await import('./commands/init'), | |||
@@ -26,14 +28,14 @@ const main = async (args: string[]) => { | |||
const [commandName, ...subcommands] = commandNamesRaw as [keyof typeof COMMANDS, ...string[]]; | |||
if (typeof commandName === 'undefined') { | |||
yargsBuilder.showHelp(); | |||
return -1; | |||
return UNKNOWN_COMMAND_EXIT_CODE; | |||
} | |||
const { [commandName]: commandDef } = COMMANDS; | |||
if (typeof commandDef === 'undefined') { | |||
process.stderr.write(`Unknown command: ${commandName}\n`); | |||
yargsBuilder.showHelp(); | |||
return -1; | |||
return UNKNOWN_COMMAND_EXIT_CODE; | |||
} | |||
const { default: handler } = commandDef; | |||
@@ -49,5 +51,5 @@ main(hideBin(process.argv)) | |||
process.exit(code); | |||
}) | |||
.catch(() => { | |||
process.exit(-1); | |||
process.exit(-255); | |||
}); |
@@ -1,12 +1,11 @@ | |||
import { resolve } from 'path'; | |||
import { searchForConfigFile } from '../utils/paths'; | |||
import { CommandError } from '../utils/error'; | |||
export enum UseBasePathReturnCode { | |||
COULD_NOT_FIND_CONFIG_FILE = -1, | |||
COULD_NOT_FIND_CONFIG_FILE = -2, | |||
} | |||
export const useBasePath = async (...args: string[]) => { | |||
export const useBasePath = async () => { | |||
const basePathPrefix = await searchForConfigFile(); | |||
if (typeof basePathPrefix !== 'string') { | |||
@@ -16,7 +15,5 @@ export const useBasePath = async (...args: string[]) => { | |||
); | |||
} | |||
const basePath = resolve(basePathPrefix, ...args); | |||
process.stdout.write(`Using base path: ${basePath}\n`); | |||
return basePath; | |||
return basePathPrefix; | |||
}; |
@@ -1,7 +1,7 @@ | |||
import { resolve } from 'path'; | |||
export const useInternalPath = (...args: string[]) => { | |||
export const useInternalPath = () => { | |||
return Promise.resolve( | |||
resolve(__dirname, '..', '..', '..', ...args), | |||
resolve(__dirname, '..', '..', '..'), | |||
); | |||
}; |