chore: dox++

This commit is contained in:
cha0s 2024-01-23 09:06:00 -06:00
parent 64bd11e975
commit 901be18ac9
43 changed files with 1281 additions and 956 deletions

View File

@ -92,6 +92,7 @@ flecks strives to provide powerful defaults that minimize the need to override c
The simplest example of a flecks server application:
```yml
'@flecks/build': {}
'@flecks/core': {}
'@flecks/server': {}
```

View File

@ -36,6 +36,7 @@
},
"dependencies": {
"@docusaurus/theme-mermaid": "3.0.1",
"@flecks/build": "*",
"lerna": "^8.0.2"
}
}

View File

@ -54,7 +54,7 @@ module.exports = class Build extends Flecks {
aliased = {};
buildConfigs = {};
buildFiles = {};
platforms = ['server'];
@ -245,10 +245,10 @@ module.exports = class Build extends Flecks {
Object.entries(this.invoke('@flecks/build.files'))
.forEach(([fleck, configs]) => {
configs.forEach((config) => {
this.buildConfigs[config] = fleck;
this.buildFiles[config] = fleck;
});
});
debugSilly('build configs loaded: %O', this.buildConfigs);
debugSilly('build configs loaded: %O', this.buildFiles);
}
get realiasedConfig() {
@ -262,7 +262,7 @@ module.exports = class Build extends Flecks {
}
async resolveBuildConfig(config, override) {
const fleck = this.buildConfigs[config];
const fleck = this.buildFiles[config];
if (!fleck) {
throw new Error(`Unknown build config: '${config}'`);
}
@ -371,7 +371,7 @@ module.exports = class Build extends Flecks {
}
get targets() {
const targets = this.invoke('@flecks/core.targets');
const targets = this.invoke('@flecks/build.targets');
const duplicates = {};
const entries = Object.entries(targets);
const set = new Set();
@ -391,9 +391,9 @@ module.exports = class Build extends Flecks {
`Multiple flecks ('${flecks.join("', '")})' tried to build target '${target}'`
)).join('\n');
if (errorMessage) {
throw new Error(`@flecks/core.targets:\n${errorMessage}`);
throw new Error(`@flecks/build.targets:\n${errorMessage}`);
}
this.invoke('@flecks/core.targets.alter', set);
this.invoke('@flecks/build.targets.alter', set);
return entries
.map(([fleck, targets]) => (
targets

View File

@ -15,7 +15,7 @@ module.exports = async (flecks) => ({
ignorePatterns: [
'dist/**',
// Not even gonna try.
'build/dox/hooks.js',
'build/flecks.hooks.js',
],
overrides: [
{

View File

@ -0,0 +1,103 @@
export const hooks = {
/**
* Hook into webpack configuration.
* @param {string} target The build target; e.g. `server`.
* @param {Object} config The webpack configuration.
* @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
*/
'@flecks/build.config': (target, config, env, argv) => {
if ('something' === target) {
if ('production' === argv.mode) {
config.plugins.push(new SomePlugin());
}
}
},
/**
* Alter build configurations after they have been hooked.
* @param {Object} configs The webpack configurations keyed by target.
* @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
*/
'@flecks/build.config.alter': (configs) => {
// Maybe we want to do something if a target exists..?
if (configs.someTarget) {
configs.plugins.push('...');
}
},
/**
* Add implicitly resolved extensions.
*/
'@flecks/build.extensions': () => ['.coffee'],
/**
* Register build files.
*/
'@flecks/build.files': () => [
/**
* If you document your build files like this, documentation will be automatically generated.
*/
'.myrc.js',
],
/**
* 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/build.commands': (program, flecks) => {
return {
// So this could be invoked like:
// npx flecks something -t --blow-up blah
something: {
action: (...args) => {
// Run the command...
},
args: [
program.createArgument('<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',
],
},
};
},
/**
* Process the `package.json` for a built fleck.
* @param {Object} json The JSON.
* @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation.
*/
'@flecks/build.packageJson': (json, compilation) => {
json.files.push('something');
},
/**
* Process assets during a compilation.
* @param {Record&lt;string, Source&gt;} assets The assets.
* @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation.
*/
'@flecks/build.processAssets': (assets, compilation) => {
assets['my-file.js'] = new Source();
},
/**
* Define build targets.
*/
'@flecks/build.targets': () => ['sometarget'],
/**
* Alter defined build targets.
* @param {Set&lt;string&gt;} targets The targets to build.
*/
'@flecks/build.targets.alter': (targets) => {
targets.delete('some-target');
},
};

View File

@ -1,98 +0,0 @@
# Build directory ⚡️
The `build` directory is where build-time configuration is specified.
The prime example of this for Flecks is `flecks.yml`, but it extends to other more general
configuration such as `.eslintrc.js`, `babel.config.js`, etc.
For a list of all build configuration, see the
[build configuration page](https://github.com/cha0s/flecks/blob/gh-pages/build-configs.md)
## `flecks.yml` ⛏️
`flecks.yml` specifies the flecks that compose your project.
Using `@flecks/create-fleck` creates the following `flecks.yml`:
```yml
'@flecks/core': {}
'@flecks/fleck': {}
```
This means that by default a new fleck will pull in the `@flecks/core` fleck, and the
`@flecks/fleck` fleck, both with default configuration.
### Overriding configuration 💪
`@flecks/core`'s configuration has an `id` key. Starting from the example above, overriding the
ID to, say, `'example'`, would look like this:
```yml
'@flecks/core':
id: 'example'
'@flecks/fleck': {}
```
See [the configuration page](https://github.com/cha0s/flecks/blob/gh-pages/config.md) for a list of
all configuration.
### Aliasing 🕵️‍♂️
Flecks may be aliased to alternative paths.
Say you have an application structured as a monorepo with a `packages` directory. If you have a
subpackage named `@my-monorepo/foo`, you could alias your fleck, like so:
```yml
'@flecks/core': {}
'@flecks/server': {}
'@my-monorepo/foo:./packages/foo/src': {}
```
Within your application, the fleck will be referred to as `@my-monorepo/foo` even though
`./packages/foo/src` is where the package is actually located.
This way you can use package structure without having to worry about actually publishing them to
npm (or running verdaccio, for instance).
## On-the-fly compilation(!) 🤯
If your flecks are aliased (as above) or symlinked (e.g. `yarn link`), they will be treated
specially and will be compiled on-the-fly. The flecks are searched for a local `babel.config.js`,
which is used to compile the code if present.
This means you can e.g. develop your `packages` in a monorepo with full HMR support, on both the
server and the client, each with their own babel configuration!
Have fun!
## Resolution order 🤔
The flecks server provides an interface (`flecks.resolveBuildConfig()`) for gathering configuration files
from the `build` directory. The resolution order is determined by a few variables:
- `filename` specifies the name of the configuration file, e.g. `server.webpack.config.js`.
- `general` specifies a general variation of the given configuration. The general form of `server.webpack.config.js` is `webpack.config.js`.
- `root` specifies an alternative location to search. Defaults to `FLECKS_CORE_ROOT`.
- `fleck` specifies the fleck owning the configuration. `@flecks/server` owns `server.webpack.config.js`.
Given these considerations, and supposing we had the above variables set like:
```javascript
const filename = 'server.webpack.config.js';
const general = 'webpack.config.js';
const root = '/foo/bar/baz';
const fleck = '@flecks/server';
```
Flecks will then search the following paths top-down until it finds the build configuration:
- `/foo/bar/baz/build/server.webpack.config.js`
- `/foo/bar/baz/build/webpack.config.js`
- `${FLECKS_CORE_ROOT}/build/server.webpack.config.js`
- `${FLECKS_CORE_ROOT}/build/webpack.config.js`
- `@flecks/server/build/server.webpack.config.js`
- `@flecks/server/build/webpack.config.js`

View File

@ -1,316 +0,0 @@
# Hooks
Hooks are how everything happens in flecks. There are many hooks and the hooks provided by flecks are documented at the [hooks reference page](https://github.com/cha0s/flecks/blob/gh-pages/hooks.md).
To define hooks (and turn your plain ol' boring JS modules into beautiful interesting flecks), you only have to export a `hooks` object:
```javascript
export const hooks = {
'@flecks/core.starting': () => {
console.log('hello, gorgeous');
},
};
```
**Note:** All hooks recieve an extra final argument, which is the flecks instance.
## Types
&nbsp;
### `flecks.invoke(hook, ...args)`
Invokes all hook implementations and returns the results keyed by the implementing flecks' paths.
&nbsp;
### `flecks.invokeComposed(hook, initial, ...args)`
### `flecks.invokeComposedAsync(hook, initial, ...args)`
See: [function composition](https://www.educative.io/edpresso/function-composition-in-javascript).
`initial` is passed to the first implementation, which returns a result which is passed to the second implementation, which returns a result which is passed to the third implementation, etc.
Composed hooks are [orderable](#orderable-hooks).
&nbsp;
### `flecks.invokeFlat(hook, ...args)`
Invokes all hook implementations and returns the results as an array.
&nbsp;
### `flecks.invokeFleck(hook, fleck, ...args)`
Invoke a single fleck's hook implementation and return the result.
&nbsp;
### `flecks.invokeMerge(hook, ...args)`
### `flecks.invokeMergeAsync(hook, ...args)`
Invokes all hook implementations and returns the result of merging all implementations' returned objects together.
&nbsp;
### `flecks.invokeReduce(hook, reducer, initial, ...args)`
### `flecks.invokeReduceAsync(hook, reducer, initial, ...args)`
See: [Array.prototype.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)
Invokes hook implementations one at a time, their results being passed to the reducer as `currentValue`. Returns the final reduction.
&nbsp;
### `flecks.invokeSequential(hook, ...args)`
### `flecks.invokeSequentialAsync(hook, ...args)`
Invokes all hook implementations, one after another. In the async variant, each implementation's result is `await`ed before invoking the next implementation.
Sequential hooks are [orderable](#orderable-hooks).
&nbsp;
## Idioms
### `flecks.gather(hook, options)`
Gathering is useful when your fleck defines some sort of specification, and then expects its sibling flecks to actually implement it. Examples of this in flecks would be:
- Models, defined through `@flecks/db/server.models`.
- Packets, defined through `@flecks/socket.packets`.
One constraint of using `flecks.gather()` is that whatever you are gathering must be able to be extended as a class. You can't `flecks.gather()` plain objects, numbers, strings... you get the idea.
The most basic usage:
```javascript
const Gathered = flecks.gather('my-gather-hook');
```
Suppose `my-gather-hook` above resulted in gathering two classes, `Foo` and `Bar`. In this case, `Gathered` would be such as:
```javascript
import {ById, ByType} from '@flecks/core';
const Gathered = {
1: Bar,
2: Foo,
'Bar': Bar,
'Foo': Foo,
[ById]: {
1: Bar,
2: Foo,
},
[ByType]: {
'Bar': Bar,
'Foo': Foo,
},
};
```
`flecks.gather()` gives each of your classes a numeric (nonzero) ID as well as a type name. It also merges all numeric keys and type labels together into the result, so `Gathered[1] === Gathered.Bar` would evaluate to `true` in the example above.
The symbol keys `ById` and `ByType` are useful if you need to iterate over *either* all IDs or all types. Since the numeric IDs and types are merged, iterating over the entire `Gathered` object would otherwise result in duplicates.
Each class gathered by `flecks.gather()` will be extended with two properties by default: `id` and `type`. These correspond to the ID and type referenced above, and are useful for e.g. serialization.
Following from the example above:
```javascript
const foo = new Gathered.Foo();
assert(foo.id === 2);
assert(foo.type === 'Foo');
```
`flecks.gather()` also supports some options:
```javascript
{
// The property added when extending the class to return the numeric ID.
idProperty = 'id',
// The property added when extending the class to return the type.
typeProperty = 'type',
// A function called with the `Gathered` object to allow checking validity.
check = () => {},
}
```
As an example, when `@flecks/db/server` gathers models, `typeProperty` is set to `name`, because Sequelize requires its model classes to have a unique `name` property.
**Note:** the numeric IDs are useful for efficient serialization between the client and server, but **if you are using this property, ensure that `flecks.gather()` is called equivalently on both the client and the server**. As a rule of thumb, if you have serializable `Gathered`s, they should be invoked and defined in `your-fleck`, and not in `your-fleck/[platform]`, so that they are invoked for every platform.
#### `Flecks.provide(context, options)`
Complementary to gather hooks above, `Flecks.provide()` allows you to ergonomically provide your flecks' implementations to a gather hook.
Here's an example of how you could manually provide `@flecks/db/server.models` in your own fleck:
```javascript
import SomeModel from './models/some-model';
import AnotherModel from './models/another-model';
export const hooks = {
'@flecks/db/server.models': () => ({
SomeModel,
AnotherModel,
}),
}
```
If you think about the example above, you might realize that it will become a lot of typing to keep adding new models over time. Provider hooks exist to reduce this maintenance burden for you.
Webpack provides an API called [require.context](https://webpack.js.org/guides/dependency-management/#requirecontext), and the flecks provider is optimized to work with this API.
Supposing our fleck is structured like so:
```
index.js
models/
├─ some-model.js
└─ another-model.js
```
then, this `index.js`:
```javascript
import {Flecks} from '@flecks/core';
export const hooks = {
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
};
```
is *exactly equivalent* to the gather example above. By default, `Flecks.provide()` *CamelCase*s the paths, so `some-model` becomes `SomeModel`, just as in the example above.
`Flecks.provide()` also supports some options:
```javascript
{
// Whether to invoke the default export as a function.
invoke = true,
// The transformation used on the class path.
transformer = camelCase,
}
```
**Note:** There is no requirement to use `Flecks.provide()`, it is merely a convenience.
### Decorator hooks
When a Model (or any other) is gathered as above, an implicit hook is called: `${hook}.decorate`. This allows other flecks to decorate whatever has been gathered:
```javascript
export const hooks = {
'@flecks/db/server.models.decorate': (Models) => {
return {
...Models,
User: class extends Models.User {
// Let's mix in some logging...
constructor(...args) {
super(...args);
console.log ('Another user decorated!');
}
},
};
},
};
```
#### `Flecks.decorate(context, options)`
As with above, there exists an API for making the maintenance of decorators even more ergonomic.
Supposing our fleck is structured like so:
```
index.js
models/
└─ decorators/
└─ user.js
```
and supposing that `./models/decorators/user.js` is written like so:
```javascript
export default (User) => {
return class extends User {
// Let's mix in some logging...
constructor(...args) {
super(...args);
console.log ('Another user decorated!');
}
};
};
```
then, this `index.js`:
```javascript
import {Flecks} from '@flecks/core';
export const hooks = {
'@flecks/db/server.models.decorate': (
Flecks.decorate(require.context('./models/decorators', false, /\.js$/))
),
};
```
is *exactly equivalent* to the decorator example above.
`Flecks.decorate()` also supports some options:
```javascript
{
// The transformation used on the class path.
transformer = camelCase,
}
```
Decorator hooks are [orderable](#orderable-hooks).
## Orderable hooks
In many of the instances above, reference was made to the fact that certain hook types are "orderable".
Suppose we are composing an application and we have HTTP session state using cookies. When a user hits a route, we need to load their session and subsequently read a value from said session to determine if the user prefers dark mode. Clearly, we will have to ensure that the session reification happens first. This is one function of orderable hooks.
Flecks uses the name of the hook as a configuration key in order to determine the ordering of a hook. Let's take the hook we alluded to earlier as an example, `@flecks/http/server.request.route`:
Our `flecks.yml` could be configured like so:
```yaml
'@flecks/http/server':
'request.route':
- '@flecks/user/session'
- 'my-cool-fleck'
```
In this application, when `@flecks/http/server.request.route` is invoked, `@flecks/user/session`'s implementation is invoked (which reifies the user's session from cookies), followed by `my-cool-fleck`'s (which, we assume, does some kind of very cool dark mode check).
### Ellipses/elision
It may not always be ergonomic to configure the order of every single implementation, but enough to specify which implementations must run first (or last).
For example, suppose we have multiple implementations that require there to have been a reified user session, but which order those implementations run might not be a concern. For this, flecks provides you with the ellipses entry:
```yaml
'@flecks/http/server':
'request.route':
- '@flecks/user/session'
- '...'
- 'some-final-fleck'
```
In this application, we first reify the user session as before, but instead of listing `my-cool-fleck` immediately after, we specify ellipses. After the ellipses we specify `some-final-fleck` to, we assume, do some finalization work.
Ellipses essentially translate to: "every implementing fleck which has not already been explicitly listed in the ordering configuration".
Using more than one ellipses entry in an ordering configuration is ambiguous and will throw an error.
The default ordering configuration for any orderable hook is: `['...']` which translates to all implementations in an undefined order.

View File

@ -1,143 +0,0 @@
export const hooks = {
/**
* Hook into webpack configuration.
* @param {string} target The build target; e.g. `server`.
* @param {Object} config The webpack configuration.
* @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
*/
'@flecks/core.build': (target, config, env, argv) => {
if ('something' === target) {
if ('production' === argv.mode) {
config.plugins.push(new SomePlugin());
}
}
},
/**
* Alter build configurations after they have been hooked.
* @param {Object} configs The webpack configurations keyed by target.
* @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
*/
'@flecks/core.build.alter': (configs) => {
// Maybe we want to do something if a target exists..?
if (configs.someTarget) {
// Do something...
// And then maybe we want to remove it from the build configuration..? That's ok!
delete configs.someTarget;
}
},
/**
* Register build configuration.
*/
'@flecks/core.build.config': () => [
/**
* If you document your config files like this, documentation will be automatically
* generated.
*/
'.myrc.js',
/**
* Make sure you return them as an array expression, like this.
*/
['something.config.js', {specifier: (specific) => `something.${specific}.config.js`}],
],
/**
* 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, 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',
],
},
};
},
/**
* Define configuration.
*/
'@flecks/core.config': () => ({
whatever: 'configuration',
your: 1337,
fleck: 'needs',
/**
* Also, comments like this will be used to automatically generate documentation.
*/
though: 'you should keep the values serializable',
}),
/**
* Invoked when a fleck is HMR'd
* @param {string} path The path of the fleck
* @param {Module} updatedFleck The updated fleck module.
*/
'@flecks/core.hmr': (path, updatedFleck) => {
if ('my-fleck' === path) {
updatedFleck.doSomething();
}
},
/**
* Invoked when a gathered set is HMR'd.
* @param {constructor} gathered The gathered set.
* @param {string} hook The gather hook; e.g. `@flecks/db/server.models`.
*/
'@flecks/core.hmr.gathered': (gathered, hook) => {
// Do something with the gathered set...
},
/**
* Invoked when a gathered class is HMR'd.
* @param {constructor} Class The class.
* @param {string} hook The gather hook; e.g. `@flecks/db/server.models`.
*/
'@flecks/core.hmr.gathered.class': (Class, hook) => {
// Do something with Class...
},
/**
* Invoked when flecks is building a fleck dependency graph.
* @param {Digraph} graph The dependency graph.
* @param {string} hook The hook; e.g. `@flecks/db/server`.
*/
'@flecks/core.priority': (graph, hook) => {
// Make `@flecks/user/server`'s `@flecks/server.up` implementation depend on
// `@flecks/db/server`'s:
if ('@flecks/server.up' === hook) {
graph.addDependency('@flecks/user/server', '@flecks/db/server');
// Remove a dependency.
graph.removeDependency('@flecks/user/server', '@flecks/db/server');
}
},
/**
* Invoked when the application is starting. Use for order-independent initialization tasks.
*/
'@flecks/core.starting': () => {
console.log('starting!');
},
/**
* Define build targets.
*/
'@flecks/core.targets': () => ['sometarget'],
};

View File

@ -0,0 +1,74 @@
export const hooks = {
/**
* Babel configuration.
*/
'@flecks/core.babel': () => ({
plugins: ['...'],
}),
/**
* Define configuration.
*/
'@flecks/core.config': () => ({
whatever: 'configuration',
your: 1337,
fleck: 'needs',
/**
* Also, comments like this will be used to automatically generate documentation.
*/
though: 'you should keep the values serializable',
}),
/**
* Invoked when a fleck is HMR'd
* @param {string} path The path of the fleck
* @param {Module} updatedFleck The updated fleck module.
*/
'@flecks/core.hmr': (path, updatedFleck) => {
if ('my-fleck' === path) {
updatedFleck.doSomething();
}
},
/**
* Invoked when a gathered set is HMR'd.
* @param {constructor} gathered The gathered set.
* @param {string} hook The gather hook; e.g. `@flecks/db/server.models`.
*/
'@flecks/core.hmr.gathered': (gathered, hook) => {
// Do something with the gathered set...
},
/**
* Invoked when a gathered class is HMR'd.
* @param {constructor} Class The class.
* @param {string} hook The gather hook; e.g. `@flecks/db/server.models`.
*/
'@flecks/core.hmr.gathered.class': (Class, hook) => {
// Do something with Class...
},
/**
* Invoked when flecks is building a fleck dependency graph.
* @param {Digraph} graph The dependency graph.
* @param {string} hook The hook; e.g. `@flecks/db/server`.
*/
'@flecks/core.priority': (graph, hook) => {
// Make `@flecks/socket/server`'s `@flecks/server.up` implementation depend on
// `@flecks/db/server`'s:
if ('@flecks/server.up' === hook) {
graph.addDependency('@flecks/socket/server', '@flecks/db/server');
// Remove a dependency.
graph.removeDependency('@flecks/socket/server', '@flecks/db/server');
}
},
/**
* Invoked when the application is starting. Use for order-independent initialization tasks.
*/
'@flecks/core.starting': () => {
console.log('starting!');
},
};

View File

@ -1,12 +1,19 @@
const {mkdir, writeFile} = require('fs/promises');
const {isAbsolute, join, resolve} = require('path');
const {
basename,
dirname,
extname,
isAbsolute,
join,
resolve,
} = require('path');
const {spawnWith} = require('@flecks/core/server');
const {themes: prismThemes} = require('prism-react-renderer');
const {rimraf} = require('rimraf');
const {
generateBuildConfigsPage,
generateBuildFilesPage,
generateConfigPage,
generateHookPage,
generateTodoPage,
@ -63,14 +70,134 @@ exports.generate = async function generate(flecks, siteDir) {
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);
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 ('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, hook, line}) => {
ensureHook(hook);
r.hooks[hook].implementations.push({
column,
filename: join(`**${root}**`, path),
line,
});
});
hookInvocations.forEach(({
column,
hook,
line,
type,
}) => {
ensureHook(hook);
r.hooks[hook].invocations.push({
column,
filename: join(`**${root}**`, path),
line,
type,
});
});
hookSpecifications.forEach(({
hook,
description,
example,
params,
}) => {
ensureHook(hook);
r.hooks[hook].specification = {
description,
example,
params,
};
});
},
);
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}'`);
}
});
const hookPage = generateHookPage(sortedHooks, flecks);
const todoPage = generateTodoPage(todos, flecks);
const buildFilesPage = generateBuildFilesPage(buildFiles);
const configPage = generateConfigPage(config);
await writeFile(join(generatedDirectory, 'hooks.mdx'), hookPage);
await writeFile(join(generatedDirectory, 'TODO.mdx'), todoPage);
await writeFile(join(generatedDirectory, 'build-configs.mdx'), buildFilesPage);
await writeFile(join(generatedDirectory, 'config.mdx'), configPage);
};

View File

@ -2,23 +2,26 @@ const makeFilenameRewriter = (filenameRewriters) => (filename, line, column) =>
Object.entries(filenameRewriters)
.reduce(
(filename, [from, to]) => filename.replace(new RegExp(from), to),
`${filename}:${line}:${column}`,
[filename, ...(line ? [line, column] : [])].join(':'),
)
);
exports.generateBuildConfigsPage = (buildConfigs) => {
exports.generateBuildFilesPage = (buildFiles) => {
const source = [];
source.push('# Build configuration');
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 (buildConfigs.length > 0) {
buildConfigs
.sort(({config: l}, {config: r}) => (l < r ? -1 : 1))
.forEach(({config, comment}) => {
source.push(`## \`${config}\``);
if (buildFiles.length > 0) {
buildFiles
.sort(({filename: l}, {filename: r}) => (l < r ? -1 : 1))
.forEach(({filename, description}) => {
source.push(`## \`${filename}\``);
source.push('');
source.push(comment);
source.push(description);
source.push('');
});
}
@ -27,9 +30,12 @@ exports.generateBuildConfigsPage = (buildConfigs) => {
exports.generateConfigPage = (configs) => {
const source = [];
source.push("const CodeBlock = require('@theme/CodeBlock');");
source.push('---');
source.push('title: Fleck configuration');
source.push('description: All the configurable flecks in this project.');
source.push('---');
source.push('');
source.push('# Fleck configuration');
source.push("import CodeBlock from '@theme/CodeBlock';");
source.push('');
source.push('<style>td > .theme-code-block \\{ margin: 0; \\}</style>');
source.push('');
@ -41,9 +47,9 @@ exports.generateConfigPage = (configs) => {
source.push(`## \`${fleck}\``);
source.push('|Name|Default|Description|');
source.push('|-|-|-|');
configs.forEach(({comment, config, defaultValue}) => {
configs.forEach(({defaultValue, description, key}) => {
// Leading and trailing empty cell to make table rendering easier.
const row = ['', config];
const row = ['', key];
let code = defaultValue.replace(/`/g, '\\`');
// Multiline code. Fix indentation.
if (defaultValue.includes('\n')) {
@ -53,7 +59,7 @@ exports.generateConfigPage = (configs) => {
code = [first, ...rest.map((line) => line.substring(indent))].join('\\n');
}
row.push(`<CodeBlock language="javascript">{\`${code}\`}</CodeBlock>`);
row.push(comment, '');
row.push(description, '');
source.push(row.join('|'));
});
source.push('');
@ -65,7 +71,10 @@ exports.generateHookPage = (hooks, flecks) => {
const {filenameRewriters} = flecks.get('@flecks/dox');
const rewriteFilename = makeFilenameRewriter(filenameRewriters);
const source = [];
source.push('# Hooks');
source.push('---');
source.push('title: Hooks');
source.push('description: All the hooks in this project.');
source.push('---');
source.push('');
source.push('This page documents all the hooks in this project.');
source.push('');
@ -81,6 +90,16 @@ exports.generateHookPage = (hooks, flecks) => {
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>`);
@ -94,7 +113,7 @@ exports.generateHookPage = (hooks, flecks) => {
source.push('<details>');
source.push('<summary>Implementations</summary>');
source.push('<ul>');
implementations.forEach(({filename, loc: {start: {column, line}}}) => {
implementations.forEach(({filename, column, line}) => {
source.push(`<li>${rewriteFilename(filename, line, column)}</li>`);
});
source.push('</ul>');
@ -105,23 +124,13 @@ exports.generateHookPage = (hooks, flecks) => {
source.push('<details>');
source.push('<summary>Invocations</summary>');
source.push('<ul>');
invocations.forEach(({filename, loc: {start: {column, line}}}) => {
invocations.forEach(({filename, column, line}) => {
source.push(`<li>${rewriteFilename(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 const hooks = {');
source.push(` '${hook}': ${example}`);
source.push('};');
source.push('```');
source.push('');
}
});
return source.join('\n');
};
@ -130,16 +139,25 @@ exports.generateTodoPage = (todos, flecks) => {
const {filenameRewriters} = flecks.get('@flecks/dox');
const rewriteFilename = makeFilenameRewriter(filenameRewriters);
const source = [];
source.push('# TODO list');
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, loc: {start: {column, line}}, text}) => {
source.push(`- ${rewriteFilename(filename, line, column)}`);
text.split('\n').forEach((line) => {
source.push(` > ${line}`);
});
todos.forEach(({
filename,
context,
description,
}) => {
source.push(rewriteFilename(filename));
source.push(`> ## ${description}`);
source.push(`> <CodeBlock>${context}</CodeBlock>`);
source.push('');
});
source.push('');
}

View File

@ -1,328 +1,130 @@
const {readFile} = require('fs/promises');
const {
basename,
dirname,
extname,
join,
} = require('path');
const {join, relative} = require('path');
const {transformAsync} = require('@babel/core');
const {default: traverse} = require('@babel/traverse');
const {
isArrayExpression,
isArrowFunctionExpression,
isIdentifier,
isLiteral,
isMemberExpression,
isObjectExpression,
isStringLiteral,
isThisExpression,
isVariableDeclaration,
} = require('@babel/types');
const {glob} = require('@flecks/core/server');
const {parse: parseComment} = require('comment-parser');
class ParserState {
constructor() {
this.buildConfigs = [];
this.configs = {};
this.hooks = {};
this.todos = [];
}
addBuildConfig(config, comment) {
this.buildConfigs.push({comment, config});
}
addConfig(config, comment, filename, defaultValue) {
const ext = extname(filename);
const trimmed = join(dirname(filename), basename(filename, ext)).replace('/src', '');
const fleck = 'index' === basename(trimmed) ? dirname(trimmed) : trimmed;
if (!this.configs[fleck]) {
this.configs[fleck] = [];
}
this.configs[fleck].push({
comment,
config,
defaultValue,
});
}
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 implementationVisitor = (fn) => ({
ExportNamedDeclaration(path) {
const {declaration} = path.node;
if (isVariableDeclaration(declaration)) {
const {declarations} = declaration;
declarations.forEach((declarator) => {
if ('hooks' === declarator.id.name) {
if (isObjectExpression(declarator.init)) {
const {properties} = declarator.init;
properties.forEach((property) => {
const {key} = property;
if (isLiteral(key)) {
fn(property);
}
});
}
}
});
}
},
});
const FlecksBuildConfigs = (state) => (
implementationVisitor((property) => {
if ('@flecks/build.files' === property.key.value) {
if (isArrowFunctionExpression(property.value)) {
if (isArrayExpression(property.value.body)) {
property.value.body.elements.forEach((element) => {
let config;
if (isStringLiteral(element)) {
config = element.value;
}
if (isArrayExpression(element)) {
if (element.elements.length > 0 && isStringLiteral(element.elements[0])) {
config = element.elements[0].value;
}
}
if (config) {
state.addBuildConfig(
config,
(element.leadingComments?.length > 0)
? element.leadingComments.pop().value.split('\n')
.map((line) => line.trim())
.map((line) => line.replace(/^\*/, ''))
.map((line) => line.trim())
.filter((line) => !!line)
.join(' ')
.trim()
: '*No description provided.*',
);
}
});
}
}
}
})
);
const FlecksConfigs = (state, filename, source) => (
implementationVisitor((property) => {
if ('@flecks/core.config' === property.key.value) {
if (isArrowFunctionExpression(property.value)) {
if (isObjectExpression(property.value.body)) {
property.value.body.properties.forEach((property) => {
if (isIdentifier(property.key) || isStringLiteral(property.key)) {
state.addConfig(
property.key.name || property.key.value,
(property.leadingComments?.length > 0)
? property.leadingComments.pop().value.split('\n')
.map((line) => line.trim())
.map((line) => line.replace(/^\*/, ''))
.map((line) => line.trim())
.filter((line) => !!line)
.join(' ')
.trim()
: '*No description provided.*',
filename,
source.slice(property.value.start, property.value.end),
);
}
});
}
}
}
})
);
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 === '@flecks/core/src/flecks.js')
|| (filename === '@flecks/core/src/server/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,
'invokeMerge',
filename,
path.node.loc,
);
state.addInvocation(
`${path.node.arguments[0].value}.decorate`,
'invokeComposed',
filename,
path.node.loc,
);
}
}
}
}
}
}
},
});
const FlecksImplementations = (state, filename) => (
implementationVisitor(({key}) => {
state.addImplementation(
key.value,
filename,
key.loc,
);
})
);
const FlecksSpecifications = (state, source) => (
implementationVisitor((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);
}
});
}
},
});
const {
buildFileVisitor,
configVisitor,
hookImplementationVisitor,
hookInvocationVisitor,
hookSpecificationVisitor,
todoVisitor,
} = require('./visitors');
exports.parseCode = async (code) => {
const config = {
ast: true,
code: false,
};
const {ast} = await transformAsync(code, config);
const {ast} = await transformAsync(code, {ast: true, code: false});
return ast;
};
exports.parseFile = async (filename, resolved, state) => {
const buffer = await readFile(filename);
const source = buffer.toString('utf8');
exports.parseNormalSource = async (path, source) => {
const ast = await exports.parseCode(source);
traverse(ast, FlecksBuildConfigs(state, resolved));
traverse(ast, FlecksConfigs(state, resolved, source));
traverse(ast, FlecksInvocations(state, resolved));
traverse(ast, FlecksImplementations(state, resolved));
traverse(ast, FlecksTodos(state, resolved));
};
const fleckSources = async (path) => glob(join(path, 'src', '**', '*.js'));
exports.parseFleckRoot = async (root, state) => {
const resolved = dirname(require.resolve(join(root, 'package.json')));
const sources = await fleckSources(resolved);
await Promise.all(
sources.map(async (source) => {
// @todo Aliased fleck paths are gonna be bad.
await exports.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 exports.parseCode(source);
traverse(ast, FlecksSpecifications(state, source));
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
const buildFiles = [];
const configs = [];
const hookImplementations = [];
const hookInvocations = [];
const todos = [];
const pushOne = (array) => (element) => {
array.push(element);
};
traverse(ast, buildFileVisitor(pushOne(buildFiles)));
traverse(ast, configVisitor((config) => {
const {description, key, location: {start: {index: start}, end: {index: end}}} = config;
configs.push({
defaultValue: source.slice(start, end),
description,
key,
});
}));
traverse(ast, hookImplementationVisitor((hookImplementation) => {
const {key: {value: hook}, loc: {start: {column, line}}} = hookImplementation;
hookImplementations.push({
column,
hook,
line,
});
}));
traverse(ast, hookInvocationVisitor((hookInvocation) => {
const isClassFile = [
'@flecks/core/build/flecks.js',
'@flecks/build/build/build.js',
]
.includes(path);
if (!isClassFile && hookInvocation.isThis) {
return;
}
}
const {location: {start: {column, line}}, hook, type} = hookInvocation;
hookInvocations.push({
column,
hook,
line,
type,
});
}));
traverse(ast, todoVisitor((todo) => {
const {description, location: {start: {index: start}, end: {index: end}}} = todo;
todos.push({
context: source.slice(start, end),
description,
});
}));
return {
buildFiles,
config: configs,
hookImplementations,
hookInvocations,
todos,
};
};
exports.parseFlecks = async (flecks) => {
const state = new ParserState();
await Promise.all(
Object.keys(flecks.roots)
.map(async (root) => {
await exports.parseFleckRoot(root, state);
}),
);
return state;
exports.parseHookSpecificationSource = async (path, source) => {
const ast = await exports.parseCode(source);
const hookSpecifications = [];
traverse(ast, hookSpecificationVisitor((hookSpecification) => {
const {
description,
hook,
location: {start: {index: start}, end: {index: end}},
params,
} = hookSpecification;
hookSpecifications.push({
description,
example: source.slice(start, end),
hook,
params,
});
}));
return {
hookSpecifications,
};
};
exports.parseSource = async (path, source) => {
if (path.match(/build\/flecks\.hooks\.js$/)) {
return exports.parseHookSpecificationSource(path, source);
}
return exports.parseNormalSource(path, source);
};
exports.parseFleckRoot = async (path, root) => (
Promise.all(
(await Promise.all([
...await glob(join(root, 'src', '**', '*.js')),
...await glob(join(root, 'build', '**', '*.js')),
]))
.map((filename) => [relative(root, filename), filename])
.map(async ([path, filename]) => {
const buffer = await readFile(filename);
return [path, await exports.parseSource(path, buffer.toString('utf8'))];
}),
)
);
exports.parseFlecks = async (flecks) => (
Promise.all(
Object.entries(flecks.roots)
.map(async ([path, {root}]) => [path, await exports.parseFleckRoot(path, root)]),
)
);

View File

@ -0,0 +1,223 @@
const {
isArrayExpression,
isArrowFunctionExpression,
isIdentifier,
isLiteral,
isMemberExpression,
isObjectExpression,
isStringLiteral,
isThisExpression,
isVariableDeclaration,
isBlockStatement,
isReturnStatement,
isFunction,
} = require('@babel/types');
const {parse: parseComment} = require('comment-parser');
function visitProperties(properties, fn) {
properties.forEach((property) => {
const {key} = property;
if (isLiteral(key)) {
fn(property);
}
});
}
exports.hookImplementationVisitor = (fn) => ({
// exports.hooks = {...}
AssignmentExpression(path) {
const {left, right} = path.node;
if (isMemberExpression(left)) {
if (isIdentifier(left.object) && 'exports' === left.object.name) {
if (isIdentifier(left.property) && 'hooks' === left.property.name) {
if (isObjectExpression(right)) {
visitProperties(right.properties, fn);
}
}
}
}
},
// export const hooks = {...}
ExportNamedDeclaration(path) {
const {declaration} = path.node;
if (isVariableDeclaration(declaration)) {
const {declarations} = declaration;
declarations.forEach((declarator) => {
if ('hooks' === declarator.id.name) {
if (isObjectExpression(declarator.init)) {
visitProperties(declarator.init.properties, fn);
}
}
});
}
},
});
exports.hookVisitor = (hook) => (fn) => (
exports.hookImplementationVisitor((property) => (
hook === property.key.value ? fn(property) : undefined
))
);
function functionResultVisitor(value, fn) {
if (isArrowFunctionExpression(value)) {
fn(value.body);
}
if (isFunction(value)) {
if (isBlockStatement(value.body)) {
for (let i = 0; i < value.body.body.length; ++i) {
if (isReturnStatement(value.body.body[i])) {
fn(value.body.body[i].argument);
}
}
}
}
}
exports.buildFileVisitor = (fn) => exports.hookVisitor('@flecks/build.files')(
(property) => {
functionResultVisitor(property.value, (node) => {
if (isArrayExpression(node)) {
node.elements
.map((element) => {
let filename;
if (isStringLiteral(element)) {
filename = element.value;
}
if (!filename) {
return undefined;
}
return {
filename,
description: (
(element.leadingComments?.length > 0)
? element.leadingComments.pop().value.split('\n')
.map((line) => line.trim())
.map((line) => line.replace(/^\*/, ''))
.map((line) => line.trim())
.join('\n')
.trim()
: undefined
),
};
})
.filter((buildConfig) => buildConfig)
.forEach(fn);
}
});
},
);
exports.configVisitor = (fn) => exports.hookVisitor('@flecks/core.config')(
(property) => {
functionResultVisitor(property.value, (node) => {
if (isObjectExpression(node)) {
node.properties.forEach((property) => {
if (isIdentifier(property.key) || isStringLiteral(property.key)) {
fn({
key: property.key.name || property.key.value,
description: (property.leadingComments?.length > 0)
? property.leadingComments.pop().value.split('\n')
.map((line) => line.trim())
.map((line) => line.replace(/^\*/, ''))
.map((line) => line.trim())
.filter((line) => !!line)
.join(' ')
.trim()
: undefined,
location: property.value.loc,
});
}
});
}
});
},
);
exports.hookInvocationVisitor = (fn) => ({
CallExpression(path) {
if (isMemberExpression(path.node.callee)) {
if (
(isIdentifier(path.node.callee.object) && 'flecks' === path.node.callee.object.name)
|| isThisExpression(path.node.callee.object)
) {
if (isIdentifier(path.node.callee.property)) {
const invocation = {
isThis: isThisExpression(path.node.callee.object),
location: path.node.loc,
};
if (path.node.callee.property.name.match(/^invoke.*/)) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
fn({
...invocation,
hook: path.node.arguments[0].value,
type: path.node.callee.property.name,
});
}
}
}
if ('gather' === path.node.callee.property.name) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
fn({
...invocation,
hook: path.node.arguments[0].value,
type: 'invokeMerge',
});
fn({
...invocation,
hook: `${path.node.arguments[0].value}.decorate`,
type: 'invokeComposed',
});
}
}
}
}
}
}
},
});
exports.hookSpecificationVisitor = (fn) => (
exports.hookImplementationVisitor((property) => {
if (property.leadingComments) {
const {key, value: example} = property;
const [{value}] = property.leadingComments;
const [{description, tags}] = parseComment(`/**\n${value}\n*/`, {spacing: 'preserve'});
const [returns] = tags
.filter(({tag}) => 'returns' === tag)
.map(({name, type}) => ({description: name, type}));
fn({
hook: key.value,
description: description.trim(),
location: example.loc,
params: tags
.filter(({tag}) => 'param' === tag)
.map(({description, name, type}) => ({description, name, type})),
...returns && {returns},
});
}
})
);
exports.todoVisitor = (fn) => ({
enter(path) {
if (path.node.leadingComments) {
path.node.leadingComments.forEach((comment, i) => {
if (comment.value.toLowerCase().match('@todo')) {
fn({
description: path.node.leadingComments
.slice(i)
.map(({value}) => {
const index = value.indexOf('@todo');
return -1 === index ? value : value.slice(index + 6);
})
.join(''),
location: path.node.loc,
});
}
});
}
},
});

View File

@ -29,6 +29,7 @@
"@docusaurus/preset-classic": "3.0.1",
"@docusaurus/types": "3.0.1",
"@flecks/core": "^3.0.0",
"@flecks/react": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"comment-parser": "^1.3.0",

View File

@ -0,0 +1,89 @@
import {expect} from 'chai';
import {
parseFleckRoot,
parseSource,
} from '@flecks/dox/build/parser';
import verifiedRoot from './verified-root';
it('parses build files', async () => {
const source = `
export const hooks = {
'@flecks/build.files': () => ([
/**
* Description.
*/
'filename',
]),
}
`;
expect((await parseSource('', source)).buildFiles)
.to.deep.equal([
{description: 'Description.', filename: 'filename'},
]);
});
it('parses config', async () => {
const source = `
export const hooks = {
'@flecks/core.config': () => ({
/**
* Hello
*/
foo: 'bar',
}),
}
`;
expect((await parseSource('', source)).config)
.to.deep.equal([
{key: 'foo', description: 'Hello', defaultValue: "'bar'"},
]);
});
it('parses config', async () => {
const source = `
export const hooks = {
'@flecks/core.config': () => ({
/**
* Hello
*/
foo: 'bar',
}),
}
`;
expect((await parseSource('', source)).config)
.to.deep.equal([
{key: 'foo', description: 'Hello', defaultValue: "'bar'"},
]);
});
it('parses hook invocations based on context', async () => {
const source = `
flecks.invoke('foo');
this.invokeFlat('bar');
`;
let hookInvocations;
({hookInvocations} = await parseSource('@flecks/core/build/flecks.js', source));
expect(hookInvocations.length)
.to.equal(2);
({hookInvocations} = await parseSource('nope', source));
expect(hookInvocations.length)
.to.equal(1);
});
it('parses todos', async () => {
const source = `
// @todo Do a thing.
notAThing();
`;
expect((await parseSource('', source)).todos)
.to.deep.equal([
{description: 'Do a thing.', context: 'notAThing();'},
]);
});
it('parses a root', async () => {
expect(await parseFleckRoot('root', './test/server/root'))
.to.deep.equal(verifiedRoot);
});

View File

@ -0,0 +1,11 @@
export const hooks = {
/**
* Description
* @param {string} foo Foo
* @param {number} bar Bar
* @returns {Baz} Baz
*/
'@something/blah': (foo, bar) => {
this.doSomethingTo(foo, bar);
},
};

View File

@ -0,0 +1,13 @@
export const hooks = {
'@flecks/build.files': () => (['file']),
'@flecks/core.config': () => ({
foo: 'bar',
}),
'@something/else': () => {},
};
export function whatever(flecks) {
flecks.invoke('@something/else');
// @todo Skipped cuz not class.
this.invoke('@something/blah');
}

View File

@ -0,0 +1,39 @@
export default [
[
'src/index.js',
{
buildFiles: [{description: undefined, filename: 'file'}],
config: [{defaultValue: "'bar'", description: undefined, key: 'foo'}],
hookImplementations: [
{column: 2, hook: '@flecks/build.files', line: 2},
{column: 2, hook: '@flecks/core.config', line: 3},
{column: 2, hook: '@something/else', line: 6},
],
hookInvocations: [
{
column: 2,
hook: '@something/else',
line: 10,
type: 'invoke',
},
],
todos: [{context: "this.invoke('@something/blah');", description: 'Skipped cuz not class.'}],
},
],
[
'build/flecks.hooks.js',
{
hookSpecifications: [
{
description: 'Description',
example: '(foo, bar) => {\n this.doSomethingTo(foo, bar);\n }',
hook: '@something/blah',
params: [
{description: 'Foo', name: 'foo', type: 'string'},
{description: 'Bar', name: 'bar', type: 'number'},
],
},
],
},
],
];

View File

@ -0,0 +1,296 @@
import traverse from '@babel/traverse';
import {expect} from 'chai';
import {parseCode} from '@flecks/dox/build/parser';
import {
buildFileVisitor,
configVisitor,
hookImplementationVisitor,
hookInvocationVisitor,
hookSpecificationVisitor,
hookVisitor,
todoVisitor,
} from '@flecks/dox/build/visitors';
async function confirmVisited(code, visitor, test) {
const visited = [];
traverse(
await parseCode(code),
visitor((result) => {
visited.push(result);
}),
);
test(visited);
}
function constHooksWrapper(code) {
return `
export const hooks = {
${code}
};
`;
}
function exportsHooksWrapper(code) {
return `
exports.hooks = {
${code}
};
`;
}
async function confirmVisitedWrapped(code, visitor, test) {
await Promise.all(
[constHooksWrapper, exportsHooksWrapper]
.map(async (wrapper) => {
const source = wrapper(code);
await confirmVisited(source, visitor, test(source));
}),
);
}
it('parses hooks', async () => {
await confirmVisitedWrapped(
`
'hello': () => {},
`,
hookImplementationVisitor,
() => (visited) => {
expect(visited.length)
.to.equal(1);
expect(visited[0].key.value)
.to.equal('hello');
},
);
});
it('visits hooks', async () => {
await confirmVisitedWrapped(
`
'hello': () => {},
'goodbye': () => {},
`,
hookVisitor('hello'),
() => (visited) => {
expect(visited.length)
.to.equal(1);
expect(visited[0].key.value)
.to.equal('hello');
},
);
});
function buildFileWrappers(code) {
return [
`
'@flecks/build.files': () => ([
${code}
]),
`,
`
'@flecks/build.files': () => {
return [
${code}
];
},
`,
`
'@flecks/build.files': function f() {
return [
${code}
];
},
`,
];
}
it('visits build files', async () => {
await Promise.all(
buildFileWrappers(
`
/**
* This is a description.
*
* It is on multiple lines.
*/
'first.config.js',
/**
* This is another description.
*/
'second.config.js',
'third.config.js',
`,
)
.map(async (wrapped) => {
await confirmVisitedWrapped(
wrapped,
buildFileVisitor,
() => (visited) => {
expect(visited)
.to.deep.equal([
{
filename: 'first.config.js',
description: 'This is a description.\n\nIt is on multiple lines.',
},
{
filename: 'second.config.js',
description: 'This is another description.',
},
{
filename: 'third.config.js',
description: undefined,
},
]);
},
);
}),
);
});
function configWrappers(code) {
return [
`
'@flecks/core.config': () => ({
${code}
}),
`,
`
'@flecks/core.config': () => {
return {
${code}
};
},
`,
`
'@flecks/core.config': function f() {
return {
${code}
};
},
`,
];
}
it('visits config', async () => {
await Promise.all(
configWrappers(
`
/**
* Description.
*/
first: 'default',
second: 1111,
`,
)
.map(async (wrapped) => {
await confirmVisitedWrapped(
wrapped,
configVisitor,
(source) => (visited) => {
const expected = [
{
key: 'first',
description: 'Description.',
},
{
key: 'second',
description: undefined,
},
];
visited.forEach((config, i) => {
expect(config.key)
.to.equal(expected[i].key);
expect(config.description)
.to.equal(expected[i].description);
});
expect(source.slice(visited[0].location.start.index, visited[0].location.end.index))
.to.equal("'default'");
expect(source.slice(visited[1].location.start.index, visited[1].location.end.index))
.to.equal('1111');
},
);
}),
);
});
it('visits hook invocations', async () => {
await confirmVisited(
`
flecks.invoke('sup');
flecks.invokeAsync('yep');
flecks.gather('stuff');
flecks.nope();
this.invoke('sup');
this.invokeAsync('yep');
this.gather('stuff');
this.nope();
`,
hookInvocationVisitor,
(visited) => {
expect(visited.length)
.to.equal(8);
},
);
});
it('visits hook specifications', async () => {
const f = '(one, two) => { return one + parseInt(two, 10); }';
await confirmVisitedWrapped(
`
/**
* This is a hook.
* @param {number} one First.
* @param {string} two Second.
* @returns {number} Result.
*/
'hook': ${f},
`,
hookSpecificationVisitor,
(source) => (visited) => {
expect(visited.length)
.to.equal(1);
expect(visited[0].description)
.to.equal('This is a hook.');
expect(visited[0].hook)
.to.equal('hook');
expect(visited[0].returns)
.to.deep.equal({description: 'Result.', type: 'number'});
expect(visited[0].params)
.to.deep.equal([
{description: 'First.', name: 'one', type: 'number'},
{description: 'Second.', name: 'two', type: 'string'},
]);
expect(source.slice(visited[0].location.start.index, visited[0].location.end.index))
.to.equal(f);
},
);
});
it('visits todos', async () => {
const source = `
someStuff();
// @todo This should be a function.
new OhYeah();
// @todo This is a really
// long todo item.
something();
`;
await confirmVisited(
source,
todoVisitor,
(visited) => {
expect(visited.map(({description}) => description))
.to.deep.equal([
'This should be a function.',
'This is a really long todo item.',
]);
expect(visited.map(({location: {start, end}}) => (source.slice(start.index, end.index))))
.to.deep.equal([
'new OhYeah();',
'something();',
]);
},
);
});

View File

@ -1,20 +0,0 @@
export const hooks = {
/**
* Invoked when electron is initializing.
* @param {Electron} electron The electron module.
*/
'@flecks/electron/server.initialize': (electron) => {
electron.app.on('will-quit', () => {
// ...
});
},
/**
* Invoked when a window is created
* @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window
*/
'@flecks/electron/server.window': (win) => {
win.maximize();
},
};

View File

@ -0,0 +1,40 @@
export const hooks = {
/**
* Alter the options for initialization of the Electron browser window.
* @param {[BrowserWindowConstructorOptions](https://www.electronjs.org/docs/latest/api/structures/browser-window-options)} browserWindowOptions The options.
*/
'@flecks/electron/server.browserWindowOptions.alter': (browserWindowOptions) => {
browserWindowOptions.icon = 'cute-kitten.png';
},
/**
* Extensions to install.
* @param {[Installer](https://github.com/MarshallOfSound/electron-devtools-installer)} installer The installer.
*/
'@flecks/electron/server.extensions': (installer) => [
// Some defaults provided...
installer.BACKBONE_DEBUGGER,
// By ID (Tamper Monkey):
'dhdgffkkebhmkfjojejmpbldmpobfkfo',
],
/**
* Invoked when electron is initializing.
* @param {Electron} electron The electron module.
*/
'@flecks/electron/server.initialize': (electron) => {
electron.app.on('will-quit', () => {
// ...
});
},
/**
* Invoked when a window is created
* @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window
*/
'@flecks/electron/server.window': (win) => {
win.maximize();
},
};

View File

@ -30,7 +30,7 @@ module.exports = (program, flecks) => {
} = opts;
const {build} = coreCommands(program, flecks);
const child = await build.action(undefined, opts);
const testPaths = await glob(join(FLECKS_CORE_ROOT, 'test/*.js'));
const testPaths = await glob(join(FLECKS_CORE_ROOT, 'test/**/*.js'));
if (0 === testPaths.length) {
// eslint-disable-next-line no-console
console.log('No fleck tests found.');

View File

@ -14,6 +14,6 @@ exports.hooks = {
errorDetails: true,
},
}),
'@flecks/core.targets': () => ['fleck'],
'@flecks/build.targets': () => ['fleck'],
'@flecks/build.processAssets': hook,
};

View File

@ -0,0 +1,10 @@
export const hooks = {
/**
* Define React components for login strategies.
*/
'@flecks/passport-react.strategies': () => ({
MyService: SomeBeautifulComponent,
}),
};

View File

@ -0,0 +1,11 @@
export const hooks = {
/**
* Define passport login strategies. See: https://www.passportjs.org/concepts/authentication/strategies/
* @param {Passport} passport The passport instance.
*/
'@flecks/passport.strategies': (passport) => ({
MyService: SomeStrategy,
}),
};

View File

@ -30,8 +30,8 @@ exports.hooks = {
errorDetails: true,
},
}),
'@flecks/core.targets': () => ['server'],
'@flecks/core.targets.alter': (targets) => {
'@flecks/build.targets': () => ['server'],
'@flecks/build.targets.alter': (targets) => {
// Don't build if there's a fleck target.
if (targets.has('fleck')) {
targets.delete('server');

View File

@ -1,8 +1,17 @@
export const hooks = {
/**
* Pass information to the runtime.
*/
'@flecks/server.runtime': async () => ({
something: '...',
}),
/**
* Define sequential actions to run when the server comes up.
*/
'@flecks/server.up': async () => {
await youCanDoAsyncThingsHere();
},
};

View File

@ -0,0 +1,17 @@
export const hooks = {
/**
* Configure the session. See: https://github.com/expressjs/session#sessionoptions
*/
'@flecks/session.config': async () => ({
saveUninitialized: true,
}),
/**
* Define sequential actions to run when the server comes up.
*/
'@flecks/server.up': async () => {
await youCanDoAsyncThingsHere();
},
};

View File

@ -297,11 +297,11 @@ exports.hooks = {
json.files.push('assets');
}
},
'@flecks/core.targets': (flecks) => [
'@flecks/build.targets': (flecks) => [
'web',
...(flecks.get('@flecks/web.dll').length > 0 ? ['web-vendor'] : []),
],
'@flecks/core.targets.alter': (targets) => {
'@flecks/build.targets.alter': (targets) => {
// Don't build if there's a fleck target.
if (targets.has('fleck')) {
targets.delete('web');

View File

@ -71,7 +71,7 @@ You can implement your own command by implementing
[`@flecks/build.commands`](/docs/flecks/@flecks/dox/hooks#flecksbuildcommands) in your fleck. Let's
run through the process.
### Implement <code>@flecks/core&#8203;.commands</code>
### Implement <code>@flecks/build&#8203;.commands</code>
First, create an application:

View File

@ -6,11 +6,13 @@ description: Configure `build/flecks.yml` and your application.
# Configuration
You have a flecks application! ...but it doesn't do much. By default, your application will have
only two flecks:
`@flecks/core` and `@flecks/server`. Your `build/flecks.yml` file will look like this:
only three flecks: `@flecks/build`, `@flecks/core`, and `@flecks/server`. Your `build/flecks.yml`
file will look like this:
```yml title="build/flecks.yml"
'@flecks/core': {}
'@flecks/build': {}
'@flecks/core':
id: 'hello-world'
'@flecks/server': {}
```
@ -27,6 +29,7 @@ flecks are configured.
Your application's ID is configured at the `id` key of `@flecks/core`'s configuration:
```yml title="build/flecks.yml"
'@flecks/build': {}
// highlight-start
'@flecks/core':
id: 'hello-world'

View File

@ -4,16 +4,19 @@ description: A fleck is a module but also so much more.
---
If you are following along from the previous getting started
[configuration page](./configuration), you have an application with 2 flecks:
[configuration page](./configuration), you have an application with 3 flecks:
- `@flecks/build`
- `@flecks/core`
- `@flecks/server`
<details>
<summary>About that "2 flecks" thing...</summary>
<summary>About that "3 flecks" thing...</summary>
Actually, your server application has **3 flecks** at this point:
Actually, your server application has **5 flecks** at this point:
- `@flecks/build`
- `@flecks/build/server`
- `@flecks/core`
- `@flecks/core/server`
- `@flecks/server`
@ -53,8 +56,9 @@ After some output, you'll find your new fleck at `packages/say-hello`. Let's ins
`build/flecks.yml`:
```yml title="build/flecks.yml"
'@flecks/build': {}
'@flecks/core':
id: 'hello_world'
id: 'hello-world'
'@flecks/server': {}
// highlight-next-line
'@hello-world/say-hello:./packages/say-hello': {}

View File

@ -248,6 +248,7 @@ We have to configure `build/flecks.yml` so that the database comes up before we
```yml
'@db_test/content:./packages/content/src': {}
'@flecks/build': {}
'@flecks/core':
id: db_test
'@flecks/db': {}
@ -328,6 +329,7 @@ but this isn't very helpful for any real purpose. Let's make a change to our `bu
```yml title="build/flecks.yml"
'@db_test/content:./packages/content/src': {}
'@flecks/build': {}
'@flecks/core':
id: db_test
'@flecks/db': {}
@ -392,6 +394,7 @@ Configure `build/flecks.yml`:
```yml title="build/flecks.yml"
'@db_test/content:./packages/content/src': {}
'@flecks/build': {}
'@flecks/core':
id: db_test
'@flecks/db': {}

View File

@ -13,7 +13,13 @@ import ThemedImage from '@theme/ThemedImage';
Ah, the most ~~boring~~ awesome part of development.
flecks provides a fleck called `@flecks/dox` that helps you generate a documentation website for
your project. In fact, this very website you're viewing now has been built with the same tooling!
your project.
:::tip[Mmm, dog food]
In fact, this very website you're viewing now has been built with the same tooling!
:::
## Install `@flecks/dox`

View File

@ -19,7 +19,7 @@ architectural opinions.
flecks is built with supreme attention to the developer and end-user experience.
- 🧩 **Small but pluggable**
- The simplest application is two flecks, `core` and `server`: you don't pay for what you don't buy
- The simplest application is two flecks, `core` and `server` (**7 `MiB` production server size**): you don't pay for what you don't buy
- Endlessly configurable through built-in [hooks](./flecks/@flecks/dox/hooks) and then your own
- 🛠️ **Ready to build maintainable and performant production applications**
- [Documentation website](./documentation) generation for your project with no fuss

View File

@ -69,6 +69,7 @@ is because **Server-Side Rendering (SSR) is enabled by default**! If you don't w
`build/flecks.yml`:
```yml title="build/flecks.yml"
'@flecks/build': {}
'@flecks/core':
id: react-test
// highlight-start