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'" "test": "lerna exec 'yarn && yarn test'"
}, },
"devDependencies": { "devDependencies": {
"@flecks/build": "*", "@flecks/build": "^3.1.3",
"lerna": "^8.0.2" "lerna": "^8.0.2"
} }
} }

View File

@ -66,7 +66,7 @@ module.exports = class Build extends Flecks {
async babel() { async babel() {
return babelmerge.all([ return babelmerge.all([
{configFile: await this.resolveBuildConfig('babel.config.js')}, {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( static async from(
{ {
config: configParameter, config: configParameter,

View File

@ -41,7 +41,7 @@ program
const flecks = await Build.from(); const flecks = await Build.from();
debugSilly('bootstrapped'); debugSilly('bootstrapped');
// Register commands. // 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(); const keys = Object.keys(commands).sort();
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
const { const {

View File

@ -33,12 +33,7 @@ if (FLECKS_CORE_SYNC_FOR_ESLINT) {
const webpackConfigs = { const webpackConfigs = {
fleck: await require(webpackConfigPath)(env, argv, flecks), fleck: await require(webpackConfigPath)(env, argv, flecks),
}; };
await Promise.all( await flecks.configureBuilds(webpackConfigs, env, argv);
flecks.invokeFlat('@flecks/build.config', 'fleck', webpackConfigs.fleck, env, argv),
);
await Promise.all(
flecks.invokeFlat('@flecks/build.config.alter', webpackConfigs, env, argv),
);
const {resolve} = webpackConfigs.fleck; const {resolve} = webpackConfigs.fleck;
eslintConfig.settings['import/resolver'].webpack = {config: {resolve}}; eslintConfig.settings['import/resolver'].webpack = {config: {resolve}};
// Write it out to stdout. // Write it out to stdout.

View File

@ -7,6 +7,7 @@ export const hooks = {
* @param {Object} env The webpack environment. * @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments. * @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function} * @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
* @invoke SequentialAsync
*/ */
'@flecks/build.config': (target, config, env, argv) => { '@flecks/build.config': (target, config, env, argv) => {
if ('something' === target) { if ('something' === target) {
@ -22,6 +23,7 @@ export const hooks = {
* @param {Object} env The webpack environment. * @param {Object} env The webpack environment.
* @param {Object} argv The webpack commandline arguments. * @param {Object} argv The webpack commandline arguments.
* @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function} * @see {@link https://webpack.js.org/configuration/configuration-types/#exporting-a-function}
* @invoke SequentialAsync
*/ */
'@flecks/build.config.alter': (configs) => { '@flecks/build.config.alter': (configs) => {
// Maybe we want to do something if a target exists..? // Maybe we want to do something if a target exists..?
@ -32,11 +34,13 @@ export const hooks = {
/** /**
* Add implicitly resolved extensions. * Add implicitly resolved extensions.
* @invoke Flat
*/ */
'@flecks/build.extensions': () => ['.coffee'], '@flecks/build.extensions': () => ['.coffee'],
/** /**
* Register build files. See [the build files page](./build-files) for more details. * Register build files. See [the build files page](./build-files) for more details.
* @invoke
*/ */
'@flecks/build.files': () => [ '@flecks/build.files': () => [
/** /**
@ -48,6 +52,7 @@ export const hooks = {
/** /**
* Define CLI commands. * 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. * @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) => { '@flecks/build.commands': (program, flecks) => {
return { return {
@ -74,6 +79,7 @@ export const hooks = {
* @param {string} target The build target. * @param {string} target The build target.
* @param {Record&lt;string, Source&gt;} assets The assets. * @param {Record&lt;string, Source&gt;} assets The assets.
* @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation. * @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation.
* @invoke SequentialAsync
*/ */
'@flecks/build.processAssets': (target, assets, compilation) => { '@flecks/build.processAssets': (target, assets, compilation) => {
if (this.myTargets.includes(target)) { if (this.myTargets.includes(target)) {
@ -83,12 +89,14 @@ export const hooks = {
/** /**
* Define build targets. * Define build targets.
* @invoke
*/ */
'@flecks/build.targets': () => ['sometarget'], '@flecks/build.targets': () => ['sometarget'],
/** /**
* Alter defined build targets. * Alter defined build targets.
* @param {Set&lt;string&gt;} targets The targets to build. * @param {Set&lt;string&gt;} targets The targets to build.
* @invoke
*/ */
'@flecks/build.targets.alter': (targets) => { '@flecks/build.targets.alter': (targets) => {
targets.delete('some-target'); targets.delete('some-target');

View File

@ -28,27 +28,25 @@ module.exports = async (env, argv) => {
debug('no build configuration found! aborting...'); debug('no build configuration found! aborting...');
await new Promise(() => {}); await new Promise(() => {});
} }
const entries = await Promise.all(building.map( const webpackConfigs = Object.fromEntries(
async ([fleck, target]) => { await Promise.all(building.map(
const configFn = require(await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck)); async ([fleck, target]) => {
if ('function' !== typeof configFn) { const configFn = require(
debug(`'${ await flecks.resolveBuildConfig(`${target}.webpack.config.js`, fleck),
target );
}' build configuration expected function got ${ if ('function' !== typeof configFn) {
typeof configFn debug(`'${
}! aborting...`); target
return undefined; }' build configuration expected function got ${
} typeof configFn
return [target, await configFn(env, argv, flecks)]; }! aborting...`);
}, return undefined;
)); }
await Promise.all( return [target, await configFn(env, argv, flecks)];
entries.map(async ([target, config]) => ( },
Promise.all(flecks.invokeFlat('@flecks/build.config', target, config, env, argv))
)), )),
); );
const webpackConfigs = Object.fromEntries(entries); await flecks.configureBuilds(webpackConfigs, env, argv);
await Promise.all(flecks.invokeFlat('@flecks/build.config.alter', webpackConfigs, env, argv));
const enterableWebpackConfigs = Object.values(webpackConfigs) const enterableWebpackConfigs = Object.values(webpackConfigs)
.filter((webpackConfig) => { .filter((webpackConfig) => {
if (!webpackConfig.entry) { if (!webpackConfig.entry) {

View File

@ -2,6 +2,7 @@ export const hooks = {
/** /**
* Babel configuration. * Babel configuration.
* @invoke SequentialAsync
*/ */
'@flecks/core.babel': () => ({ '@flecks/core.babel': () => ({
plugins: ['...'], plugins: ['...'],
@ -9,6 +10,7 @@ export const hooks = {
/** /**
* Define configuration. See [the configuration page](./config) for more details. * Define configuration. See [the configuration page](./config) for more details.
* @invoke Fleck
*/ */
'@flecks/core.config': () => ({ '@flecks/core.config': () => ({
whatever: 'configuration', whatever: 'configuration',
@ -24,6 +26,7 @@ export const hooks = {
* Let flecks gather for you. * Let flecks gather for you.
* *
* See [the Gathering guide](../gathering). * See [the Gathering guide](../gathering).
* @invoke Async
*/ */
'@flecks/core.gathered': () => ({ '@flecks/core.gathered': () => ({
// If this hook is implemented by a fleck called `@some/fleck`, then: // 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 * Invoked when a fleck is HMR'd
* @param {string} path The path of the fleck * @param {string} path The path of the fleck
* @param {Module} updatedFleck The updated fleck module. * @param {Module} updatedFleck The updated fleck module.
* @invoke
*/ */
'@flecks/core.hmr': (path, updatedFleck) => { '@flecks/core.hmr': (path, updatedFleck) => {
if ('my-fleck' === path) { if ('my-fleck' === path) {
@ -52,6 +56,7 @@ export const hooks = {
* Invoked when a gathered set is HMR'd. * Invoked when a gathered set is HMR'd.
* @param {constructor} gathered The gathered set. * @param {constructor} gathered The gathered set.
* @param {string} hook The gather hook; e.g. `@flecks/db.models`. * @param {string} hook The gather hook; e.g. `@flecks/db.models`.
* @invoke
*/ */
'@flecks/core.hmr.gathered': (gathered, hook) => { '@flecks/core.hmr.gathered': (gathered, hook) => {
// Do something with the gathered set... // Do something with the gathered set...
@ -61,6 +66,7 @@ export const hooks = {
* Invoked when a gathered class is HMR'd. * Invoked when a gathered class is HMR'd.
* @param {constructor} Class The class. * @param {constructor} Class The class.
* @param {string} hook The gather hook; e.g. `@flecks/db.models`. * @param {string} hook The gather hook; e.g. `@flecks/db.models`.
* @invoke
*/ */
'@flecks/core.hmr.gathered.class': (Class, hook) => { '@flecks/core.hmr.gathered.class': (Class, hook) => {
// Do something with Class... // Do something with Class...
@ -70,6 +76,7 @@ export const hooks = {
* Invoked when flecks is building a fleck dependency graph. * Invoked when flecks is building a fleck dependency graph.
* @param {Digraph} graph The dependency graph. * @param {Digraph} graph The dependency graph.
* @param {string} hook The hook; e.g. `@flecks/server.up`. * @param {string} hook The hook; e.g. `@flecks/server.up`.
* @invoke
*/ */
'@flecks/core.priority': (graph, hook) => { '@flecks/core.priority': (graph, hook) => {
// Make `@flecks/socket/server`'s `@flecks/server.up` implementation depend on // Make `@flecks/socket/server`'s `@flecks/server.up` implementation depend on
@ -85,6 +92,7 @@ export const hooks = {
* Invoked when a fleck is registered. * Invoked when a fleck is registered.
* @param {string} fleck * @param {string} fleck
* @param {Module} M * @param {Module} M
* @invoke
*/ */
'@flecks/core.registered': (fleck, M) => { '@flecks/core.registered': (fleck, M) => {
if ('@something/or-other' === fleck) { if ('@something/or-other' === fleck) {
@ -94,6 +102,7 @@ export const hooks = {
/** /**
* Invoked when the application is starting. * Invoked when the application is starting.
* @invoke SequentialAsync
*/ */
'@flecks/core.starting': () => { '@flecks/core.starting': () => {
console.log('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. // Gather classes and check.
check(raw, hook); check(raw, hook);
// Decorate and check. // Decorate and check.
const decorated = this.invokeComposed(`${hook}.decorate`, raw); const decorated = await this.invokeComposedAsync(`${hook}.decorate`, raw);
check(decorated, `${hook}.decorate`); check(decorated, `${hook}.decorate`);
return decorated; return decorated;
} }
@ -383,7 +383,7 @@ exports.Flecks = class Flecks {
* @param {function} [config.check=() => {}] Check the validity of the gathered classes. * @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}. * @returns {object} An object with keys for ID, type, {@link ById}, and {@link ByType}.
*/ */
gather( async gather(
hook, hook,
{ {
idProperty = 'id', idProperty = 'id',
@ -395,8 +395,8 @@ exports.Flecks = class Flecks {
throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string'); throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string');
} }
// Gather classes and check. // Gather classes and check.
const raw = this.invokeMerge(hook); const raw = await this.invokeMergeAsync(hook);
const decorated = this.checkAndDecorateRawGathered(hook, raw, check); const decorated = await this.checkAndDecorateRawGathered(hook, raw, check);
// Assign unique IDs to each class and sort by type. // Assign unique IDs to each class and sort by type.
let uid = 1; let uid = 1;
const ids = {}; const ids = {};
@ -834,45 +834,47 @@ exports.Flecks = class Flecks {
* @param {string} fleck * @param {string} fleck
*/ */
async refreshGathered(fleck) { async refreshGathered(fleck) {
Object.entries(this.$$gathered) await Promise.all(
.forEach(([ Object.entries(this.$$gathered)
hook, .map(async ([
{ hook,
check, {
idProperty, check,
gathered, idProperty,
typeProperty, gathered,
}, typeProperty,
]) => { },
let raw; ]) => {
// If decorating, gather all again let raw;
if (this.fleckImplementation(fleck, `${hook}.decorate`)) { // If decorating, gather all again
raw = this.invokeMergeAsync(hook); if (this.fleckImplementation(fleck, `${hook}.decorate`)) {
debugSilly('%s implements %s.decorate', fleck, 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)) { // If only implementing, gather and decorate.
raw = this.invokeFleck(hook, fleck); else if (this.fleckImplementation(fleck, hook)) {
debugSilly('%s implements %s', fleck, hook); raw = await this.invokeFleck(hook, fleck);
} debugSilly('%s implements %s', fleck, hook);
if (raw) { }
const decorated = this.checkAndDecorateRawGathered(hook, raw, check); if (raw) {
debug('updating gathered %s from %s...', hook, fleck); const decorated = await this.checkAndDecorateRawGathered(hook, raw, check);
debugSilly('%O', decorated); debug('updating gathered %s from %s...', hook, fleck);
const entries = Object.entries(decorated); debugSilly('%O', decorated);
entries.forEach(([type, Class]) => { const entries = Object.entries(decorated);
const {[type]: {[idProperty]: id}} = gathered; entries.forEach(([type, Class]) => {
const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty); const {[type]: {[idProperty]: id}} = gathered;
// eslint-disable-next-line no-multi-assign const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty);
gathered[type] = Subclass; // eslint-disable-next-line no-multi-assign
gathered[id] = Subclass; gathered[type] = Subclass;
gathered[exports.ById][id] = Subclass; gathered[id] = Subclass;
gathered[exports.ByType][type] = Subclass; gathered[exports.ById][id] = Subclass;
this.invoke('@flecks/core.hmr.gathered.class', Subclass, hook); gathered[exports.ByType][type] = Subclass;
}); this.invoke('@flecks/core.hmr.gathered.class', Subclass, hook);
this.invoke('@flecks/core.hmr.gathered', gathered, hook); });
} this.invoke('@flecks/core.hmr.gathered', gathered, hook);
}); }
}),
);
} }
/** /**

View File

@ -12,7 +12,7 @@ it('can gather', async () => {
'@flecks/core/two': testTwo, '@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) expect(Object.keys(Gathered[ByType]).length)
.to.equal(Object.keys(Gathered[ById]).length); .to.equal(Object.keys(Gathered[ById]).length);
const typeKeys = Object.keys(Gathered[ByType]); const typeKeys = Object.keys(Gathered[ByType]);

View File

@ -3,20 +3,18 @@ export const hooks = {
/** /**
* Gather database models. * Gather database models.
* *
* In the example below, your fleck would have a `models` subdirectory, and each model would be * See: [the Gathering guide](../gathering).
* defined in its own file. * @invoke MergeAsync
* See: https://github.com/cha0s/flecks/tree/master/packages/user/src/server/models
*/ */
'@flecks/db.models': Flecks.provide(require.context('./models', false, /\.js$/)), '@flecks/db.models': Flecks.provide(require.context('./models', false, /\.js$/)),
/** /**
* Decorate database models. * Decorate database models.
* *
* In the example below, your fleck would have a `models/decorators` subdirectory, and each * See: [the Gathering guide](../gathering).
* decorator would be defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/user/src/local/server/models/decorators
* *
* @param {constructor} Model The model to decorate. * @param {constructor} Model The model to decorate.
* @invoke ComposedAsync
*/ */
'@flecks/db.models.decorate': ( '@flecks/db.models.decorate': (
Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) 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 * See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
* *
* ::: * :::
* @invoke MergeUniqueAsync
*/ */
'@flecks/docker.containers': () => ({ '@flecks/docker.containers': () => ({
someContainer: { someContainer: {
@ -29,6 +30,7 @@ export const hooks = {
* @param {string} dockerfile The content of the Dockerfile. * @param {string} dockerfile The content of the Dockerfile.
* *
* @returns The new content of the Dockerfile. * @returns The new content of the Dockerfile.
* @invoke ComposedAsync
*/ */
'@flecks/docker.Dockerfile': (dockerfile) => ( '@flecks/docker.Dockerfile': (dockerfile) => (
dockerfile.replace('DEBUG=*', 'DEBUG=*,-*:silly') dockerfile.replace('DEBUG=*', 'DEBUG=*,-*:silly')
@ -37,6 +39,7 @@ export const hooks = {
/** /**
* *
* @param {Object} config The object representing the docker compose configuration. * @param {Object} config The object representing the docker compose configuration.
* @invoke SequentialAsync
*/ */
'@flecks/docker.docker-compose.yml': (config) => { '@flecks/docker.docker-compose.yml': (config) => {
config.version = '3.1'; config.version = '3.1';

View File

@ -1,4 +1,4 @@
exports.generateDockerFile = (flecks) => { exports.generateDockerFile = async (flecks) => {
const dockerfile = [ const dockerfile = [
'FROM node:20', 'FROM node:20',
'', '',
@ -16,7 +16,7 @@ exports.generateDockerFile = (flecks) => {
'VOLUME /var/www/node_modules', 'VOLUME /var/www/node_modules',
'', '',
].join('\n'); ].join('\n');
return flecks.invokeComposed('@flecks/docker.Dockerfile', dockerfile); return flecks.invokeComposedAsync('@flecks/docker.Dockerfile', dockerfile);
}; };
exports.generateComposeConfig = async (flecks) => { 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( await Promise.all(
Object.entries(containers) Object.entries(containers)
@ -69,6 +69,6 @@ exports.generateComposeConfig = async (flecks) => {
}); });
}); });
const config = {version: '3', services}; const config = {version: '3', services};
flecks.invoke('@flecks/docker.docker-compose.yml', config); await flecks.invokeSequentialAsync('@flecks/docker.docker-compose.yml', config);
return config; return config;
}; };

View File

@ -12,7 +12,7 @@ module.exports = class FlecksDockerOutput {
apply(compiler) { apply(compiler) {
compiler.hooks.compilation.tap('FlecksDockerOutput', (compilation) => { compiler.hooks.compilation.tap('FlecksDockerOutput', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('FlecksDockerOutput', async (callback) => { compilation.hooks.additionalAssets.tapAsync('FlecksDockerOutput', async (callback) => {
const dockerFile = generateDockerFile(this.options.flecks); const dockerFile = await generateDockerFile(this.options.flecks);
compilation.assets.Dockerfile = { compilation.assets.Dockerfile = {
source: () => dockerFile, source: () => dockerFile,
size: () => dockerFile.length, size: () => dockerFile.length,

View File

@ -5,7 +5,7 @@ export const hooks = {
if (!flecks.get('@flecks/docker.enabled')) { if (!flecks.get('@flecks/docker.enabled')) {
return; return;
} }
const containers = await flecks.invokeMergeAsync('@flecks/docker.containers'); const containers = await flecks.invokeMergeUniqueAsync('@flecks/docker.containers');
await Promise.all( await Promise.all(
Object.entries(containers) Object.entries(containers)
.map(([key, config]) => startContainer(flecks, key, config)), .map(([key, config]) => startContainer(flecks, key, config)),

View File

@ -104,11 +104,22 @@ exports.generateDocusaurusHookPage = (hooks) => {
Object.entries(hooks) Object.entries(hooks)
.sort(([lhook], [rhook]) => (lhook < rhook ? -1 : 1)) .sort(([lhook], [rhook]) => (lhook < rhook ? -1 : 1))
.forEach(([hook, {implementations = [], invocations = [], specification}]) => { .forEach(([hook, {implementations = [], invocations = [], specification}]) => {
const {description, example, params} = specification || { const {
description,
example,
invoke,
params,
} = specification || {
params: [], params: [],
}; };
source.push(`## \`${hook}\``); source.push(`## \`${hook}\``);
source.push(''); 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) { if (description) {
source.push(...description.split('\n')); source.push(...description.split('\n'));
source.push(''); source.push('');
@ -151,6 +162,7 @@ exports.generateDocusaurusHookPage = (hooks) => {
source.push('</div>'); source.push('</div>');
} }
source.push('</div>'); source.push('</div>');
source.push('\n');
} }
}); });
return source.join('\n'); return source.join('\n');
@ -310,18 +322,9 @@ exports.generateJson = async function generate(flecks) {
type, type,
}); });
}); });
hookSpecifications.forEach(({ hookSpecifications.forEach(({hook, ...specification}) => {
hook,
description,
example,
params,
}) => {
ensureHook(hook); ensureHook(hook);
r.hooks[hook].specification = { r.hooks[hook].specification = specification;
description,
example,
params,
};
}); });
}, },
); );

View File

@ -84,17 +84,10 @@ exports.parseHookSpecificationSource = async (path, source, options) => {
const hookSpecifications = []; const hookSpecifications = [];
traverse(ast, hookSpecificationVisitor((hookSpecification) => { traverse(ast, hookSpecificationVisitor((hookSpecification) => {
const { const {
description,
hook,
location: {start: {index: start}, end: {index: end}}, location: {start: {index: start}, end: {index: end}},
params, ...specification
} = hookSpecification; } = hookSpecification;
hookSpecifications.push({ hookSpecifications.push({...specification, example: source.slice(start, end)});
description,
example: source.slice(start, end),
hook,
params,
});
})); }));
return { return {
hookSpecifications, hookSpecifications,

View File

@ -203,6 +203,9 @@ exports.hookSpecificationVisitor = (fn) => (
const {key, value: example} = property; const {key, value: example} = property;
const [{value}] = property.leadingComments; const [{value}] = property.leadingComments;
const [{description, tags}] = parseComment(`/**\n${value}\n*/`, {spacing: 'preserve'}); 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 const [returns] = tags
.filter(({tag}) => 'returns' === tag) .filter(({tag}) => 'returns' === tag)
.map(({name, type}) => ({description: name, type})); .map(({name, type}) => ({description: name, type}));
@ -214,6 +217,7 @@ exports.hookSpecificationVisitor = (fn) => (
.filter(({tag}) => 'param' === tag) .filter(({tag}) => 'param' === tag)
.map(({description, name, type}) => ({description, name, type})), .map(({description, name, type}) => ({description, name, type})),
...returns && {returns}, ...returns && {returns},
...invoke && {invoke},
}); });
} }
}) })

View File

@ -32,6 +32,10 @@ export default [
{description: 'Foo', name: 'foo', type: 'string'}, {description: 'Foo', name: 'foo', type: 'string'},
{description: 'Bar', name: 'bar', type: 'number'}, {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. * 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. * @param {[BrowserWindowConstructorOptions](https://www.electronjs.org/docs/latest/api/structures/browser-window-options)} browserWindowOptions The options.
* @invoke SequentialAsync
*/ */
'@flecks/electron/server.browserWindowOptions.alter': (browserWindowOptions) => { '@flecks/electron/server.browserWindowOptions.alter': (browserWindowOptions) => {
browserWindowOptions.icon = 'cute-kitten.png'; browserWindowOptions.icon = 'cute-kitten.png';
@ -11,6 +12,7 @@ export const hooks = {
/** /**
* Extensions to install. * Extensions to install.
* @param {[Installer](https://github.com/MarshallOfSound/electron-devtools-installer)} installer The installer. * @param {[Installer](https://github.com/MarshallOfSound/electron-devtools-installer)} installer The installer.
* @invoke Flat
*/ */
'@flecks/electron/server.extensions': (installer) => [ '@flecks/electron/server.extensions': (installer) => [
// Some defaults provided... // Some defaults provided...
@ -22,6 +24,7 @@ export const hooks = {
/** /**
* Invoked when electron is initializing. * Invoked when electron is initializing.
* @param {Electron} electron The electron module. * @param {Electron} electron The electron module.
* @invoke SequentialAsync
*/ */
'@flecks/electron/server.initialize': (electron) => { '@flecks/electron/server.initialize': (electron) => {
electron.app.on('will-quit', () => { electron.app.on('will-quit', () => {
@ -32,6 +35,7 @@ export const hooks = {
/** /**
* Invoked when a window is created * Invoked when a window is created
* @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window * @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) => { '@flecks/electron/server.window': (win) => {
win.maximize(); win.maximize();

View File

@ -7,7 +7,10 @@ let win;
async function createWindow(flecks) { async function createWindow(flecks) {
const {BrowserWindow} = flecks.electron; const {BrowserWindow} = flecks.electron;
const {browserWindowOptions} = flecks.get('@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); win = new BrowserWindow(browserWindowOptions);
await flecks.invokeSequentialAsync('@flecks/electron/server.window', win); 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. * Process the `package.json` for a built fleck.
* @param {Object} json The JSON. * @param {Object} json The JSON.
* @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation. * @param {[Compilation](https://webpack.js.org/api/compilation-object/)} compilation The webpack compilation.
* @invoke SequentialAsync
*/ */
'@flecks/fleck.packageJson': (json, compilation) => { '@flecks/fleck.packageJson': (json, compilation) => {
json.files.push('something'); json.files.push('something');

View File

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

View File

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

View File

@ -41,7 +41,7 @@ export const hooks = {
flecks.passport = { flecks.passport = {
initialize: passport.initialize(), initialize: passport.initialize(),
session: passport.session(), session: passport.session(),
strategies: flecks.invokeMergeUnique('@flecks/passport.strategies', passport), strategies: await flecks.invokeMergeUniqueAsync('@flecks/passport.strategies', passport),
}; };
Object.entries(flecks.passport.strategies) Object.entries(flecks.passport.strategies)
.forEach(([name, strategy]) => { .forEach(([name, strategy]) => {
@ -51,7 +51,7 @@ export const hooks = {
{before: '@flecks/web/server', after: ['@flecks/db/server', '@flecks/session/server']}, {before: '@flecks/web/server', after: ['@flecks/db/server', '@flecks/session/server']},
), ),
'@flecks/socket.intercom': () => ({ '@flecks/socket.intercom': () => ({
'@flecks/passport.users': async (sids, server) => { users: async (sids, server) => {
const sockets = await server.sockets(); const sockets = await server.sockets();
return sids return sids
.filter((sid) => sockets.has(sid)) .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. * Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object. * @param {http.ClientRequest} req The HTTP request object.
* @invoke SequentialAsync
*/ */
'@flecks/react.providers': (req) => { '@flecks/react.providers': (req) => {
// Generally it makes more sense to separate client and server concerns using platform // 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 * or an array of two elements where the first element is the component and the second element
* is the props passed to the component. * is the props passed to the component.
* @param {http.ClientRequest} req The HTTP request object. * @param {http.ClientRequest} req The HTTP request object.
* @invoke Async
*/ */
'@flecks/react.roots': (req) => { '@flecks/react.roots': (req) => {
// Note that we're not returning `<Component />`, but `Component`. // 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'); const debugSilly = debug.extend('silly');
export default async (flecks, req) => { 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); debugSilly('roots: %O', Roots);
const Providers = await flecks.invokeSequentialAsync('@flecks/react.providers', req); const Providers = await flecks.invokeSequentialAsync('@flecks/react.providers', req);
const FlattenedProviders = []; const FlattenedProviders = [];

View File

@ -4,7 +4,7 @@ export default async (flecks, opts = {}) => {
const { const {
host, host,
port, port,
} = flecks.get('@flecks/redis/server'); } = flecks.get('@flecks/redis');
const client = createClient({ const client = createClient({
url: `redis://${host}:${port}`, url: `redis://${host}:${port}`,
...opts, ...opts,

View File

@ -1,6 +1,7 @@
export const hooks = { export const hooks = {
/** /**
* Define side-effects to run against Redux actions. * Define side-effects to run against Redux actions.
* @invoke SequentialAsync
*/ */
'@flecks/redux.effects': () => ({ '@flecks/redux.effects': () => ({
someActionName: (store, action) => { someActionName: (store, action) => {
@ -9,6 +10,7 @@ export const hooks = {
}), }),
/** /**
* Define root-level reducers for the Redux store. * Define root-level reducers for the Redux store.
* @invoke SequentialAsync
*/ */
'@flecks/redux.reducers': () => { '@flecks/redux.reducers': () => {
return (state, action) => { return (state, action) => {
@ -20,6 +22,7 @@ export const hooks = {
* Define Redux slices. * Define Redux slices.
* *
* See: https://redux-toolkit.js.org/api/createSlice * See: https://redux-toolkit.js.org/api/createSlice
* @invoke MergeUniqueAsync
*/ */
'@flecks/redux.slices': () => { '@flecks/redux.slices': () => {
const something = createSlice( const something = createSlice(
@ -32,6 +35,7 @@ export const hooks = {
/** /**
* Modify Redux store configuration. * Modify Redux store configuration.
* @param {Object} options A mutable object with keys for enhancers and middleware. * @param {Object} options A mutable object with keys for enhancers and middleware.
* @invoke SequentialAsync
*/ */
'@flecks/redux.store': (options) => { '@flecks/redux.store': (options) => {
options.enhancers.splice(someIndex, 1); options.enhancers.splice(someIndex, 1);

View File

@ -6,8 +6,8 @@ import localStorageEnhancer from './local-storage';
export const hooks = { export const hooks = {
'@flecks/web/client.up': Flecks.priority( '@flecks/web/client.up': Flecks.priority(
async (flecks) => { async (flecks) => {
const slices = await flecks.invokeMergeUnique('@flecks/redux.slices'); const slices = await flecks.invokeMergeUniqueAsync('@flecks/redux.slices');
const reducer = createReducer(flecks, slices); const reducer = await createReducer(flecks, slices);
// Hydrate from server. // Hydrate from server.
const {preloadedState} = flecks.get('@flecks/redux'); const {preloadedState} = flecks.get('@flecks/redux');
const store = await configureStore(flecks, reducer, {preloadedState}); const store = await configureStore(flecks, reducer, {preloadedState});

View File

@ -10,8 +10,8 @@ const debugSilly = debug.extend('silly');
export const hooks = { export const hooks = {
'@flecks/electron/server.extensions': (installer) => [installer.REDUX_DEVTOOLS], '@flecks/electron/server.extensions': (installer) => [installer.REDUX_DEVTOOLS],
'@flecks/web/server.request.route': (flecks) => async (req, res, next) => { '@flecks/web/server.request.route': (flecks) => async (req, res, next) => {
const slices = await flecks.invokeMergeUnique('@flecks/redux.slices'); const slices = await flecks.invokeMergeUniqueAsync('@flecks/redux.slices');
const reducer = createReducer(flecks, slices); const reducer = await createReducer(flecks, slices);
// Let the slices have a(n async) chance to hydrate with server data. // Let the slices have a(n async) chance to hydrate with server data.
await Promise.all( await Promise.all(
Object.values(slices).map(({hydrateServer}) => hydrateServer?.(req, flecks)), Object.values(slices).map(({hydrateServer}) => hydrateServer?.(req, flecks)),

View File

@ -1,8 +1,8 @@
import {combineReducers} from '@reduxjs/toolkit'; import {combineReducers} from '@reduxjs/toolkit';
import reduceReducers from 'reduce-reducers'; import reduceReducers from 'reduce-reducers';
export default (flecks, slices) => { export default async (flecks, slices) => {
let reducers = flecks.invokeFlat('@flecks/redux.reducers'); let reducers = await flecks.invokeSequentialAsync('@flecks/redux.reducers');
if (Object.keys(slices).length > 0) { if (Object.keys(slices).length > 0) {
reducers = reducers.concat(combineReducers(slices)); reducers = reducers.concat(combineReducers(slices));
} }

View File

@ -11,10 +11,10 @@ export default async function configureStore(flecks, reducer, {preloadedState})
], ],
middleware: [ middleware: [
'@flecks/redux/defaultMiddleware', '@flecks/redux/defaultMiddleware',
effectsMiddleware(flecks), await effectsMiddleware(flecks),
], ],
}; };
flecks.invokeFlat('@flecks/redux.store', options); await flecks.invokeSequentialAsync('@flecks/redux.store', options);
return configureStoreR({ return configureStoreR({
enhancers: (defaultEnhancers) => { enhancers: (defaultEnhancers) => {
const index = options.enhancers.indexOf('@flecks/redux/defaultEnhancers'); const index = options.enhancers.indexOf('@flecks/redux/defaultEnhancers');

View File

@ -1,5 +1,5 @@
export default (flecks) => { export default async (flecks) => {
const effects = flecks.invokeFlat('@flecks/redux.effects'); const effects = await flecks.invokeSequentialAsync('@flecks/redux.effects');
const effect = (store, action) => { const effect = (store, action) => {
effects.forEach((map) => { effects.forEach((map) => {
if (map[action.type]) { if (map[action.type]) {

View File

@ -3,6 +3,7 @@ export const hooks = {
* Define REPL commands. * Define REPL commands.
* *
* Note: commands will be prefixed with a period in the Node REPL. * Note: commands will be prefixed with a period in the Node REPL.
* @invoke MergeUniqueAsync
*/ */
'@flecks/repl.commands': () => ({ '@flecks/repl.commands': () => ({
someCommand: (...args) => { someCommand: (...args) => {
@ -13,6 +14,7 @@ export const hooks = {
}), }),
/** /**
* Provide global context to the REPL. * Provide global context to the REPL.
* @invoke MergeUniqueAsync
*/ */
'@flecks/repl.context': () => { '@flecks/repl.context': () => {
// Now you'd be able to do like: // Now you'd be able to do like:

View File

@ -11,19 +11,20 @@ const debugSilly = debug.extend('silly');
export async function createReplServer(flecks) { export async function createReplServer(flecks) {
const {id} = flecks.get('@flecks/core'); const {id} = flecks.get('@flecks/core');
const context = (await Promise.all(flecks.invokeFlat('@flecks/repl.context'))) const context = {
.reduce((r, vars) => ({...r, ...vars}), {flecks}); ...await flecks.invokeMergeUniqueAsync('@flecks/repl.context'),
flecks,
};
debug( debug(
'Object.keys(context) === %O', 'Object.keys(context) === %O',
Object.keys(context), Object.keys(context),
); );
const commands = {}; const commands = {};
Object.entries( Object.entries(await flecks.invokeMergeUniqueAsync('@flecks/repl.commands'))
flecks.invokeFlat('@flecks/repl.commands').reduce((r, commands) => ({...r, ...commands}), {}), .forEach(([key, value]) => {
).forEach(([key, value]) => { commands[key] = value;
commands[key] = value; debugSilly('registered command: %s', key);
debugSilly('registered command: %s', key); });
});
const netServer = createServer((socket) => { const netServer = createServer((socket) => {
debug('client connection to repl established'); debug('client connection to repl established');
socket.on('close', () => { socket.on('close', () => {

View File

@ -2,6 +2,7 @@ export const hooks = {
/** /**
* Pass information to the runtime. * Pass information to the runtime.
* @invoke Async
*/ */
'@flecks/server.runtime': async () => ({ '@flecks/server.runtime': async () => ({
something: '...', something: '...',
@ -9,6 +10,7 @@ export const hooks = {
/** /**
* Define sequential actions to run when the server comes up. * Define sequential actions to run when the server comes up.
* @invoke SequentialAsync
*/ */
'@flecks/server.up': async () => { '@flecks/server.up': async () => {
await youCanDoAsyncThingsHere(); await youCanDoAsyncThingsHere();

View File

@ -2,16 +2,10 @@ export const hooks = {
/** /**
* Configure the session. See: https://github.com/expressjs/session#sessionoptions * Configure the session. See: https://github.com/expressjs/session#sessionoptions
* @invoke MergeAsync
*/ */
'@flecks/session.config': async () => ({ '@flecks/session.config': async () => ({
saveUninitialized: true, 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. * Modify Socket.io client configuration.
* *
* See: https://socket.io/docs/v4/client-options/ * See: https://socket.io/docs/v4/client-options/
* @invoke MergeAsync
*/ */
'@flecks/socket.client': () => ({ '@flecks/socket.client': () => ({
timeout: Infinity, timeout: Infinity,
@ -10,36 +11,34 @@ export const hooks = {
/** /**
* Define server-side intercom channels. * Define server-side intercom channels.
* @invoke Async
*/ */
'@flecks/socket.intercom': (req) => ({ '@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)`. // `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 // `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 // element in the array will be the result of `someServiceSpecificInformation()` running
// against that server instance. // against that server instance.
'@my/fleck.key': async (payload, server) => { key: async (payload, server) => {
return someServiceSpecificInformation(); return someServiceSpecificInformation();
}, },
}), }),
/** /**
* Define socket packets. * Gather socket packets.
* *
* In the example below, your fleck would have a `packets` subdirectory, and each * See: [the Gathering guide](../gathering).
* decorator would be defined in its own file. * @invoke MergeAsync
* See: https://github.com/cha0s/flecks/tree/master/packages/redux/src/packets
*
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/packet.js
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/redirect.js
*/ */
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)), '@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. * @param {constructor} Packet The packet to decorate.
* @invoke ComposedAsync
*/ */
'@flecks/socket.packets.decorate': ( '@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/)) Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
@ -49,6 +48,7 @@ export const hooks = {
* Modify Socket.io server configuration. * Modify Socket.io server configuration.
* *
* See: https://socket.io/docs/v4/server-options/ * See: https://socket.io/docs/v4/server-options/
* @invoke MergeAsync
*/ */
'@flecks/socket.server': () => ({ '@flecks/socket.server': () => ({
pingTimeout: Infinity, pingTimeout: Infinity,
@ -58,6 +58,7 @@ export const hooks = {
* Do something with a connecting socket. * 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. * @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) => { '@flecks/socket/server.connect': (socket) => {
socket.on('disconnect', () => { 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/ * See: https://socket.io/docs/v4/server-instance/
* @param {SocketIo} io The Socket.IO server instance. * @param {SocketIo} io The Socket.IO server instance.
* @invoke SequentialAsync
*/ */
'@flecks/socket/server.io': (io) => { '@flecks/socket/server.io': (io) => {
io.engine.on("headers", (headers, req) => { io.engine.on("headers", (headers, req) => {
@ -79,6 +81,7 @@ export const hooks = {
/** /**
* Define middleware to run when a socket connection is established. * Define middleware to run when a socket connection is established.
* @invoke Middleware
*/ */
'@flecks/socket/server.request.socket': () => (socket, next) => { '@flecks/socket/server.request.socket': () => (socket, next) => {
// Express-style route middleware... // Express-style route middleware...

View File

@ -1,10 +1,10 @@
import SocketClient from './socket'; import SocketClient from './socket';
export const hooks = { export const hooks = {
'@flecks/web/client.up': (flecks) => { '@flecks/web/client.up': async (flecks) => {
const socket = new SocketClient(flecks); const socket = new SocketClient(flecks);
flecks.socket.client = socket; flecks.socket.client = socket;
socket.connect(); await socket.connect();
socket.listen(); socket.listen();
}, },
'@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({ '@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({

View File

@ -19,7 +19,7 @@ export default class SocketClient extends decorate(Socket) {
this.socket = null; this.socket = null;
} }
connect(address) { async connect(address) {
if (this.socket) { if (this.socket) {
this.socket.destroy(); this.socket.destroy();
} }
@ -30,7 +30,7 @@ export default class SocketClient extends decorate(Socket) {
{ {
reconnectionDelay: 'production' === process.env.NODE_ENV ? 1000 : 100, reconnectionDelay: 'production' === process.env.NODE_ENV ? 1000 : 100,
reconnectionDelayMax: 'production' === process.env.NODE_ENV ? 5000 : 500, 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)); 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.onConnect = this.onConnect.bind(this);
this.flecks = flecks; this.flecks = flecks;
this.httpServer = httpServer; 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) { close(fn) {
@ -31,13 +23,32 @@ export default class SocketServer {
} }
async connect() { 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, { this.io = SocketIoServer(this.httpServer, {
...await this.flecks.invokeMergeAsync('@flecks/socket.server'), ...await this.flecks.invokeMergeAsync('@flecks/socket.server'),
serveClient: false, serveClient: false,
}); });
this.io.use(this.makeSocketMiddleware()); this.io.use(this.makeSocketMiddleware());
this.io.on('@flecks/socket.intercom', this.localIntercom); 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); this.io.on('connect', this.onConnect);
} }

View File

@ -1,6 +1,7 @@
export const hooks = { export const hooks = {
/** /**
* Define sequential actions to run when the client comes up. * Define sequential actions to run when the client comes up.
* @invoke SequentialAsync
*/ */
'@flecks/web/client.up': async () => { '@flecks/web/client.up': async () => {
await youCanDoAsyncThingsHere(); await youCanDoAsyncThingsHere();
@ -8,12 +9,14 @@ export const hooks = {
/** /**
* Send configuration to clients. * Send configuration to clients.
* @param {http.ClientRequest} req The HTTP request object. * @param {http.ClientRequest} req The HTTP request object.
* @invoke Async
*/ */
'@flecks/web.config': (req) => ({ '@flecks/web.config': (req) => ({
someConfig: req.someConfig, someConfig: req.someConfig,
}), }),
/** /**
* Define HTTP routes. * Define HTTP routes.
* @invoke Async
*/ */
'@flecks/web.routes': () => [ '@flecks/web.routes': () => [
{ {
@ -27,6 +30,7 @@ export const hooks = {
], ],
/** /**
* Define middleware to run when a route is matched. * Define middleware to run when a route is matched.
* @invoke Middleware
*/ */
'@flecks/web/server.request.route': () => (req, res, next) => { '@flecks/web/server.request.route': () => (req, res, next) => {
// Express-style route middleware... // Express-style route middleware...
@ -34,6 +38,7 @@ export const hooks = {
}, },
/** /**
* Define middleware to run when an HTTP socket connection is established. * Define middleware to run when an HTTP socket connection is established.
* @invoke Middleware
*/ */
'@flecks/web/server.request.socket': () => (req, res, next) => { '@flecks/web/server.request.socket': () => (req, res, next) => {
// Express-style route middleware... // Express-style route middleware...
@ -43,12 +48,14 @@ export const hooks = {
* Define composition functions to run over the HTML stream prepared for the client. * Define composition functions to run over the HTML stream prepared for the client.
* @param {stream.Readable} stream The HTML stream. * @param {stream.Readable} stream The HTML stream.
* @param {http.ClientRequest} req The HTTP request object. * @param {http.ClientRequest} req The HTTP request object.
* @invoke ComposedAsync
*/ */
'@flecks/web/server.stream.html': (stream, req) => { '@flecks/web/server.stream.html': (stream, req) => {
return stream.pipe(myTransformStream); return stream.pipe(myTransformStream);
}, },
/** /**
* Define sequential actions to run when the HTTP server comes up. * Define sequential actions to run when the HTTP server comes up.
* @invoke SequentialAsync
*/ */
'@flecks/web/server.up': async () => { '@flecks/web/server.up': async () => {
await youCanDoAsyncThingsHere(); await youCanDoAsyncThingsHere();

View File

@ -6,7 +6,6 @@ import {D} from '@flecks/core';
import compression from 'compression'; import compression from 'compression';
import express from 'express'; import express from 'express';
import httpProxy from 'http-proxy'; import httpProxy from 'http-proxy';
import flatten from 'lodash.flatten';
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
@ -38,7 +37,7 @@ export const createHttpServer = async (flecks) => {
app.use(flecks.makeMiddleware('@flecks/web/server.request.socket')); app.use(flecks.makeMiddleware('@flecks/web/server.request.socket'));
// Routes. // Routes.
const routeMiddleware = flecks.makeMiddleware('@flecks/web/server.request.route'); 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); debug('routes: %O', routes);
routes.forEach(({method, path, middleware}) => app[method](path, routeMiddleware, middleware)); routes.forEach(({method, path, middleware}) => app[method](path, routeMiddleware, middleware));
// In development mode, create a proxy to the webpack-dev-server. // In development mode, create a proxy to the webpack-dev-server.
@ -135,7 +134,7 @@ export const createHttpServer = async (flecks) => {
reject(error); reject(error);
return; 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(':')); debug('HTTP server up @ %s!', [host, port].filter((e) => !!e).join(':'));
resolve(); 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, collapsed: false,
items: [ items: [
'testing', 'testing',
'hooks',
'gathering', 'gathering',
'ordering', 'ordering',
'isomorphism', 'isomorphism',