feat: dox++

This commit is contained in:
cha0s 2023-12-31 16:21:43 -06:00
parent c68bb3b6f7
commit 95e18b9978
16 changed files with 386 additions and 123 deletions

View File

@ -49,24 +49,28 @@ export const hooks = {
/**
* Define CLI commands.
* @param {[Command](https://github.com/tj/commander.js/tree/master#declaring-program-variable)} program The [Commander.js](https://github.com/tj/commander.js) program.
*/
'@flecks/core.commands': (program) => ({
// So this could be invoked like:
// npx flecks something -t --blow-up blah
something: {
action: (...args) => {
// Run the command...
'@flecks/core.commands': (program, flecks) => {
const {Argument} = flecks.fleck('@flecks/core/server');
return {
// So this could be invoked like:
// npx flecks something -t --blow-up blah
something: {
action: (...args) => {
// Run the command...
},
args: [
new Argument('<somearg>', 'some argument'),
],
description: 'This command does tests and also blows up',
options: [
'-t, --test', 'Do a test',
'-b, --blow-up', 'Blow up instead of running the command',
],
},
args: [
'<somearg>',
],
description: 'This command does tests and also blows up',
options: [
'-t, --test', 'Do a test',
'-b, --blow-up', 'Blow up instead of running the command',
],
},
}),
};
},
/**
* Define configuration.

View File

@ -16,6 +16,8 @@ const debug = D('@flecks/core/commands');
const debugSilly = debug.extend('silly');
const flecksRoot = normalize(FLECKS_CORE_ROOT);
export {Argument};
export const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {

View File

@ -19,6 +19,7 @@ defaultOptions.sorted = true;
export {dump as dumpYml, load as loadYml} from 'js-yaml';
export {
Argument,
default as commands,
processCode,
spawnWith,

View File

@ -21,15 +21,27 @@
"build",
"server.js",
"server.js.map",
"src"
"src",
"website"
],
"dependencies": {
"@babel/core": "^7.17.2",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@docusaurus/core": "3.0.1",
"@docusaurus/module-type-aliases": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@docusaurus/types": "3.0.1",
"@flecks/core": "^2.0.3",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"comment-parser": "^1.3.0",
"glob": "^7.2.0"
"glob": "^7.2.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"remark-html": "^16.0.1",
"rimraf": "^5.0.5"
},
"devDependencies": {
"@flecks/fleck": "^2.0.3"

View File

@ -1,63 +0,0 @@
import {mkdir, writeFile} from 'fs/promises';
import {join} from 'path';
import {D} from '@flecks/core';
import {
generateBuildConfigsPage,
generateConfigPage,
generateHookPage,
generateTodoPage,
} from './generate';
import {parseFlecks} from './parser';
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
const debug = D('@flecks/dox/commands');
export default (program, flecks) => {
const commands = {};
commands.dox = {
description: 'generate documentation for this project',
action: async () => {
debug('Parsing flecks...');
const state = await parseFlecks(flecks);
debug('parsed');
debug('Generating hooks page...');
const hookPage = generateHookPage(state.hooks, flecks);
debug('generated');
debug('Generating TODO page...');
const todoPage = generateTodoPage(state.todos, flecks);
debug('generated');
debug('Generating build configs page...');
const buildConfigsPage = generateBuildConfigsPage(state.buildConfigs);
debug('generated');
debug('Generating config page...');
const configPage = generateConfigPage(state.configs);
debug('generated');
const output = join(FLECKS_CORE_ROOT, 'dox');
await mkdir(output, {recursive: true});
/* eslint-disable no-console */
console.log('');
console.group('Output:');
debug('Writing hooks page...');
await writeFile(join(output, 'hooks.md'), hookPage);
console.log('hooks.md');
debug('Writing TODO page...');
await writeFile(join(output, 'TODO.md'), todoPage);
console.log('TODO.md');
debug('Writing build configs page...');
await writeFile(join(output, 'build-configs.md'), buildConfigsPage);
console.log('build-configs.md');
debug('Writing config page...');
await writeFile(join(output, 'config.md'), configPage);
console.log('config.md');
console.groupEnd();
console.log('');
/* eslint-enable no-console */
},
};
return commands;
};

