feat: docs

This commit is contained in:
cha0s 2022-03-07 00:21:16 -06:00
parent 6e6cb3f0f4
commit 5e2b825620
16 changed files with 875 additions and 0 deletions

View File

@ -0,0 +1,28 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Gather database models.
*
* In the example below, your fleck would have a `models` subdirectory, and each model would be
* defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/user/src/server/models
*/
'@flecks/db/server/models': Flecks.provide(require.context('./models', false, /\.js$/)),
/**
* Decorate database models.
*
* In the example below, your fleck would have a `models/decorators` subdirectory, and each
* decorator would be defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/user/src/local/server/models/decorators
*
* @param {constructor} Model The model to decorate.
*/
'@flecks/db/server/models.decorate': (
Flecks.decorate(require.context('./models/decorators', false, /\.js$/))
),
},
};

View File

@ -0,0 +1,27 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define docker containers.
*
* Beware: the user running the server must have Docker privileges.
* See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
*/
'@flecks/docker/containers': () => ({
someContainer: {
// Environment variables.
environment: {
SOME_CONTAINER_VAR: 'hello',
},
// The docker image.
image: 'some-image:latest',
// Some container path you'd like to persist. Flecks handles the host path.
mount: '/some/container/path',
// Expose ports.
ports: {3000: 3000},
},
}),
},
};

116
packages/dox/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

31
packages/dox/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "@flecks/dox",
"publishConfig": {
"access": "public"
},
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"server.js",
"server.js.map",
"src"
],
"dependencies": {
"@babel/core": "^7.17.2",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@flecks/core": "^1.1.1",
"comment-parser": "^1.3.0",
"glob": "^7.2.0"
},
"devDependencies": {
"@flecks/fleck": "^1.0.1"
}
}

View File

@ -0,0 +1,46 @@
import {mkdir, writeFile} from 'fs/promises';
import {join} from 'path';
import {D} from '@flecks/core';
import {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);
debug('generated');
debug('Generating TODO page...');
const todoPage = generateTodoPage(state.todos);
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');
console.groupEnd();
console.log('');
/* eslint-enable no-console */
},
};
return commands;
};

View File

@ -0,0 +1,87 @@
export const generateHookPage = (hooks) => {
const source = [];
source.push('# Hooks');
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}]) => {
source.push(`## \`${hook}\``);
source.push('');
const {description, example, params} = specification || {
params: [],
};
if (description) {
description.split('\n').forEach((line) => {
source.push(`> ${line}`);
});
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('</ul>');
source.push('</details>');
source.push('');
}
if (implementations.length > 0) {
source.push('<details>');
source.push('<summary>Implementations</summary>');
source.push('<ul>');
implementations.forEach(({filename, loc: {start: {column, line}}}) => {
source.push(`<li>${filename}:${line}:${column}</li>`);
});
source.push('</ul>');
source.push('</details>');
source.push('');
}
if (invocations.length > 0) {
source.push('<details>');
source.push('<summary>Invocations</summary>');
source.push('<ul>');
invocations.forEach(({filename, loc: {start: {column, line}}}) => {
source.push(`<li>${filename}:${line}:${column}</li>`);
});
source.push('</ul>');
source.push('</details>');
source.push('');
}
if (example) {
source.push('### Example usage');
source.push('');
source.push('```javascript');
source.push('export default {');
source.push(' [Hooks]: {');
source.push(` '${hook}': ${example}`);
source.push(' },');
source.push('};');
source.push('```');
source.push('');
}
});
return source.join('\n');
};
export const generateTodoPage = (todos) => {
const source = [];
source.push('# TODO');
source.push('');
source.push('This page documents all the TODO items in this project.');
source.push('');
if (todos.length > 0) {
todos.forEach(({filename, loc: {start: {column, line}}, text}) => {
source.push(`- ${filename}:${line}:${column}`);
text.split('\n').forEach((line) => {
source.push(` > ${line}`);
});
});
source.push('');
}
return source.join('\n');
};

259
packages/dox/src/parser.js Normal file
View File

