diff --git a/package.json b/package.json
index 87d22d6..efadeda 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"test": "lerna exec 'yarn && yarn test'"
},
"devDependencies": {
- "@flecks/build": "*",
+ "@flecks/build": "^3.1.3",
"lerna": "^8.0.2"
}
}
diff --git a/packages/build/build/build.js b/packages/build/build/build.js
index 4d7a75e..63c39e1 100644
--- a/packages/build/build/build.js
+++ b/packages/build/build/build.js
@@ -66,7 +66,7 @@ module.exports = class Build extends Flecks {
async babel() {
return babelmerge.all([
{configFile: await this.resolveBuildConfig('babel.config.js')},
- ...this.invokeFlat('@flecks/core.babel'),
+ ...await this.invokeSequentialAsync('@flecks/core.babel'),
]);
}
@@ -106,6 +106,16 @@ module.exports = class Build extends Flecks {
};
}
+ async configureBuilds(config, env, argv) {
+ await Promise.all(
+ Object.entries(config)
+ .map(([target, config]) => (
+ this.invokeSequentialAsync('@flecks/build.config', target, config, env, argv)
+ )),
+ );
+ await this.invokeSequentialAsync('@flecks/build.config.alter', config, env, argv);
+ }
+
static async from(
{
config: configParameter,
diff --git a/packages/build/build/cli.js b/packages/build/build/cli.js
index ef86936..05d47db 100755
--- a/packages/build/build/cli.js
+++ b/packages/build/build/cli.js
@@ -41,7 +41,7 @@ program
const flecks = await Build.from();
debugSilly('bootstrapped');
// Register commands.
- const commands = flecks.invokeMerge('@flecks/build.commands', program);
+ const commands = await flecks.invokeMergeUniqueAsync('@flecks/build.commands', program);
const keys = Object.keys(commands).sort();
for (let i = 0; i < keys.length; ++i) {
const {
diff --git a/packages/build/build/eslint.config.js b/packages/build/build/eslint.config.js
index bf5995f..b309dea 100644
--- a/packages/build/build/eslint.config.js
+++ b/packages/build/build/eslint.config.js
@@ -33,12 +33,7 @@ if (FLECKS_CORE_SYNC_FOR_ESLINT) {
const webpackConfigs = {
fleck: await require(webpackConfigPath)(env, argv, flecks),
};
- await Promise.all(
- flecks.invokeFlat('@flecks/build.config', 'fleck', webpackConfigs.fleck, env, argv),
- );
- await Promise.all(
- flecks.invokeFlat('@flecks/build.config.alter', webpackConfigs, env, argv),
- );
+ await flecks.configureBuilds(webpackConfigs, env, argv);
const {resolve} = webpackConfigs.fleck;
eslintConfig.settings['import/resolver'].webpack = {config: {resolve}};
// Write it out to stdout.
diff --git a/packages/build/build/flecks.hooks.js b/packages/build/build/flecks.hooks.js
index 80672b5..8dded53 100644
--- a/packages/build/build/flecks.hooks.js
+++ b/packages/build/build/flecks.hooks.js
@@ -7,6 +7,7 @@ export const hooks = {
* @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}
+ * @invoke SequentialAsync
*/
'@flecks/build.config': (target, config, env, argv) => {
if ('something' === target) {
@@ -22,6 +23,7 @@ export const hooks = {
* @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}
+ * @invoke SequentialAsync
*/
'@flecks/build.config.alter': (configs) => {
// Maybe we want to do something if a target exists..?
@@ -32,11 +34,13 @@ export const hooks = {
/**
* Add implicitly resolved extensions.
+ * @invoke Flat
*/
'@flecks/build.extensions': () => ['.coffee'],
/**
* Register build files. See [the build files page](./build-files) for more details.
+ * @invoke
*/
'@flecks/build.files': () => [
/**
@@ -48,6 +52,7 @@ export const hooks = {
/**
* Define CLI commands.
* @param {[Command](https://github.com/tj/commander.js/tree/master#declaring-program-variable)} program The [Commander.js](https://github.com/tj/commander.js) program.
+ * @invoke MergeUniqueAsync
*/
'@flecks/build.commands': (program, flecks) => {
return {
@@ -74,6 +79,7 @@ export const hooks = {
* @param {string} target The build target.
* @param {Record<string, Source>} assets The assets.
* @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation.
+ * @invoke SequentialAsync
*/
'@flecks/build.processAssets': (target, assets, compilation) => {
if (this.myTargets.includes(target)) {
@@ -83,12 +89,14 @@ export const hooks = {
/**
* Define build targets.
+ * @invoke
*/
'@flecks/build.targets': () => ['sometarget'],
/**
* Alter defined build targets.
* @param {Set<string>} targets The targets to build.
+ * @invoke
*/
'@flecks/build.targets.alter': (targets) => {
targets.delete('some-target');
diff --git a/packages/build/build/fleckspack.config.js b/packages/build/build/fleckspack.config.js
index a7a0142..91176dd 100644
--- a/packages/build/build/fleckspack.config.js
+++ b/packages/build/build/fleckspack.config.js
@@ -28,27 +28,25 @@ module.exports = async (env, argv) => {
debug('no build configuration found! aborting...');
await new Promise(() => {});
}
- const entries = await Promise.all(building.map(
- async ([fleck, target]) => {
- const configFn = require(await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck));
- if ('function' !== typeof configFn) {
- debug(`'${
- target
- }' build configuration expected function got ${
- typeof configFn
- }! aborting...`);
- return undefined;
- }
- return [target, await configFn(env, argv, flecks)];
- },
- ));
- await Promise.all(
- entries.map(async ([target, config]) => (
- Promise.all(flecks.invokeFlat('@flecks/build.config', target, config, env, argv))
+ const webpackConfigs = Object.fromEntries(
+ await Promise.all(building.map(
+ async ([fleck, target]) => {
+ const configFn = require(
+ await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck),
+ );
+ if ('function' !== typeof configFn) {
+ debug(`'${
+ target
+ }' build configuration expected function got ${
+ typeof configFn
+ }! aborting...`);
+ return undefined;
+ }
+ return [target, await configFn(env, argv, flecks)];
+ },
)),
);
- const webpackConfigs = Object.fromEntries(entries);
- await Promise.all(flecks.invokeFlat('@flecks/build.config.alter', webpackConfigs, env, argv));
+ await flecks.configureBuilds(webpackConfigs, env, argv);
const enterableWebpackConfigs = Object.values(webpackConfigs)
.filter((webpackConfig) => {
if (!webpackConfig.entry) {
diff --git a/packages/core/build/flecks.hooks.js b/packages/core/build/flecks.hooks.js
index 9d6094d..12b1632 100644
--- a/packages/core/build/flecks.hooks.js
+++ b/packages/core/build/flecks.hooks.js
@@ -2,6 +2,7 @@ export const hooks = {
/**
* Babel configuration.
+ * @invoke SequentialAsync
*/
'@flecks/core.babel': () => ({
plugins: ['...'],
@@ -9,6 +10,7 @@ export const hooks = {
/**
* Define configuration. See [the configuration page](./config) for more details.
+ * @invoke Fleck
*/
'@flecks/core.config': () => ({
whatever: 'configuration',
@@ -24,6 +26,7 @@ export const hooks = {
* Let flecks gather for you.
*
* See [the Gathering guide](../gathering).
+ * @invoke Async
*/
'@flecks/core.gathered': () => ({
// If this hook is implemented by a fleck called `@some/fleck`, then:
@@ -41,6 +44,7 @@ export const hooks = {
* Invoked when a fleck is HMR'd
* @param {string} path The path of the fleck
* @param {Module} updatedFleck The updated fleck module.
+ * @invoke
*/
'@flecks/core.hmr': (path, updatedFleck) => {
if ('my-fleck' === path) {
@@ -52,6 +56,7 @@ export const hooks = {
* Invoked when a gathered set is HMR'd.
* @param {constructor} gathered The gathered set.
* @param {string} hook The gather hook; e.g. `@flecks/db.models`.
+ * @invoke
*/
'@flecks/core.hmr.gathered': (gathered, hook) => {
// Do something with the gathered set...
@@ -61,6 +66,7 @@ export const hooks = {
* Invoked when a gathered class is HMR'd.
* @param {constructor} Class The class.
* @param {string} hook The gather hook; e.g. `@flecks/db.models`.
+ * @invoke
*/
'@flecks/core.hmr.gathered.class': (Class, hook) => {
// Do something with Class...
@@ -70,6 +76,7 @@ export const hooks = {
* Invoked when flecks is building a fleck dependency graph.
* @param {Digraph} graph The dependency graph.
* @param {string} hook The hook; e.g. `@flecks/server.up`.
+ * @invoke
*/
'@flecks/core.priority': (graph, hook) => {
// Make `@flecks/socket/server`'s `@flecks/server.up` implementation depend on
@@ -85,6 +92,7 @@ export const hooks = {
* Invoked when a fleck is registered.
* @param {string} fleck
* @param {Module} M
+ * @invoke
*/
'@flecks/core.registered': (fleck, M) => {
if ('@something/or-other' === fleck) {
@@ -94,6 +102,7 @@ export const hooks = {
/**
* Invoked when the application is starting.
+ * @invoke SequentialAsync
*/
'@flecks/core.starting': () => {
console.log('starting!');
diff --git a/packages/core/build/flecks.js b/packages/core/build/flecks.js
index 58c9e2c..67f873c 100644
--- a/packages/core/build/flecks.js
+++ b/packages/core/build/flecks.js
@@ -170,11 +170,11 @@ exports.Flecks = class Flecks {
);
}
- checkAndDecorateRawGathered(hook, raw, check) {
+ async checkAndDecorateRawGathered(hook, raw, check) {
// Gather classes and check.
check(raw, hook);
// Decorate and check.
- const decorated = this.invokeComposed(`${hook}.decorate`, raw);
+ const decorated = await this.invokeComposedAsync(`${hook}.decorate`, raw);
check(decorated, `${hook}.decorate`);
return decorated;
}
@@ -383,7 +383,7 @@ exports.Flecks = class Flecks {
* @param {function} [config.check=() => {}] Check the validity of the gathered classes.
* @returns {object} An object with keys for ID, type, {@link ById}, and {@link ByType}.
*/
- gather(
+ async gather(
hook,
{
idProperty = 'id',
@@ -395,8 +395,8 @@ exports.Flecks = class Flecks {
throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string');
}
// Gather classes and check.
- const raw = this.invokeMerge(hook);
- const decorated = this.checkAndDecorateRawGathered(hook, raw, check);
+ const raw = await this.invokeMergeAsync(hook);
+ const decorated = await this.checkAndDecorateRawGathered(hook, raw, check);
// Assign unique IDs to each class and sort by type.
let uid = 1;
const ids = {};
@@ -834,45 +834,47 @@ exports.Flecks = class Flecks {
* @param {string} fleck
*/
async refreshGathered(fleck) {
- Object.entries(this.$$gathered)
- .forEach(([
- hook,
- {
- check,
- idProperty,
- gathered,
- typeProperty,
- },
- ]) => {
- let raw;
- // If decorating, gather all again
- if (this.fleckImplementation(fleck, `${hook}.decorate`)) {
- raw = this.invokeMergeAsync(hook);
- debugSilly('%s implements %s.decorate', fleck, hook);
- }
- // If only implementing, gather and decorate.
- else if (this.fleckImplementation(fleck, hook)) {
- raw = this.invokeFleck(hook, fleck);
- debugSilly('%s implements %s', fleck, hook);
- }
- if (raw) {
- const decorated = this.checkAndDecorateRawGathered(hook, raw, check);
- debug('updating gathered %s from %s...', hook, fleck);
- debugSilly('%O', decorated);
- const entries = Object.entries(decorated);
- entries.forEach(([type, Class]) => {
- const {[type]: {[idProperty]: id}} = gathered;
- const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty);
- // eslint-disable-next-line no-multi-assign
- gathered[type] = Subclass;
- gathered[id] = Subclass;
- gathered[exports.ById][id] = Subclass;
- gathered[exports.ByType][type] = Subclass;
- this.invoke('@flecks/core.hmr.gathered.class', Subclass, hook);
- });
- this.invoke('@flecks/core.hmr.gathered', gathered, hook);
- }
- });
+ await Promise.all(
+ Object.entries(this.$$gathered)
+ .map(async ([
+ hook,
+ {
+ check,
+ idProperty,
+ gathered,
+ typeProperty,
+ },
+ ]) => {
+ let raw;
+ // If decorating, gather all again
+ if (this.fleckImplementation(fleck, `${hook}.decorate`)) {
+ raw = await this.invokeMergeAsync(hook);
+ debugSilly('%s implements %s.decorate', fleck, hook);
+ }
+ // If only implementing, gather and decorate.
+ else if (this.fleckImplementation(fleck, hook)) {
+ raw = await this.invokeFleck(hook, fleck);
+ debugSilly('%s implements %s', fleck, hook);
+ }
+ if (raw) {
+ const decorated = await this.checkAndDecorateRawGathered(hook, raw, check);
+ debug('updating gathered %s from %s...', hook, fleck);
+ debugSilly('%O', decorated);
+ const entries = Object.entries(decorated);
+ entries.forEach(([type, Class]) => {
+ const {[type]: {[idProperty]: id}} = gathered;
+ const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty);
+ // eslint-disable-next-line no-multi-assign
+ gathered[type] = Subclass;
+ gathered[id] = Subclass;
+ gathered[exports.ById][id] = Subclass;
+ gathered[exports.ByType][type] = Subclass;
+ this.invoke('@flecks/core.hmr.gathered.class', Subclass, hook);
+ });
+ this.invoke('@flecks/core.hmr.gathered', gathered, hook);
+ }
+ }),
+ );
}
/**
diff --git a/packages/core/test/gather.js b/packages/core/test/gather.js
index d542724..f480682 100644
--- a/packages/core/test/gather.js
+++ b/packages/core/test/gather.js
@@ -12,7 +12,7 @@ it('can gather', async () => {
'@flecks/core/two': testTwo,
},
});
- const Gathered = flecks.gather('@flecks/core/one/test-gather');
+ const Gathered = await flecks.gather('@flecks/core/one/test-gather');
expect(Object.keys(Gathered[ByType]).length)
.to.equal(Object.keys(Gathered[ById]).length);
const typeKeys = Object.keys(Gathered[ByType]);
diff --git a/packages/db/build/flecks.hooks.js b/packages/db/build/flecks.hooks.js
index 6f3c05e..22631ca 100644
--- a/packages/db/build/flecks.hooks.js
+++ b/packages/db/build/flecks.hooks.js
@@ -3,20 +3,18 @@ export const 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
+ * See: [the Gathering guide](../gathering).
+ * @invoke MergeAsync
*/
'@flecks/db.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
+ * See: [the Gathering guide](../gathering).
*
* @param {constructor} Model The model to decorate.
+ * @invoke ComposedAsync
*/
'@flecks/db.models.decorate': (
Flecks.decorate(require.context('./models/decorators', false, /\.js$/))
diff --git a/packages/docker/build/flecks.hooks.js b/packages/docker/build/flecks.hooks.js
index 232fc47..7b12651 100644
--- a/packages/docker/build/flecks.hooks.js
+++ b/packages/docker/build/flecks.hooks.js
@@ -8,6 +8,7 @@ export const hooks = {
* See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
*
* :::
+ * @invoke MergeUniqueAsync
*/
'@flecks/docker.containers': () => ({
someContainer: {
@@ -29,6 +30,7 @@ export const hooks = {
* @param {string} dockerfile The content of the Dockerfile.
*
* @returns The new content of the Dockerfile.
+ * @invoke ComposedAsync
*/
'@flecks/docker.Dockerfile': (dockerfile) => (
dockerfile.replace('DEBUG=*', 'DEBUG=*,-*:silly')
@@ -37,6 +39,7 @@ export const hooks = {
/**
*
* @param {Object} config The object representing the docker compose configuration.
+ * @invoke SequentialAsync
*/
'@flecks/docker.docker-compose.yml': (config) => {
config.version = '3.1';
diff --git a/packages/docker/build/generate.js b/packages/docker/build/generate.js
index 2bbbcf3..ad97755 100644
--- a/packages/docker/build/generate.js
+++ b/packages/docker/build/generate.js
@@ -1,4 +1,4 @@
-exports.generateDockerFile = (flecks) => {
+exports.generateDockerFile = async (flecks) => {
const dockerfile = [
'FROM node:20',
'',
@@ -16,7 +16,7 @@ exports.generateDockerFile = (flecks) => {
'VOLUME /var/www/node_modules',
'',
].join('\n');
- return flecks.invokeComposed('@flecks/docker.Dockerfile', dockerfile);
+ return flecks.invokeComposedAsync('@flecks/docker.Dockerfile', dockerfile);
};
exports.generateComposeConfig = async (flecks) => {
@@ -36,7 +36,7 @@ exports.generateComposeConfig = async (flecks) => {
],
},
};
- const containers = flecks.invoke('@flecks/docker.containers');
+ const containers = await flecks.invokeAsync('@flecks/docker.containers');
(
await Promise.all(
Object.entries(containers)
@@ -69,6 +69,6 @@ exports.generateComposeConfig = async (flecks) => {
});
});
const config = {version: '3', services};
- flecks.invoke('@flecks/docker.docker-compose.yml', config);
+ await flecks.invokeSequentialAsync('@flecks/docker.docker-compose.yml', config);
return config;
};
diff --git a/packages/docker/build/plugin.js b/packages/docker/build/plugin.js
index 194d820..4c6fcc6 100644
--- a/packages/docker/build/plugin.js
+++ b/packages/docker/build/plugin.js
@@ -12,7 +12,7 @@ module.exports = class FlecksDockerOutput {
apply(compiler) {
compiler.hooks.compilation.tap('FlecksDockerOutput', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('FlecksDockerOutput', async (callback) => {
- const dockerFile = generateDockerFile(this.options.flecks);
+ const dockerFile = await generateDockerFile(this.options.flecks);
compilation.assets.Dockerfile = {
source: () => dockerFile,
size: () => dockerFile.length,
diff --git a/packages/docker/src/server.js b/packages/docker/src/server.js
index 3b5ac0f..ba789a8 100644
--- a/packages/docker/src/server.js
+++ b/packages/docker/src/server.js
@@ -5,7 +5,7 @@ export const hooks = {
if (!flecks.get('@flecks/docker.enabled')) {
return;
}
- const containers = await flecks.invokeMergeAsync('@flecks/docker.containers');
+ const containers = await flecks.invokeMergeUniqueAsync('@flecks/docker.containers');
await Promise.all(
Object.entries(containers)
.map(([key, config]) => startContainer(flecks, key, config)),
diff --git a/packages/dox/build/generate.js b/packages/dox/build/generate.js
index a3fe3fc..3dfb519 100644
--- a/packages/dox/build/generate.js
+++ b/packages/dox/build/generate.js
@@ -104,11 +104,22 @@ exports.generateDocusaurusHookPage = (hooks) => {
Object.entries(hooks)
.sort(([lhook], [rhook]) => (lhook < rhook ? -1 : 1))
.forEach(([hook, {implementations = [], invocations = [], specification}]) => {
- const {description, example, params} = specification || {
+ const {
+ description,
+ example,
+ invoke,
+ params,
+ } = specification || {
params: [],
};
source.push(`## \`${hook}\``);
source.push('');
+ if (invoke) {
+ source.push('
');
+ source.push(`[${invoke}](../hooks#${invoke.toLowerCase()})`);
+ source.push('
');
+ source.push('');
+ }
if (description) {
source.push(...description.split('\n'));
source.push('');
@@ -151,6 +162,7 @@ exports.generateDocusaurusHookPage = (hooks) => {
source.push('');
}
source.push('');
+ source.push('\n');
}
});
return source.join('\n');
@@ -310,18 +322,9 @@ exports.generateJson = async function generate(flecks) {
type,
});
});
- hookSpecifications.forEach(({
- hook,
- description,
- example,
- params,
- }) => {
+ hookSpecifications.forEach(({hook, ...specification}) => {
ensureHook(hook);
- r.hooks[hook].specification = {
- description,
- example,
- params,
- };
+ r.hooks[hook].specification = specification;
});
},
);
diff --git a/packages/dox/build/parser.js b/packages/dox/build/parser.js
index 6969e8e..7b743c2 100644
--- a/packages/dox/build/parser.js
+++ b/packages/dox/build/parser.js
@@ -84,17 +84,10 @@ exports.parseHookSpecificationSource = async (path, source, options) => {
const hookSpecifications = [];
traverse(ast, hookSpecificationVisitor((hookSpecification) => {
const {
- description,
- hook,
location: {start: {index: start}, end: {index: end}},
- params,
+ ...specification
} = hookSpecification;
- hookSpecifications.push({
- description,
- example: source.slice(start, end),
- hook,
- params,
- });
+ hookSpecifications.push({...specification, example: source.slice(start, end)});
}));
return {
hookSpecifications,
diff --git a/packages/dox/build/visitors.js b/packages/dox/build/visitors.js
index 2e0bedc..41a0cef 100644
--- a/packages/dox/build/visitors.js
+++ b/packages/dox/build/visitors.js
@@ -203,6 +203,9 @@ exports.hookSpecificationVisitor = (fn) => (
const {key, value: example} = property;
const [{value}] = property.leadingComments;
const [{description, tags}] = parseComment(`/**\n${value}\n*/`, {spacing: 'preserve'});
+ const [invoke] = tags
+ .filter(({tag}) => 'invoke' === tag)
+ .map(({name}) => (name ? `invoke${name}` : 'invoke'));
const [returns] = tags
.filter(({tag}) => 'returns' === tag)
.map(({name, type}) => ({description: name, type}));
@@ -214,6 +217,7 @@ exports.hookSpecificationVisitor = (fn) => (
.filter(({tag}) => 'param' === tag)
.map(({description, name, type}) => ({description, name, type})),
...returns && {returns},
+ ...invoke && {invoke},
});
}
})
diff --git a/packages/dox/test/server/verified-root.js b/packages/dox/test/server/verified-root.js
index f8faeb0..edf2765 100644
--- a/packages/dox/test/server/verified-root.js
+++ b/packages/dox/test/server/verified-root.js
@@ -32,6 +32,10 @@ export default [
{description: 'Foo', name: 'foo', type: 'string'},
{description: 'Bar', name: 'bar', type: 'number'},
],
+ returns: {
+ description: 'Baz',
+ type: 'Baz',
+ },
},
],
},
diff --git a/packages/electron/build/flecks.hooks.js b/packages/electron/build/flecks.hooks.js
index e4fdc59..b0ed97d 100644
--- a/packages/electron/build/flecks.hooks.js
+++ b/packages/electron/build/flecks.hooks.js
@@ -3,6 +3,7 @@ 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.
+ * @invoke SequentialAsync
*/
'@flecks/electron/server.browserWindowOptions.alter': (browserWindowOptions) => {
browserWindowOptions.icon = 'cute-kitten.png';
@@ -11,6 +12,7 @@ export const hooks = {
/**
* Extensions to install.
* @param {[Installer](https://github.com/MarshallOfSound/electron-devtools-installer)} installer The installer.
+ * @invoke Flat
*/
'@flecks/electron/server.extensions': (installer) => [
// Some defaults provided...
@@ -22,6 +24,7 @@ export const hooks = {
/**
* Invoked when electron is initializing.
* @param {Electron} electron The electron module.
+ * @invoke SequentialAsync
*/
'@flecks/electron/server.initialize': (electron) => {
electron.app.on('will-quit', () => {
@@ -32,6 +35,7 @@ export const hooks = {
/**
* Invoked when a window is created
* @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window
+ * @invoke SequentialAsync
*/
'@flecks/electron/server.window': (win) => {
win.maximize();
diff --git a/packages/electron/src/server/index.js b/packages/electron/src/server/index.js
index b441116..c2477c7 100644
--- a/packages/electron/src/server/index.js
+++ b/packages/electron/src/server/index.js
@@ -7,7 +7,10 @@ let win;
async function createWindow(flecks) {
const {BrowserWindow} = flecks.electron;
const {browserWindowOptions} = flecks.get('@flecks/electron');
- flecks.invoke('@flecks/electron/server.browserWindowOptions.alter', browserWindowOptions);
+ await flecks.invokeSequentialAsync(
+ '@flecks/electron/server.browserWindowOptions.alter',
+ browserWindowOptions,
+ );
win = new BrowserWindow(browserWindowOptions);
await flecks.invokeSequentialAsync('@flecks/electron/server.window', win);
}
diff --git a/packages/fleck/build/flecks.hooks.js b/packages/fleck/build/flecks.hooks.js
index e74d249..9395ee6 100644
--- a/packages/fleck/build/flecks.hooks.js
+++ b/packages/fleck/build/flecks.hooks.js
@@ -4,6 +4,7 @@ export const hooks = {
* 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.
+ * @invoke SequentialAsync
*/
'@flecks/fleck.packageJson': (json, compilation) => {
json.files.push('something');
diff --git a/packages/passport-react/build/flecks.hooks.js b/packages/passport-react/build/flecks.hooks.js
index 6d472f1..18552fe 100644
--- a/packages/passport-react/build/flecks.hooks.js
+++ b/packages/passport-react/build/flecks.hooks.js
@@ -2,6 +2,7 @@ export const hooks = {
/**
* Define React components for login strategies.
+ * @invoke MergeUnique
*/
'@flecks/passport-react.strategies': () => ({
MyService: SomeBeautifulComponent,
diff --git a/packages/passport/build/flecks.hooks.js b/packages/passport/build/flecks.hooks.js
index 0e8a2b4..aee487e 100644
--- a/packages/passport/build/flecks.hooks.js
+++ b/packages/passport/build/flecks.hooks.js
@@ -3,6 +3,7 @@ export const hooks = {
/**
* Define passport login strategies. See: https://www.passportjs.org/concepts/authentication/strategies/
* @param {Passport} passport The passport instance.
+ * @invoke MergeUniqueAsync
*/
'@flecks/passport.strategies': (passport) => ({
MyService: SomeStrategy,
diff --git a/packages/passport/src/server/index.js b/packages/passport/src/server/index.js
index 79cf22b..a426d75 100644
--- a/packages/passport/src/server/index.js
+++ b/packages/passport/src/server/index.js
@@ -41,7 +41,7 @@ export const hooks = {
flecks.passport = {
initialize: passport.initialize(),
session: passport.session(),
- strategies: flecks.invokeMergeUnique('@flecks/passport.strategies', passport),
+ strategies: await flecks.invokeMergeUniqueAsync('@flecks/passport.strategies', passport),
};
Object.entries(flecks.passport.strategies)
.forEach(([name, strategy]) => {
@@ -51,7 +51,7 @@ export const hooks = {
{before: '@flecks/web/server', after: ['@flecks/db/server', '@flecks/session/server']},
),
'@flecks/socket.intercom': () => ({
- '@flecks/passport.users': async (sids, server) => {
+ users: async (sids, server) => {
const sockets = await server.sockets();
return sids
.filter((sid) => sockets.has(sid))
diff --git a/packages/react/build/flecks.hooks.js b/packages/react/build/flecks.hooks.js
index 145bf8d..9d0ccd6 100644
--- a/packages/react/build/flecks.hooks.js
+++ b/packages/react/build/flecks.hooks.js
@@ -4,6 +4,7 @@ export const hooks = {
*
* Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object.
+ * @invoke SequentialAsync
*/
'@flecks/react.providers': (req) => {
// Generally it makes more sense to separate client and server concerns using platform
@@ -18,6 +19,7 @@ export const hooks = {
* or an array of two elements where the first element is the component and the second element
* is the props passed to the component.
* @param {http.ClientRequest} req The HTTP request object.
+ * @invoke Async
*/
'@flecks/react.roots': (req) => {
// Note that we're not returning ``, but `Component`.
diff --git a/packages/react/src/root.js b/packages/react/src/root.js
index b7d6303..9de2729 100644
--- a/packages/react/src/root.js
+++ b/packages/react/src/root.js
@@ -10,7 +10,7 @@ const debug = D('@flecks/react/root');
const debugSilly = debug.extend('silly');
export default async (flecks, req) => {
- const Roots = flecks.invoke('@flecks/react.roots', req);
+ const Roots = await flecks.invokeAsync('@flecks/react.roots', req);
debugSilly('roots: %O', Roots);
const Providers = await flecks.invokeSequentialAsync('@flecks/react.providers', req);
const FlattenedProviders = [];
diff --git a/packages/redis/src/create-client.js b/packages/redis/src/create-client.js
index 6741b6f..c1b42f1 100644
--- a/packages/redis/src/create-client.js
+++ b/packages/redis/src/create-client.js
@@ -4,7 +4,7 @@ export default async (flecks, opts = {}) => {
const {
host,
port,
- } = flecks.get('@flecks/redis/server');
+ } = flecks.get('@flecks/redis');
const client = createClient({
url: `redis://${host}:${port}`,
...opts,
diff --git a/packages/redux/build/flecks.hooks.js b/packages/redux/build/flecks.hooks.js
index ad427b2..585c9e9 100644
--- a/packages/redux/build/flecks.hooks.js
+++ b/packages/redux/build/flecks.hooks.js
@@ -1,6 +1,7 @@
export const hooks = {
/**
* Define side-effects to run against Redux actions.
+ * @invoke SequentialAsync
*/
'@flecks/redux.effects': () => ({
someActionName: (store, action) => {
@@ -9,6 +10,7 @@ export const hooks = {
}),
/**
* Define root-level reducers for the Redux store.
+ * @invoke SequentialAsync
*/
'@flecks/redux.reducers': () => {
return (state, action) => {
@@ -20,6 +22,7 @@ export const hooks = {
* Define Redux slices.
*
* See: https://redux-toolkit.js.org/api/createSlice
+ * @invoke MergeUniqueAsync
*/
'@flecks/redux.slices': () => {
const something = createSlice(
@@ -32,6 +35,7 @@ export const hooks = {
/**
* Modify Redux store configuration.
* @param {Object} options A mutable object with keys for enhancers and middleware.
+ * @invoke SequentialAsync
*/
'@flecks/redux.store': (options) => {
options.enhancers.splice(someIndex, 1);
diff --git a/packages/redux/src/client/index.js b/packages/redux/src/client/index.js
index 265ec52..6ee1e22 100644
--- a/packages/redux/src/client/index.js
+++ b/packages/redux/src/client/index.js
@@ -6,8 +6,8 @@ import localStorageEnhancer from './local-storage';
export const hooks = {
'@flecks/web/client.up': Flecks.priority(
async (flecks) => {
- const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
- const reducer = createReducer(flecks, slices);
+ const slices = await flecks.invokeMergeUniqueAsync('@flecks/redux.slices');
+ const reducer = await createReducer(flecks, slices);
// Hydrate from server.
const {preloadedState} = flecks.get('@flecks/redux');
const store = await configureStore(flecks, reducer, {preloadedState});
diff --git a/packages/redux/src/server.js b/packages/redux/src/server.js
index 82e0213..8e80702 100644
--- a/packages/redux/src/server.js
+++ b/packages/redux/src/server.js
@@ -10,8 +10,8 @@ const debugSilly = debug.extend('silly');
export const hooks = {
'@flecks/electron/server.extensions': (installer) => [installer.REDUX_DEVTOOLS],
'@flecks/web/server.request.route': (flecks) => async (req, res, next) => {
- const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
- const reducer = createReducer(flecks, slices);
+ const slices = await flecks.invokeMergeUniqueAsync('@flecks/redux.slices');
+ const reducer = await createReducer(flecks, slices);
// Let the slices have a(n async) chance to hydrate with server data.
await Promise.all(
Object.values(slices).map(({hydrateServer}) => hydrateServer?.(req, flecks)),
diff --git a/packages/redux/src/store/create-reducer.js b/packages/redux/src/store/create-reducer.js
index 922f263..1cda55e 100644
--- a/packages/redux/src/store/create-reducer.js
+++ b/packages/redux/src/store/create-reducer.js
@@ -1,8 +1,8 @@
import {combineReducers} from '@reduxjs/toolkit';
import reduceReducers from 'reduce-reducers';
-export default (flecks, slices) => {
- let reducers = flecks.invokeFlat('@flecks/redux.reducers');
+export default async (flecks, slices) => {
+ let reducers = await flecks.invokeSequentialAsync('@flecks/redux.reducers');
if (Object.keys(slices).length > 0) {
reducers = reducers.concat(combineReducers(slices));
}
diff --git a/packages/redux/src/store/index.js b/packages/redux/src/store/index.js
index c4c3c25..27b4ea0 100644
--- a/packages/redux/src/store/index.js
+++ b/packages/redux/src/store/index.js
@@ -11,10 +11,10 @@ export default async function configureStore(flecks, reducer, {preloadedState})
],
middleware: [
'@flecks/redux/defaultMiddleware',
- effectsMiddleware(flecks),
+ await effectsMiddleware(flecks),
],
};
- flecks.invokeFlat('@flecks/redux.store', options);
+ await flecks.invokeSequentialAsync('@flecks/redux.store', options);
return configureStoreR({
enhancers: (defaultEnhancers) => {
const index = options.enhancers.indexOf('@flecks/redux/defaultEnhancers');
diff --git a/packages/redux/src/store/middleware/effects.js b/packages/redux/src/store/middleware/effects.js
index 91f2dbd..e7f1853 100644
--- a/packages/redux/src/store/middleware/effects.js
+++ b/packages/redux/src/store/middleware/effects.js
@@ -1,5 +1,5 @@
-export default (flecks) => {
- const effects = flecks.invokeFlat('@flecks/redux.effects');
+export default async (flecks) => {
+ const effects = await flecks.invokeSequentialAsync('@flecks/redux.effects');
const effect = (store, action) => {
effects.forEach((map) => {
if (map[action.type]) {
diff --git a/packages/repl/build/flecks.hooks.js b/packages/repl/build/flecks.hooks.js
index a2167ed..3254c3f 100644
--- a/packages/repl/build/flecks.hooks.js
+++ b/packages/repl/build/flecks.hooks.js
@@ -3,6 +3,7 @@ export const hooks = {
* Define REPL commands.
*
* Note: commands will be prefixed with a period in the Node REPL.
+ * @invoke MergeUniqueAsync
*/
'@flecks/repl.commands': () => ({
someCommand: (...args) => {
@@ -13,6 +14,7 @@ export const hooks = {
}),
/**
* Provide global context to the REPL.
+ * @invoke MergeUniqueAsync
*/
'@flecks/repl.context': () => {
// Now you'd be able to do like:
diff --git a/packages/repl/src/repl.js b/packages/repl/src/repl.js
index ef5bf35..906cdd0 100644
--- a/packages/repl/src/repl.js
+++ b/packages/repl/src/repl.js
@@ -11,19 +11,20 @@ const debugSilly = debug.extend('silly');
export async function createReplServer(flecks) {
const {id} = flecks.get('@flecks/core');
- const context = (await Promise.all(flecks.invokeFlat('@flecks/repl.context')))
- .reduce((r, vars) => ({...r, ...vars}), {flecks});
+ const context = {
+ ...await flecks.invokeMergeUniqueAsync('@flecks/repl.context'),
+ flecks,
+ };
debug(
'Object.keys(context) === %O',
Object.keys(context),
);
const commands = {};
- Object.entries(
- flecks.invokeFlat('@flecks/repl.commands').reduce((r, commands) => ({...r, ...commands}), {}),
- ).forEach(([key, value]) => {
- commands[key] = value;
- debugSilly('registered command: %s', key);
- });
+ Object.entries(await flecks.invokeMergeUniqueAsync('@flecks/repl.commands'))
+ .forEach(([key, value]) => {
+ commands[key] = value;
+ debugSilly('registered command: %s', key);
+ });
const netServer = createServer((socket) => {
debug('client connection to repl established');
socket.on('close', () => {
diff --git a/packages/server/build/flecks.hooks.js b/packages/server/build/flecks.hooks.js
index 841abc1..2f5d7ed 100644
--- a/packages/server/build/flecks.hooks.js
+++ b/packages/server/build/flecks.hooks.js
@@ -2,6 +2,7 @@ export const hooks = {
/**
* Pass information to the runtime.
+ * @invoke Async
*/
'@flecks/server.runtime': async () => ({
something: '...',
@@ -9,6 +10,7 @@ export const hooks = {
/**
* Define sequential actions to run when the server comes up.
+ * @invoke SequentialAsync
*/
'@flecks/server.up': async () => {
await youCanDoAsyncThingsHere();
diff --git a/packages/session/build/flecks.hooks.js b/packages/session/build/flecks.hooks.js
index a1d177f..df6eb08 100644
--- a/packages/session/build/flecks.hooks.js
+++ b/packages/session/build/flecks.hooks.js
@@ -2,16 +2,10 @@ export const hooks = {
/**
* Configure the session. See: https://github.com/expressjs/session#sessionoptions
+ * @invoke MergeAsync
*/
'@flecks/session.config': async () => ({
saveUninitialized: true,
}),
- /**
- * Define sequential actions to run when the server comes up.
- */
- '@flecks/server.up': async () => {
- await youCanDoAsyncThingsHere();
- },
-
};
diff --git a/packages/socket/build/flecks.hooks.js b/packages/socket/build/flecks.hooks.js
index 672c968..a17da9b 100644
--- a/packages/socket/build/flecks.hooks.js
+++ b/packages/socket/build/flecks.hooks.js
@@ -3,6 +3,7 @@ export const hooks = {
* Modify Socket.io client configuration.
*
* See: https://socket.io/docs/v4/client-options/
+ * @invoke MergeAsync
*/
'@flecks/socket.client': () => ({
timeout: Infinity,
@@ -10,36 +11,34 @@ export const hooks = {
/**
* Define server-side intercom channels.
+ * @invoke Async
*/
'@flecks/socket.intercom': (req) => ({
- // This would have been called like:
+ // Assuming `@my/fleck` implemented this hook, this could be called like:
// `const result = await req.intercom('@my/fleck.key', 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.
- '@my/fleck.key': async (payload, server) => {
+ key: async (payload, server) => {
return someServiceSpecificInformation();
},
}),
/**
- * Define socket packets.
+ * Gather 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
+ * See: [the Gathering guide](../gathering).
+ * @invoke MergeAsync
*/
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
/**
- * Decorate database models.
+ * Decorate socket packets.
+ *
+ * See: [the Gathering guide](../gathering).
*
- * 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.
+ * @invoke ComposedAsync
*/
'@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
@@ -49,6 +48,7 @@ export const hooks = {
* Modify Socket.io server configuration.
*
* See: https://socket.io/docs/v4/server-options/
+ * @invoke MergeAsync
*/
'@flecks/socket.server': () => ({
pingTimeout: Infinity,
@@ -58,6 +58,7 @@ export const hooks = {
* Do something with a connecting socket.
*
* @param {[ServerSocket](https://github.com/cha0s/flecks/blob/master/packages/socket/src/server/socket.js)} socket The connecting socket.
+ * @invoke SequentialAsync
*/
'@flecks/socket/server.connect': (socket) => {
socket.on('disconnect', () => {
@@ -66,10 +67,11 @@ export const hooks = {
},
/**
- * Get the Socket.IO instance.
+ * Do something with the Socket.IO instance.
*
* See: https://socket.io/docs/v4/server-instance/
* @param {SocketIo} io The Socket.IO server instance.
+ * @invoke SequentialAsync
*/
'@flecks/socket/server.io': (io) => {
io.engine.on("headers", (headers, req) => {
@@ -79,6 +81,7 @@ export const hooks = {
/**
* Define middleware to run when a socket connection is established.
+ * @invoke Middleware
*/
'@flecks/socket/server.request.socket': () => (socket, next) => {
// Express-style route middleware...
diff --git a/packages/socket/src/client/index.js b/packages/socket/src/client/index.js
index 1bb9944..99ceb8e 100644
--- a/packages/socket/src/client/index.js
+++ b/packages/socket/src/client/index.js
@@ -1,10 +1,10 @@
import SocketClient from './socket';
export const hooks = {
- '@flecks/web/client.up': (flecks) => {
+ '@flecks/web/client.up': async (flecks) => {
const socket = new SocketClient(flecks);
flecks.socket.client = socket;
- socket.connect();
+ await socket.connect();
socket.listen();
},
'@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({
diff --git a/packages/socket/src/client/socket.js b/packages/socket/src/client/socket.js
index c1f53d9..1833ad6 100644
--- a/packages/socket/src/client/socket.js
+++ b/packages/socket/src/client/socket.js
@@ -19,7 +19,7 @@ export default class SocketClient extends decorate(Socket) {
this.socket = null;
}
- connect(address) {
+ async connect(address) {
if (this.socket) {
this.socket.destroy();
}
@@ -30,7 +30,7 @@ export default class SocketClient extends decorate(Socket) {
{
reconnectionDelay: 'production' === process.env.NODE_ENV ? 1000 : 100,
reconnectionDelayMax: 'production' === process.env.NODE_ENV ? 5000 : 500,
- ...this.flecks.invokeMerge('@flecks/socket.client'),
+ ...await this.flecks.invokeMergeAsync('@flecks/socket.client'),
},
);
this.socket.emitPromise = promisify(this.socket.emit.bind(this.socket));
diff --git a/packages/socket/src/server/server.js b/packages/socket/src/server/server.js
index 009aaae..391fb61 100644
--- a/packages/socket/src/server/server.js
+++ b/packages/socket/src/server/server.js
@@ -15,14 +15,6 @@ export default class SocketServer {
this.onConnect = this.onConnect.bind(this);
this.flecks = flecks;
this.httpServer = httpServer;
- const hooks = flecks.invokeMerge('@flecks/socket.intercom');
- debugSilly('intercom hooks(%O)', hooks);
- this.localIntercom = async ({payload, type}, fn) => {
- debugSilly('customHook: %s(%o)', type, payload);
- if (hooks[type]) {
- fn(await hooks[type](payload, this));
- }
- };
}
close(fn) {
@@ -31,13 +23,32 @@ export default class SocketServer {
}
async connect() {
+ const results = await this.flecks.invokeAsync('@flecks/socket.intercom');
+ const hooks = Object.entries(results)
+ .reduce(
+ (hooks, [fleck, endpoints]) => ({
+ ...hooks,
+ ...Object.fromEntries(
+ Object.entries(endpoints)
+ .map(([key, fn]) => [`${fleck}.${key}`, fn]),
+ ),
+ }),
+ {},
+ );
+ debugSilly('intercom hooks(%O)', hooks);
+ this.localIntercom = async ({payload, type}, fn) => {
+ debugSilly('customHook: %s(%o)', type, payload);
+ if (hooks[type]) {
+ fn(await hooks[type](payload, this));
+ }
+ };
this.io = SocketIoServer(this.httpServer, {
...await this.flecks.invokeMergeAsync('@flecks/socket.server'),
serveClient: false,
});
this.io.use(this.makeSocketMiddleware());
this.io.on('@flecks/socket.intercom', this.localIntercom);
- this.flecks.invoke('@flecks/socket/server.io', this.io);
+ await this.flecks.invokeSequentialAsync('@flecks/socket/server.io', this.io);
this.io.on('connect', this.onConnect);
}
diff --git a/packages/web/build/flecks.hooks.js b/packages/web/build/flecks.hooks.js
index 13ca694..5aada99 100644
--- a/packages/web/build/flecks.hooks.js
+++ b/packages/web/build/flecks.hooks.js
@@ -1,6 +1,7 @@
export const hooks = {
/**
* Define sequential actions to run when the client comes up.
+ * @invoke SequentialAsync
*/
'@flecks/web/client.up': async () => {
await youCanDoAsyncThingsHere();
@@ -8,12 +9,14 @@ export const hooks = {
/**
* Send configuration to clients.
* @param {http.ClientRequest} req The HTTP request object.
+ * @invoke Async
*/
'@flecks/web.config': (req) => ({
someConfig: req.someConfig,
}),
/**
* Define HTTP routes.
+ * @invoke Async
*/
'@flecks/web.routes': () => [
{
@@ -27,6 +30,7 @@ export const hooks = {
],
/**
* Define middleware to run when a route is matched.
+ * @invoke Middleware
*/
'@flecks/web/server.request.route': () => (req, res, next) => {
// Express-style route middleware...
@@ -34,6 +38,7 @@ export const hooks = {
},
/**
* Define middleware to run when an HTTP socket connection is established.
+ * @invoke Middleware
*/
'@flecks/web/server.request.socket': () => (req, res, next) => {
// Express-style route middleware...
@@ -43,12 +48,14 @@ export const hooks = {
* 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.
+ * @invoke ComposedAsync
*/
'@flecks/web/server.stream.html': (stream, req) => {
return stream.pipe(myTransformStream);
},
/**
* Define sequential actions to run when the HTTP server comes up.
+ * @invoke SequentialAsync
*/
'@flecks/web/server.up': async () => {
await youCanDoAsyncThingsHere();
diff --git a/packages/web/src/server/http.js b/packages/web/src/server/http.js
index aca36d2..c1a44e1 100644
--- a/packages/web/src/server/http.js
+++ b/packages/web/src/server/http.js
@@ -6,7 +6,6 @@ import {D} from '@flecks/core';
import compression from 'compression';
import express from 'express';
import httpProxy from 'http-proxy';
-import flatten from 'lodash.flatten';
const {
FLECKS_CORE_ROOT = process.cwd(),
@@ -38,7 +37,7 @@ export const createHttpServer = async (flecks) => {
app.use(flecks.makeMiddleware('@flecks/web/server.request.socket'));
// Routes.
const routeMiddleware = flecks.makeMiddleware('@flecks/web/server.request.route');
- const routes = flatten(flecks.invokeFlat('@flecks/web.routes'));
+ const routes = (await Promise.all(flecks.invokeFlat('@flecks/web.routes'))).flat();
debug('routes: %O', routes);
routes.forEach(({method, path, middleware}) => app[method](path, routeMiddleware, middleware));
// In development mode, create a proxy to the webpack-dev-server.
@@ -135,7 +134,7 @@ export const createHttpServer = async (flecks) => {
reject(error);
return;
}
- await Promise.all(flecks.invokeFlat('@flecks/web/server.up', httpServer));
+ await flecks.invokeSequentialAsync('@flecks/web/server.up', httpServer);
debug('HTTP server up @ %s!', [host, port].filter((e) => !!e).join(':'));
resolve();
});
diff --git a/website/docs/hooks.mdx b/website/docs/hooks.mdx
new file mode 100644
index 0000000..6711783
--- /dev/null
+++ b/website/docs/hooks.mdx
@@ -0,0 +1,211 @@
+---
+title: Hooks
+description: The key to unlocking the power of flecks.
+---
+
+Hooks are how everything happens in flecks. There are many hooks and the hooks provided by flecks
+are documented at the [hooks reference page](./flecks/hooks).
+
+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.
+
+## Invocation
+
+Hooks may be invoked using different invocation methods which may affect the order of invocation
+as well as the final result.
+
+All methods accept an arbitrary number of arguments after the specified arguments.
+
+All methods pass the `flecks` instance as the last argument.
+
+
+
+### `invoke`
+### `invokeAsync`
+
+Invokes all hook implementations and returns the results keyed by the implementing flecks' paths.
+
+#### `hook: string`
+
+The hook to invoke.
+
+### `invokeFleck`
+
+Invoke a single fleck's hook implementation and return the result.
+
+#### `hook: string`
+
+The hook to invoke.
+
+#### `fleck: string`
+
+The fleck whose hook to invoke.
+
+### `invokeFlat`
+
+Invokes all hook implementations and returns the results as an array.
+
+#### `hook: string`
+
+The hook to invoke.
+
+:::tip[Just a spoonful of sugar]
+
+The following test would pass:
+
+```js
+expect(flecks.invokeFlat('some-hook'))
+ .to.deep.equal(Object.values(flecks.invoke('some-hook')));
+```
+
+:::
+
+### `invokeComposed`
+### `invokeComposedAsync`
+
+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.
+
+#### `hook: string`
+
+The hook to invoke.
+
+#### `initial: any`
+
+The initial value.
+
+Composed hooks are [orderable](./ordering).
+
+### `invokeMerge`
+### `invokeMergeAsync`
+
+Invokes all hook implementations and returns the result of merging all implementations' returned objects together.
+
+#### `hook: string`
+
+The hook to invoke.
+
+### `invokeMergeUnique`
+### `invokeMergeUniqueAsync`
+
+Specialization of `invokeMerge` that will throw an error if any keys overlap.
+
+#### `hook: string`
+
+The hook to invoke.
+
+### `invokeReduce`
+### `invokeReduceAsync`
+
+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.
+
+#### `hook: string`
+
+The hook to invoke.
+
+#### `reduce: function`
+
+The reducer function.
+
+#### `initial: any`
+
+The initial value.
+
+### `invokeSequential`
+### `invokeSequentialAsync`
+
+Invokes all hook implementations, one after another. In the async variant, each implementation's result is `await`ed before invoking the next implementation.
+
+#### `hook: string`
+
+The hook to invoke.
+
+Sequential hooks are [orderable](./ordering.mdx).
+
+### `makeMiddleware` {#invokemiddleware}
+
+Hooks may be implemented in the style of Express middleware.
+
+Each implementation will be expected to accept 0 or more arguments followed by a `next` function
+which the implementation invokes when passing execution on to the next implementation.
+
+Usage with express would look something like:
+
+```js
+app.use(flecks.makeMiddleware('@my/fleck.hook'));
+```
+
+For more information, see: http://expressjs.com/en/guide/using-middleware.html
diff --git a/website/sidebars.js b/website/sidebars.js
index d81339c..1ed4e37 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -34,6 +34,7 @@ export default {
collapsed: false,
items: [
'testing',
+ 'hooks',
'gathering',
'ordering',
'isomorphism',