View File

@ -0,0 +1,86 @@
import {
access,
cp,
mkdir,
rename,
rmdir,
} from 'fs/promises';
import {dirname, join} from 'path';
import {
generate,
resolveSiteDir,
spawn,
} from './docusaurus';
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
export default (program, flecks) => {
const {Argument} = flecks.fleck('@flecks/core/server');
const commands = {};
const siteDirArgument = new Argument('[siteDir]', 'Docusaurus directory', 'website');
siteDirArgument.defaultValue = 'website';
commands.docusaurus = {
description: 'create a documentation website for this project',
action: async (subcommand, siteDir) => {
const resolvedSiteDir = resolveSiteDir(siteDir);
let siteDirExisted = false;
try {
const result = await mkdir(resolvedSiteDir);
if (undefined === result) {
await rmdir(resolvedSiteDir);
}
}
catch (error) {
siteDirExisted = true;
}
switch (subcommand) {
case 'build':
if (!siteDirExisted) {
throw new Error(`There's no website directory at ${resolvedSiteDir} to build!`);
}
await generate(flecks, resolvedSiteDir);
spawn('build', resolvedSiteDir);
break;
case 'create': {
if (siteDirExisted) {
throw new Error(`A website directory at ${resolvedSiteDir} already exists!`);
}
const templateDirectory = dirname(
__non_webpack_require__.resolve('@flecks/dox/website/sidebars.js'),
);
await cp(templateDirectory, resolvedSiteDir, {recursive: true});
// Copy the docusaurus config if it doesn't already exist.
try {
await access(join(FLECKS_CORE_ROOT, 'build', 'docusaurus.config.js'));
}
catch (error) {
await rename(
join(resolvedSiteDir, 'docusaurus.config.js'),
join(FLECKS_CORE_ROOT, 'build', 'docusaurus.config.js'),
);
}
// eslint-disable-next-line no-console
console.error(`website directory created at ${resolvedSiteDir}!`);
break;
}
case 'start':
if (!siteDirExisted) {
throw new Error(`There's no website directory at ${resolvedSiteDir} to start!`);
}
await generate(flecks, resolvedSiteDir);
spawn('start', resolvedSiteDir);
break;
default:
break;
}
},
args: [
new Argument('subcommand', 'Docusaurus command to run').choices(['build', 'create', 'start']),
siteDirArgument,
],
};
return commands;
};

View File