@ -0,0 +1,259 @@
import {readFile} from 'fs/promises';
import {dirname, join} from 'path';
import {transformAsync} from '@babel/core';
import traverse from '@babel/traverse';
import {
isIdentifier,
isLiteral,
isMemberExpression,
isObjectExpression,
isStringLiteral,
isThisExpression,
} from '@babel/types';
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() {
this.hooks = {};
this.todos = [];
}
addImplementation(hook, filename, loc) {
this.hooks[hook] = this.hooks[hook] || {};
this.hooks[hook].implementations = this.hooks[hook].implementations || [];
this.hooks[hook].implementations.push({filename, loc});
}
addInvocation(hook, type, filename, loc) {
this.hooks[hook] = this.hooks[hook] || {};
this.hooks[hook].invocations = this.hooks[hook].invocations || [];
this.hooks[hook].invocations.push({filename, loc, type});
}
addSpecification(hook, specification) {
this.hooks[hook] = this.hooks[hook] || {};
this.hooks[hook].specification = specification;
}
addTodo(comment, filename) {
this.todos.push({filename, loc: comment.loc, text: comment.value.trim()});
}
}
const FlecksInvocations = (state, filename) => ({
CallExpression(path) {
if (isMemberExpression(path.node.callee)) {
if (
(isIdentifier(path.node.callee.object) && 'flecks' === path.node.callee.object.name)
|| (
(
isThisExpression(path.node.callee.object)
&& (filename === join(flecksCorePath, 'src', 'flecks.js'))
)
)
) {
if (isIdentifier(path.node.callee.property)) {
if (path.node.callee.property.name.match(/^invoke.*/)) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.addInvocation(
path.node.arguments[0].value,
path.node.callee.property.name,
filename,
path.node.loc,
);
}
}
}
if ('up' === path.node.callee.property.name) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.addInvocation(
path.node.arguments[0].value,
'invokeSequentialAsync',
filename,
path.node.loc,
);
state.addInvocation(
'@flecks/core/starting',
'invokeFlat',
filename,
path.node.loc,
);
}
}
}
if ('gather' === path.node.callee.property.name) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.addInvocation(
path.node.arguments[0].value,
'invokeReduce',
filename,
path.node.loc,
);
state.addInvocation(
`${path.node.arguments[0].value}.decorate`,
'invokeComposed',
filename,
path.node.loc,
);
}
}
}
}
}
}
},
});
const implementationVisitor = (fn) => {
let hooksSymbol;
return {
ExportDefaultDeclaration(path) {
const {declaration} = path.node;
if (isObjectExpression(declaration)) {
const {properties} = declaration;
properties.forEach((property) => {
const {key, value} = property;
if (isIdentifier(key) && key.name === hooksSymbol) {
if (isObjectExpression(value)) {
const {properties} = value;
properties.forEach((property) => {
const {key} = property;
if (isLiteral(key)) {
fn(path, property);
}
});
}
}
});
}
},
ImportDeclaration(path) {
const {source, specifiers} = path.node;
if ('@flecks/core' === source.value) {
specifiers.forEach((specifier) => {
const {imported, local} = specifier;
if ('Hooks' === imported.name) {
hooksSymbol = local.name;
}
});
}
},
};
};
const FlecksImplementations = (state, filename) => (
implementationVisitor((path, {key}) => {
state.addImplementation(
key.value,
filename,
path.node.loc,
);
})
);
const FlecksSpecifications = (state, source) => (
implementationVisitor((path, property) => {
if (property.leadingComments) {
const {key, value: example} = property;
const [{value}] = property.leadingComments;
const [{description, tags}] = parseComment(`/**\n${value}\n*/`, {spacing: 'preserve'});
const params = tags
.filter(({tag}) => 'param' === tag)
.map(({description, name, type}) => ({description, name, type}));
state.addSpecification(
key.value,
{
description,
example: source.slice(example.start, example.end),
params,
},
);
}
})
);
const FlecksTodos = (state, filename) => ({
enter(path) {
if (path.node.leadingComments) {
path.node.leadingComments.forEach((comment) => {
if (comment.value.toLowerCase().match('@todo')) {
state.addTodo(comment, filename);
}
});
}
},
});
export const parseCode = async (code) => {
const config = {
ast: true,
code: false,
};
const {ast} = await transformAsync(code, config);
return ast;
};
export const parseFile = async (filename, resolved, state) => {
const buffer = await readFile(filename);
const ast = await parseCode(buffer.toString('utf8'));
traverse(ast, FlecksInvocations(state, resolved));
traverse(ast, FlecksImplementations(state, resolved));
traverse(ast, FlecksTodos(state, resolved));
};
const fleckSources = async (path) => (
new Promise((r, e) => (
glob(
join(path, 'src', '**', '*.js'),
(error, result) => (error ? e(error) : r(result)),
)
))
);
export const parseFleckRoot = async (root, state) => {
const resolved = dirname(R.resolve(join(root, 'package.json')));
const sources = await fleckSources(resolved);
await Promise.all(
sources.map(async (source) => {
await parseFile(source, join(root, source.slice(resolved.length)), state);
}),
);
try {
const buffer = await readFile(join(resolved, 'build', 'dox', 'hooks.js'));
const source = buffer.toString('utf8');
const ast = await parseCode(source);
traverse(ast, FlecksSpecifications(state, source));
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
}
};
export const parseFlecks = async (flecks) => {
const state = new ParserState();
const paths = Object.keys(flecks.resolver);
const roots = Array.from(new Set(
paths
.map((path) => flecks.root(path))
.filter((e) => !!e),
));
await Promise.all(
roots
.map(async (root) => {
await parseFleckRoot(root, state);
}),
);
return state;
};

View File

@ -0,0 +1,9 @@
import {Hooks} from '@flecks/core';
import commands from './commands';
export default {
[Hooks]: {
'@flecks/core/commands': commands,
},
};

View File

