refactor: hooks

This commit is contained in:
cha0s 2024-01-29 08:15:22 -06:00
parent 6303681e6b
commit aed55e9c3f
45 changed files with 439 additions and 165 deletions

View File

@ -17,7 +17,7 @@
"test": "lerna exec 'yarn && yarn test'"
},
"devDependencies": {
"@flecks/build": "*",
"@flecks/build": "^3.1.3",
"lerna": "^8.0.2"
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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.

View File

@ -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&lt;string, Source&gt;} 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&lt;string&gt;} targets The targets to build.
* @invoke
*/
'@flecks/build.targets.alter': (targets) => {
targets.delete('some-target');

View File

@ -28,9 +28,12 @@ module.exports = async (env, argv) => {
debug('no build configuration found! aborting...');
await new Promise(() => {});
}
const entries = await Promise.all(building.map(
const webpackConfigs = Object.fromEntries(
await Promise.all(building.map(
async ([fleck, target]) => {
const configFn = require(await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck));
const configFn = require(
await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck),
);
if ('function' !== typeof configFn) {
debug(`'${
target
@ -41,14 +44,9 @@ module.exports = async (env, argv) => {
}
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(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) {

View File

@ -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!');

View File

@ -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,8 +834,9 @@ exports.Flecks = class Flecks {
* @param {string} fleck
*/
async refreshGathered(fleck) {
await Promise.all(
Object.entries(this.$$gathered)
.forEach(([
.map(async ([
hook,
{
check,
@ -847,16 +848,16 @@ exports.Flecks = class Flecks {
let raw;
// If decorating, gather all again
if (this.fleckImplementation(fleck, `${hook}.decorate`)) {
raw = this.invokeMergeAsync(hook);
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 = this.invokeFleck(hook, fleck);
raw = await this.invokeFleck(hook, fleck);
debugSilly('%s implements %s', fleck, hook);
}
if (raw) {
const decorated = this.checkAndDecorateRawGathered(hook, raw, check);
const decorated = await this.checkAndDecorateRawGathered(hook, raw, check);
debug('updating gathered %s from %s...', hook, fleck);
debugSilly('%O', decorated);
const entries = Object.entries(decorated);
@ -872,7 +873,8 @@ exports.Flecks = class Flecks {
});
this.invoke('@flecks/core.hmr.gathered', gathered, hook);
}
});
}),
);
}
/**

View File

@ -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]);

View File

@ -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$/))

View File

@ -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';

View File

@ -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;
};

View File

@ -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,

View File

@ -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)),

View File

@ -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('<h3 style={{fontSize: "1.125rem", marginTop: 0}}>');
source.push(`[${invoke}](../hooks#${invoke.toLowerCase()})`);
source.push('</h3>');
source.push('');
}
if (description) {
source.push(...description.split('\n'));
source.push('');
@ -151,6 +162,7 @@ exports.generateDocusaurusHookPage = (hooks) => {
source.push('</div>');
}
source.push('</div>');
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;
});
},
);

View File

@ -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,

View File

@ -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},
});
}
})

View File

@ -32,6 +32,10 @@ export default [
{description: 'Foo', name: 'foo', type: 'string'},
{description: 'Bar', name: 'bar', type: 'number'},
],
returns: {
description: 'Baz',
type: 'Baz',
},
},
],
},

View File

@ -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();

View File

@ -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);
}

View File

@ -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');

View File

@ -2,6 +2,7 @@ export const hooks = {
/**
* Define React components for login strategies.
* @invoke MergeUnique
*/
'@flecks/passport-react.strategies': () => ({
MyService: SomeBeautifulComponent,

View File

@ -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,

View File

@ -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))

View File

@ -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 `<Component />`, but `Component`.

View File

@ -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 = [];

View File

@ -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,

View File

@ -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);

View File

@ -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});

View File

@ -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)),

View File

@ -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));
}

View File

@ -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');

View File

@ -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]) {

View File

@ -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:

View File

@ -11,16 +11,17 @@ 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]) => {
Object.entries(await flecks.invokeMergeUniqueAsync('@flecks/repl.commands'))
.forEach(([key, value]) => {
commands[key] = value;
debugSilly('registered command: %s', key);
});

View File

@ -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();

View File

@ -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();
},
};

View File

@ -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...

View File

@ -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}}}) => ({

View File

@ -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));

View File

@ -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);
}

View File

@ -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();

View File

@ -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();
});

211
website/docs/hooks.mdx Normal file
View File

@ -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.
<style>{`
h3 > code:before {
content: 'flecks.';
}
#invoke > code:after,
#invokeasync > code:after
{
content: '(hook, ...args)';
}
#invokeflat > code:after {
content: '(hook, ...args)';
}
#invoke,
#invokecomposed,
#invokemerge,
#invokemergeunique,
#invokereduce,
#invokesequential
{
margin-bottom: calc( var(--ifm-heading-vertical-rhythm-bottom) * var(--ifm-leading) / 2 )
}
#invokeasync,
#invokecomposedasync,
#invokemergeasync,
#invokemergeuniqueasync,
#invokereduceasync,
#invokesequentialasync
{
margin-top: 0;
}
#invokecomposed > code:after,
#invokecomposedasync > code:after
{
content: '(hook, initial, ...args)';
}
#invokefleck > code:after {
content: '(hook, fleck, ...args)';
}
#invokemerge > code:after,
#invokemergeasync > code:after,
#invokemergeunique > code:after,
#invokemergeuniqueasync > code:after
{
content: '(hook, ...args)';
}
#invokereduce > code:after,
#invokereduceasync > code:after
{
content: '(hook, reducer, initial, ...args)';
}
#invokesequential > code:after,
#invokesequentialasync > code:after
{
content: '(hook, ...args)';
}
#invokemiddleware > code:after
{
content: '(hook, ...args)';
}
`}</style>
### `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

View File

@ -34,6 +34,7 @@ export default {
collapsed: false,
items: [
'testing',
'hooks',
'gathering',
'ordering',
'isomorphism',