@ -0,0 +1,124 @@
import {mkdir, writeFile} from 'fs/promises';
import {isAbsolute, join, resolve} from 'path';
import {spawnWith} from '@flecks/core/server';
import {themes as prismThemes} from 'prism-react-renderer';
import {rimraf} from 'rimraf';
import {
generateBuildConfigsPage,
generateConfigPage,
generateHookPage,
generateTodoPage,
} from './generate';
import {parseFlecks} from './parser';
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
export function configDefaults() {
/** @type {import('@docusaurus/types').Config} */
const config = {
tagline: 'built with flecks',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
['classic', {
docs: {
sidebarPath: './sidebars.js',
},
pages: {
path: 'pages',
},
}],
],
themeConfig: {
// navbar: {
// title: 'flecks',
// logo: {
// alt: 'flecks logo',
// src: 'img/flecks.svg',
// },
// items: [
// ],
// },
footer: {
style: 'dark',
copyright: 'Built with flecks and Docusaurus.',
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
},
};
return config;
}
export function resolveSiteDir(siteDir) {
return isAbsolute(siteDir)
? siteDir
: resolve(FLECKS_CORE_ROOT, siteDir);
}
export async function generate(flecks, siteDir) {
// Generate "docs".
const docsDirectory = join(siteDir, 'docs', 'flecks');
await rimraf(docsDirectory);
const generatedDirectory = join(docsDirectory, '@flecks', 'dox');
await mkdir(generatedDirectory, {recursive: true});
const state = await parseFlecks(flecks);
const hookPage = generateHookPage(state.hooks, flecks);
const todoPage = generateTodoPage(state.todos, flecks);
const buildConfigsPage = generateBuildConfigsPage(state.buildConfigs);
const configPage = generateConfigPage(state.configs);
await writeFile(join(generatedDirectory, 'hooks.md'), hookPage);
await writeFile(join(generatedDirectory, 'TODO.md'), todoPage);
await writeFile(join(generatedDirectory, 'build-configs.md'), buildConfigsPage);
await writeFile(join(generatedDirectory, 'config.mdx'), configPage);
}
export function spawn(subcommand, siteDir) {
const args = [];
switch (subcommand) {
case 'start':
args.push('start', '--no-open');
break;
case 'build':
args.push('build', '--out-dir', join(FLECKS_CORE_ROOT, 'dist', 'dox'));
break;
default: {
const docusaurusCall = `npx docusaurus <subcommand> --config ${join(FLECKS_CORE_ROOT, 'build', 'docusaurus.config.js')}`;
throw new Error(`@flecks/dox only supports the 'build' and 'start' subcommands. You can run docusaurus yourself with:\n\n${docusaurusCall}`);
}
}
args.push('--config', join(FLECKS_CORE_ROOT, 'build', 'docusaurus.config.js'));
const cacheDirectory = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'dox');
// Spawn `docusaurus`.
const cmd = [
// `npx` doesn't propagate signals!
// 'npx', 'docusaurus',
join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'docusaurus'),
...args,
siteDir,
];
const child = spawnWith(
cmd,
{
env: {
// Override docusaurus generation directory for cleanliness.
DOCUSAURUS_GENERATED_FILES_DIR_NAME: join(cacheDirectory, '.docusaurus'),
},
},
);
// Clean up on exit.
process.on('exit', () => {
child.kill();
});
return child;
}

View File

@ -8,7 +8,7 @@ const makeFilenameRewriter = (filenameRewriters) => (filename, line, column) =>
export const generateBuildConfigsPage = (buildConfigs) => {
const source = [];
source.push('# Build configuration files');
source.push('# Build configuration');
source.push('');
source.push('This page documents all the build configuration files in this project.');
source.push('');
@ -27,38 +27,36 @@ export const generateBuildConfigsPage = (buildConfigs) => {
export const generateConfigPage = (configs) => {
const source = [];
source.push('# Configuration');
source.push("import CodeBlock from '@theme/CodeBlock';");
source.push('');
source.push('This page documents all the configuration in this project.');
source.push('# Fleck configuration');
source.push('');
source.push('<style>td > .theme-code-block \\{ margin: 0; \\}</style>');
source.push('');
source.push('This page documents all configurable flecks in this project.');
source.push('');
Object.entries(configs)
.sort(([l], [r]) => (l < r ? -1 : 1))
.forEach(([fleck, configs]) => {
// source.push(`## \`${fleck}\``);
source.push('```javascript');
source.push(`'${fleck}': {`);
source.push(`## \`${fleck}\``);
source.push('|Name|Default|Description|');
source.push('|-|-|-|');
configs.forEach(({comment, config, defaultValue}) => {
comment.split('\n').forEach((line) => {
source.push(` // ${line}`);
});
const value = defaultValue
.split('\n')
.map((line, i, array) => {
let output = '';
if (array.length - 1 === i) {
output += ' ';
}
else if (0 !== i) {
output += ' ';
}
output += line.trim();
return output;
})
.join('\n');
source.push(` ${config}: ${value}`);
// Leading and trailing empty cell to make table rendering easier.
const row = ['', config];
let code = defaultValue.replace(/`/g, '\\`');
// Multiline code. Fix indentation.
if (defaultValue.includes('\n')) {
const defaultValueLines = code.split('\n');
const [first, ...rest] = defaultValueLines;
const indent = (rest[0].length - rest[0].trimStart().length) - 2;
code = [first, ...rest.map((line) => line.substring(indent))].join('\\n');
}
row.push(`<CodeBlock language="javascript">{\`${code}\`}</CodeBlock>`);
row.push(comment, '');
source.push(row.join('|'));
});
source.push('}');
source.push('```');
source.push('');
});
return source.join('\n');
};
@ -74,27 +72,22 @@ export const generateHookPage = (hooks, flecks) => {
Object.entries(hooks)
.sort(([lhook], [rhook]) => (lhook < rhook ? -1 : 1))
.forEach(([hook, {implementations = [], invocations = [], specification}]) => {
source.push(`## \`${hook}\``);
source.push('');
const {description, example, params} = specification || {
params: [],
};
source.push(`## \`${hook}\``);
source.push('');
if (description) {
description.split('\n').forEach((line) => {
source.push(`> ${line}`);
});
source.push(...description.split('\n'));
source.push('');
}
if (params.length > 0) {
source.push('<details>');
source.push('<summary>Parameters</summary>');
source.push('<ul>');
params.forEach(({description, name, type}) => {
source.push(`<li><strong><code>{${type}}</code></strong> <code>${name}</code>`);
source.push(`<blockquote>${description}</blockquote></li>`);
source.push(`### <code>${name}: ${type}</code>`);
source.push('');
source.push(`<p>${description.trim()}</p>`);
source.push('');
});
source.push('</ul>');
source.push('</details>');
source.push('');
}
if (implementations.length > 0) {
@ -137,7 +130,7 @@ export const generateTodoPage = (todos, flecks) => {
const {filenameRewriters} = flecks.get('@flecks/dox/server');
const rewriteFilename = makeFilenameRewriter(filenameRewriters);
const source = [];
source.push('# TODO');
source.push('# TODO list');
source.push('');
source.push('This page documents all the TODO items in this project.');
source.push('');

View File

@ -1,5 +1,7 @@
import commands from './commands';
export {configDefaults} from './docusaurus';
export const hooks = {
'@flecks/core.commands': commands,
'@flecks/core.config': () => ({

View File

@ -23,8 +23,6 @@ import {require as R} from '@flecks/core/server';
import {parse as parseComment} from 'comment-parser';
import glob from 'glob';
const flecksCorePath = dirname(__non_webpack_require__.resolve('@flecks/core/package.json'));
class ParserState {
constructor() {
@ -170,7 +168,10 @@ const FlecksInvocations = (state, filename) => ({
|| (
(
isThisExpression(path.node.callee.object)
&& (filename === join(flecksCorePath, 'src', 'flecks.js'))
&& (
(filename === '@flecks/core/src/flecks.js')
|| (filename === '@flecks/core/src/server/flecks.js')
)
)
)
) {

2
packages/dox/website/docs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# This ignores the automatically-generated `flecks` subfolder.
/flecks

View File

@ -0,0 +1,9 @@
---
description: This project was built with flecks and is totally awesome!
slug: /
---
# Introduction
⚡️ Welcome to your documentation website! From here, you will want to consult the
[Docusaurus](https://docusaurus.io/) documentation to learn how to build this website out!

View File

@ -0,0 +1,21 @@
// @ts-check
// `@type` JSDoc annotations allow editor autocompletion and type checking
// (when paired with `@ts-check`).
// There are various equivalent ways to declare your Docusaurus config.
// See: https://docusaurus.io/docs/api/docusaurus-config
// For some reason we get a webpack warning if we use import here...
const {configDefaults} = require('@flecks/dox/server'); // eslint-disable-line import/no-extraneous-dependencies
export default async function flecksDocusaurus() {
const defaults = configDefaults();
/** @type {import('@docusaurus/types').Config} */
const config = {
...defaults,
title: 'My documentation website',
tagline: 'built with flecks',
url: 'http://localhost',
baseUrl: '/',
};
return config;
}

View File

@ -0,0 +1,41 @@
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs"
>
Your documentation here
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
</Layout>
);
}

View File

@ -0,0 +1,23 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,5 @@
export default {
sidebar: [
'introduction',
],
};