diff --git a/.amanuensis/components/Wrapper.tsx b/.amanuensis/components/Wrapper.tsx new file mode 100644 index 0000000..1d68959 --- /dev/null +++ b/.amanuensis/components/Wrapper.tsx @@ -0,0 +1,9 @@ +export const Wrapper = ({ + children, +}) => { + return ( +
+ {children} +
+ ) +}; diff --git a/packages/amanuensis/.eslintrc b/packages/amanuensis/.eslintrc index 35cfb79..e8684d5 100644 --- a/packages/amanuensis/.eslintrc +++ b/packages/amanuensis/.eslintrc @@ -1,8 +1,16 @@ { "root": true, + "ignorePatterns": [ + "pages/**/*.tsx", + "components/**/*.tsx" + ], "extends": [ "lxsmnsyc/typescript" ], + "rules": { + "no-tabs": "off", + "indent": "off" + }, "parserOptions": { "project": "./tsconfig.eslint.json" } diff --git a/packages/amanuensis/.gitignore b/packages/amanuensis/.gitignore index 06b929d..64fbf8b 100644 --- a/packages/amanuensis/.gitignore +++ b/packages/amanuensis/.gitignore @@ -108,3 +108,4 @@ dist .next/ types/ .amanuensis/ +components/ diff --git a/packages/amanuensis/default/components/Wrapper.tsx b/packages/amanuensis/default/components/Wrapper.tsx new file mode 100644 index 0000000..9a63374 --- /dev/null +++ b/packages/amanuensis/default/components/Wrapper.tsx @@ -0,0 +1,9 @@ +export const Wrapper = ({ + children, +}) => { + return ( +
+ {children} +
+ ) +}; diff --git a/packages/amanuensis/package.json b/packages/amanuensis/package.json index 108c40f..ac2d7b3 100644 --- a/packages/amanuensis/package.json +++ b/packages/amanuensis/package.json @@ -55,9 +55,11 @@ }, "dependencies": { "execa": "^7.2.0", + "mkdirp": "^3.0.1", "next": "13.4.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^8.0.7", "typedoc": "^0.24.8", "yargs": "^17.7.2" }, diff --git a/packages/amanuensis/src/pages/_app.tsx b/packages/amanuensis/pages/_app.tsx similarity index 56% rename from packages/amanuensis/src/pages/_app.tsx rename to packages/amanuensis/pages/_app.tsx index 3d4b080..5c3b54a 100644 --- a/packages/amanuensis/src/pages/_app.tsx +++ b/packages/amanuensis/pages/_app.tsx @@ -1,6 +1,7 @@ import type { AppProps } from 'next/app'; +import { FC } from 'react'; -const App = ({ Component, pageProps }: AppProps) => { +const App: FC = ({ Component, pageProps }: AppProps) => { return ( ); diff --git a/packages/amanuensis/pages/index.tsx b/packages/amanuensis/pages/index.tsx new file mode 100644 index 0000000..5a75574 --- /dev/null +++ b/packages/amanuensis/pages/index.tsx @@ -0,0 +1,36 @@ +import {GetStaticProps, NextPage} from 'next'; +import {getReadmeText} from '../src/data'; +import ReactMarkdown from 'react-markdown'; +import {Wrapper} from '../components/Wrapper'; + +interface IndexPageProps { + readmeType: 'markdown'; + readmeText: string; +} + +const IndexPage: NextPage = ({ + readmeType, + readmeText, +}) => { + return ( + + {readmeType === 'markdown' && ( + + {readmeText} + + )} + + ); +}; + +export const getStaticProps: GetStaticProps = async () => { + const readmeText = await getReadmeText(); + return { + props: { + readmeType: 'markdown', + readmeText, + }, + }; +}; + +export default IndexPage; diff --git a/packages/amanuensis/src/commands/generate.ts b/packages/amanuensis/src/commands/generate.ts index 54f047e..2ee105f 100644 --- a/packages/amanuensis/src/commands/generate.ts +++ b/packages/amanuensis/src/commands/generate.ts @@ -1,27 +1,25 @@ import { stat } from 'fs/promises'; import { resolve } from 'path'; import { Argv } from 'yargs'; -import {Stats} from 'fs'; +import { Stats } from 'fs'; -export const description = 'Generate documentation from typedoc.json' as const; - -interface GenerateArgs { - typedocJsonPath: string; -} - -const ensureTypedocJson = async (typedocPath = resolve(process.cwd(), 'typedoc.json')) => { +const ensureTypedocJson = async (typedocPath: string) => { const trueTypedocPath = resolve(typedocPath); - process.stdout.write(`In path: ${trueTypedocPath}\n`); + process.stdout.write(`Using typedoc.json path: ${trueTypedocPath}\n`); process.stdout.write('Does the file exist? '); let statResult: Stats; try { statResult = await stat(trueTypedocPath); } catch (errRaw) { - if (errRaw.code === 'ENOENT') { + 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'); } + process.stdout.write('maybe?\n'); + process.stderr.write('Could not ensure typedoc.json\n'); + throw err; } if (statResult.isDirectory()) { process.stdout.write('no\n'); @@ -31,38 +29,58 @@ const ensureTypedocJson = async (typedocPath = resolve(process.cwd(), 'typedoc.j process.stdout.write('yes\n'); }; -const generateTypedocData = async (cwd = process.cwd()) => { +const generateTypedocData = async () => { process.stdout.write('Generating typedoc data...\n'); + const outPath = resolve(__dirname, '..', '..', '..', '.amanuensis', 'data.json'); const typedocBinPath = resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'typedoc'); const { execa } = await import('execa'); - await execa(typedocBinPath, ['--json', outPath]) - .pipeStdout(process.stdout) - .pipeStderr(process.stderr) + + await execa(typedocBinPath, ['--json', outPath], { + stdout: 'inherit', + stderr: 'inherit', + }); + process.stdout.write('done\n'); }; +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, +} + +export interface GenerateArgs { + typedocJsonPath?: string; + subcommands?: string[]; +} + export const builder = (yargs: Argv) => yargs .option('typedocJsonPath', { type: 'string', - default: 'typedoc.json', alias: 't', }); const generate = async (args: GenerateArgs) => { + const { + typedocJsonPath = resolve(process.cwd(), 'typedoc.json'), + } = args; + try { - await ensureTypedocJson(args.typedocJsonPath); + await ensureTypedocJson(typedocJsonPath); } catch { - return -1; + return GenerateReturnCode.NO_TYPEDOC_JSON; } try { await generateTypedocData(); } catch { - return -2; + return GenerateReturnCode.COULD_NOT_GENERATE_TYPEDOC_DATA; } - return 0; + return GenerateReturnCode.SUCCESS; }; export default generate; diff --git a/packages/amanuensis/src/commands/serve.ts b/packages/amanuensis/src/commands/serve.ts index 42885a8..4bb867e 100644 --- a/packages/amanuensis/src/commands/serve.ts +++ b/packages/amanuensis/src/commands/serve.ts @@ -1,12 +1,108 @@ -import {Argv} from 'yargs'; +import { Argv } from 'yargs'; +import { resolve, dirname } from 'path'; +import { cp, stat, unlink } from 'fs/promises'; +import { mkdirp } from 'mkdirp'; + +const DEFAULT_PORT = 3000 as const; + +const linkComponents = async () => { + process.stdout.write('Linking components...\n'); + + const projectCwd = resolve(process.cwd(), '.amanuensis'); + const defaultCwd = resolve(__dirname, '..', '..', '..', 'default'); + const destCwd = resolve(__dirname, '..', '..', '..'); + const componentsList = [ + 'components/Wrapper.tsx', + ]; + + await Promise.all(componentsList.map(async (componentPath) => { + const destPath = resolve(destCwd, componentPath); + try { + await unlink(destPath); + } catch { + // noop + } + + let baseCwd = projectCwd; + + try { + await stat(resolve(baseCwd, componentPath)); + } catch (errRaw) { + const err = errRaw as NodeJS.ErrnoException; + if (err.code === 'ENOENT') { + baseCwd = defaultCwd; + } + } + + await mkdirp(dirname(destPath)); + await cp( + resolve(baseCwd, componentPath), + destPath, + ); + process.stdout.write(`Linked ${componentPath}\n`); + })); + + process.stdout.write('done\n'); +}; + +const buildApp = async () => { + process.stdout.write('Building app...\n'); + + const cwd = resolve(__dirname, '..', '..', '..'); + const nextBinPath = resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'next'); + const { execa } = await import('execa'); + + await execa(nextBinPath, ['build'], { + stdout: 'inherit', + stderr: 'inherit', + cwd, + }); + + process.stdout.write('done\n'); +}; + +const serveApp = async (port: number) => { + process.stdout.write(`Using port: ${port}...\n`); + process.stdout.write('Serving app...\n'); + + const cwd = resolve(__dirname, '..', '..', '..'); + const nextBinPath = resolve(__dirname, '..', '..', '..', 'node_modules', '.bin', 'next'); + const { execa } = await import('execa'); + + await execa(nextBinPath, ['start', '-p', port.toString()], { + stdout: 'inherit', + stderr: 'inherit', + cwd, + }); +}; export const description = 'Start a development server' as const; -export const builder = (yargs: Argv) => yargs; +export enum ServeReturnCode { + SUCCESS = 0, +} + +export interface ServeArgs { + port?: number; + subcommands?: string[]; +} + +export const builder = (yargs: Argv) => yargs + .option('port', { + type: 'number', + default: DEFAULT_PORT, + alias: 'p', + }); + +const serve = async (args: ServeArgs) => { + const { + port = DEFAULT_PORT, + } = args; -const serve = async (args: Record) => { - console.log('serve', args); - return 0; + await linkComponents(); + await buildApp(); + await serveApp(port); + return ServeReturnCode.SUCCESS; }; export default serve; diff --git a/packages/amanuensis/src/data.ts b/packages/amanuensis/src/data.ts new file mode 100644 index 0000000..8f0b511 --- /dev/null +++ b/packages/amanuensis/src/data.ts @@ -0,0 +1,35 @@ +import { readFile } from 'fs/promises'; +import { resolve } from 'path'; + +interface TypedocDataTextNode { + kind: 'text'; + text: string; +} + +interface TypedocDataInlineTagNode { + kind: 'inline-tag'; + tag: string; + text: string; + target: number; + tsLinkText: string; +} + +type TypedocDataNode = TypedocDataTextNode | TypedocDataInlineTagNode; + +export interface TypedocData { + readme: TypedocDataNode[]; +} + +export const getReadmeText = async () => { + const typedocDataJson = await readFile(resolve('.amanuensis', 'data.json'), 'utf-8'); + const typedocData = JSON.parse(typedocDataJson) as TypedocData; + return typedocData.readme.reduce( + (theText, node) => { + if (node.kind === 'text') { + return `${theText}${node.text}`; + } + return theText; + }, + '', + ); +}; diff --git a/packages/amanuensis/src/index.ts b/packages/amanuensis/src/index.ts index 92d3c63..1135c6b 100644 --- a/packages/amanuensis/src/index.ts +++ b/packages/amanuensis/src/index.ts @@ -3,56 +3,49 @@ import { hideBin } from 'yargs/helpers'; import yargs from 'yargs'; -import * as serve from './commands/serve'; -import * as generate from './commands/generate'; - -const COMMANDS = { - serve, - generate, -}; - -type CommandName = keyof typeof COMMANDS; - const main = async (args: string[]) => { + const COMMANDS = { + serve: await import('./commands/serve'), + generate: await import('./commands/generate'), + }; + const yargsBuilder = Object.entries(COMMANDS).reduce( - (theYargs, [name, command]) => { - return theYargs.command( + (theYargs, [name, command]) => theYargs.command( name, command.description ?? '', - command.builder ?? (yargs => yargs), - ); - }, + command.builder ?? ((commandYargs) => commandYargs), + ), yargs .scriptName('amanuensis'), ); const { _: commandNamesRaw, ...etcArgs } = await yargsBuilder.parse(args); - const [commandName, ...subcommands] = commandNamesRaw as [CommandName, ...string[]]; + const [commandName, ...subcommands] = commandNamesRaw as [keyof typeof COMMANDS, ...string[]]; if (typeof commandName === 'undefined') { yargsBuilder.showHelp(); - return; + return -1; } const { [commandName]: commandDef } = COMMANDS; if (typeof commandDef === 'undefined') { process.stderr.write(`Unknown command: ${commandName}\n`); yargsBuilder.showHelp(); - process.exit(-1); - return; + return -1; } const { default: handler } = commandDef; - - try { - const returnCode = await handler({ - ...etcArgs, - _: subcommands, - }); - process.exit(returnCode); - } catch { - process.exit(-1); - } + return handler({ + ...etcArgs, + subcommands, + }); }; -void main(hideBin(process.argv)); +main(hideBin(process.argv)) + .then((code = 0) => { + // noop + process.exit(code); + }) + .catch(() => { + process.exit(-1); + }); diff --git a/packages/amanuensis/src/pages/index.tsx b/packages/amanuensis/src/pages/index.tsx deleted file mode 100644 index a749644..0000000 --- a/packages/amanuensis/src/pages/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const IndexPage = () => { - return ( -
- Hello -
- ); -}; - -export default IndexPage; diff --git a/packages/amanuensis/tsconfig.eslint.json b/packages/amanuensis/tsconfig.eslint.json index 459f2a1..8cc71b1 100644 --- a/packages/amanuensis/tsconfig.eslint.json +++ b/packages/amanuensis/tsconfig.eslint.json @@ -1,5 +1,9 @@ { - "exclude": ["node_modules"], + "exclude": [ + "node_modules", + "pages/**", + "components/**" + ], "include": ["src", "types", "test"], "compilerOptions": { "module": "ESNext", diff --git a/packages/amanuensis/tsconfig.json b/packages/amanuensis/tsconfig.json index 70e049f..09bbf1c 100644 --- a/packages/amanuensis/tsconfig.json +++ b/packages/amanuensis/tsconfig.json @@ -1,6 +1,8 @@ { "exclude": [ - "node_modules" + "node_modules", + "pages/**", + "components/**", ], "include": [ "src", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6519ecd..753af77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -715,6 +715,9 @@ importers: execa: specifier: ^7.2.0 version: 7.2.0 + mkdirp: + specifier: ^3.0.1 + version: 3.0.1 next: specifier: 13.4.7 version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) @@ -724,6 +727,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-markdown: + specifier: ^8.0.7 + version: 8.0.7(@types/react@18.2.18)(react@18.2.0) typedoc: specifier: ^0.24.8 version: 0.24.8(typescript@4.9.5) @@ -5741,6 +5747,12 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + dev: false + /mlly@1.4.0: resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} dependencies: @@ -6461,6 +6473,33 @@ packages: - supports-color dev: false + /react-markdown@8.0.7(@types/react@18.2.18)(react@18.2.0): + resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + dependencies: + '@types/hast': 2.3.4 + '@types/prop-types': 15.7.5 + '@types/react': 18.2.18 + '@types/unist': 2.0.6 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 2.0.1 + prop-types: 15.8.1 + property-information: 6.2.0 + react: 18.2.0 + react-is: 18.2.0 + remark-parse: 10.0.2 + remark-rehype: 10.1.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.1 + unified: 10.1.2 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + transitivePeerDependencies: + - supports-color + dev: false + /react-phone-number-input@3.3.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-6d1lq9parRGnVz6laEN7ijU7MeUCkFEJsTnzB/97nVrm/WE48EDEV5/2bu08mzfZjvX6shpryqG0DUCNiP07Cg==} peerDependencies: