flecks/packages/dox/build/generate.js
2024-02-15 08:34:06 -06:00

387 lines
11 KiB
JavaScript

const {
basename,
dirname,
extname,
join,
} = require('path');
const {parseFlecks} = require('./parser');
exports.generateDocusaurus = function generate({
'build-files': buildFiles,
config,
hooks,
todos,
}) {
return {
'build-files': exports.generateDocusaurusBuildFilesPage(buildFiles),
config: exports.generateDocusaurusConfigPage(config),
hooks: exports.generateDocusaurusHookPage(hooks),
todos: exports.generateDocusaurusTodoPage(todos),
};
};
exports.generateDocusaurusBuildFilesPage = (buildFiles) => {
const source = [];
source.push('---');
source.push('title: Build files');
source.push('description: All the build files in this project.');
source.push('---');
source.push('');
source.push('This page documents all the build configuration files in this project.');
source.push('');
if (buildFiles.length > 0) {
buildFiles
.sort(({filename: l}, {filename: r}) => (l < r ? -1 : 1))
.forEach(({filename, description}) => {
source.push(`## \`${filename}\``);
source.push('');
source.push(description);
source.push('');
});
}
return source.join('\n');
};
exports.generateDocusaurusConfigPage = (configs) => {
const source = [];
source.push('---');
source.push('title: Fleck configuration');
source.push('description: All the configurable flecks in this project.');
source.push('---');
source.push('');
source.push("import CodeBlock from '@theme/CodeBlock';");
source.push('');
source.push("import styles from './dox.module.css';");
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('<table className={styles.configTable}>');
source.push('<thead>');
source.push('<td className={styles.configBig}>Description</td>');
source.push('<td>Name</td>');
source.push('<td>Default value</td>');
source.push('</thead>');
source.push('<tbody>');
configs.forEach(({defaultValue, description, key}) => {
// Leading and trailing empty cell to make table rendering easier.
source.push('<tr className={styles.configSmall}><td colspan="2"></td></tr>');
source.push(`<tr className={styles.configSmall}><td colspan="2">${description}</td></tr>`);
source.push('<tr>');
source.push(`<td className={styles.configBig}>${description}</td>`);
source.push(`<td>\`${key}\`</td>`);
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');
}
source.push(`<td><CodeBlock language="javascript">{\`${code}\`}</CodeBlock></td>`);
source.push('</tr>');
});
source.push('</tbody>');
source.push('</table>');
});
return source.join('\n');
};
exports.generateDocusaurusHookPage = (hooks) => {
const source = [];
source.push('---');
source.push('title: Hooks');
source.push('description: All the hooks in this project.');
source.push('---');
source.push('');
source.push("import styles from './dox.module.css';");
source.push('');
source.push('This page documents all the hooks in this project.');
source.push('');
Object.entries(hooks)
.sort(([lhook], [rhook]) => (lhook < rhook ? -1 : 1))
.forEach(([hook, {implementations = [], invocations = [], specification}]) => {
const {
description,
example,
invoke,
params,
returns,
} = specification || {
params: [],
};
source.push(`## \`${hook}\``);
source.push('');
if (invoke) {
source.push('<h3 style={{fontSize: "1.125rem", marginTop: 0}}>');
source.push(`[${invoke}](../hooks#${invoke.toLowerCase()})`);
source.push('</h3>');
source.push('');
}
if (description) {
source.push(...description.split('\n'));
source.push('');
}
if (example) {
source.push('### Example usage');
source.push('');
source.push('```javascript');
source.push('export const hooks = {');
source.push(` '${hook}': ${example}`);
source.push('};');
source.push('```');
source.push('');
}
if (params.length > 0) {
params.forEach(({description, name, type}) => {
source.push(`### <code>${name}: ${type}</code>`);
source.push('');
source.push(`<p>${description.trim()}</p>`);
source.push('');
});
source.push('');
}
if (returns) {
const {description, type} = returns;
source.push(`### Returns \`${type}\``);
source.push('');
source.push(`<p>${description.trim()}</p>`);
source.push('');
source.push('');
}
if (implementations.length > 0 || invocations.length > 0) {
source.push('<div className={styles.hooks}>');
if (implementations.length > 0) {
source.push('<div>');
source.push('<h3>Implementations</h3>');
implementations.forEach(({filename}) => {
source.push(`<div>${filename}</div>`);
});
source.push('</div>');
}
if (invocations.length > 0) {
source.push('<div>');
source.push('<h3>Invocations</h3>');
invocations.forEach(({filename}) => {
source.push(`<div>${filename}</div>`);
});
source.push('</div>');
}
source.push('</div>');
source.push('\n');
}
});
return source.join('\n');
};
exports.generateDocusaurusStyle = () => `
@media screen and (max-width: 640px) {
.configBig {
display: none;
}
}
@media screen and (min-width: 641px) {
.configSmall {
display: none;
}
}
tr.configSmall:first-child {
display: none;
}
.configTable {
display: table;
table-layout: fixed;
width: 100%;
}
.configTable td:nth-child(2) code,
.configTable td:nth-child(2) pre,
.configTable td:nth-child(3) :global(.theme-code-block),
.configTable td:nth-child(3) :global(.theme-code-block) pre {
background-color: transparent !important;
border: none;
box-shadow: none;
}
.configTable td:nth-child(3) :global(.theme-code-block) {
margin: 0;
}
.hooks > div {
margin-bottom: var(--ifm-heading-margin-bottom);
}
.hooks > div code {
background-color: transparent;
border: none;
white-space: nowrap;
}
@media screen and (min-width: 641px) {
.hooks {
display: flex;
}
.hooks > div:first-child {
width: 50%;
}
.hooks > div:last-child {
padding-left: var(--ifm-spacing-horizontal);
width: 50%;
}
.hooks > div:only-child {
padding-left: 0;
width: 100%;
}
}
`;
exports.generateDocusaurusTodoPage = (todos) => {
const source = [];
source.push('---');
source.push('title: TODO list');
source.push('description: All the TODO items in this project.');
source.push('---');
source.push('');
source.push("import CodeBlock from '@theme/CodeBlock';");
source.push('');
source.push('This page documents all the TODO items in this project.');
source.push('');
if (todos.length > 0) {
todos.forEach(({
filename,
context,
description,
}) => {
source.push(filename);
source.push(`> ## ${description}`);
source.push(`> <CodeBlock>${context}</CodeBlock>`);
source.push('');
});
source.push('');
}
return source.join('\n');
};
exports.generateJson = async function generate(flecks) {
const parsed = await parseFlecks(flecks);
const {
buildFiles,
config,
hooks,
todos,
} = parsed
.reduce(
(
r,
[
root,
sources,
],
) => {
const ensureHook = (hook) => {
if (!r.hooks[hook]) {
r.hooks[hook] = {
implementations: [],
invocations: [],
specification: undefined,
};
}
};
sources.forEach(
(
[
path,
{
buildFiles = [],
config = [],
hookImplementations = [],
hookInvocations = [],
hookSpecifications = [],
todos = [],
},
],
) => {
r.buildFiles.push(...buildFiles);
r.todos.push(...todos.map((todo) => ({
...todo,
filename: join(root, path),
})));
if (config.length > 0) {
let fleck = root;
if (path.match(/@flecks\/core\.config(?:\.[^.]+)*/)) {
fleck = join(root, path).split('/').slice(0, -4).join('/');
}
else if ('build/flecks.bootstrap.js' !== path) {
fleck = join(fleck, path.startsWith('src') ? path.slice(4) : path);
fleck = join(dirname(fleck), basename(fleck, extname(fleck)));
fleck = fleck.endsWith('/index') ? fleck.slice(0, -6) : fleck;
}
r.config[fleck] = config;
}
hookImplementations.forEach(({
column,
filename,
hook,
line,
}) => {
ensureHook(hook);
r.hooks[hook].implementations.push({
filename: filename || [join(root, path), line, column].join(':'),
});
});
hookInvocations.forEach(({
column,
hook,
line,
type,
}) => {
ensureHook(hook);
r.hooks[hook].invocations.push({
filename: [join(root, path), line, column].join(':'),
type,
});
});
hookSpecifications.forEach(({hook, ...specification}) => {
ensureHook(hook);
r.hooks[hook].specification = specification;
});
},
);
return r;
},
{
buildFiles: [],
config: {},
hooks: {},
todos: [],
},
);
const sortedHooks = Object.fromEntries(
Object.entries(hooks)
.map(([hook, {implementations, invocations, specification}]) => (
[
hook,
{
implementations: implementations
.sort(({filename: l}, {filename: r}) => (l < r ? -1 : 1)),
invocations: invocations
.sort(({filename: l}, {filename: r}) => (l < r ? -1 : 1)),
specification,
},
]
))
.sort(([l], [r]) => (l < r ? -1 : 1)),
);
Object.entries(sortedHooks)
.forEach(([hook, {specification}]) => {
if (!specification) {
// eslint-disable-next-line no-console
console.warn(`Warning: no specification for hook: '${hook}'`);
}
});
return {
'build-files': buildFiles,
config,
hooks: sortedHooks,
todos,
};
};