@ -0,0 +1,69 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define sequential actions to run when the client comes up.
*/
'@flecks/http/client/up': async () => {
await youCanDoAsyncThingsHere();
},
/**
* Override flecks configuration sent to client flecks.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/http/config': (req) => ({
someClientFleck: {
someConfig: req.someConfig,
},
}),
/**
* Define HTTP routes.
*/
'@flecks/http/routes': () => [
{
method: 'get',
path: '/some-path',
middleware: (req, res, next) => {
// Express-style route middleware...
next();
},
},
],
/**
* Define neutrino compilation middleware (e.g. @neutrinojs/react).
*/
'@flecks/http/server/compiler': () => {
return require('@neutrinojs/node');
},
/**
* Define middleware to run when a route is matched.
*/
'@flecks/http/server/request.route': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define middleware to run when an HTTP socket connection is established.
*/
'@flecks/http/server/request.socket': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define composition functions to run over the HTML stream prepared for the client.
* @param {stream.Readable} stream The HTML stream.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/http/server/stream.html': (stream, req) => {
return stream.pipe(myTransformStream);
},
/**
* Define sequential actions to run when the HTTP server comes up.
*/
'@flecks/http/server/up': async () => {
await youCanDoAsyncThingsHere();
},
},
};

View File

@ -0,0 +1,27 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define React Providers.
*
* Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react/providers': (req) => {
// Generally it makes more sense to separate client and server concerns using platform
// naming conventions, but this is just a small contrived example.
return req ? serverSideProvider(req) : clientSideProvider();
},
/**
* Define root-level React components that are mounted as siblings on `#main`.
* Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react/roots': (req) => {
// Note that we're not returning `<Component />`, but `Component`.
return Component;
},
},
};

View File

@ -0,0 +1,45 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define side-effects to run against Redux actions.
*/
'@flecks/redux/effects': () => ({
someActionName: (store, action, flecks) => {
// Runs when `someActionName` actions are dispatched.
},
}),
/**
* Define root-level reducers for the Redux store.
*/
'@flecks/redux/reducers': () => {
return (state, action) => {
// Whatever you'd like.
return state;
};
},
/**
* Define Redux slices.
*
* See: https://redux-toolkit.js.org/api/createSlice
*/
'@flecks/redux/slices': () => {
const something = createSlice(
// ...
);
return {
something: something.reducer,
};
},
/**
* Modify Redux store configuration.
* @param {Object} options A mutable object with keys for enhancers and middleware.
*/
'@flecks/redux/store': (options) => {
options.enhancers.splice(someIndex, 1);
options.middleware.push(mySpecialMiddleware);
},
},
};

View File

@ -0,0 +1,30 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define REPL commands.
*
* Note: commands will be prefixed with a period in the Node REPL.
*/
'@flecks/repl/commands': () => ({
someCommand: (...args) => {
// args are passed from the Node REPL. So, you could invoke it like:
// .someCommand foo bar
// and `args` would be `['foo', 'bar']`.
},
}),
/**
* Provide global context to the REPL.
*/
'@flecks/repl/context': () => {
// Now you'd be able to do like:
// `node> someValue;`
// and the REPL would evaluate it to `'foobar'`.
return {
someValue: 'foobar',
};
},
},
};

View File

@ -0,0 +1,19 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define neutrino compilation middleware (e.g. @neutrinojs/react).
*/
'@flecks/server/compiler': () => {
return require('@neutrinojs/node');
},
/**
* Define sequential actions to run when the server comes up.
*/
'@flecks/server/up': async () => {
await youCanDoAsyncThingsHere();
},
},
};

View File

@ -0,0 +1,65 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Modify Socket.io client configuration.
*
* See: https://socket.io/docs/v4/client-options/
*/
'@flecks/socket/client': () => ({
timeout: Infinity,
}),
/**
* Define server-side intercom channels.
*/
'@flecks/socket/intercom': (req) => ({
// This would have been called like:
// `const result = await req.intercom('someChannel', payload)`.
// `result` will be an `n`-length array, where `n` is the number of server instances. Each
// element in the array will be the result of `someServiceSpecificInformation()` running
// against that server instance.
someChannel: async (payload, server) => {
return someServiceSpecificInformation();
},
}),
/**
* Define socket packets.
*
* In the example below, your fleck would have a `packets` subdirectory, and each
* decorator would be defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/redux/src/packets
*
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/packet.js
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/redirect.js
*/
'@flecks/socket/packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
/**
* Decorate database models.
*
* In the example below, your fleck would have a `packets/decorators` subdirectory, and each
* decorator would be defined in its own file.
* @param {constructor} Packet The packet to decorate.
*/
'@flecks/socket/packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
),
/**
* Modify Socket.io server configuration.
*
* See: https://socket.io/docs/v4/server-options/
*/
'@flecks/socket/server': () => ({
pingTimeout: Infinity,
}),
/**
* Define middleware to run when a socket connection is established.
*/
'@flecks/socket/server/request.socket': () => (socket, next) => {
// Express-style route middleware...
next();
},
},
};

View File

@ -0,0 +1,15 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Modify express-session configuration.
*
* See: https://www.npmjs.com/package/express-session
*/
'@flecks/user/session': () => ({
saveUninitialized: true,
}),
},
};