From aed55e9c3f28c4846b131a525cd9810035d28b0f Mon Sep 17 00:00:00 2001 From: cha0s Date: Mon, 29 Jan 2024 08:15:22 -0600 Subject: [PATCH] refactor: hooks --- package.json | 2 +- packages/build/build/build.js | 12 +- packages/build/build/cli.js | 2 +- packages/build/build/eslint.config.js | 7 +- packages/build/build/flecks.hooks.js | 8 + packages/build/build/fleckspack.config.js | 36 ++- packages/core/build/flecks.hooks.js | 9 + packages/core/build/flecks.js | 90 ++++---- packages/core/test/gather.js | 2 +- packages/db/build/flecks.hooks.js | 10 +- packages/docker/build/flecks.hooks.js | 3 + packages/docker/build/generate.js | 8 +- packages/docker/build/plugin.js | 2 +- packages/docker/src/server.js | 2 +- packages/dox/build/generate.js | 27 ++- packages/dox/build/parser.js | 11 +- packages/dox/build/visitors.js | 4 + packages/dox/test/server/verified-root.js | 4 + packages/electron/build/flecks.hooks.js | 4 + packages/electron/src/server/index.js | 5 +- packages/fleck/build/flecks.hooks.js | 1 + packages/passport-react/build/flecks.hooks.js | 1 + packages/passport/build/flecks.hooks.js | 1 + packages/passport/src/server/index.js | 4 +- packages/react/build/flecks.hooks.js | 2 + packages/react/src/root.js | 2 +- packages/redis/src/create-client.js | 2 +- packages/redux/build/flecks.hooks.js | 4 + packages/redux/src/client/index.js | 4 +- packages/redux/src/server.js | 4 +- packages/redux/src/store/create-reducer.js | 4 +- packages/redux/src/store/index.js | 4 +- .../redux/src/store/middleware/effects.js | 4 +- packages/repl/build/flecks.hooks.js | 2 + packages/repl/src/repl.js | 17 +- packages/server/build/flecks.hooks.js | 2 + packages/session/build/flecks.hooks.js | 8 +- packages/socket/build/flecks.hooks.js | 29 +-- packages/socket/src/client/index.js | 4 +- packages/socket/src/client/socket.js | 4 +- packages/socket/src/server/server.js | 29 ++- packages/web/build/flecks.hooks.js | 7 + packages/web/src/server/http.js | 5 +- website/docs/hooks.mdx | 211 ++++++++++++++++++ website/sidebars.js | 1 + 45 files changed, 439 insertions(+), 165 deletions(-) create mode 100644 website/docs/hooks.mdx 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',