diff --git a/README.md b/README.md index c641563..24efb57 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

flecks

- Flecks is a dynamic, configuration-driven, fullstack application production system. Its purpose + Flecks is an exceptionally extensible fullstack application production system. Its true purpose is to make application development a more joyful endeavor. Intelligent defaults combined with a highly dynamic structure encourage consistency while allowing you to easily express your own opinions. diff --git a/TODO.md b/TODO.md index 491effd..3f87ef8 100644 --- a/TODO.md +++ b/TODO.md @@ -22,7 +22,7 @@ - [x] remove `invokeParallel()` - [x] Specialize `invokeReduce()` with `invokeMerge()`. - [x] Rename all hooks to dot-first notation; rewrite `lookupFlecks()`. -- [ ] ensureUniqueReduction moved into invokeMerge +- [x] ensureUniqueReduction moved into invokeMerge - [x] `bootstrap({without: ['badplatform']})` should be handled by passing `{platforms: ['!badplatform']}` - [ ] user redux server hydrate fails if no user in req - [ ] governor fails if not in server up @@ -31,3 +31,4 @@ - [ ] rename `@flecks/web` to `@flecks/web` - [ ] simultaneous babel compilation across all compiled flecks - [ ] add building to publish process ... +- [ ] @babel/register@7.18.x has a bug diff --git a/packages/core/QUIRKS.md b/packages/core/QUIRKS.md new file mode 100644 index 0000000..12a2a8f --- /dev/null +++ b/packages/core/QUIRKS.md @@ -0,0 +1 @@ +- I use the variable `r` a lot when referencing a reducer's accumulator value diff --git a/packages/core/build/.neutrinorc.js b/packages/core/build/.neutrinorc.js index 40c4cbc..eb7167d 100644 --- a/packages/core/build/.neutrinorc.js +++ b/packages/core/build/.neutrinorc.js @@ -35,8 +35,10 @@ config.use.push(({config}) => { } }); +// Fleck build configuration. config.use.unshift(fleck()); +// AirBnb linting. config.use.unshift( airbnb({ eslint: { @@ -45,13 +47,13 @@ config.use.unshift( }), ); +// Include a shebang and set the executable bit.. config.use.push(banner({ banner: '#!/usr/bin/env node', include: /^cli\.js$/, pluginId: 'shebang', raw: true, })) - config.use.push(({config}) => { config .plugin('executable') diff --git a/packages/core/build/dox/concepts/hooks.md b/packages/core/build/dox/concepts/hooks.md index 7042a98..4bbe26f 100755 --- a/packages/core/build/dox/concepts/hooks.md +++ b/packages/core/build/dox/concepts/hooks.md @@ -2,16 +2,12 @@ Hooks are how everything happens in flecks. There are many hooks and the hooks provided by flecks are documented at the [hooks reference page](https://github.com/cha0s/flecks/blob/gh-pages/hooks.md). -To define hooks (and turn your plain ol' boring JS modules into beautiful interesting flecks), you only have to import the `Hooks` symbol and key your default export: +To define hooks (and turn your plain ol' boring JS modules into beautiful interesting flecks), you only have to export a `hooks` object: ```javascript -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - '@flecks/core.starting': () => { - console.log('hello, gorgeous'); - }, +export const hooks = { + '@flecks/core.starting': () => { + console.log('hello, gorgeous'); }, }; ``` @@ -133,15 +129,15 @@ assert(foo.type === 'Foo'); ```javascript { // The property added when extending the class to return the numeric ID. - idAttribute = 'id', + idProperty = 'id', // The property added when extending the class to return the type. - typeAttribute = 'type', + typeProperty = 'type', // A function called with the `Gathered` object to allow checking validity. check = () => {}, } ``` -As an example, when `@flecks/db/server` gathers models, `typeAttribute` is set to `name`, because Sequelize requires its model classes to have a unique `name` property. +As an example, when `@flecks/db/server` gathers models, `typeProperty` is set to `name`, because Sequelize requires its model classes to have a unique `name` property. **Note:** the numeric IDs are useful for efficient serialization between the client and server, but **if you are using this property, ensure that `flecks.gather()` is called equivalently on both the client and the server**. As a rule of thumb, if you have serializable `Gathered`s, they should be invoked and defined in `your-fleck`, and not in `your-fleck/[platform]`, so that they are invoked for every platform. @@ -152,19 +148,15 @@ Complementary to gather hooks above, `Flecks.provide()` allows you to ergonomica Here's an example of how you could manually provide `@flecks/db/server.models` in your own fleck: ```javascript -import {Hooks} foom '@flecks/core'; - import SomeModel from './models/some-model'; import AnotherModel from './models/another-model'; -export default { - [Hooks]: { - '@flecks/db/server.models': () => ({ - SomeModel, - AnotherModel, - }), - }, -}; +export const hooks = { + '@flecks/db/server.models': () => ({ + SomeModel, + AnotherModel, + }), +} ``` If you think about the example above, you might realize that it will become a lot of typing to keep adding new models over time. Provider hooks exist to reduce this maintenance burden for you. @@ -183,12 +175,10 @@ models/ then, this `index.js`: ```javascript -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; -export default { - [Hooks]: { - '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), - }, +export const hooks = { + '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), }; ``` @@ -212,31 +202,27 @@ is *exactly equivalent* to the gather example above. By default, `Flecks.provide When a Model (or any other) is gathered as above, an implicit hook is called: `${hook}.decorate`. This allows other flecks to decorate whatever has been gathered: ```javascript -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - '@flecks/db/server.models.decorate': (Models) => { - return { - ...Models, - User: class extends Models.User { - - // Let's mix in some logging... - constructor(...args) { - super(...args); - console.log ('Another user decorated!'); - } - - }, - }; - }, +export const hooks = { + '@flecks/db/server.models.decorate': (Models) => { + return { + ...Models, + User: class extends Models.User { + + // Let's mix in some logging... + constructor(...args) { + super(...args); + console.log ('Another user decorated!'); + } + + }, + }; }, }; ``` #### `Flecks.decorate(context, options)` -As with above, there exists an API for making the maintenance of decorators more ergonomic. +As with above, there exists an API for making the maintenance of decorators even more ergonomic. Supposing our fleck is structured like so: @@ -266,12 +252,12 @@ export default (User) => { then, this `index.js`: ```javascript -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; -export default { - [Hooks]: { - '@flecks/db/server.models.decorate': Flecks.decorate(require.context('./models/decorators', false, /\.js$/)), - }, +export const hooks = { + '@flecks/db/server.models.decorate': ( + Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) + ), }; ``` @@ -307,7 +293,7 @@ Our `flecks.yml` could be configured like so: In this application, when `@flecks/http/server.request.route` is invoked, `@flecks/user/session`'s implementation is invoked (which reifies the user's session from cookies), followed by `my-cool-fleck`'s (which, we assume, does some kind of very cool dark mode check). -### Ellipses +### Ellipses/elision It may not always be ergonomic to configure the order of every single implementation, but enough to specify which implementations must run first (or last). diff --git a/packages/core/build/dox/hooks.js b/packages/core/build/dox/hooks.js index 3f34046..edd1ad6 100644 --- a/packages/core/build/dox/hooks.js +++ b/packages/core/build/dox/hooks.js @@ -1,122 +1,118 @@ -import {Hooks} from '@flecks/core'; +export const hooks = { -export default { - [Hooks]: { - /** - * Hook into neutrino configuration. - * @param {string} target The build target; e.g. `server`. - * @param {Object} config The neutrino configuration. - */ + /** + * Hook into neutrino configuration. + * @param {string} target The build target; e.g. `server`. + * @param {Object} config The neutrino configuration. + */ '@flecks/core.build': (target, config) => { - if ('something' === target) { - config[target].use.push(someNeutrinoMiddleware); - } - }, + if ('something' === target) { + config[target].use.push(someNeutrinoMiddleware); + } + }, - /** - * Alter build configurations after they have been hooked. - * @param {Object} configs The neutrino configurations. - */ - '@flecks/core.build.alter': (configs) => { - // Maybe we want to do something if a config exists..? - if (configs.something) { - // Do something... - // And then maybe we want to remove it from the build configuration..? - delete configs.something; - } - }, - - /** - * Register build configuration. - */ - '@flecks/core.build.config': () => [ - /** - * If you document your config files like this, documentation will be automatically - * generated. - */ - '.myrc.js', - /** - * Make sure you return them as an array expression, like this. - */ - ['mygeneralrc.js', {specifier: (specific) => `${specific}.mygeneralrc.js`}], - ], - - /** - * Define CLI commands. - */ - '@flecks/core.commands': (program) => ({ - // So this could be invoked like: - // npx flecks something -t --blow-up blah - something: { - action: (...args) => { - // Run the command... - }, - args: [ - '', - ], - description: 'This sure is some command', - options: [ - '-t, --test', 'Do a test', - '-b, --blow-up', 'Blow up instead of running the command', - ], - }, - }), - - /** - * Define configuration. - */ - '@flecks/core.config': () => ({ - whatever: 'configuration', - your: 1337, - fleck: 'needs', - /** - * Also, comments like this will be used to automatically generate documentation. - */ - though: 'you should keep the values serializable', - }), - - /** - * Invoked when a fleck is HMR'd - * @param {string} path The path of the fleck - * @param {Module} updatedFleck The updated fleck module. + /** + * Alter build configurations after they have been hooked. + * @param {Object} configs The neutrino configurations. */ - '@flecks/core.hmr': (path, updatedFleck) => { - if ('my-fleck' === path) { - updatedFleck.doSomething(); - } - }, + '@flecks/core.build.alter': (configs) => { + // Maybe we want to do something if a config exists..? + if (configs.something) { + // Do something... + // And then maybe we want to remove it from the build configuration..? + delete configs.something; + } + }, + /** + * Register build configuration. + */ + '@flecks/core.build.config': () => [ /** - * Invoked when a gathered class is HMR'd. - * @param {constructor} Class The class. - * @param {string} hook The gather hook; e.g. `@flecks/db/server.models`. - */ - '@flecks/core.hmr.gathered': (Class, hook) => { - // Do something with Class... - }, + * If you document your config files like this, documentation will be automatically + * generated. + */ + '.myrc.js', + /** + * Make sure you return them as an array expression, like this. + */ + ['mygeneralrc.js', {specifier: (specific) => `${specific}.mygeneralrc.js`}], + ], - /** - * Invoked when the application is starting. Use for order-independent initialization tasks. - */ - '@flecks/core.starting': (flecks) => { - flecks.set('$my-fleck/value', initializeMyValue()); + /** + * Define CLI commands. + */ + '@flecks/core.commands': (program) => ({ + // So this could be invoked like: + // npx flecks something -t --blow-up blah + something: { + action: (...args) => { + // Run the command... + }, + args: [ + '', + ], + description: 'This command does tests and also blows up', + options: [ + '-t, --test', 'Do a test', + '-b, --blow-up', 'Blow up instead of running the command', + ], }, + }), + /** + * Define configuration. + */ + '@flecks/core.config': () => ({ + whatever: 'configuration', + your: 1337, + fleck: 'needs', /** - * Define neutrino build targets. - */ - '@flecks/core.targets': () => ['sometarget'], + * Also, comments like this will be used to automatically generate documentation. + */ + though: 'you should keep the values serializable', + }), - /** - * Hook into webpack configuration. - * @param {string} target The build target; e.g. `server`. - * @param {Object} config The neutrino configuration. - */ - '@flecks/core.webpack': (target, config) => { - if ('something' === target) { - config.stats = 'verbose'; - } - }, + /** + * Invoked when a fleck is HMR'd + * @param {string} path The path of the fleck + * @param {Module} updatedFleck The updated fleck module. + */ + '@flecks/core.hmr': (path, updatedFleck) => { + if ('my-fleck' === path) { + updatedFleck.doSomething(); + } + }, + + /** + * Invoked when a gathered class is HMR'd. + * @param {constructor} Class The class. + * @param {string} hook The gather hook; e.g. `@flecks/db/server.models`. + */ + '@flecks/core.hmr.gathered': (Class, hook) => { + // Do something with Class... + }, + + /** + * Invoked when the application is starting. Use for order-independent initialization tasks. + */ + '@flecks/core.starting': (flecks) => { + flecks.set('$my-fleck/value', initializeMyValue()); + }, + + /** + * Define neutrino build targets. + */ + '@flecks/core.targets': () => ['sometarget'], + + /** + * Hook into webpack configuration. + * @param {string} target The build target; e.g. `server`. + * @param {Object} config The neutrino configuration. + */ + '@flecks/core.webpack': (target, config) => { + if ('something' === target) { + config.stats = 'verbose'; + } }, }; - diff --git a/packages/core/package.json b/packages/core/package.json index 692b460..26aee49 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -59,6 +59,7 @@ "babel-merge": "^3.0.0", "babel-plugin-prepend": "^1.0.2", "chai": "4.2.0", + "chai-as-promised": "7.1.1", "commander": "^8.3.0", "debug": "4.3.1", "enhanced-resolve": "^5.9.2", diff --git a/packages/core/src/bootstrap/autoentry.js b/packages/core/src/bootstrap/autoentry.js index c1609f9..8e6a533 100644 --- a/packages/core/src/bootstrap/autoentry.js +++ b/packages/core/src/bootstrap/autoentry.js @@ -11,7 +11,7 @@ const { FLECKS_CORE_ROOT = process.cwd(), } = process.env; -const resolver = (source) => (path) => { +const resolveValidModulePath = (source) => (path) => { // Does the file resolve as source? try { R.resolve(`${source}/${path}`); @@ -39,7 +39,7 @@ module.exports = () => ({config, options}) => { .set(name, join(FLECKS_CORE_ROOT, 'src')); // Calculate entry points from `files`. files - .filter(resolver(source)) + .filter(resolveValidModulePath(source)) .forEach((file) => { const trimmed = join(dirname(file), basename(file, extname(file))); config diff --git a/packages/core/src/bootstrap/require.js b/packages/core/src/bootstrap/require.js index 168357d..76e08c0 100644 --- a/packages/core/src/bootstrap/require.js +++ b/packages/core/src/bootstrap/require.js @@ -1,2 +1,4 @@ +// Get a runtime require function by hook or by crook. :) + // eslint-disable-next-line no-eval module.exports = eval('"undefined" !== typeof require ? require : undefined'); diff --git a/packages/core/src/ensure-unique-reduction.js b/packages/core/src/ensure-unique-reduction.js deleted file mode 100644 index 32e5855..0000000 --- a/packages/core/src/ensure-unique-reduction.js +++ /dev/null @@ -1,16 +0,0 @@ -export default async (flecks, hook, ...args) => { - const track = {}; - return Object.entries(flecks.invoke(hook, ...args)) - .reduce(async (r, [pkg, impl]) => { - const aimpl = await impl; - Object.keys(aimpl).forEach((key) => { - if (track[key]) { - throw new ReferenceError( - `Conflict in ${hook}: '${track[key]}' implemented '${key}', followed by '${pkg}'`, - ); - } - track[key] = pkg; - }); - return {...(await r), ...aimpl}; - }, {}); -}; diff --git a/packages/core/src/flecks.js b/packages/core/src/flecks.js index 195625f..54f0c43 100644 --- a/packages/core/src/flecks.js +++ b/packages/core/src/flecks.js @@ -16,24 +16,38 @@ import Middleware from './middleware'; const debug = D('@flecks/core/flecks'); const debugSilly = debug.extend('silly'); +// Symbols for Gathered classes. export const ById = Symbol.for('@flecks/core.byId'); export const ByType = Symbol.for('@flecks/core.byType'); -export const Hooks = Symbol.for('@flecks/core.hooks'); +/** + * Capitalize a string. + * + * @param {string} string + * @returns {string} + */ const capitalize = (string) => string.substring(0, 1).toUpperCase() + string.substring(1); +/** + * CamelCase a string. + * + * @param {string} string + * @returns {string} + */ const camelCase = (string) => string.split(/[_-]/).map(capitalize).join(''); +// Track gathered for HMR. const hotGathered = new Map(); -const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => { +// Wrap classes to expose their flecks ID and type. +const wrapGathered = (Class, id, idProperty, type, typeProperty) => { class Subclass extends Class { - static get [idAttribute]() { + static get [idProperty]() { return id; } - static get [typeAttribute]() { + static get [typeProperty]() { return type; } @@ -43,72 +57,121 @@ const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => { export default class Flecks { + config = {}; + + flecks = {}; + + hooks = {}; + + platforms = {}; + + /** + * @param {object} init + * @param {object} init.config The Flecks configuration (e.g. loaded from `flecks.yml`). + * @param {string[]} init.platforms Platforms this instance is running on. + */ constructor({ config = {}, flecks = {}, platforms = [], } = {}) { - this.config = { - ...Object.fromEntries(Object.keys(flecks).map((path) => [path, {}])), - ...config, - }; - this.hooks = {}; - this.flecks = {}; + const emptyConfigForAllFlecks = Object.fromEntries( + Object.keys(flecks).map((path) => [path, {}]), + ); + this.config = {...emptyConfigForAllFlecks, ...config}; this.platforms = platforms; const entries = Object.entries(flecks); debugSilly('paths: %O', entries.map(([fleck]) => fleck)); for (let i = 0; i < entries.length; i++) { const [fleck, M] = entries[i]; - this.registerFleck(fleck, M); + this.registerFleckHooks(fleck, M); + this.invoke('@flecks/core.registered', fleck, M); } - this.configureFlecks(); + this.configureFlecksDefaults(); debugSilly('config: %O', this.config); } - configureFleck(fleck) { + /** + * Configure defaults for a fleck. + * + * @param {string} fleck + * @protected + */ + configureFleckDefaults(fleck) { this.config[fleck] = { ...this.invokeFleck('@flecks/core.config', fleck), ...this.config[fleck], }; } - configureFlecks() { - const defaultConfig = this.invoke('@flecks/core.config'); - const flecks = Object.keys(defaultConfig); + /** + * Configure defaults for all flecks. + * + * @protected + */ + configureFlecksDefaults() { + const flecks = this.flecksImplementing('@flecks/core.config'); for (let i = 0; i < flecks.length; i++) { - this.configureFleck(flecks[i]); + this.configureFleckDefaults(flecks[i]); } } + /** + * [Dasherize]{@link https://en.wiktionary.org/wiki/dasherize} a fleck path. + * + * @param {string} path The path to dasherize. + * @returns {string} + */ + static dasherizePath(path) { + const parts = dirname(path).split('/'); + if ('.' === parts[0]) { + parts.shift(); + } + if ('index' === parts[parts.length - 1]) { + parts.pop(); + } + return join(parts.join('-'), basename(path, extname(path))); + } + + /** + * Generate a decorator from a require context. + * + * @param {*} context @see {@link https://webpack.js.org/guides/dependency-management/#requirecontext} + * @param {object} config + * @param {function} [config.transformer = {@link camelCase}] + * Function to run on each context path. + * @returns {function} The decorator. + */ static decorate( context, { transformer = camelCase, } = {}, ) { - return (Gathered, flecks) => { + return (Gathered, flecks) => ( context.keys() - .forEach((path) => { - const {default: M} = context(path); - if ('function' !== typeof M) { - throw new ReferenceError( - `Flecks.decorate(): require(${ - path - }).default is not a function (from: ${ - context.id - })`, - ); - } - const key = transformer(this.symbolizePath(path)); - if (Gathered[key]) { - // eslint-disable-next-line no-param-reassign - Gathered[key] = M(Gathered[key], flecks); - } - }); - return Gathered; - }; + .reduce( + (Gathered, path) => { + const key = transformer(this.dasherizePath(path)); + if (!Gathered[key]) { + return Gathered; + } + const {default: M} = context(path); + if ('function' !== typeof M) { + throw new ReferenceError( + `Flecks.decorate(): require(${path}).default is not a function (from: ${context.id})`, + ); + } + return {...Gathered, [key]: M(Gathered[key], flecks)}; + }, + Gathered, + ) + ); } + /** + * Destroy this instance. + */ destroy() { this.config = {}; this.hooks = {}; @@ -116,12 +179,20 @@ export default class Flecks { this.platforms = []; } + /** + * Lists all flecks implementing a hook, including platform-specific and elided variants. + * + * @param {string} hook + * @returns {string[]} The expanded list of flecks. + */ expandedFlecks(hook) { const flecks = this.lookupFlecks(hook); let expanded = []; for (let i = 0; i < flecks.length; ++i) { const fleck = flecks[i]; + // Just the fleck. expanded.push(fleck); + // Platform-specific variants. for (let j = 0; j < this.platforms.length; ++j) { const platform = this.platforms[j]; const variant = join(fleck, platform); @@ -130,6 +201,7 @@ export default class Flecks { } } } + // Expand elided flecks. const index = expanded.findIndex((fleck) => '...' === fleck); if (-1 !== index) { if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) { @@ -158,33 +230,66 @@ export default class Flecks { return expanded; } + /** + * Get the module for a fleck. + * + * @param {*} fleck + * + * @returns {*} + */ fleck(fleck) { return this.flecks[fleck]; } + /** + * Test whether a fleck implements a hook. + * + * @param {*} fleck + * @param {string} hook + * @returns {boolean} + */ fleckImplements(fleck, hook) { return !!this.hooks[hook].find(({fleck: candidate}) => fleck === candidate); } + /** + * Get a list of flecks implementing a hook. + * + * @param {string} hook + * @returns {string[]} + */ flecksImplementing(hook) { return this.hooks[hook]?.map(({fleck}) => fleck) || []; } + /** + * Gather and register class types. + * + * @param {string} hook + * @param {object} config + * @param {string} [config.idProperty='id'] The property used to get/set the class ID. + * @param {string} [config.typeProperty='type'] The property used to get/set the class type. + * @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( hook, { - idAttribute = 'id', - typeAttribute = 'type', + idProperty = 'id', + typeProperty = 'type', check = () => {}, } = {}, ) { if (!hook || 'string' !== typeof hook) { throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string'); } + // Gather classes and check. const raw = this.invokeMerge(hook); check(raw, hook); + // Decorate and check. const decorated = this.invokeComposed(`${hook}.decorate`, raw); check(decorated, `${hook}.decorate`); + // Assign unique IDs to each class and sort by type. let uid = 1; const ids = {}; const types = ( @@ -193,50 +298,78 @@ export default class Flecks { .sort(([ltype], [rtype]) => (ltype < rtype ? -1 : 1)) .map(([type, Class]) => { const id = uid++; - ids[id] = wrapperClass(Class, id, idAttribute, type, typeAttribute); + ids[id] = wrapGathered(Class, id, idProperty, type, typeProperty); return [type, ids[id]]; }), ) ); + // Conglomerate all ID and type keys along with Symbols for accessing either/or. const gathered = { ...ids, ...types, [ById]: ids, [ByType]: types, }; - hotGathered.set(hook, {idAttribute, gathered, typeAttribute}); + // Register for HMR? + if (module.hot) { + hotGathered.set(hook, {idProperty, gathered, typeProperty}); + } debug("gathered '%s': %O", hook, Object.keys(gathered[ByType])); return gathered; } + /** + * Get a configuration value. + * + * @param {string} path The configuration path e.g. `@flecks/example.config`. + * @param {*} defaultValue The default value if no configuration value is found. + * @returns {*} + */ get(path, defaultValue) { return get(this.config, path, defaultValue); } + /** + * Return an object whose keys are fleck paths and values are the result of invoking the hook. + * @param {string} hook + * @param {...any} args Arguments passed to each implementation. + * @returns {*} + */ invoke(hook, ...args) { if (!this.hooks[hook]) { return {}; } return this.flecksImplementing(hook) - .reduce((r, fleck) => ({ - ...r, - [fleck]: this.invokeFleck(hook, fleck, ...args), - }), {}); + .reduce((r, fleck) => ({...r, [fleck]: this.invokeFleck(hook, fleck, ...args)}), {}); } - invokeComposed(hook, arg, ...args) { + /** + * See: [function composition](https://www.educative.io/edpresso/function-composition-in-javascript). + * + * @configurable + * @param {string} hook + * @param {*} initial The initial value passed to the composition chain. + * @param {...any} args The arguments passed after the accumulator to each implementation. + * @returns {*} The final composed value. + */ + invokeComposed(hook, initial, ...args) { if (!this.hooks[hook]) { - return arg; + return initial; } const flecks = this.expandedFlecks(hook); if (0 === flecks.length) { - return arg; + return initial; } return flecks .filter((fleck) => this.fleckImplements(fleck, hook)) - .reduce((r, fleck) => this.invokeFleck(hook, fleck, r, ...args), arg); + .reduce((r, fleck) => this.invokeFleck(hook, fleck, r, ...args), initial); } + /** + * An async version of `invokeComposed`. + * + * @see {@link Flecks#invokeComposed} + */ async invokeComposedAsync(hook, arg, ...args) { if (!this.hooks[hook]) { return arg; @@ -250,6 +383,13 @@ export default class Flecks { .reduce(async (r, fleck) => this.invokeFleck(hook, fleck, await r, ...args), arg); } + /** + * Invokes a hook and returns a flat array of results. + * + * @param {string} hook + * @param {...any} args The arguments passed to each implementation. + * @returns {any[]} + */ invokeFlat(hook, ...args) { if (!this.hooks[hook]) { return []; @@ -257,6 +397,14 @@ export default class Flecks { return this.hooks[hook].map(({fleck}) => this.invokeFleck(hook, fleck, ...args)); } + /** + * Invokes a hook on a single fleck. + * + * @param {string} hook + * @param {*} fleck + * @param {...any} args + * @returns {*} + */ invokeFleck(hook, fleck, ...args) { debugSilly('invokeFleck(%s, %s, ...)', hook, fleck); if (!this.hooks[hook]) { @@ -270,33 +418,116 @@ export default class Flecks { return candidate.fn(...(args.concat(this))); } + static $$invokeMerge(r, o) { + return {...r, ...o}; + } + + /** + * Specialization of `invokeReduce`. Invokes a hook and reduces an object from all the resulting + * objects. + * + * @param {string} hook + * @param {...any} args + * @returns {object} + */ invokeMerge(hook, ...args) { - return this.invokeReduce(hook, (r, o) => ({...r, ...o}), {}, ...args); + return this.invokeReduce(hook, this.constructor.$$invokeMerge, {}, ...args); } + /** + * An async version of `invokeMerge`. + * + * @see {@link Flecks#invokeMerge} + */ async invokeMergeAsync(hook, ...args) { - return this.invokeReduceAsync(hook, (r, o) => ({...r, ...o}), {}, ...args); + return this.invokeReduceAsync(hook, this.constructor.$$invokeMerge, {}, ...args); } + static $$invokeMergeUnique() { + const track = {}; + return (r, o, fleck, hook) => { + const keys = Object.keys(o); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (track[key]) { + throw new ReferenceError( + `Conflict in ${hook}: '${track[key]}' implemented '${key}', followed by '${fleck}'`, + ); + } + track[key] = fleck; + } + return ({...r, ...o}); + }; + } + + /** + * Specialization of `invokeMerge`. Invokes a hook and reduces an object from all the resulting + * objects. + * + * @param {string} hook + * @param {...any} args + * @returns {object} + */ + invokeMergeUnique(hook, ...args) { + return this.invokeReduce(hook, this.constructor.$$invokeMergeUnique(), {}, ...args); + } + + /** + * An async version of `invokeMergeUnique`. + * + * @see {@link Flecks#invokeMergeUnique} + */ + async invokeMergeUniqueAsync(hook, ...args) { + return this.invokeReduceAsync(hook, this.constructor.$$invokeMergeUnique(), {}, ...args); + } + + /** + * See: [Array.prototype.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) + * + * @param {string} hook + * @param {*} reducer + * @param {*} initial + * @param {...any} args The arguments passed after the accumulator to each implementation. + * @returns {*} + */ invokeReduce(hook, reducer, initial, ...args) { if (!this.hooks[hook]) { return initial; } return this.hooks[hook] - .reduce((r, {fleck}) => reducer(r, this.invokeFleck(hook, fleck, ...args)), initial); + .reduce( + (r, {fleck}) => reducer(r, this.invokeFleck(hook, fleck, ...args), fleck, hook), + initial, + ); } + /** + * An async version of `invokeReduce`. + * + * @see {@link Flecks#invokeReduce} + */ async invokeReduceAsync(hook, reducer, initial, ...args) { if (!this.hooks[hook]) { return initial; } return this.hooks[hook] .reduce( - async (r, {fleck}) => reducer(await r, await this.invokeFleck(hook, fleck, ...args)), + async (r, {fleck}) => ( + reducer(await r, await this.invokeFleck(hook, fleck, ...args), fleck, hook) + ), initial, ); } + /** + * Invokes hooks on a fleck one after another. This is effectively a configurable version of + * {@link Flecks#invokeFlat}. + * + * @configurable + * @param {string} hook + * @param {...any} args The arguments passed to each implementation. + * @returns {any[]} + */ invokeSequential(hook, ...args) { if (!this.hooks[hook]) { return []; @@ -315,6 +546,11 @@ export default class Flecks { return results; } + /** + * An async version of `invokeSequential`. + * + * @see {@link Flecks#invokeSequential} + */ async invokeSequentialAsync(hook, ...args) { if (!this.hooks[hook]) { return []; @@ -334,10 +570,18 @@ export default class Flecks { return results; } - isOnPlatform(platform) { - return -1 !== this.platforms.indexOf(platform); - } - + /** + * Lookup flecks configured for a hook. + * + * If no configuration is found, defaults to ellipses. + * + * @param {string} hook + * @example + * # Given hook @flecks/example.hook, `flecks.yml` could be configured as such: + * '@flecks/example': + * hook: ['...'] + * @returns {string[]} + */ lookupFlecks(hook) { const index = hook.indexOf('.'); if (-1 === index) { @@ -346,31 +590,37 @@ export default class Flecks { return this.get([hook.slice(0, index), hook.slice(index + 1)], ['...']); } + /** + * Make a middleware function from configured middleware. + * @param {string} hook + * @returns {function} + */ makeMiddleware(hook) { debugSilly('makeMiddleware(...): %s', hook); if (!this.hooks[hook]) { - return Promise.resolve(); + return (...args) => args.pop()(); } const flecks = this.expandedFlecks(hook); if (0 === flecks.length) { - return Promise.resolve(); + return (...args) => args.pop()(); } const middleware = flecks .filter((fleck) => this.fleckImplements(fleck, hook)); debugSilly('middleware: %O', middleware); const instance = new Middleware(middleware.map((fleck) => this.invokeFleck(hook, fleck))); - return async (...args) => { - const next = args.pop(); - try { - await instance.promise(...args); - next(); - } - catch (error) { - next(error); - } - }; + return instance.dispatch.bind(instance); } + /** + * Provide classes for e.g. {@link Flecks#gather} + * + * @param {*} context @see {@link https://webpack.js.org/guides/dependency-management/#requirecontext} + * @param {object} config + * @param {function} [config.invoke = true] Invoke the default exports as a function? + * @param {function} [config.transformer = {@link camelCase}] + * Function to run on each context path. + * @returns {object} + */ static provide( context, { @@ -393,7 +643,7 @@ export default class Flecks { ); } return [ - transformer(this.symbolizePath(path)), + transformer(this.dasherizePath(path)), invoke ? M(flecks) : M, ]; }), @@ -401,9 +651,103 @@ export default class Flecks { ); } + /** + * Refresh a fleck's hooks, configuration, and any gathered classes. + * + * @example + * module.hot.accept('@flecks/example', async () => { + * flecks.refresh('@flecks/example', require('@flecks/example')); + * }); + * @param {string} fleck + * @param {object} M The fleck module + * @protected + */ refresh(fleck, M) { debug('refreshing %s...', fleck); // Remove old hook implementations. + this.unregisterFleckHooks(fleck); + // Replace the fleck. + this.registerFleckHooks(fleck, M); + // Write config. + this.configureFleckDefaults(fleck); + // HMR. + if (module.hot) { + this.refreshGathered(fleck); + } + } + + /** + * Refresh gathered classes for a fleck. + * + * @param {string} fleck + */ + refreshGathered(fleck) { + const it = hotGathered.entries(); + for (let current = it.next(); current.done !== true; current = it.next()) { + const { + value: [ + hook, + { + idProperty, + gathered, + typeProperty, + }, + ], + } = current; + const updates = this.invokeFleck(hook, fleck); + if (updates) { + debug('updating gathered %s from %s...', hook, fleck); + const entries = Object.entries(updates); + for (let i = 0, [type, Class] = entries[i]; i < entries.length; ++i) { + const {[type]: {[idProperty]: id}} = gathered; + const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty); + // eslint-disable-next-line no-multi-assign + gathered[type] = gathered[id] = gathered[ById][id] = gathered[ByType][type] = Subclass; + this.invoke('@flecks/core.hmr.gathered', Subclass, hook); + } + } + } + } + + /** + * Register hooks for a fleck. + * + * @param {string} fleck + * @param {object} M The fleck module + * @protected + */ + registerFleckHooks(fleck, M) { + debugSilly('registering %s...', fleck); + this.flecks[fleck] = M; + if (M.hooks) { + const keys = Object.keys(M.hooks); + debugSilly("hooks for '%s': %O", fleck, keys); + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + if (!this.hooks[key]) { + this.hooks[key] = []; + } + this.hooks[key].push({fleck, fn: M.hooks[key]}); + } + } + } + + /** + * Set a configuration value. + * + * @param {string} path The configuration path e.g. `@flecks/example.config`. + * @param {*} value The value to set. + * @returns {*} The value that was set. + */ + set(path, value) { + return set(this.config, path, value); + } + + /** + * Unregister hooks for a fleck. + * @param {*} fleck + */ + unregisterFleckHooks(fleck) { const keys = Object.keys(this.hooks); for (let j = 0; j < keys.length; j++) { const key = keys[j]; @@ -414,82 +758,6 @@ export default class Flecks { } } } - // Replace the fleck. - this.registerFleck(fleck, M); - // Write config. - this.configureFleck(fleck); - // HMR. - this.updateHotGathered(fleck); - } - - registerFleck(fleck, M) { - debugSilly('registering %s...', fleck); - this.flecks[fleck] = M; - if (M.default) { - const {default: {[Hooks]: hooks}} = M; - if (hooks) { - const keys = Object.keys(hooks); - debugSilly("hooks for '%s': %O", fleck, keys); - for (let j = 0; j < keys.length; j++) { - const key = keys[j]; - if (!this.hooks[key]) { - this.hooks[key] = []; - } - this.hooks[key].push({fleck, fn: hooks[key]}); - } - } - } - else { - debugSilly("'%s' has no default export", fleck); - } - } - - set(path, value) { - return set(this.config, path, value); - } - - static symbolizePath(path) { - const parts = dirname(path).split('/'); - if ('.' === parts[0]) { - parts.shift(); - } - if ('index' === parts[parts.length - 1]) { - parts.pop(); - } - return join(parts.join('-'), basename(path, extname(path))); - } - - async up(hook) { - await Promise.all(this.invokeFlat('@flecks/core.starting')); - await this.invokeSequentialAsync(hook); - } - - updateHotGathered(fleck) { - const it = hotGathered.entries(); - for (let current = it.next(); current.done !== true; current = it.next()) { - const { - value: [ - hook, - { - idAttribute, - gathered, - typeAttribute, - }, - ], - } = current; - const updates = this.invokeFleck(hook, fleck); - if (updates) { - debug('updating gathered %s from %s...', hook, fleck); - const entries = Object.entries(updates); - for (let i = 0, [type, Class] = entries[i]; i < entries.length; ++i) { - const {[type]: {[idAttribute]: id}} = gathered; - const Subclass = wrapperClass(Class, id, idAttribute, type, typeAttribute); - // eslint-disable-next-line no-multi-assign - gathered[type] = gathered[id] = gathered[ById][id] = gathered[ByType][type] = Subclass; - this.invoke('@flecks/core.hmr.gathered', Subclass, hook); - } - } - } } } diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 73aeee9..e1d8f51 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,24 +1,18 @@ -import {Hooks} from './flecks'; - export {default as Class} from './class'; export {default as compose} from './compose'; export {default as D} from './debug'; -export {default as ensureUniqueReduction} from './ensure-unique-reduction'; export {default as EventEmitter} from './event-emitter'; export { default as Flecks, ById, ByType, - Hooks, } from './flecks'; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * The ID of your application. - */ - id: 'flecks', - }), - }, +export const hooks = { + '@flecks/core.config': () => ({ + /** + * The ID of your application. + */ + id: 'flecks', + }), }; diff --git a/packages/core/src/server/build/.eslint.defaults.js b/packages/core/src/server/build/.eslint.defaults.js index 8cb1d1a..d6b5166 100644 --- a/packages/core/src/server/build/.eslint.defaults.js +++ b/packages/core/src/server/build/.eslint.defaults.js @@ -34,6 +34,7 @@ module.exports = { rules: { 'babel/object-curly-spacing': 'off', 'brace-style': ['error', 'stroustrup'], + 'import/prefer-default-export': 'off', 'jsx-a11y/control-has-associated-label': ['error', {assert: 'either'}], 'jsx-a11y/label-has-associated-control': ['error', {assert: 'either'}], 'no-plusplus': 'off', diff --git a/packages/core/src/server/build/eslintrc.js b/packages/core/src/server/build/eslintrc.js index d1d2a15..3a73bdc 100644 --- a/packages/core/src/server/build/eslintrc.js +++ b/packages/core/src/server/build/eslintrc.js @@ -22,6 +22,7 @@ const { FLECKS_CORE_SYNC_FOR_ESLINT = false, } = process.env; +// This is kinda nuts, but ESLint doesn't support its configuration files returning a promise! if (FLECKS_CORE_SYNC_FOR_ESLINT) { (async () => { debug('bootstrapping flecks...'); @@ -50,6 +51,7 @@ else { module.exports = JSON.parse(readFileSync(join(cacheDirectory, 'eslintrc.json')).toString()); } catch (error) { + // Just silly. By synchronously spawning... ourselves, the spawned copy can use async. const {stderr, stdout} = spawnSync('node', [__filename], { env: { FLECKS_CORE_SYNC_FOR_ESLINT: true, diff --git a/packages/core/src/server/index.js b/packages/core/src/server/index.js index c7fed24..c355366 100644 --- a/packages/core/src/server/index.js +++ b/packages/core/src/server/index.js @@ -4,7 +4,6 @@ import {inspect} from 'util'; import airbnb from '@neutrinojs/airbnb'; import neutrino from 'neutrino'; -import {Hooks} from '../flecks'; import commands from './commands'; import R from '../bootstrap/require'; @@ -31,81 +30,79 @@ export {default as fleck} from '../bootstrap/fleck'; export {default as require} from '../bootstrap/require'; export {JsonStream, transform} from './stream'; -export default { - [Hooks]: { - '@flecks/core.build': (target, config, flecks) => { - const { - 'eslint.exclude': exclude, - profile, - } = flecks.get('@flecks/core/server'); - if (-1 !== profile.indexOf(target)) { - config.use.push(({config}) => { - config - .plugin('profiler') - .use( - R.resolve('webpack/lib/debug/ProfilingPlugin'), - [{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}], - ); - }); - } - if (-1 === exclude.indexOf(target)) { - const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target)); - const webpackConfig = neutrino(config).webpack(); - config.use.unshift( - airbnb({ - eslint: { - baseConfig: { - ...baseConfig, - settings: { - ...(baseConfig.settings || {}), - 'import/resolver': { - ...(baseConfig.settings['import/resolver'] || {}), - webpack: { - config: { - resolve: webpackConfig.resolve, - }, +export const hooks = { + '@flecks/core.build': (target, config, flecks) => { + const { + 'eslint.exclude': exclude, + profile, + } = flecks.get('@flecks/core/server'); + if (-1 !== profile.indexOf(target)) { + config.use.push(({config}) => { + config + .plugin('profiler') + .use( + R.resolve('webpack/lib/debug/ProfilingPlugin'), + [{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}], + ); + }); + } + if (-1 === exclude.indexOf(target)) { + const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target)); + const webpackConfig = neutrino(config).webpack(); + config.use.unshift( + airbnb({ + eslint: { + baseConfig: { + ...baseConfig, + settings: { + ...(baseConfig.settings || {}), + 'import/resolver': { + ...(baseConfig.settings['import/resolver'] || {}), + webpack: { + config: { + resolve: webpackConfig.resolve, }, }, }, }, }, - }), - ); - } - }, - '@flecks/core.build.config': () => [ - /** - * Babel configuration. See: https://babeljs.io/docs/en/config-files - */ - 'babel.config.js', - /** - * ESLint defaults. The default .eslintrc.js just reads from this file so that the build - * process can dynamically configure parts of ESLint. - */ - ['.eslint.defaults.js', {specifier: (specific) => `${specific}.eslint.defaults.js`}], - /** - * ESLint configuration. See: https://eslint.org/docs/user-guide/configuring/ - */ - ['.eslintrc.js', {specifier: (specific) => `${specific}.eslintrc.js`}], - /** - * Neutrino build configuration. See: https://neutrinojs.org/usage/ - */ - ['.neutrinorc.js', {specifier: (specific) => `${specific}.neutrinorc.js`}], - /** - * Webpack (v4) configuration. See: https://v4.webpack.js.org/configuration/ - */ - 'webpack.config.js', - ], - '@flecks/core.commands': commands, - '@flecks/core.config': () => ({ - /** - * Build targets to exclude from ESLint. - */ - 'eslint.exclude': [], - /** - * Build targets to profile with `webpack.debug.ProfilingPlugin`. - */ - profile: [], - }), + }, + }), + ); + } }, + '@flecks/core.build.config': () => [ + /** + * Babel configuration. See: https://babeljs.io/docs/en/config-files + */ + 'babel.config.js', + /** + * ESLint defaults. The default .eslintrc.js just reads from this file so that the build + * process can dynamically configure parts of ESLint. + */ + ['.eslint.defaults.js', {specifier: (specific) => `${specific}.eslint.defaults.js`}], + /** + * ESLint configuration. See: https://eslint.org/docs/user-guide/configuring/ + */ + ['.eslintrc.js', {specifier: (specific) => `${specific}.eslintrc.js`}], + /** + * Neutrino build configuration. See: https://neutrinojs.org/usage/ + */ + ['.neutrinorc.js', {specifier: (specific) => `${specific}.neutrinorc.js`}], + /** + * Webpack (v4) configuration. See: https://v4.webpack.js.org/configuration/ + */ + 'webpack.config.js', + ], + '@flecks/core.commands': commands, + '@flecks/core.config': () => ({ + /** + * Build targets to exclude from ESLint. + */ + 'eslint.exclude': [], + /** + * Build targets to profile with `webpack.debug.ProfilingPlugin`. + */ + profile: [], + }), }; diff --git a/packages/core/test/invoke.js b/packages/core/test/invoke.js index 708a62f..a830487 100644 --- a/packages/core/test/invoke.js +++ b/packages/core/test/invoke.js @@ -1,7 +1,12 @@ -import {expect} from 'chai'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; import {Flecks} from '@flecks/core'; +chai.use(chaiAsPromised); + +const {expect} = chai; + const testOne = require('./one'); const testTwo = require('./two'); @@ -33,3 +38,13 @@ it('can invoke merge async', async () => { expect(await flecks.invokeMergeAsync('@flecks/core/test/invoke-merge-async')) .to.deep.equal({foo: 69, bar: 420}); }); + +it('can enforce uniqueness', () => { + expect(() => flecks.invokeMergeUnique('@flecks/core/test/invoke-merge-unique')) + .to.throw(ReferenceError); +}); + +it('can enforce uniqueness async', async () => { + expect(flecks.invokeMergeUniqueAsync('@flecks/core/test/invoke-merge-unique-async')) + .to.be.rejectedWith(ReferenceError); +}); diff --git a/packages/core/test/middleware.js b/packages/core/test/middleware.js new file mode 100644 index 0000000..bbeb25e --- /dev/null +++ b/packages/core/test/middleware.js @@ -0,0 +1,51 @@ +import {expect} from 'chai'; + +import {Flecks} from '@flecks/core'; + +const testOne = require('./one'); +const testTwo = require('./two'); + +it('can make middleware', (done) => { + let flecks; + let foo; + let mw; + flecks = new Flecks({ + config: { + '@flecks/core/test': { + middleware: [ + '@flecks/core/one', + '@flecks/core/two', + ], + }, + }, + flecks: { + '@flecks/core/one': testOne, + '@flecks/core/two': testTwo, + }, + }); + foo = {bar: 1}; + mw = flecks.makeMiddleware('@flecks/core/test.middleware'); + mw(foo, () => { + expect(foo.bar).to.equal(4); + flecks = new Flecks({ + config: { + '@flecks/core/test': { + middleware: [ + '@flecks/core/two', + '@flecks/core/one', + ], + }, + }, + flecks: { + '@flecks/core/one': testOne, + '@flecks/core/two': testTwo, + }, + }); + foo = {bar: 1}; + mw = flecks.makeMiddleware('@flecks/core/test.middleware'); + mw(foo, () => { + expect(foo.bar).to.equal(3); + done(); + }); + }); +}); diff --git a/packages/core/test/one/index.js b/packages/core/test/one/index.js index c8c3840..329ea1b 100644 --- a/packages/core/test/one/index.js +++ b/packages/core/test/one/index.js @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; export const testNodespace = () => [ /* eslint-disable no-eval */ @@ -8,23 +8,28 @@ export const testNodespace = () => [ /* eslint-enable no-eval */ ]; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - foo: 'bar', - }), - '@flecks/core/one/test-gather': ( - Flecks.provide(require.context('./things', false, /\.js$/)) - ), - '@flecks/core/one/test-gather.decorate': ( - Flecks.decorate(require.context('./things/decorators', false, /\.js$/)) - ), - '@flecks/core/test/invoke': () => 69, - '@flecks/core/test/invoke-parallel': (O) => { - // eslint-disable-next-line no-param-reassign - O.foo *= 2; - }, - '@flecks/core/test/invoke-merge': () => ({foo: 69}), - '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({foo: 69})), +export const hooks = { + '@flecks/core.config': () => ({ + foo: 'bar', + }), + '@flecks/core/one/test-gather': ( + Flecks.provide(require.context('./things', false, /\.js$/)) + ), + '@flecks/core/one/test-gather.decorate': ( + Flecks.decorate(require.context('./things/decorators', false, /\.js$/)) + ), + '@flecks/core/test/invoke': () => 69, + '@flecks/core/test/invoke-parallel': (O) => { + // eslint-disable-next-line no-param-reassign + O.foo *= 2; + }, + '@flecks/core/test/invoke-merge': () => ({foo: 69}), + '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({foo: 69})), + '@flecks/core/test/invoke-merge-unique': () => ({foo: 69}), + '@flecks/core/test/invoke-merge-unique-async': () => new Promise((resolve) => resolve({foo: 69})), + '@flecks/core/test.middleware': () => (foo, next) => { + // eslint-disable-next-line no-param-reassign + foo.bar += 1; + next(); }, }; diff --git a/packages/core/test/two/index.js b/packages/core/test/two/index.js index 86e323b..847dbe8 100644 --- a/packages/core/test/two/index.js +++ b/packages/core/test/two/index.js @@ -1,19 +1,24 @@ -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; -export default { - [Hooks]: { - '@flecks/core/one/test-gather': ( - Flecks.provide(require.context('./things', false, /\.js$/)) - ), - '@flecks/core/test/invoke': () => 420, - '@flecks/core/test/invoke-parallel': (O) => new Promise((resolve) => { - setTimeout(() => { - // eslint-disable-next-line no-param-reassign - O.foo += 2; - resolve(); - }, 0); - }), - '@flecks/core/test/invoke-merge': () => ({bar: 420}), - '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({bar: 420})), +export const hooks = { + '@flecks/core/one/test-gather': ( + Flecks.provide(require.context('./things', false, /\.js$/)) + ), + '@flecks/core/test/invoke': () => 420, + '@flecks/core/test/invoke-parallel': (O) => new Promise((resolve) => { + setTimeout(() => { + // eslint-disable-next-line no-param-reassign + O.foo += 2; + resolve(); + }, 0); + }), + '@flecks/core/test/invoke-merge': () => ({bar: 420}), + '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({bar: 420})), + '@flecks/core/test/invoke-merge-unique': () => ({foo: 69}), + '@flecks/core/test/invoke-merge-unique-async': () => new Promise((resolve) => resolve({foo: 69})), + '@flecks/core/test.middleware': () => (foo, next) => { + // eslint-disable-next-line no-param-reassign + foo.bar *= 2; + next(); }, }; diff --git a/packages/db/build/dox/hooks.js b/packages/db/build/dox/hooks.js index b395991..82b8696 100644 --- a/packages/db/build/dox/hooks.js +++ b/packages/db/build/dox/hooks.js @@ -1,28 +1,24 @@ -import {Hooks} from '@flecks/core'; +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 + */ + '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), -export default { - [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 - */ - '@flecks/db/server.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 - * - * @param {constructor} Model The model to decorate. - */ - '@flecks/db/server.models.decorate': ( - Flecks.decorate(require.context('./models/decorators', 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 + * + * @param {constructor} Model The model to decorate. + */ + '@flecks/db/server.models.decorate': ( + Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) + ), }; diff --git a/packages/db/src/server.js b/packages/db/src/server.js index 45eed1d..b26c2a3 100644 --- a/packages/db/src/server.js +++ b/packages/db/src/server.js @@ -1,5 +1,3 @@ -import {Hooks} from '@flecks/core'; - import {createDatabaseConnection} from './connection'; import containers from './containers'; @@ -9,49 +7,47 @@ export {default as Model} from './model'; export {createDatabaseConnection}; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * The database to connect to. - */ - database: ':memory:', - /** - * SQL dialect. - * - * See: https://sequelize.org/v5/manual/dialects.html - */ - dialect: 'sqlite', - /** - * Database server host. - */ - host: undefined, - /** - * Database server password. - */ - password: undefined, - /** - * Database server port. - */ - port: undefined, - /** - * Database server username. - */ - username: undefined, - }), - '@flecks/core.starting': (flecks) => { - flecks.set('$flecks/db.models', flecks.gather( - '@flecks/db/server.models', - {typeAttribute: 'name'}, - )); - }, - '@flecks/docker.containers': containers, - '@flecks/server.up': async (flecks) => { - flecks.set('$flecks/db/sequelize', await createDatabaseConnection(flecks)); - }, - '@flecks/repl.context': (flecks) => ({ - Models: flecks.get('$flecks/db.models'), - sequelize: flecks.get('$flecks/db/sequelize'), - }), +export const hooks = { + '@flecks/core.config': () => ({ + /** + * The database to connect to. + */ + database: ':memory:', + /** + * SQL dialect. + * + * See: https://sequelize.org/v5/manual/dialects.html + */ + dialect: 'sqlite', + /** + * Database server host. + */ + host: undefined, + /** + * Database server password. + */ + password: undefined, + /** + * Database server port. + */ + port: undefined, + /** + * Database server username. + */ + username: undefined, + }), + '@flecks/core.starting': (flecks) => { + flecks.set('$flecks/db.models', flecks.gather( + '@flecks/db/server.models', + {typeProperty: 'name'}, + )); }, + '@flecks/docker.containers': containers, + '@flecks/server.up': async (flecks) => { + flecks.set('$flecks/db/sequelize', await createDatabaseConnection(flecks)); + }, + '@flecks/repl.context': (flecks) => ({ + Models: flecks.get('$flecks/db.models'), + sequelize: flecks.get('$flecks/db/sequelize'), + }), }; diff --git a/packages/docker/build/dox/hooks.js b/packages/docker/build/dox/hooks.js index 1acbb32..ddb653b 100644 --- a/packages/docker/build/dox/hooks.js +++ b/packages/docker/build/dox/hooks.js @@ -1,27 +1,23 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define docker containers. - * - * Beware: the user running the server must have Docker privileges. - * See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user - */ - '@flecks/docker.containers': () => ({ - someContainer: { - // Environment variables. - environment: { - SOME_CONTAINER_VAR: 'hello', - }, - // The docker image. - image: 'some-image:latest', - // Some container path you'd like to persist. Flecks handles the host path. - mount: '/some/container/path', - // Expose ports. - ports: {3000: 3000}, +export const hooks = { + /** + * Define docker containers. + * + * Beware: the user running the server must have Docker privileges. + * See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user + */ + '@flecks/docker.containers': () => ({ + someContainer: { + // Environment variables. + environment: { + SOME_CONTAINER_VAR: 'hello', }, - }), - }, + // The docker image. + image: 'some-image:latest', + // Some container path you'd like to persist. Flecks handles the host path. + mount: '/some/container/path', + // Expose ports. + ports: {3000: 3000}, + }, + }), }; diff --git a/packages/docker/src/server.js b/packages/docker/src/server.js index 413df06..b77ac02 100644 --- a/packages/docker/src/server.js +++ b/packages/docker/src/server.js @@ -1,26 +1,22 @@ -import {Hooks} from '@flecks/core'; - import commands from './commands'; import startContainer from './start-container'; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Whether to run docker containers. - */ - enabled: true, - }), - '@flecks/core.commands': commands, - '@flecks/server.up': async (flecks) => { - if (!flecks.get('@flecks/docker/server.enabled')) { - return; - } - const containers = await flecks.invokeMergeAsync('@flecks/docker.containers'); - await Promise.all( - Object.entries(containers) - .map(([key, config]) => startContainer(flecks, key, config)), - ); - }, +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Whether to run docker containers. + */ + enabled: true, + }), + '@flecks/core.commands': commands, + '@flecks/server.up': async (flecks) => { + if (!flecks.get('@flecks/docker/server.enabled')) { + return; + } + const containers = await flecks.invokeMergeAsync('@flecks/docker.containers'); + await Promise.all( + Object.entries(containers) + .map(([key, config]) => startContainer(flecks, key, config)), + ); }, }; diff --git a/packages/dox/src/parser.js b/packages/dox/src/parser.js index ff70fc1..21d5f26 100644 --- a/packages/dox/src/parser.js +++ b/packages/dox/src/parser.js @@ -17,6 +17,7 @@ import { isObjectExpression, isStringLiteral, isThisExpression, + isVariableDeclaration, } from '@babel/types'; import {require as R} from '@flecks/core/server'; import {parse as parseComment} from 'comment-parser'; @@ -75,15 +76,14 @@ class ParserState { } const implementationVisitor = (fn) => ({ - ExportDefaultDeclaration(path) { + ExportNamedDeclaration(path) { const {declaration} = path.node; - if (isObjectExpression(declaration)) { - const {properties} = declaration; - properties.forEach((property) => { - const {key, value} = property; - if (isIdentifier(key) && key.name === 'Hooks') { - if (isObjectExpression(value)) { - const {properties} = value; + if (isVariableDeclaration(declaration)) { + const {declarations} = declaration; + declarations.forEach((declarator) => { + if ('hooks' === declarator.id.name) { + if (isObjectExpression(declarator.init)) { + const {properties} = declarator.init; properties.forEach((property) => { const {key} = property; if (isLiteral(key)) { diff --git a/packages/dox/src/server.js b/packages/dox/src/server.js index be9d656..ada37ce 100644 --- a/packages/dox/src/server.js +++ b/packages/dox/src/server.js @@ -1,17 +1,13 @@ -import {Hooks} from '@flecks/core'; - import commands from './commands'; -export default { - [Hooks]: { - '@flecks/core.commands': commands, - '@flecks/core.config': () => ({ - /** - * Rewrite the output filenames of source files. - * - * `filename.replace(new RegExp([key]), [value]);` - */ - filenameRewriters: {}, - }), - }, +export const hooks = { + '@flecks/core.commands': commands, + '@flecks/core.config': () => ({ + /** + * Rewrite the output filenames of source files. + * + * `filename.replace(new RegExp([key]), [value]);` + */ + filenameRewriters: {}, + }), }; diff --git a/packages/electron/build/dox/hooks.js b/packages/electron/build/dox/hooks.js index 2e716eb..5263dd2 100644 --- a/packages/electron/build/dox/hooks.js +++ b/packages/electron/build/dox/hooks.js @@ -1,24 +1,20 @@ -import {Hooks} from '@flecks/core'; +export const hooks = { + /** + * Invoked when electron is initializing. + * @param {Electron.App} app The electron app. See: https://www.electronjs.org/docs/latest/api/app + */ + '@flecks/electron/server.initialize': (app) => { + app.on('will-quit', () => { + // ... + }); + }, -export default { - [Hooks]: { - /** - * Invoked when electron is initializing. - * @param {Electron.App} app The electron app. See: https://www.electronjs.org/docs/latest/api/app - */ - '@flecks/electron/server.initialize': (app) => { - app.on('will-quit', () => { - // ... - }); - }, - - /** - * Invoked when a window is created - * @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window - */ - '@flecks/electron/server.window': (win) => { - win.maximize(); - }, + /** + * Invoked when a window is created + * @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window + */ + '@flecks/electron/server.window': (win) => { + win.maximize(); }, }; diff --git a/packages/electron/src/server/index.js b/packages/electron/src/server/index.js index 99714cc..a34305f 100644 --- a/packages/electron/src/server/index.js +++ b/packages/electron/src/server/index.js @@ -1,7 +1,6 @@ import cluster from 'cluster'; import {join} from 'path'; -import {Hooks} from '@flecks/core'; import {require as R} from '@flecks/core/server'; import { app, @@ -21,119 +20,117 @@ async function createWindow(flecks) { await flecks.invokeSequentialAsync('@flecks/electron/server.window', win); } -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Browser window options. - * - * See: https://www.electronjs.org/docs/latest/api/browser-window - */ - browserWindowOptions: {}, - /** - * Install devtools extensions (by default). - * - * If `true`, will install some devtools extensions based on which flecks are enabled. - * - * You can pass an array of Chrome store IDs to install a list of custom extensions. - * - * Extensions will not be installed if `'production' === process.env.NODE_ENV` - */ - installExtensions: true, - /** - * Quit the app when all windows are closed. - */ - quitOnClosed: true, - /** - * The URL to load in electron by default. - * - * Defaults to `http://${flecks.get('@flecks/web/server.public')}`. - */ - url: undefined, - }), - '@flecks/core.webpack': (target, config) => { - const StartServerWebpackPlugin = R('start-server-webpack-plugin'); - const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin); - // Extremely hackish, c'est la vie. - if (plugin) { - /* eslint-disable no-underscore-dangle */ - plugin._startServer = function _startServerHacked(callback) { - const execArgv = this._getArgs(); - const inspectPort = this._getInspectPort(execArgv); - const clusterOptions = { - args: [this._entryPoint], - exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'), - execArgv, - }; - if (inspectPort) { - clusterOptions.inspectPort = inspectPort; - } - cluster.setupMaster(clusterOptions); - cluster.on('online', (worker) => { - callback(worker); - }); - cluster.fork(); +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Browser window options. + * + * See: https://www.electronjs.org/docs/latest/api/browser-window + */ + browserWindowOptions: {}, + /** + * Install devtools extensions (by default). + * + * If `true`, will install some devtools extensions based on which flecks are enabled. + * + * You can pass an array of Chrome store IDs to install a list of custom extensions. + * + * Extensions will not be installed if `'production' === process.env.NODE_ENV` + */ + installExtensions: true, + /** + * Quit the app when all windows are closed. + */ + quitOnClosed: true, + /** + * The URL to load in electron by default. + * + * Defaults to `http://${flecks.get('@flecks/web/server.public')}`. + */ + url: undefined, + }), + '@flecks/core.webpack': (target, config) => { + const StartServerWebpackPlugin = R('start-server-webpack-plugin'); + const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin); + // Extremely hackish, c'est la vie. + if (plugin) { + /* eslint-disable no-underscore-dangle */ + plugin._startServer = function _startServerHacked(callback) { + const execArgv = this._getArgs(); + const inspectPort = this._getInspectPort(execArgv); + const clusterOptions = { + args: [this._entryPoint], + exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'), + execArgv, }; - /* eslint-enable no-underscore-dangle */ - } - }, - '@flecks/electron/server.initialize': async (app, flecks) => { - app.on('window-all-closed', () => { - const {quitOnClosed} = flecks.get('@flecks/electron/server'); - if (!quitOnClosed) { - return; + if (inspectPort) { + clusterOptions.inspectPort = inspectPort; } - // Apple has to be *special*. - if (process.platform === 'darwin') { - return; - } - app.quit(); - }); - app.on('activate', async () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); - await app.whenReady(); - await createWindow(flecks); - }, - '@flecks/electron/server.window': async (win, flecks) => { - const {public: $$public} = flecks.get('@flecks/web/server'); - const { - installExtensions, - url = `http://${$$public}`, - } = flecks.get('@flecks/electron/server'); - if (installExtensions && 'production' !== NODE_ENV) { - const { - default: installExtension, - REDUX_DEVTOOLS, - REACT_DEVELOPER_TOOLS, - } = __non_webpack_require__('electron-devtools-installer'); - let extensions = installExtensions; - if (!Array.isArray(extensions)) { - extensions = []; - if (flecks.fleck('@flecks/react')) { - extensions.push(REACT_DEVELOPER_TOOLS); - } - if (flecks.fleck('@flecks/redux')) { - extensions.push(REDUX_DEVTOOLS); - } - } - await installExtension(extensions); - } - await win.loadURL(url); - }, - '@flecks/repl.context': (flecks) => ({ - electron: { - createWindow: () => createWindow(flecks), - }, - }), - '@flecks/server.up': async (flecks) => { - // `app` will be undefined if we aren't running in an electron environment. Just bail. - if (!app) { + cluster.setupMaster(clusterOptions); + cluster.on('online', (worker) => { + callback(worker); + }); + cluster.fork(); + }; + /* eslint-enable no-underscore-dangle */ + } + }, + '@flecks/electron/server.initialize': async (app, flecks) => { + app.on('window-all-closed', () => { + const {quitOnClosed} = flecks.get('@flecks/electron/server'); + if (!quitOnClosed) { return; } - await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', app); + // Apple has to be *special*. + if (process.platform === 'darwin') { + return; + } + app.quit(); + }); + app.on('activate', async () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); + await app.whenReady(); + await createWindow(flecks); + }, + '@flecks/electron/server.window': async (win, flecks) => { + const {public: $$public} = flecks.get('@flecks/web/server'); + const { + installExtensions, + url = `http://${$$public}`, + } = flecks.get('@flecks/electron/server'); + if (installExtensions && 'production' !== NODE_ENV) { + const { + default: installExtension, + REDUX_DEVTOOLS, + REACT_DEVELOPER_TOOLS, + } = __non_webpack_require__('electron-devtools-installer'); + let extensions = installExtensions; + if (!Array.isArray(extensions)) { + extensions = []; + if (flecks.fleck('@flecks/react')) { + extensions.push(REACT_DEVELOPER_TOOLS); + } + if (flecks.fleck('@flecks/redux')) { + extensions.push(REDUX_DEVTOOLS); + } + } + await installExtension(extensions); + } + await win.loadURL(url); + }, + '@flecks/repl.context': (flecks) => ({ + electron: { + createWindow: () => createWindow(flecks), }, + }), + '@flecks/server.up': async (flecks) => { + // `app` will be undefined if we aren't running in an electron environment. Just bail. + if (!app) { + return; + } + await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', app); }, }; diff --git a/packages/fleck/src/server/index.js b/packages/fleck/src/server/index.js index b14ad94..c88ff94 100644 --- a/packages/fleck/src/server/index.js +++ b/packages/fleck/src/server/index.js @@ -1,21 +1,17 @@ -import {Hooks} from '@flecks/core'; - import commands from './commands'; -export default { - [Hooks]: { - '@flecks/core.commands': commands, - '@flecks/core.config': () => ({ - /** - * Webpack stats configuration when building fleck target. - */ - stats: { - children: false, - chunks: false, - colors: true, - modules: false, - }, - }), - '@flecks/core.targets': () => ['fleck'], - }, +export const hooks = { + '@flecks/core.commands': commands, + '@flecks/core.config': () => ({ + /** + * Webpack stats configuration when building fleck target. + */ + stats: { + children: false, + chunks: false, + colors: true, + modules: false, + }, + }), + '@flecks/core.targets': () => ['fleck'], }; diff --git a/packages/governor/src/server.js b/packages/governor/src/server.js index fad2f82..042d7a7 100644 --- a/packages/governor/src/server.js +++ b/packages/governor/src/server.js @@ -1,135 +1,133 @@ -import {ByType, Flecks, Hooks} from '@flecks/core'; +import {ByType, Flecks} from '@flecks/core'; import LimitedPacket from './limited-packet'; import createLimiter from './limiter'; export {default as createLimiter} from './limiter'; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * All keys used to determine fingerprint. - */ +export const hooks = { + '@flecks/core.config': () => ({ + /** + * All keys used to determine fingerprint. + */ + keys: ['ip'], + web: { keys: ['ip'], - web: { - keys: ['ip'], - points: 60, - duration: 30, - ttl: 30, - }, - socket: { - keys: ['ip'], - points: 60, - duration: 30, - ttl: 30, - }, - }), - '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), - '@flecks/web/server.request.route': (flecks) => { - const {web} = flecks.get('@flecks/governor/server'); - const limiter = flecks.get('$flecks/governor.web.limiter'); - return async (req, res, next) => { - const {Ban} = flecks.get('$flecks/db.models'); - try { - await Ban.check(req); - } - catch (error) { - res.status(403).send(`

${error.message}
`); - return; - } - req.ban = async (keys, ttl = 0) => { - const ban = Ban.fromRequest(req, keys, ttl); - await Ban.create({...ban}); - res.status(403).send(`
${Ban.format([ban])}
`); - }; - try { - await limiter.consume(req.ip); - next(); - } - catch (error) { - const {ttl, keys} = web; - const ban = Ban.fromRequest(req, keys, ttl); - await Ban.create({...ban}); - res.status(429).send(`
${Ban.format([ban])}
`); - } - }; + points: 60, + duration: 30, + ttl: 30, }, - '@flecks/server.up': async (flecks) => { - if (flecks.fleck('@flecks/web/server')) { - const {web} = flecks.get('@flecks/governor/server'); - const limiter = await createLimiter( - flecks, - { - keyPrefix: '@flecks/governor.web.request.route', - ...web, - }, - ); - flecks.set('$flecks/governor.web.limiter', limiter); + socket: { + keys: ['ip'], + points: 60, + duration: 30, + ttl: 30, + }, + }), + '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), + '@flecks/web/server.request.route': (flecks) => { + const {web} = flecks.get('@flecks/governor/server'); + const limiter = flecks.get('$flecks/governor.web.limiter'); + return async (req, res, next) => { + const {Ban} = flecks.get('$flecks/db.models'); + try { + await Ban.check(req); } - if (flecks.fleck('@flecks/socket/server')) { - const {[ByType]: Packets} = flecks.get('$flecks/socket.packets'); - const limiters = Object.fromEntries( - await Promise.all( - Object.entries(Packets) - .filter(([, Packet]) => Packet.limit) - .map(async ([name, Packet]) => ( - [ - name, - await createLimiter( - flecks, - {keyPrefix: `@flecks/governor.packet.${name}`, ...Packet.limit}, - ), - ] - )), - ), - ); - flecks.set('$flecks/governor.packet.limiters', limiters); - const {socket} = flecks.get('@flecks/governor/server'); - const limiter = await createLimiter( - flecks, - { - keyPrefix: '@flecks/governor.socket.request.socket', - ...socket, - }, - ); - flecks.set('$flecks/governor.socket.limiter', limiter); + catch (error) { + res.status(403).send(`
${error.message}
`); + return; } - }, - '@flecks/socket/server.request.socket': (flecks) => { - const limiter = flecks.get('$flecks/governor.socket.limiter'); - return async (socket, next) => { - const {handshake: req} = socket; - const {Ban} = flecks.get('$flecks/db.models'); - try { - await Ban.check(req); - } - catch (error) { - next(error); - return; - } - req.ban = async (keys, ttl) => { - await Ban.create(Ban.fromRequest(req, keys, ttl)); - socket.disconnect(); - }; - try { - await limiter.consume(req.ip); - next(); - } - catch (error) { - const {ttl, keys} = socket; - await Ban.create(Ban.fromRequest(req, keys, ttl)); - next(error); - } + req.ban = async (keys, ttl = 0) => { + const ban = Ban.fromRequest(req, keys, ttl); + await Ban.create({...ban}); + res.status(403).send(`
${Ban.format([ban])}
`); }; - }, - '@flecks/socket.packets.decorate': (Packets, flecks) => ( - Object.fromEntries( - Object.entries(Packets).map(([keyPrefix, Packet]) => [ - keyPrefix, - !Packet.limit ? Packet : LimitedPacket(flecks, [keyPrefix, Packet]), - ]), - ) - ), + try { + await limiter.consume(req.ip); + next(); + } + catch (error) { + const {ttl, keys} = web; + const ban = Ban.fromRequest(req, keys, ttl); + await Ban.create({...ban}); + res.status(429).send(`
${Ban.format([ban])}
`); + } + }; }, + '@flecks/server.up': async (flecks) => { + if (flecks.fleck('@flecks/web/server')) { + const {web} = flecks.get('@flecks/governor/server'); + const limiter = await createLimiter( + flecks, + { + keyPrefix: '@flecks/governor.web.request.route', + ...web, + }, + ); + flecks.set('$flecks/governor.web.limiter', limiter); + } + if (flecks.fleck('@flecks/socket/server')) { + const {[ByType]: Packets} = flecks.get('$flecks/socket.packets'); + const limiters = Object.fromEntries( + await Promise.all( + Object.entries(Packets) + .filter(([, Packet]) => Packet.limit) + .map(async ([name, Packet]) => ( + [ + name, + await createLimiter( + flecks, + {keyPrefix: `@flecks/governor.packet.${name}`, ...Packet.limit}, + ), + ] + )), + ), + ); + flecks.set('$flecks/governor.packet.limiters', limiters); + const {socket} = flecks.get('@flecks/governor/server'); + const limiter = await createLimiter( + flecks, + { + keyPrefix: '@flecks/governor.socket.request.socket', + ...socket, + }, + ); + flecks.set('$flecks/governor.socket.limiter', limiter); + } + }, + '@flecks/socket/server.request.socket': (flecks) => { + const limiter = flecks.get('$flecks/governor.socket.limiter'); + return async (socket, next) => { + const {handshake: req} = socket; + const {Ban} = flecks.get('$flecks/db.models'); + try { + await Ban.check(req); + } + catch (error) { + next(error); + return; + } + req.ban = async (keys, ttl) => { + await Ban.create(Ban.fromRequest(req, keys, ttl)); + socket.disconnect(); + }; + try { + await limiter.consume(req.ip); + next(); + } + catch (error) { + const {ttl, keys} = socket; + await Ban.create(Ban.fromRequest(req, keys, ttl)); + next(error); + } + }; + }, + '@flecks/socket.packets.decorate': (Packets, flecks) => ( + Object.fromEntries( + Object.entries(Packets).map(([keyPrefix, Packet]) => [ + keyPrefix, + !Packet.limit ? Packet : LimitedPacket(flecks, [keyPrefix, Packet]), + ]), + ) + ), }; diff --git a/packages/react/build/dox/hooks.js b/packages/react/build/dox/hooks.js index d3f8eb8..145bf8d 100644 --- a/packages/react/build/dox/hooks.js +++ b/packages/react/build/dox/hooks.js @@ -1,36 +1,32 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define React Providers. - * - * Note: `req` will be only be defined when server-side rendering. - * @param {http.ClientRequest} req The HTTP request object. - */ - '@flecks/react.providers': (req) => { - // Generally it makes more sense to separate client and server concerns using platform - // naming conventions, but this is just a small contrived example. - return req ? serverSideProvider(req) : clientSideProvider(); - }, - /** - * Define root-level React components that are mounted as siblings on `#main`. - * Note: `req` will be only be defined when server-side rendering. - * - * Return either a React component or an array whose elements must either be a React component - * 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. - */ - '@flecks/react.roots': (req) => { - // Note that we're not returning ``, but `Component`. - return [ - Component, - [SomeOtherComponent, {prop: 'value'}] - ]; - // You can also just: - return Component; - }, +export const hooks = { + /** + * Define React Providers. + * + * Note: `req` will be only be defined when server-side rendering. + * @param {http.ClientRequest} req The HTTP request object. + */ + '@flecks/react.providers': (req) => { + // Generally it makes more sense to separate client and server concerns using platform + // naming conventions, but this is just a small contrived example. + return req ? serverSideProvider(req) : clientSideProvider(); + }, + /** + * Define root-level React components that are mounted as siblings on `#main`. + * Note: `req` will be only be defined when server-side rendering. + * + * Return either a React component or an array whose elements must either be a React component + * 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. + */ + '@flecks/react.roots': (req) => { + // Note that we're not returning ``, but `Component`. + return [ + Component, + [SomeOtherComponent, {prop: 'value'}] + ]; + // You can also just: + return Component; }, }; diff --git a/packages/react/src/client.js b/packages/react/src/client.js index ac08dfd..8bb8850 100644 --- a/packages/react/src/client.js +++ b/packages/react/src/client.js @@ -1,4 +1,4 @@ -import {D, Hooks} from '@flecks/core'; +import {D} from '@flecks/core'; import {hydrate, render} from '@hot-loader/react-dom'; import React from 'react'; @@ -10,20 +10,18 @@ const debug = D('@flecks/react/client'); export {FlecksContext}; -export default { - [Hooks]: { - '@flecks/web/client.up': async (flecks) => { - const {ssr} = flecks.get('@flecks/react'); - debug('%sing...', ssr ? 'hydrat' : 'render'); - (ssr ? hydrate : render)( - React.createElement( - React.StrictMode, - {}, - [React.createElement(await root(flecks), {key: 'root'})], - ), - window.document.getElementById('root'), - ); - debug('rendered'); - }, +export const hooks = { + '@flecks/web/client.up': async (flecks) => { + const {ssr} = flecks.get('@flecks/react'); + debug('%sing...', ssr ? 'hydrat' : 'render'); + (ssr ? hydrate : render)( + React.createElement( + React.StrictMode, + {}, + [React.createElement(await root(flecks), {key: 'root'})], + ), + window.document.getElementById('root'), + ); + debug('rendered'); }, }; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 05352a8..97599c1 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -1,5 +1,3 @@ -import {Hooks} from '@flecks/core'; - export {default as ReactDom} from '@hot-loader/react-dom'; export {default as classnames} from 'classnames'; export {default as PropTypes} from 'prop-types'; @@ -14,13 +12,11 @@ export {default as useEvent} from './hooks/use-event'; export {default as useFlecks} from './hooks/use-flecks'; export {default as usePrevious} from './hooks/use-previous'; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Whether to enable server-side rendering. - */ - ssr: true, - }), - }, +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Whether to enable server-side rendering. + */ + ssr: true, + }), }; diff --git a/packages/react/src/router/client.js b/packages/react/src/router/client.js index 011a5a2..eca628e 100644 --- a/packages/react/src/router/client.js +++ b/packages/react/src/router/client.js @@ -1,15 +1,12 @@ -import {Hooks} from '@flecks/core'; // eslint-disable-next-line import/no-extraneous-dependencies import {createReduxHistory, history} from '@flecks/react/router/context'; import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom'; import {HistoryRouter as ReduxHistoryRouter} from 'redux-first-history/rr6'; -export default { - [Hooks]: { - '@flecks/react.providers': (req, flecks) => ( - flecks.fleck('@flecks/redux') - ? [ReduxHistoryRouter, {history: createReduxHistory(flecks.get('$flecks/redux.store'))}] - : [HistoryRouter, {history}] - ), - }, +export const hooks = { + '@flecks/react.providers': (req, flecks) => ( + flecks.fleck('@flecks/redux') + ? [ReduxHistoryRouter, {history: createReduxHistory(flecks.get('$flecks/redux.store'))}] + : [HistoryRouter, {history}] + ), }; diff --git a/packages/react/src/router/index.js b/packages/react/src/router/index.js index 591c801..a3981f2 100644 --- a/packages/react/src/router/index.js +++ b/packages/react/src/router/index.js @@ -1,17 +1,14 @@ -import {Hooks} from '@flecks/core'; // eslint-disable-next-line import/no-extraneous-dependencies import {routerMiddleware, routerReducer} from '@flecks/react/router/context'; export * from 'react-router-dom'; export * from 'redux-first-history'; -export default { - [Hooks]: { - '@flecks/redux.slices': () => ({ - router: routerReducer, - }), - '@flecks/redux.store': (options) => { - options.middleware.push(routerMiddleware); - }, +export const hooks = { + '@flecks/redux.slices': () => ({ + router: routerReducer, + }), + '@flecks/redux.store': (options) => { + options.middleware.push(routerMiddleware); }, }; diff --git a/packages/react/src/router/server.js b/packages/react/src/router/server.js index b393727..0486759 100644 --- a/packages/react/src/router/server.js +++ b/packages/react/src/router/server.js @@ -1,10 +1,7 @@ -import {Hooks} from '@flecks/core'; import {StaticRouter} from 'react-router-dom/server'; -export default { - [Hooks]: { - '@flecks/react.providers': (req, flecks) => ( - flecks.get('@flecks/react.ssr') ? [StaticRouter, {location: req.url}] : [] - ), - }, +export const hooks = { + '@flecks/react.providers': (req, flecks) => ( + flecks.get('@flecks/react.ssr') ? [StaticRouter, {location: req.url}] : [] + ), }; diff --git a/packages/react/src/server.js b/packages/react/src/server.js index 00537c2..2bd2d98 100644 --- a/packages/react/src/server.js +++ b/packages/react/src/server.js @@ -1,28 +1,25 @@ -import {Hooks} from '@flecks/core'; import {augmentBuild} from '@flecks/web/server'; import ssr from './ssr'; -export default { - [Hooks]: { - '@flecks/core.build': (target, config, flecks) => { - // Resolution. - config.use.push(({config}) => { - config.resolve.alias - .set('react-native', 'react-native-web'); - config.resolve.extensions - .prepend('.web.js') - .prepend('.web.jsx'); - }); - // Augment the build on behalf of a missing `@flecks/web`. - if (!flecks.fleck('@flecks/web/server')) { - flecks.registerBuildConfig('postcss.config.js', {fleck: '@flecks/web/server'}); - flecks.registerResolver('@flecks/web'); - augmentBuild(target, config, flecks); - } - }, - '@flecks/web/server.stream.html': (stream, req, flecks) => ( - flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream - ), +export const hooks = { + '@flecks/core.build': (target, config, flecks) => { + // Resolution. + config.use.push(({config}) => { + config.resolve.alias + .set('react-native', 'react-native-web'); + config.resolve.extensions + .prepend('.web.js') + .prepend('.web.jsx'); + }); + // Augment the build on behalf of a missing `@flecks/web`. + if (!flecks.fleck('@flecks/web/server')) { + flecks.registerBuildConfig('postcss.config.js', {fleck: '@flecks/web/server'}); + flecks.registerResolver('@flecks/web'); + augmentBuild(target, config, flecks); + } }, + '@flecks/web/server.stream.html': (stream, req, flecks) => ( + flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream + ), }; diff --git a/packages/redis/src/server.js b/packages/redis/src/server.js index 29ade25..316a961 100644 --- a/packages/redis/src/server.js +++ b/packages/redis/src/server.js @@ -1,5 +1,3 @@ -import {Hooks} from '@flecks/core'; - import containers from './containers'; import createClient from './create-client'; @@ -27,21 +25,19 @@ const safeKeys = async (client, pattern, caret) => { export const keys = (client, pattern) => safeKeys(client, pattern, 0); -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Redis server host. - */ - host: 'localhost', - /** - * Redis server port. - */ - port: 6379, - }), - '@flecks/docker.containers': containers, - '@flecks/repl.context': (flecks) => ({ - redisClient: createClient(flecks), - }), - }, +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Redis server host. + */ + host: 'localhost', + /** + * Redis server port. + */ + port: 6379, + }), + '@flecks/docker.containers': containers, + '@flecks/repl.context': (flecks) => ({ + redisClient: createClient(flecks), + }), }; diff --git a/packages/redis/src/session/server.js b/packages/redis/src/session/server.js index 282d2e4..d126ac7 100644 --- a/packages/redis/src/session/server.js +++ b/packages/redis/src/session/server.js @@ -1,4 +1,4 @@ -import {D, Hooks} from '@flecks/core'; +import {D} from '@flecks/core'; import redisAdapter from '@socket.io/redis-adapter'; import ConnectRedis from 'connect-redis'; import session from 'express-session'; @@ -10,23 +10,21 @@ const debugSilly = debug.extend('silly'); const RedisStore = ConnectRedis(session); -export default { - [Hooks]: { - '@flecks/user.session': async (flecks) => { - const client = createClient(flecks, {legacyMode: true}); - await client.connect(); - return { - store: new RedisStore({client}), - }; - }, - '@flecks/socket.server': async (flecks) => { - const pubClient = createClient(flecks); - const subClient = createClient(flecks); - await Promise.all([pubClient.connect(), subClient.connect()]); - debugSilly('creating adapter'); - return { - adapter: redisAdapter(pubClient, subClient), - }; - }, +export const hooks = { + '@flecks/user.session': async (flecks) => { + const client = createClient(flecks, {legacyMode: true}); + await client.connect(); + return { + store: new RedisStore({client}), + }; + }, + '@flecks/socket.server': async (flecks) => { + const pubClient = createClient(flecks); + const subClient = createClient(flecks); + await Promise.all([pubClient.connect(), subClient.connect()]); + debugSilly('creating adapter'); + return { + adapter: redisAdapter(pubClient, subClient), + }; }, }; diff --git a/packages/redux/build/dox/hooks.js b/packages/redux/build/dox/hooks.js index 0939102..ad427b2 100644 --- a/packages/redux/build/dox/hooks.js +++ b/packages/redux/build/dox/hooks.js @@ -1,45 +1,41 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define side-effects to run against Redux actions. - */ - '@flecks/redux.effects': () => ({ - someActionName: (store, action) => { - // Runs when `someActionName` actions are dispatched. - }, - }), - /** - * Define root-level reducers for the Redux store. - */ - '@flecks/redux.reducers': () => { - return (state, action) => { - // Whatever you'd like. - return state; - }; - }, - /** - * Define Redux slices. - * - * See: https://redux-toolkit.js.org/api/createSlice - */ - '@flecks/redux.slices': () => { - const something = createSlice( - // ... - ); - return { - something: something.reducer, - }; - }, - /** - * Modify Redux store configuration. - * @param {Object} options A mutable object with keys for enhancers and middleware. - */ - '@flecks/redux.store': (options) => { - options.enhancers.splice(someIndex, 1); - options.middleware.push(mySpecialMiddleware); +export const hooks = { + /** + * Define side-effects to run against Redux actions. + */ + '@flecks/redux.effects': () => ({ + someActionName: (store, action) => { + // Runs when `someActionName` actions are dispatched. }, + }), + /** + * Define root-level reducers for the Redux store. + */ + '@flecks/redux.reducers': () => { + return (state, action) => { + // Whatever you'd like. + return state; + }; + }, + /** + * Define Redux slices. + * + * See: https://redux-toolkit.js.org/api/createSlice + */ + '@flecks/redux.slices': () => { + const something = createSlice( + // ... + ); + return { + something: something.reducer, + }; + }, + /** + * Modify Redux store configuration. + * @param {Object} options A mutable object with keys for enhancers and middleware. + */ + '@flecks/redux.store': (options) => { + options.enhancers.splice(someIndex, 1); + options.middleware.push(mySpecialMiddleware); }, }; diff --git a/packages/redux/src/client/index.js b/packages/redux/src/client/index.js index 2356f77..074447c 100644 --- a/packages/redux/src/client/index.js +++ b/packages/redux/src/client/index.js @@ -1,26 +1,24 @@ -import {ensureUniqueReduction, Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; import {Provider} from 'react-redux'; import configureStore, {createReducer} from '../store'; import localStorageEnhancer from './local-storage'; -export default { - [Hooks]: { - '@flecks/react.providers': async (req, flecks) => { - const slices = await ensureUniqueReduction(flecks, '@flecks/redux.slices'); - const reducer = createReducer(flecks, slices); - // Hydrate from server. - const {preloadedState} = flecks.get('@flecks/redux/client'); - const store = await configureStore(flecks, reducer, {preloadedState}); - flecks.set('$flecks/redux.store', store); - return [Provider, {store}]; - }, - '@flecks/redux.store': ({enhancers}) => { - // Hydrate from and subscribe to localStorage. - enhancers.push(localStorageEnhancer); - }, - '@flecks/socket.packets.decorate': ( - Flecks.decorate(require.context('./packets/decorators', false, /\.js$/)) - ), +export const hooks = { + '@flecks/react.providers': async (req, flecks) => { + const slices = await flecks.invokeMergeUnique('@flecks/redux.slices'); + const reducer = createReducer(flecks, slices); + // Hydrate from server. + const {preloadedState} = flecks.get('@flecks/redux/client'); + const store = await configureStore(flecks, reducer, {preloadedState}); + flecks.set('$flecks/redux.store', store); + return [Provider, {store}]; }, + '@flecks/redux.store': ({enhancers}) => { + // Hydrate from and subscribe to localStorage. + enhancers.push(localStorageEnhancer); + }, + '@flecks/socket.packets.decorate': ( + Flecks.decorate(require.context('./packets/decorators', false, /\.js$/)) + ), }; diff --git a/packages/redux/src/index.js b/packages/redux/src/index.js index 1e5ff24..fed21b7 100644 --- a/packages/redux/src/index.js +++ b/packages/redux/src/index.js @@ -1,12 +1,10 @@ -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; export * from '@reduxjs/toolkit'; export * from 'react-redux'; export * from './actions'; -export default { - [Hooks]: { - '@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)), - }, +export const hooks = { + '@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)), }; diff --git a/packages/redux/src/server.js b/packages/redux/src/server.js index 2381f57..6ecb4ee 100644 --- a/packages/redux/src/server.js +++ b/packages/redux/src/server.js @@ -1,4 +1,4 @@ -import {D, ensureUniqueReduction, Hooks} from '@flecks/core'; +import {D} from '@flecks/core'; import {Provider} from 'react-redux'; import {hydrateServer} from './actions'; @@ -8,29 +8,27 @@ import configureStore from './store'; const debug = D('@flecks/redux/server'); const debugSilly = debug.extend('silly'); -export default { - [Hooks]: { - '@flecks/web/server.request.route': (flecks) => async (req, res, next) => { - const slices = await ensureUniqueReduction(flecks, '@flecks/redux.slices'); - const reducer = 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)), - ); - const preloadedState = reducer(undefined, hydrateServer({flecks, req})); - debugSilly( - 'creating redux store with slices(%O) and state(%O)', - Object.keys(slices), - preloadedState, - ); - req.redux = await configureStore(flecks, reducer, {preloadedState}); - next(); - }, - '@flecks/web.config': async (req) => ({ - '@flecks/redux/client': { - preloadedState: req.redux.getState(), - }, - }), - '@flecks/react.providers': (req) => [Provider, {store: req.redux}], +export const hooks = { + '@flecks/web/server.request.route': (flecks) => async (req, res, next) => { + const slices = await flecks.invokeMergeUnique('@flecks/redux.slices'); + const reducer = 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)), + ); + const preloadedState = reducer(undefined, hydrateServer({flecks, req})); + debugSilly( + 'creating redux store with slices(%O) and state(%O)', + Object.keys(slices), + preloadedState, + ); + req.redux = await configureStore(flecks, reducer, {preloadedState}); + next(); }, + '@flecks/web.config': async (req) => ({ + '@flecks/redux/client': { + preloadedState: req.redux.getState(), + }, + }), + '@flecks/react.providers': (req) => [Provider, {store: req.redux}], }; diff --git a/packages/repl/build/dox/hooks.js b/packages/repl/build/dox/hooks.js index fdd351f..a2167ed 100644 --- a/packages/repl/build/dox/hooks.js +++ b/packages/repl/build/dox/hooks.js @@ -1,30 +1,26 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define REPL commands. - * - * Note: commands will be prefixed with a period in the Node REPL. - */ - '@flecks/repl.commands': () => ({ - someCommand: (...args) => { - // args are passed from the Node REPL. So, you could invoke it like: - // .someCommand foo bar - // and `args` would be `['foo', 'bar']`. - }, - }), - /** - * Provide global context to the REPL. - */ - '@flecks/repl.context': () => { - // Now you'd be able to do like: - // `node> someValue;` - // and the REPL would evaluate it to `'foobar'`. - return { - someValue: 'foobar', - }; +export const hooks = { + /** + * Define REPL commands. + * + * Note: commands will be prefixed with a period in the Node REPL. + */ + '@flecks/repl.commands': () => ({ + someCommand: (...args) => { + // args are passed from the Node REPL. So, you could invoke it like: + // .someCommand foo bar + // and `args` would be `['foo', 'bar']`. }, + }), + /** + * Provide global context to the REPL. + */ + '@flecks/repl.context': () => { + // Now you'd be able to do like: + // `node> someValue;` + // and the REPL would evaluate it to `'foobar'`. + return { + someValue: 'foobar', + }; }, }; diff --git a/packages/repl/src/server.js b/packages/repl/src/server.js index 74a2f5b..ecf266a 100644 --- a/packages/repl/src/server.js +++ b/packages/repl/src/server.js @@ -1,11 +1,7 @@ -import {Hooks} from '@flecks/core'; - import commands from './commands'; import {createReplServer} from './repl'; -export default { - [Hooks]: { - '@flecks/core.commands': commands, - '@flecks/server.up': (flecks) => createReplServer(flecks), - }, +export const hooks = { + '@flecks/core.commands': commands, + '@flecks/server.up': (flecks) => createReplServer(flecks), }; diff --git a/packages/server/build/dox/hooks.js b/packages/server/build/dox/hooks.js index b6f1ac0..a780043 100644 --- a/packages/server/build/dox/hooks.js +++ b/packages/server/build/dox/hooks.js @@ -1,12 +1,8 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define sequential actions to run when the server comes up. - */ - '@flecks/server.up': async () => { - await youCanDoAsyncThingsHere(); - }, +export const hooks = { + /** + * Define sequential actions to run when the server comes up. + */ + '@flecks/server.up': async () => { + await youCanDoAsyncThingsHere(); }, }; diff --git a/packages/server/src/entry.js b/packages/server/src/entry.js index 869992e..75fe283 100644 --- a/packages/server/src/entry.js +++ b/packages/server/src/entry.js @@ -33,7 +33,8 @@ const {version} = require('../package.json'); rcs, }); try { - await global.flecks.up('@flecks/server.up'); + await Promise.all(global.flecks.invokeFlat('@flecks/core.starting')); + await global.flecks.invokeSequentialAsync('@flecks/server.up'); debug('up!'); } catch (error) { diff --git a/packages/server/src/index.js b/packages/server/src/index.js index 522d68c..4a688d5 100644 --- a/packages/server/src/index.js +++ b/packages/server/src/index.js @@ -1,28 +1,24 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Whether HMR is enabled. - */ - hot: false, - /** - * Arguments to pass along to node. See: https://nodejs.org/api/cli.html - */ - nodeArgs: [], - /** - * Whether to start the server after building. - */ - start: true, - /** - * Webpack stats configuration when building server target. - */ - stats: { - chunks: false, - colors: true, - modules: false, - }, - }), - }, +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Whether HMR is enabled. + */ + hot: false, + /** + * Arguments to pass along to node. See: https://nodejs.org/api/cli.html + */ + nodeArgs: [], + /** + * Whether to start the server after building. + */ + start: true, + /** + * Webpack stats configuration when building server target. + */ + stats: { + chunks: false, + colors: true, + modules: false, + }, + }), }; diff --git a/packages/server/src/server/index.js b/packages/server/src/server/index.js index 0ad1557..ee501fb 100644 --- a/packages/server/src/server/index.js +++ b/packages/server/src/server/index.js @@ -1,7 +1,3 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - '@flecks/core.targets': () => ['server'], - }, +export const hooks = { + '@flecks/core.targets': () => ['server'], }; diff --git a/packages/socket/build/dox/hooks.js b/packages/socket/build/dox/hooks.js index 482336d..878cd8a 100644 --- a/packages/socket/build/dox/hooks.js +++ b/packages/socket/build/dox/hooks.js @@ -1,65 +1,61 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Modify Socket.io client configuration. - * - * See: https://socket.io/docs/v4/client-options/ - */ - '@flecks/socket.client': () => ({ - timeout: Infinity, - }), - /** - * Define server-side intercom channels. - */ - '@flecks/socket.intercom': (req) => ({ - // This would have been called like: - // `const result = await req.intercom('someChannel', 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. - someChannel: async (payload, server) => { - return someServiceSpecificInformation(); - }, - }), - /** - * Define 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 - */ - '@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)), - /** - * Decorate database models. - * - * 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. - */ - '@flecks/socket.packets.decorate': ( - Flecks.decorate(require.context('./packets/decorators', false, /\.js$/)) - ), - - /** - * Modify Socket.io server configuration. - * - * See: https://socket.io/docs/v4/server-options/ - */ - '@flecks/socket.server': () => ({ - pingTimeout: Infinity, - }), - /** - * Define middleware to run when a socket connection is established. - */ - '@flecks/socket/server.request.socket': () => (socket, next) => { - // Express-style route middleware... - next(); +export const hooks = { + /** + * Modify Socket.io client configuration. + * + * See: https://socket.io/docs/v4/client-options/ + */ + '@flecks/socket.client': () => ({ + timeout: Infinity, + }), + /** + * Define server-side intercom channels. + */ + '@flecks/socket.intercom': (req) => ({ + // This would have been called like: + // `const result = await req.intercom('someChannel', 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. + someChannel: async (payload, server) => { + return someServiceSpecificInformation(); }, + }), + /** + * Define 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 + */ + '@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)), + /** + * Decorate database models. + * + * 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. + */ + '@flecks/socket.packets.decorate': ( + Flecks.decorate(require.context('./packets/decorators', false, /\.js$/)) + ), + + /** + * Modify Socket.io server configuration. + * + * See: https://socket.io/docs/v4/server-options/ + */ + '@flecks/socket.server': () => ({ + pingTimeout: Infinity, + }), + /** + * Define middleware to run when a socket connection is established. + */ + '@flecks/socket/server.request.socket': () => (socket, next) => { + // Express-style route middleware... + next(); }, }; diff --git a/packages/socket/src/client/index.js b/packages/socket/src/client/index.js index 6b92626..db2f3b1 100644 --- a/packages/socket/src/client/index.js +++ b/packages/socket/src/client/index.js @@ -1,20 +1,16 @@ -import {Hooks} from '@flecks/core'; - import SocketClient from './socket'; -export default { - [Hooks]: { - '@flecks/web/client.up': (flecks) => { - const socket = new SocketClient(flecks); - flecks.set('$flecks/socket.socket', socket); - socket.connect(); - socket.listen(); - }, - '@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({ - cors: { - origin: false, - }, - path: `/${id}/socket.io`, - }), +export const hooks = { + '@flecks/web/client.up': (flecks) => { + const socket = new SocketClient(flecks); + flecks.set('$flecks/socket.socket', socket); + socket.connect(); + socket.listen(); }, + '@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({ + cors: { + origin: false, + }, + path: `/${id}/socket.io`, + }), }; diff --git a/packages/socket/src/index.js b/packages/socket/src/index.js index 0ead1f5..ca8f6a5 100644 --- a/packages/socket/src/index.js +++ b/packages/socket/src/index.js @@ -1,5 +1,3 @@ -import {Hooks} from '@flecks/core'; - import badPacketsCheck from './packet/bad-packets-check'; import Bundle from './packet/bundle'; import Redirect from './packet/redirect'; @@ -9,28 +7,26 @@ export {default as normalize} from './normalize'; export * from './hooks'; export {default as Packet, Packer, ValidationError} from './packet'; -export default { - [Hooks]: { - '@flecks/core.starting': (flecks) => { - flecks.set('$flecks/socket.packets', flecks.gather( - '@flecks/socket.packets', - {check: badPacketsCheck}, - )); - }, - '@flecks/web.config': async ( - req, - {config: {'@flecks/socket': {'packets.decorate': decorators = ['...']}}}, - ) => ({ - '@flecks/socket': { - 'packets.decorate': decorators.filter( - (decorator) => 'server' !== decorator.split('/').pop(), - ), - }, - }), - '@flecks/socket.packets': (flecks) => ({ - Bundle: Bundle(flecks), - Redirect, - Refresh, - }), +export const hooks = { + '@flecks/core.starting': (flecks) => { + flecks.set('$flecks/socket.packets', flecks.gather( + '@flecks/socket.packets', + {check: badPacketsCheck}, + )); }, + '@flecks/web.config': async ( + req, + {config: {'@flecks/socket': {'packets.decorate': decorators = ['...']}}}, + ) => ({ + '@flecks/socket': { + 'packets.decorate': decorators.filter( + (decorator) => 'server' !== decorator.split('/').pop(), + ), + }, + }), + '@flecks/socket.packets': (flecks) => ({ + Bundle: Bundle(flecks), + Redirect, + Refresh, + }), }; diff --git a/packages/socket/src/server/index.js b/packages/socket/src/server/index.js index 31065c0..13bfe7d 100644 --- a/packages/socket/src/server/index.js +++ b/packages/socket/src/server/index.js @@ -1,25 +1,21 @@ -import {Hooks} from '@flecks/core'; - import createIntercom from './create-intercom'; import Sockets from './sockets'; -export default { - [Hooks]: { - '@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => { - req.intercom = createIntercom(sockets, 'web'); - next(); - }, - '@flecks/web/server.up': async (httpServer, flecks) => { - const sockets = new Sockets(httpServer, flecks); - await sockets.connect(); - flecks.set('$flecks/socket.sockets', sockets); - }, - '@flecks/repl.context': (flecks) => ({ - Packets: flecks.get('$flecks/socket.packets'), - sockets: flecks.get('$flecks/socket.sockets'), - }), - '@flecks/socket.server': ({config: {'@flecks/core': {id}}}) => ({ - path: `/${id}/socket.io`, - }), +export const hooks = { + '@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => { + req.intercom = createIntercom(sockets, 'web'); + next(); }, + '@flecks/web/server.up': async (httpServer, flecks) => { + const sockets = new Sockets(httpServer, flecks); + await sockets.connect(); + flecks.set('$flecks/socket.sockets', sockets); + }, + '@flecks/repl.context': (flecks) => ({ + Packets: flecks.get('$flecks/socket.packets'), + sockets: flecks.get('$flecks/socket.sockets'), + }), + '@flecks/socket.server': ({config: {'@flecks/core': {id}}}) => ({ + path: `/${id}/socket.io`, + }), }; diff --git a/packages/user/build/dox/hooks.js b/packages/user/build/dox/hooks.js index 6ccbe07..ef438c0 100644 --- a/packages/user/build/dox/hooks.js +++ b/packages/user/build/dox/hooks.js @@ -1,15 +1,10 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Modify express-session configuration. - * - * See: https://www.npmjs.com/package/express-session - */ - '@flecks/user.session': () => ({ - saveUninitialized: true, - }), - }, +export const hooks = { + /** + * Modify express-session configuration. + * + * See: https://www.npmjs.com/package/express-session + */ + '@flecks/user.session': () => ({ + saveUninitialized: true, + }), }; - diff --git a/packages/user/src/index.js b/packages/user/src/index.js index f15676c..38ba658 100644 --- a/packages/user/src/index.js +++ b/packages/user/src/index.js @@ -1,19 +1,15 @@ -import {Hooks} from '@flecks/core'; - import {Logout} from './packets'; import {user, users} from './state'; export * from './state'; -export default { - [Hooks]: { - '@flecks/redux.slices': () => ({ - user, - users, - }), - '@flecks/socket.packets': (flecks) => ({ - Logout: Logout(flecks), - }), - }, +export const hooks = { + '@flecks/redux.slices': () => ({ + user, + users, + }), + '@flecks/socket.packets': (flecks) => ({ + Logout: Logout(flecks), + }), }; diff --git a/packages/user/src/local/server/index.js b/packages/user/src/local/server/index.js index 744bb32..a9bcf26 100644 --- a/packages/user/src/local/server/index.js +++ b/packages/user/src/local/server/index.js @@ -1,70 +1,68 @@ import {randomBytes} from 'crypto'; -import {Flecks, Hooks} from '@flecks/core'; +import {Flecks} from '@flecks/core'; import passport from 'passport'; import LocalStrategy from 'passport-local'; -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Path to redirect to after failed login. - */ - failureRedirect: '/', - /** - * Path to redirect to after successful login. - */ - successRedirect: '/', - }), - '@flecks/db/server.models.decorate': ( - Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) - ), - '@flecks/web.routes': (flecks) => { - const {failureRedirect, successRedirect} = flecks.get('@flecks/user/local/server'); - return [ - { - method: 'post', - path: '/auth/local', - middleware: passport.authenticate('local', {failureRedirect, successRedirect}), - }, - ]; - }, - '@flecks/repl.commands': (flecks) => { - const {User} = flecks.get('$flecks/db.models'); - return { - createUser: async (spec) => { - const [email, maybePassword] = spec.split(' ', 2); - const password = maybePassword || randomBytes(8).toString('hex'); - const user = User.build({email}); +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Path to redirect to after failed login. + */ + failureRedirect: '/', + /** + * Path to redirect to after successful login. + */ + successRedirect: '/', + }), + '@flecks/db/server.models.decorate': ( + Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) + ), + '@flecks/web.routes': (flecks) => { + const {failureRedirect, successRedirect} = flecks.get('@flecks/user/local/server'); + return [ + { + method: 'post', + path: '/auth/local', + middleware: passport.authenticate('local', {failureRedirect, successRedirect}), + }, + ]; + }, + '@flecks/repl.commands': (flecks) => { + const {User} = flecks.get('$flecks/db.models'); + return { + createUser: async (spec) => { + const [email, maybePassword] = spec.split(' ', 2); + const password = maybePassword || randomBytes(8).toString('hex'); + const user = User.build({email}); + await user.addHashedPassword(password); + await user.save(); + }, + resetPassword: async (email) => { + const password = randomBytes(8).toString('hex'); + const user = await User.findOne({where: {email}}); + if (user) { await user.addHashedPassword(password); await user.save(); - }, - resetPassword: async (email) => { - const password = randomBytes(8).toString('hex'); + return `\nNew password: ${password}\n\n`; + } + return 'User not found.\n'; + }, + }; + }, + '@flecks/server.up': (flecks) => { + passport.use(new LocalStrategy( + {usernameField: 'email'}, + async (email, password, fn) => { + const {User} = flecks.get('$flecks/db.models'); + try { const user = await User.findOne({where: {email}}); - if (user) { - await user.addHashedPassword(password); - await user.save(); - return `\nNew password: ${password}\n\n`; - } - return 'User not found.\n'; - }, - }; - }, - '@flecks/server.up': (flecks) => { - passport.use(new LocalStrategy( - {usernameField: 'email'}, - async (email, password, fn) => { - const {User} = flecks.get('$flecks/db.models'); - try { - const user = await User.findOne({where: {email}}); - fn(undefined, user && await user.validatePassword(password) && user); - } - catch (error) { - fn(error); - } - }, - )); - }, + fn(undefined, user && await user.validatePassword(password) && user); + } + catch (error) { + fn(error); + } + }, + )); }, }; diff --git a/packages/user/src/server/index.js b/packages/user/src/server/index.js index 00b3b34..d5d45e9 100644 --- a/packages/user/src/server/index.js +++ b/packages/user/src/server/index.js @@ -1,85 +1,83 @@ -import {D, Flecks, Hooks} from '@flecks/core'; +import {D, Flecks} from '@flecks/core'; import passport from 'passport'; import LogOps from 'passport/lib/http/request'; const debug = D('@flecks/user/passport'); const debugSilly = debug.extend('silly'); -export default { - [Hooks]: { - '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), - '@flecks/web/server.request.route': (flecks) => (req, res, next) => { - debugSilly('@flecks/web/server.request.route: passport.initialize()'); - passport.initialize()(req, res, () => { - debugSilly('@flecks/web/server.request.route: passport.session()'); - passport.session()(req, res, () => { - if (!req.user) { - const {User} = flecks.get('$flecks/db.models'); - req.user = new User(); - req.user.id = 0; - } - next(); - }); - }); - }, - '@flecks/web.routes': () => [ - { - method: 'get', - path: '/auth/logout', - middleware: (req, res) => { - req.logout(); - res.redirect('/'); - }, - }, - ], - '@flecks/server.up': (flecks) => { - passport.serializeUser((user, fn) => fn(null, user.id)); - passport.deserializeUser(async (id, fn) => { - const {User} = flecks.get('$flecks/db.models'); - try { - fn(undefined, await User.findByPk(id)); - } - catch (error) { - fn(error); +export const hooks = { + '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), + '@flecks/web/server.request.route': (flecks) => (req, res, next) => { + debugSilly('@flecks/web/server.request.route: passport.initialize()'); + passport.initialize()(req, res, () => { + debugSilly('@flecks/web/server.request.route: passport.session()'); + passport.session()(req, res, () => { + if (!req.user) { + const {User} = flecks.get('$flecks/db.models'); + req.user = new User(); + req.user.id = 0; } + next(); }); - }, - '@flecks/socket.intercom': () => ({ - '@flecks/user/users': async (sids, server) => { - const sockets = await server.sockets(); - return sids - .filter((sid) => sockets.has(sid)) - .reduce( - (r, sid) => ({ - ...r, - [sid]: sockets.get(sid).handshake.user.id, - }), - {}, - ); + }); + }, + '@flecks/web.routes': () => [ + { + method: 'get', + path: '/auth/logout', + middleware: (req, res) => { + req.logout(); + res.redirect('/'); }, - }), - '@flecks/socket/server.request.socket': (flecks) => (socket, next) => { - debugSilly('@flecks/socket/server.request.socket: passport.initialize()'); - passport.initialize()(socket.handshake, undefined, () => { - debugSilly('@flecks/socket/server.request.socket: passport.session()'); - passport.session()(socket.handshake, undefined, async () => { - /* eslint-disable no-param-reassign */ - if (!socket.handshake.user) { - const {User} = flecks.get('$flecks/db.models'); - socket.handshake.user = new User(); - socket.handshake.user.id = 0; - } - socket.handshake.login = LogOps.logIn; - socket.handshake.logIn = LogOps.logIn; - socket.handshake.logout = LogOps.logOut; - socket.handshake.logOut = LogOps.logOut; - socket.handshake.isAuthenticated = LogOps.isAuthenticated; - socket.handshake.isUnauthenticated = LogOps.isUnauthenticated; - /* eslint-enable no-param-reassign */ - await socket.join(`/u/${socket.handshake.user.id}`); - next(); - }); - }); }, + ], + '@flecks/server.up': (flecks) => { + passport.serializeUser((user, fn) => fn(null, user.id)); + passport.deserializeUser(async (id, fn) => { + const {User} = flecks.get('$flecks/db.models'); + try { + fn(undefined, await User.findByPk(id)); + } + catch (error) { + fn(error); + } + }); + }, + '@flecks/socket.intercom': () => ({ + '@flecks/user/users': async (sids, server) => { + const sockets = await server.sockets(); + return sids + .filter((sid) => sockets.has(sid)) + .reduce( + (r, sid) => ({ + ...r, + [sid]: sockets.get(sid).handshake.user.id, + }), + {}, + ); + }, + }), + '@flecks/socket/server.request.socket': (flecks) => (socket, next) => { + debugSilly('@flecks/socket/server.request.socket: passport.initialize()'); + passport.initialize()(socket.handshake, undefined, () => { + debugSilly('@flecks/socket/server.request.socket: passport.session()'); + passport.session()(socket.handshake, undefined, async () => { + /* eslint-disable no-param-reassign */ + if (!socket.handshake.user) { + const {User} = flecks.get('$flecks/db.models'); + socket.handshake.user = new User(); + socket.handshake.user.id = 0; + } + socket.handshake.login = LogOps.logIn; + socket.handshake.logIn = LogOps.logIn; + socket.handshake.logout = LogOps.logOut; + socket.handshake.logOut = LogOps.logOut; + socket.handshake.isAuthenticated = LogOps.isAuthenticated; + socket.handshake.isUnauthenticated = LogOps.isUnauthenticated; + /* eslint-enable no-param-reassign */ + await socket.join(`/u/${socket.handshake.user.id}`); + next(); + }); + }); }, }; diff --git a/packages/user/src/session/server.js b/packages/user/src/session/server.js index 7443289..fea4442 100644 --- a/packages/user/src/session/server.js +++ b/packages/user/src/session/server.js @@ -1,59 +1,57 @@ -import {D, Hooks} from '@flecks/core'; +import {D} from '@flecks/core'; import express from 'express'; import expressSession from 'express-session'; const debug = D('@flecks/user/session'); const debugSilly = debug.extend('silly'); -export default { - [Hooks]: { - '@flecks/core.config': () => ({ - /** - * Set the cookie secret for session encryption. - * - * See: http://expressjs.com/en/resources/middleware/cookie-parser.html - */ - cookieSecret: ( - 'Set the FLECKS_ENV_FLECKS_USER_SESSION_SERVER_cookieSecret environment variable!' - ), - }), - '@flecks/web/server.request.route': (flecks) => { - const urle = express.urlencoded({extended: true}); - return (req, res, next) => { - debugSilly('@flecks/web/server.request.route: express.urlencoded()'); - urle(req, res, (error) => { +export const hooks = { + '@flecks/core.config': () => ({ + /** + * Set the cookie secret for session encryption. + * + * See: http://expressjs.com/en/resources/middleware/cookie-parser.html + */ + cookieSecret: ( + 'Set the FLECKS_ENV_FLECKS_USER_SESSION_SERVER_cookieSecret environment variable!' + ), + }), + '@flecks/web/server.request.route': (flecks) => { + const urle = express.urlencoded({extended: true}); + return (req, res, next) => { + debugSilly('@flecks/web/server.request.route: express.urlencoded()'); + urle(req, res, (error) => { + if (error) { + next(error); + return; + } + debugSilly('@flecks/web/server.request.route: session()'); + flecks.get('$flecks/user.session')(req, res, (error) => { if (error) { next(error); return; } - debugSilly('@flecks/web/server.request.route: session()'); - flecks.get('$flecks/user.session')(req, res, (error) => { - if (error) { - next(error); - return; - } - debugSilly('session ID: %s', req.session.id); - next(); - }); + debugSilly('session ID: %s', req.session.id); + next(); }); - }; - }, - '@flecks/server.up': async (flecks) => { - flecks.set('$flecks/user.session', expressSession({ - resave: false, - sameSite: true, - saveUninitialized: false, - secret: flecks.get('@flecks/user/session/server.cookieSecret'), - ...await flecks.invokeMergeAsync('@flecks/user.session'), - })); - }, - '@flecks/socket/server.request.socket': (flecks) => (socket, next) => { - debugSilly('@flecks/socket/server.request.socket: session()'); - flecks.get('$flecks/user.session')(socket.handshake, {}, () => { - const id = socket.handshake.session?.id; - socket.join(id); - next(); }); - }, + }; + }, + '@flecks/server.up': async (flecks) => { + flecks.set('$flecks/user.session', expressSession({ + resave: false, + sameSite: true, + saveUninitialized: false, + secret: flecks.get('@flecks/user/session/server.cookieSecret'), + ...await flecks.invokeMergeAsync('@flecks/user.session'), + })); + }, + '@flecks/socket/server.request.socket': (flecks) => (socket, next) => { + debugSilly('@flecks/socket/server.request.socket: session()'); + flecks.get('$flecks/user.session')(socket.handshake, {}, () => { + const id = socket.handshake.session?.id; + socket.join(id); + next(); + }); }, }; diff --git a/packages/web/build/dox/hooks.js b/packages/web/build/dox/hooks.js index c16dfa6..ece001a 100644 --- a/packages/web/build/dox/hooks.js +++ b/packages/web/build/dox/hooks.js @@ -1,62 +1,58 @@ -import {Hooks} from '@flecks/core'; - -export default { - [Hooks]: { - /** - * Define sequential actions to run when the client comes up. - */ - '@flecks/web/client.up': async () => { - await youCanDoAsyncThingsHere(); +export const hooks = { + /** + * Define sequential actions to run when the client comes up. + */ + '@flecks/web/client.up': async () => { + await youCanDoAsyncThingsHere(); + }, + /** + * Override flecks configuration sent to client flecks. + * @param {http.ClientRequest} req The HTTP request object. + */ + '@flecks/web.config': (req) => ({ + someClientFleck: { + someConfig: req.someConfig, }, - /** - * Override flecks configuration sent to client flecks. - * @param {http.ClientRequest} req The HTTP request object. - */ - '@flecks/web.config': (req) => ({ - someClientFleck: { - someConfig: req.someConfig, + }), + /** + * Define HTTP routes. + */ + '@flecks/web.routes': () => [ + { + method: 'get', + path: '/some-path', + middleware: (req, res, next) => { + // Express-style route middleware... + next(); }, - }), - /** - * Define HTTP routes. - */ - '@flecks/web.routes': () => [ - { - method: 'get', - path: '/some-path', - middleware: (req, res, next) => { - // Express-style route middleware... - next(); - }, - }, - ], - /** - * Define middleware to run when a route is matched. - */ - '@flecks/web/server.request.route': () => (req, res, next) => { - // Express-style route middleware... - next(); - }, - /** - * Define middleware to run when an HTTP socket connection is established. - */ - '@flecks/web/server.request.socket': () => (req, res, next) => { - // Express-style route middleware... - next(); - }, - /** - * 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. - */ - '@flecks/web/server.stream.html': (stream, req) => { - return stream.pipe(myTransformStream); - }, - /** - * Define sequential actions to run when the HTTP server comes up. - */ - '@flecks/web/server.up': async () => { - await youCanDoAsyncThingsHere(); }, + ], + /** + * Define middleware to run when a route is matched. + */ + '@flecks/web/server.request.route': () => (req, res, next) => { + // Express-style route middleware... + next(); + }, + /** + * Define middleware to run when an HTTP socket connection is established. + */ + '@flecks/web/server.request.socket': () => (req, res, next) => { + // Express-style route middleware... + next(); + }, + /** + * 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. + */ + '@flecks/web/server.stream.html': (stream, req) => { + return stream.pipe(myTransformStream); + }, + /** + * Define sequential actions to run when the HTTP server comes up. + */ + '@flecks/web/server.up': async () => { + await youCanDoAsyncThingsHere(); }, }; diff --git a/packages/web/src/server/build/entry.js b/packages/web/src/server/build/entry.js index 085a7fb..61748a9 100644 --- a/packages/web/src/server/build/entry.js +++ b/packages/web/src/server/build/entry.js @@ -1,6 +1,6 @@ import {D, Flecks} from '@flecks/core'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved const {version} = require('@flecks/web/package.json'); (async () => { @@ -56,7 +56,8 @@ const {version} = require('@flecks/web/package.json'); const flecks = new Flecks(runtime); window.flecks = flecks; try { - await flecks.up('@flecks/web/client.up'); + await Promise.all(flecks.invokeFlat('@flecks/core.starting')); + await flecks.invokeSequentialAsync('@flecks/web/client.up'); window.document.querySelector('#root').style.display = 'block'; debug('up!'); } diff --git a/packages/web/src/server/index.js b/packages/web/src/server/index.js index 9823061..6509a0e 100644 --- a/packages/web/src/server/index.js +++ b/packages/web/src/server/index.js @@ -1,7 +1,7 @@ import {stat, unlink} from 'fs/promises'; import {join} from 'path'; -import {D, Hooks} from '@flecks/core'; +import {D} from '@flecks/core'; import {Flecks, spawnWith} from '@flecks/core/server'; import augmentBuild from './augment-build'; @@ -16,199 +16,197 @@ const debug = D('@flecks/web/server'); export {augmentBuild}; -export default { - [Hooks]: { - '@flecks/core.build': augmentBuild, - '@flecks/core.build.alter': async (neutrinoConfigs, flecks) => { - // Don't build if there's a fleck target. - if (neutrinoConfigs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) { +export const hooks = { + '@flecks/core.build': augmentBuild, + '@flecks/core.build.alter': async (neutrinoConfigs, flecks) => { + // Don't build if there's a fleck target. + if (neutrinoConfigs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) { + // eslint-disable-next-line no-param-reassign + delete neutrinoConfigs.web; + return; + } + // Only build vendor in dev. + if (neutrinoConfigs['web-vendor']) { + if (process.argv.find((arg) => 'production' === arg)) { // eslint-disable-next-line no-param-reassign - delete neutrinoConfigs.web; - return; + delete neutrinoConfigs['web-vendor']; } - // Only build vendor in dev. - if (neutrinoConfigs['web-vendor']) { - if (process.argv.find((arg) => 'production' === arg)) { - // eslint-disable-next-line no-param-reassign - delete neutrinoConfigs['web-vendor']; + // Only build if something actually changed. + const dll = flecks.get('@flecks/web/server.dll'); + if (dll.length > 0) { + const manifest = join( + FLECKS_CORE_ROOT, + 'node_modules', + '.cache', + 'flecks', + 'web-vendor.manifest.json', + ); + let timestamp = 0; + try { + const stats = await stat(manifest); + timestamp = stats.mtime; } - // Only build if something actually changed. - const dll = flecks.get('@flecks/web/server.dll'); - if (dll.length > 0) { - const manifest = join( - FLECKS_CORE_ROOT, - 'node_modules', - '.cache', - 'flecks', - 'web-vendor.manifest.json', - ); - let timestamp = 0; + // eslint-disable-next-line no-empty + catch (error) {} + let latest = 0; + for (let i = 0; i < dll.length; ++i) { + const path = dll[i]; try { - const stats = await stat(manifest); - timestamp = stats.mtime; + // eslint-disable-next-line no-await-in-loop + const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path)); + if (stats.mtime > latest) { + latest = stats.mtime; + } } // eslint-disable-next-line no-empty catch (error) {} - let latest = 0; - for (let i = 0; i < dll.length; ++i) { - const path = dll[i]; - try { - // eslint-disable-next-line no-await-in-loop - const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path)); - if (stats.mtime > latest) { - latest = stats.mtime; - } - } - // eslint-disable-next-line no-empty - catch (error) {} - } - if (timestamp > latest) { - // eslint-disable-next-line no-param-reassign - delete neutrinoConfigs['web-vendor']; - } - else if (timestamp > 0) { - await unlink(manifest); - } + } + if (timestamp > latest) { + // eslint-disable-next-line no-param-reassign + delete neutrinoConfigs['web-vendor']; + } + else if (timestamp > 0) { + await unlink(manifest); } } - // Bail if there's no web build. - if (!neutrinoConfigs.web) { - return; - } - // Bail if the build isn't watching. - if (!process.argv.find((arg) => '--watch' === arg)) { - return; - } - // Otherwise, spawn `webpack-dev-server` (WDS). - const cmd = [ - 'npx', 'webpack-dev-server', - '--mode', 'development', - '--hot', - '--config', flecks.buildConfig('webpack.config.js'), - ]; - spawnWith( - cmd, - { - env: { - FLECKS_CORE_BUILD_LIST: 'web', - }, - }, - ); - // Remove the build config since we're handing off to WDS. - // eslint-disable-next-line no-param-reassign - delete neutrinoConfigs.web; - }, - '@flecks/core.build.config': () => [ - /** - * Template file used to generate the client HTML. - * - * See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md - */ - 'template.ejs', - /** - * PostCSS config file. - * - * See: https://github.com/postcss/postcss#usage - */ - 'postcss.config.js', - ], - '@flecks/core.config': () => ({ - /** - * (webpack-dev-server) Disable the host check. - * - * See: https://github.com/webpack/webpack-dev-server/issues/887 - */ - devDisableHostCheck: false, - /** - * (webpack-dev-server) Host to bind. - */ - devHost: 'localhost', - /** - * (webpack-dev-server) Port to bind. - */ - devPort: undefined, - /** - * (webpack-dev-server) Public path to serve. - * - * Defaults to `flecks.get('@flecks/web/server.public')`. - */ - devPublic: undefined, - /** - * (webpack-dev-server) Webpack stats output. - */ - devStats: { - assets: false, - chunks: false, - colors: true, - modules: false, - }, - /** - * Modules to externalize using `webpack.DllPlugin`. - */ - dll: [], - /** - * Force building http target even if there's a fleck target. - */ - forceBuildWithFleck: false, - /** - * Host to bind. - */ - host: '0.0.0.0', - /** - * Build path. - */ - output: 'web', - /** - * Port to bind. - */ - port: 32340, - /** - * Public path to server. - */ - public: 'localhost:32340', - /** - * Webpack stats configuration when building HTTP target. - */ - stats: { - children: false, - chunks: false, - colors: true, - modules: false, - }, - /** - * Proxies to trust. - * - * See: https://www.npmjs.com/package/proxy-addr - */ - trust: false, - }), - '@flecks/core.starting': (flecks) => { - debug('bootstrapping flecks...'); - const webFlecks = Flecks.bootstrap({ - config: flecks.config, - platforms: ['client', '!server'], - }); - debug('bootstrapped'); - flecks.set('$flecks/web.flecks', webFlecks); - }, - '@flecks/core.targets': (flecks) => [ - 'web', - ...(flecks.get('@flecks/web/server.dll').length > 0 ? ['web-vendor'] : []), - ], - '@flecks/web.routes': (flecks) => [ + } + // Bail if there's no web build. + if (!neutrinoConfigs.web) { + return; + } + // Bail if the build isn't watching. + if (!process.argv.find((arg) => '--watch' === arg)) { + return; + } + // Otherwise, spawn `webpack-dev-server` (WDS). + const cmd = [ + 'npx', 'webpack-dev-server', + '--mode', 'development', + '--hot', + '--config', flecks.buildConfig('webpack.config.js'), + ]; + spawnWith( + cmd, { - method: 'get', - path: '/flecks.config.js', - middleware: async (req, res) => { - res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); - res.send(await configSource(flecks, req)); + env: { + FLECKS_CORE_BUILD_LIST: 'web', }, }, - ], - '@flecks/web/server.stream.html': inlineConfig, - '@flecks/server.up': (flecks) => createHttpServer(flecks), - '@flecks/repl.context': (flecks) => ({ - httpServer: flecks.get('$flecks/web/server.instance'), - }), + ); + // Remove the build config since we're handing off to WDS. + // eslint-disable-next-line no-param-reassign + delete neutrinoConfigs.web; }, + '@flecks/core.build.config': () => [ + /** + * Template file used to generate the client HTML. + * + * See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md + */ + 'template.ejs', + /** + * PostCSS config file. + * + * See: https://github.com/postcss/postcss#usage + */ + 'postcss.config.js', + ], + '@flecks/core.config': () => ({ + /** + * (webpack-dev-server) Disable the host check. + * + * See: https://github.com/webpack/webpack-dev-server/issues/887 + */ + devDisableHostCheck: false, + /** + * (webpack-dev-server) Host to bind. + */ + devHost: 'localhost', + /** + * (webpack-dev-server) Port to bind. + */ + devPort: undefined, + /** + * (webpack-dev-server) Public path to serve. + * + * Defaults to `flecks.get('@flecks/web/server.public')`. + */ + devPublic: undefined, + /** + * (webpack-dev-server) Webpack stats output. + */ + devStats: { + assets: false, + chunks: false, + colors: true, + modules: false, + }, + /** + * Modules to externalize using `webpack.DllPlugin`. + */ + dll: [], + /** + * Force building http target even if there's a fleck target. + */ + forceBuildWithFleck: false, + /** + * Host to bind. + */ + host: '0.0.0.0', + /** + * Build path. + */ + output: 'web', + /** + * Port to bind. + */ + port: 32340, + /** + * Public path to server. + */ + public: 'localhost:32340', + /** + * Webpack stats configuration when building HTTP target. + */ + stats: { + children: false, + chunks: false, + colors: true, + modules: false, + }, + /** + * Proxies to trust. + * + * See: https://www.npmjs.com/package/proxy-addr + */ + trust: false, + }), + '@flecks/core.starting': (flecks) => { + debug('bootstrapping flecks...'); + const webFlecks = Flecks.bootstrap({ + config: flecks.config, + platforms: ['client', '!server'], + }); + debug('bootstrapped'); + flecks.set('$flecks/web.flecks', webFlecks); + }, + '@flecks/core.targets': (flecks) => [ + 'web', + ...(flecks.get('@flecks/web/server.dll').length > 0 ? ['web-vendor'] : []), + ], + '@flecks/web.routes': (flecks) => [ + { + method: 'get', + path: '/flecks.config.js', + middleware: async (req, res) => { + res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); + res.send(await configSource(flecks, req)); + }, + }, + ], + '@flecks/web/server.stream.html': inlineConfig, + '@flecks/server.up': (flecks) => createHttpServer(flecks), + '@flecks/repl.context': (flecks) => ({ + httpServer: flecks.get('$flecks/web/server.instance'), + }), };