dox, hook registration, ensureUniqueReduction, middleware, ...
This commit is contained in:
cha0s 2022-08-10 10:09:02 -05:00
parent 23f2fae001
commit c3910ba5f0
60 changed files with 1921 additions and 1736 deletions

View File

@ -1,7 +1,7 @@
<div align="center"> <div align="center">
<h1>flecks</h1> <h1>flecks</h1>
<p> <p>
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 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 a highly dynamic structure encourage consistency while allowing you to easily express your own
opinions. opinions.

View File

@ -22,7 +22,7 @@
- [x] remove `invokeParallel()` - [x] remove `invokeParallel()`
- [x] Specialize `invokeReduce()` with `invokeMerge()`. - [x] Specialize `invokeReduce()` with `invokeMerge()`.
- [x] Rename all hooks to dot-first notation; rewrite `lookupFlecks()`. - [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']}` - [x] `bootstrap({without: ['badplatform']})` should be handled by passing `{platforms: ['!badplatform']}`
- [ ] user redux server hydrate fails if no user in req - [ ] user redux server hydrate fails if no user in req
- [ ] governor fails if not in server up - [ ] governor fails if not in server up
@ -31,3 +31,4 @@
- [ ] rename `@flecks/web` to `@flecks/web` - [ ] rename `@flecks/web` to `@flecks/web`
- [ ] simultaneous babel compilation across all compiled flecks - [ ] simultaneous babel compilation across all compiled flecks
- [ ] add building to publish process ... - [ ] add building to publish process ...
- [ ] @babel/register@7.18.x has a bug

1
packages/core/QUIRKS.md Normal file
View File

@ -0,0 +1 @@
- I use the variable `r` a lot when referencing a reducer's accumulator value

View File

@ -35,8 +35,10 @@ config.use.push(({config}) => {
} }
}); });
// Fleck build configuration.
config.use.unshift(fleck()); config.use.unshift(fleck());
// AirBnb linting.
config.use.unshift( config.use.unshift(
airbnb({ airbnb({
eslint: { eslint: {
@ -45,13 +47,13 @@ config.use.unshift(
}), }),
); );
// Include a shebang and set the executable bit..
config.use.push(banner({ config.use.push(banner({
banner: '#!/usr/bin/env node', banner: '#!/usr/bin/env node',
include: /^cli\.js$/, include: /^cli\.js$/,
pluginId: 'shebang', pluginId: 'shebang',
raw: true, raw: true,
})) }))
config.use.push(({config}) => { config.use.push(({config}) => {
config config
.plugin('executable') .plugin('executable')

View File

@ -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). 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 ```javascript
import {Hooks} from '@flecks/core'; export const hooks = {
'@flecks/core.starting': () => {
export default { console.log('hello, gorgeous');
[Hooks]: {
'@flecks/core.starting': () => {
console.log('hello, gorgeous');
},
}, },
}; };
``` ```
@ -133,15 +129,15 @@ assert(foo.type === 'Foo');
```javascript ```javascript
{ {
// The property added when extending the class to return the numeric ID. // 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. // 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. // A function called with the `Gathered` object to allow checking validity.
check = () => {}, 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. **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: Here's an example of how you could manually provide `@flecks/db/server.models` in your own fleck:
```javascript ```javascript
import {Hooks} foom '@flecks/core';
import SomeModel from './models/some-model'; import SomeModel from './models/some-model';
import AnotherModel from './models/another-model'; import AnotherModel from './models/another-model';
export default { export const hooks = {
[Hooks]: { '@flecks/db/server.models': () => ({
'@flecks/db/server.models': () => ({ SomeModel,
SomeModel, AnotherModel,
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. 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`: then, this `index.js`:
```javascript ```javascript
import {Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
export default { export const hooks = {
[Hooks]: { '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@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: 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 ```javascript
import {Hooks} from '@flecks/core'; export const hooks = {
'@flecks/db/server.models.decorate': (Models) => {
return {
...Models,
User: class extends Models.User {
export default { // Let's mix in some logging...
[Hooks]: { constructor(...args) {
'@flecks/db/server.models.decorate': (Models) => { super(...args);
return { console.log ('Another user decorated!');
...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)` #### `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: Supposing our fleck is structured like so:
@ -266,12 +252,12 @@ export default (User) => {
then, this `index.js`: then, this `index.js`:
```javascript ```javascript
import {Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
export default { export const hooks = {
[Hooks]: { '@flecks/db/server.models.decorate': (
'@flecks/db/server.models.decorate': Flecks.decorate(require.context('./models/decorators', false, /\.js$/)), 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). 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). 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).

View File

@ -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`.
* Hook into neutrino configuration. * @param {Object} config The neutrino configuration.
* @param {string} target The build target; e.g. `server`. */
* @param {Object} config The neutrino configuration.
*/
'@flecks/core.build': (target, config) => { '@flecks/core.build': (target, config) => {
if ('something' === target) { if ('something' === target) {
config[target].use.push(someNeutrinoMiddleware); config[target].use.push(someNeutrinoMiddleware);
} }
}, },
/** /**
* Alter build configurations after they have been hooked. * Alter build configurations after they have been hooked.
* @param {Object} configs The neutrino configurations. * @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: [
'<somearg>',
],
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.
*/ */
'@flecks/core.hmr': (path, updatedFleck) => { '@flecks/core.build.alter': (configs) => {
if ('my-fleck' === path) { // Maybe we want to do something if a config exists..?
updatedFleck.doSomething(); 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. * If you document your config files like this, documentation will be automatically
* @param {constructor} Class The class. * generated.
* @param {string} hook The gather hook; e.g. `@flecks/db/server.models`. */
*/ '.myrc.js',
'@flecks/core.hmr.gathered': (Class, hook) => { /**
// Do something with Class... * 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. * Define CLI commands.
*/ */
'@flecks/core.starting': (flecks) => { '@flecks/core.commands': (program) => ({
flecks.set('$my-fleck/value', initializeMyValue()); // So this could be invoked like:
// npx flecks something -t --blow-up blah
something: {
action: (...args) => {
// Run the command...
},
args: [
'<somearg>',
],
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. * Also, comments like this will be used to automatically generate documentation.
*/ */
'@flecks/core.targets': () => ['sometarget'], though: 'you should keep the values serializable',
}),
/** /**
* Hook into webpack configuration. * Invoked when a fleck is HMR'd
* @param {string} target The build target; e.g. `server`. * @param {string} path The path of the fleck
* @param {Object} config The neutrino configuration. * @param {Module} updatedFleck The updated fleck module.
*/ */
'@flecks/core.webpack': (target, config) => { '@flecks/core.hmr': (path, updatedFleck) => {
if ('something' === target) { if ('my-fleck' === path) {
config.stats = 'verbose'; 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';
}
}, },
}; };

View File

@ -59,6 +59,7 @@
"babel-merge": "^3.0.0", "babel-merge": "^3.0.0",
"babel-plugin-prepend": "^1.0.2", "babel-plugin-prepend": "^1.0.2",
"chai": "4.2.0", "chai": "4.2.0",
"chai-as-promised": "7.1.1",
"commander": "^8.3.0", "commander": "^8.3.0",
"debug": "4.3.1", "debug": "4.3.1",
"enhanced-resolve": "^5.9.2", "enhanced-resolve": "^5.9.2",

View File

@ -11,7 +11,7 @@ const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
const resolver = (source) => (path) => { const resolveValidModulePath = (source) => (path) => {
// Does the file resolve as source? // Does the file resolve as source?
try { try {
R.resolve(`${source}/${path}`); R.resolve(`${source}/${path}`);
@ -39,7 +39,7 @@ module.exports = () => ({config, options}) => {
.set(name, join(FLECKS_CORE_ROOT, 'src')); .set(name, join(FLECKS_CORE_ROOT, 'src'));
// Calculate entry points from `files`. // Calculate entry points from `files`.
files files
.filter(resolver(source)) .filter(resolveValidModulePath(source))
.forEach((file) => { .forEach((file) => {
const trimmed = join(dirname(file), basename(file, extname(file))); const trimmed = join(dirname(file), basename(file, extname(file)));
config config

View File

@ -1,2 +1,4 @@
// Get a runtime require function by hook or by crook. :)
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
module.exports = eval('"undefined" !== typeof require ? require : undefined'); module.exports = eval('"undefined" !== typeof require ? require : undefined');

View File

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

View File

@ -16,24 +16,38 @@ import Middleware from './middleware';
const debug = D('@flecks/core/flecks'); const debug = D('@flecks/core/flecks');
const debugSilly = debug.extend('silly'); const debugSilly = debug.extend('silly');
// Symbols for Gathered classes.
export const ById = Symbol.for('@flecks/core.byId'); export const ById = Symbol.for('@flecks/core.byId');
export const ByType = Symbol.for('@flecks/core.byType'); 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); 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(''); const camelCase = (string) => string.split(/[_-]/).map(capitalize).join('');
// Track gathered for HMR.
const hotGathered = new Map(); 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 { class Subclass extends Class {
static get [idAttribute]() { static get [idProperty]() {
return id; return id;
} }
static get [typeAttribute]() { static get [typeProperty]() {
return type; return type;
} }
@ -43,72 +57,121 @@ const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => {
export default class Flecks { 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({ constructor({
config = {}, config = {},
flecks = {}, flecks = {},
platforms = [], platforms = [],
} = {}) { } = {}) {
this.config = { const emptyConfigForAllFlecks = Object.fromEntries(
...Object.fromEntries(Object.keys(flecks).map((path) => [path, {}])), Object.keys(flecks).map((path) => [path, {}]),
...config, );
}; this.config = {...emptyConfigForAllFlecks, ...config};
this.hooks = {};
this.flecks = {};
this.platforms = platforms; this.platforms = platforms;
const entries = Object.entries(flecks); const entries = Object.entries(flecks);
debugSilly('paths: %O', entries.map(([fleck]) => fleck)); debugSilly('paths: %O', entries.map(([fleck]) => fleck));
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
const [fleck, M] = entries[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); debugSilly('config: %O', this.config);
} }
configureFleck(fleck) { /**
* Configure defaults for a fleck.
*
* @param {string} fleck
* @protected
*/
configureFleckDefaults(fleck) {
this.config[fleck] = { this.config[fleck] = {
...this.invokeFleck('@flecks/core.config', fleck), ...this.invokeFleck('@flecks/core.config', fleck),
...this.config[fleck], ...this.config[fleck],
}; };
} }
configureFlecks() { /**
const defaultConfig = this.invoke('@flecks/core.config'); * Configure defaults for all flecks.
const flecks = Object.keys(defaultConfig); *
* @protected
*/
configureFlecksDefaults() {
const flecks = this.flecksImplementing('@flecks/core.config');
for (let i = 0; i < flecks.length; i++) { 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( static decorate(
context, context,
{ {
transformer = camelCase, transformer = camelCase,
} = {}, } = {},
) { ) {
return (Gathered, flecks) => { return (Gathered, flecks) => (
context.keys() context.keys()
.forEach((path) => { .reduce(
const {default: M} = context(path); (Gathered, path) => {
if ('function' !== typeof M) { const key = transformer(this.dasherizePath(path));
throw new ReferenceError( if (!Gathered[key]) {
`Flecks.decorate(): require(${ return Gathered;
path }
}).default is not a function (from: ${ const {default: M} = context(path);
context.id 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]) { return {...Gathered, [key]: M(Gathered[key], flecks)};
// eslint-disable-next-line no-param-reassign },
Gathered[key] = M(Gathered[key], flecks); Gathered,
} )
}); );
return Gathered;
};
} }
/**
* Destroy this instance.
*/
destroy() { destroy() {
this.config = {}; this.config = {};
this.hooks = {}; this.hooks = {};
@ -116,12 +179,20 @@ export default class Flecks {
this.platforms = []; 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) { expandedFlecks(hook) {
const flecks = this.lookupFlecks(hook); const flecks = this.lookupFlecks(hook);
let expanded = []; let expanded = [];
for (let i = 0; i < flecks.length; ++i) { for (let i = 0; i < flecks.length; ++i) {
const fleck = flecks[i]; const fleck = flecks[i];
// Just the fleck.
expanded.push(fleck); expanded.push(fleck);
// Platform-specific variants.
for (let j = 0; j < this.platforms.length; ++j) { for (let j = 0; j < this.platforms.length; ++j) {
const platform = this.platforms[j]; const platform = this.platforms[j];
const variant = join(fleck, platform); const variant = join(fleck, platform);
@ -130,6 +201,7 @@ export default class Flecks {
} }
} }
} }
// Expand elided flecks.
const index = expanded.findIndex((fleck) => '...' === fleck); const index = expanded.findIndex((fleck) => '...' === fleck);
if (-1 !== index) { if (-1 !== index) {
if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) { if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) {
@ -158,33 +230,66 @@ export default class Flecks {
return expanded; return expanded;
} }
/**
* Get the module for a fleck.
*
* @param {*} fleck
*
* @returns {*}
*/
fleck(fleck) { fleck(fleck) {
return this.flecks[fleck]; return this.flecks[fleck];
} }
/**
* Test whether a fleck implements a hook.
*
* @param {*} fleck
* @param {string} hook
* @returns {boolean}
*/
fleckImplements(fleck, hook) { fleckImplements(fleck, hook) {
return !!this.hooks[hook].find(({fleck: candidate}) => fleck === candidate); return !!this.hooks[hook].find(({fleck: candidate}) => fleck === candidate);
} }
/**
* Get a list of flecks implementing a hook.
*
* @param {string} hook
* @returns {string[]}
*/
flecksImplementing(hook) { flecksImplementing(hook) {
return this.hooks[hook]?.map(({fleck}) => fleck) || []; 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( gather(
hook, hook,
{ {
idAttribute = 'id', idProperty = 'id',
typeAttribute = 'type', typeProperty = 'type',
check = () => {}, check = () => {},
} = {}, } = {},
) { ) {
if (!hook || 'string' !== typeof hook) { if (!hook || 'string' !== typeof hook) {
throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string'); throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string');
} }
// Gather classes and check.
const raw = this.invokeMerge(hook); const raw = this.invokeMerge(hook);
check(raw, hook); check(raw, hook);
// Decorate and check.
const decorated = this.invokeComposed(`${hook}.decorate`, raw); const decorated = this.invokeComposed(`${hook}.decorate`, raw);
check(decorated, `${hook}.decorate`); check(decorated, `${hook}.decorate`);
// Assign unique IDs to each class and sort by type.
let uid = 1; let uid = 1;
const ids = {}; const ids = {};
const types = ( const types = (
@ -193,50 +298,78 @@ export default class Flecks {
.sort(([ltype], [rtype]) => (ltype < rtype ? -1 : 1)) .sort(([ltype], [rtype]) => (ltype < rtype ? -1 : 1))
.map(([type, Class]) => { .map(([type, Class]) => {
const id = uid++; const id = uid++;
ids[id] = wrapperClass(Class, id, idAttribute, type, typeAttribute); ids[id] = wrapGathered(Class, id, idProperty, type, typeProperty);
return [type, ids[id]]; return [type, ids[id]];
}), }),
) )
); );
// Conglomerate all ID and type keys along with Symbols for accessing either/or.
const gathered = { const gathered = {
...ids, ...ids,
...types, ...types,
[ById]: ids, [ById]: ids,
[ByType]: types, [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])); debug("gathered '%s': %O", hook, Object.keys(gathered[ByType]));
return gathered; 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) { get(path, defaultValue) {
return get(this.config, 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) { invoke(hook, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return {}; return {};
} }
return this.flecksImplementing(hook) return this.flecksImplementing(hook)
.reduce((r, fleck) => ({ .reduce((r, fleck) => ({...r, [fleck]: this.invokeFleck(hook, fleck, ...args)}), {});
...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]) { if (!this.hooks[hook]) {
return arg; return initial;
} }
const flecks = this.expandedFlecks(hook); const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) { if (0 === flecks.length) {
return arg; return initial;
} }
return flecks return flecks
.filter((fleck) => this.fleckImplements(fleck, hook)) .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) { async invokeComposedAsync(hook, arg, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return arg; return arg;
@ -250,6 +383,13 @@ export default class Flecks {
.reduce(async (r, fleck) => this.invokeFleck(hook, fleck, await r, ...args), arg); .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) { invokeFlat(hook, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return []; return [];
@ -257,6 +397,14 @@ export default class Flecks {
return this.hooks[hook].map(({fleck}) => this.invokeFleck(hook, fleck, ...args)); 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) { invokeFleck(hook, fleck, ...args) {
debugSilly('invokeFleck(%s, %s, ...)', hook, fleck); debugSilly('invokeFleck(%s, %s, ...)', hook, fleck);
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
@ -270,33 +418,116 @@ export default class Flecks {
return candidate.fn(...(args.concat(this))); 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) { 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) { 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) { invokeReduce(hook, reducer, initial, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return initial; return initial;
} }
return this.hooks[hook] 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) { async invokeReduceAsync(hook, reducer, initial, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return initial; return initial;
} }
return this.hooks[hook] return this.hooks[hook]
.reduce( .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, 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) { invokeSequential(hook, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return []; return [];
@ -315,6 +546,11 @@ export default class Flecks {
return results; return results;
} }
/**
* An async version of `invokeSequential`.
*
* @see {@link Flecks#invokeSequential}
*/
async invokeSequentialAsync(hook, ...args) { async invokeSequentialAsync(hook, ...args) {
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return []; return [];
@ -334,10 +570,18 @@ export default class Flecks {
return results; 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) { lookupFlecks(hook) {
const index = hook.indexOf('.'); const index = hook.indexOf('.');
if (-1 === index) { if (-1 === index) {
@ -346,31 +590,37 @@ export default class Flecks {
return this.get([hook.slice(0, index), hook.slice(index + 1)], ['...']); 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) { makeMiddleware(hook) {
debugSilly('makeMiddleware(...): %s', hook); debugSilly('makeMiddleware(...): %s', hook);
if (!this.hooks[hook]) { if (!this.hooks[hook]) {
return Promise.resolve(); return (...args) => args.pop()();
} }
const flecks = this.expandedFlecks(hook); const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) { if (0 === flecks.length) {
return Promise.resolve(); return (...args) => args.pop()();
} }
const middleware = flecks const middleware = flecks
.filter((fleck) => this.fleckImplements(fleck, hook)); .filter((fleck) => this.fleckImplements(fleck, hook));
debugSilly('middleware: %O', middleware); debugSilly('middleware: %O', middleware);
const instance = new Middleware(middleware.map((fleck) => this.invokeFleck(hook, fleck))); const instance = new Middleware(middleware.map((fleck) => this.invokeFleck(hook, fleck)));
return async (...args) => { return instance.dispatch.bind(instance);
const next = args.pop();
try {
await instance.promise(...args);
next();
}
catch (error) {
next(error);
}
};
} }
/**
* 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( static provide(
context, context,
{ {
@ -393,7 +643,7 @@ export default class Flecks {
); );
} }
return [ return [
transformer(this.symbolizePath(path)), transformer(this.dasherizePath(path)),
invoke ? M(flecks) : M, 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) { refresh(fleck, M) {
debug('refreshing %s...', fleck); debug('refreshing %s...', fleck);
// Remove old hook implementations. // 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); const keys = Object.keys(this.hooks);
for (let j = 0; j < keys.length; j++) { for (let j = 0; j < keys.length; j++) {
const key = keys[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);
}
}
}
} }
} }

View File

@ -1,24 +1,18 @@
import {Hooks} from './flecks';
export {default as Class} from './class'; export {default as Class} from './class';
export {default as compose} from './compose'; export {default as compose} from './compose';
export {default as D} from './debug'; export {default as D} from './debug';
export {default as ensureUniqueReduction} from './ensure-unique-reduction';
export {default as EventEmitter} from './event-emitter'; export {default as EventEmitter} from './event-emitter';
export { export {
default as Flecks, default as Flecks,
ById, ById,
ByType, ByType,
Hooks,
} from './flecks'; } from './flecks';
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * The ID of your application.
* The ID of your application. */
*/ id: 'flecks',
id: 'flecks', }),
}),
},
}; };

View File

@ -34,6 +34,7 @@ module.exports = {
rules: { rules: {
'babel/object-curly-spacing': 'off', 'babel/object-curly-spacing': 'off',
'brace-style': ['error', 'stroustrup'], 'brace-style': ['error', 'stroustrup'],
'import/prefer-default-export': 'off',
'jsx-a11y/control-has-associated-label': ['error', {assert: 'either'}], 'jsx-a11y/control-has-associated-label': ['error', {assert: 'either'}],
'jsx-a11y/label-has-associated-control': ['error', {assert: 'either'}], 'jsx-a11y/label-has-associated-control': ['error', {assert: 'either'}],
'no-plusplus': 'off', 'no-plusplus': 'off',

View File

@ -22,6 +22,7 @@ const {
FLECKS_CORE_SYNC_FOR_ESLINT = false, FLECKS_CORE_SYNC_FOR_ESLINT = false,
} = process.env; } = process.env;
// This is kinda nuts, but ESLint doesn't support its configuration files returning a promise!
if (FLECKS_CORE_SYNC_FOR_ESLINT) { if (FLECKS_CORE_SYNC_FOR_ESLINT) {
(async () => { (async () => {
debug('bootstrapping flecks...'); debug('bootstrapping flecks...');
@ -50,6 +51,7 @@ else {
module.exports = JSON.parse(readFileSync(join(cacheDirectory, 'eslintrc.json')).toString()); module.exports = JSON.parse(readFileSync(join(cacheDirectory, 'eslintrc.json')).toString());
} }
catch (error) { catch (error) {
// Just silly. By synchronously spawning... ourselves, the spawned copy can use async.
const {stderr, stdout} = spawnSync('node', [__filename], { const {stderr, stdout} = spawnSync('node', [__filename], {
env: { env: {
FLECKS_CORE_SYNC_FOR_ESLINT: true, FLECKS_CORE_SYNC_FOR_ESLINT: true,

View File

@ -4,7 +4,6 @@ import {inspect} from 'util';
import airbnb from '@neutrinojs/airbnb'; import airbnb from '@neutrinojs/airbnb';
import neutrino from 'neutrino'; import neutrino from 'neutrino';
import {Hooks} from '../flecks';
import commands from './commands'; import commands from './commands';
import R from '../bootstrap/require'; import R from '../bootstrap/require';
@ -31,81 +30,79 @@ export {default as fleck} from '../bootstrap/fleck';
export {default as require} from '../bootstrap/require'; export {default as require} from '../bootstrap/require';
export {JsonStream, transform} from './stream'; export {JsonStream, transform} from './stream';
export default { export const hooks = {
[Hooks]: { '@flecks/core.build': (target, config, flecks) => {
'@flecks/core.build': (target, config, flecks) => { const {
const { 'eslint.exclude': exclude,
'eslint.exclude': exclude, profile,
profile, } = flecks.get('@flecks/core/server');
} = flecks.get('@flecks/core/server'); if (-1 !== profile.indexOf(target)) {
if (-1 !== profile.indexOf(target)) { config.use.push(({config}) => {
config.use.push(({config}) => { config
config .plugin('profiler')
.plugin('profiler') .use(
.use( R.resolve('webpack/lib/debug/ProfilingPlugin'),
R.resolve('webpack/lib/debug/ProfilingPlugin'), [{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}],
[{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}], );
); });
}); }
} if (-1 === exclude.indexOf(target)) {
if (-1 === exclude.indexOf(target)) { const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target));
const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target)); const webpackConfig = neutrino(config).webpack();
const webpackConfig = neutrino(config).webpack(); config.use.unshift(
config.use.unshift( airbnb({
airbnb({ eslint: {
eslint: { baseConfig: {
baseConfig: { ...baseConfig,
...baseConfig, settings: {
settings: { ...(baseConfig.settings || {}),
...(baseConfig.settings || {}), 'import/resolver': {
'import/resolver': { ...(baseConfig.settings['import/resolver'] || {}),
...(baseConfig.settings['import/resolver'] || {}), webpack: {
webpack: { config: {
config: { resolve: webpackConfig.resolve,
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: [],
}),
}; };

View File

@ -1,7 +1,12 @@
import {expect} from 'chai'; import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {Flecks} from '@flecks/core'; import {Flecks} from '@flecks/core';
chai.use(chaiAsPromised);
const {expect} = chai;
const testOne = require('./one'); const testOne = require('./one');
const testTwo = require('./two'); const testTwo = require('./two');
@ -33,3 +38,13 @@ it('can invoke merge async', async () => {
expect(await flecks.invokeMergeAsync('@flecks/core/test/invoke-merge-async')) expect(await flecks.invokeMergeAsync('@flecks/core/test/invoke-merge-async'))
.to.deep.equal({foo: 69, bar: 420}); .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);
});

View File

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

View File

@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved // 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 = () => [ export const testNodespace = () => [
/* eslint-disable no-eval */ /* eslint-disable no-eval */
@ -8,23 +8,28 @@ export const testNodespace = () => [
/* eslint-enable no-eval */ /* eslint-enable no-eval */
]; ];
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ foo: 'bar',
foo: 'bar', }),
}), '@flecks/core/one/test-gather': (
'@flecks/core/one/test-gather': ( Flecks.provide(require.context('./things', false, /\.js$/))
Flecks.provide(require.context('./things', false, /\.js$/)) ),
), '@flecks/core/one/test-gather.decorate': (
'@flecks/core/one/test-gather.decorate': ( Flecks.decorate(require.context('./things/decorators', false, /\.js$/))
Flecks.decorate(require.context('./things/decorators', false, /\.js$/)) ),
), '@flecks/core/test/invoke': () => 69,
'@flecks/core/test/invoke': () => 69, '@flecks/core/test/invoke-parallel': (O) => {
'@flecks/core/test/invoke-parallel': (O) => { // eslint-disable-next-line no-param-reassign
// eslint-disable-next-line no-param-reassign O.foo *= 2;
O.foo *= 2; },
}, '@flecks/core/test/invoke-merge': () => ({foo: 69}),
'@flecks/core/test/invoke-merge': () => ({foo: 69}), '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({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();
}, },
}; };

View File

@ -1,19 +1,24 @@
import {Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
export default { export const hooks = {
[Hooks]: { '@flecks/core/one/test-gather': (
'@flecks/core/one/test-gather': ( Flecks.provide(require.context('./things', false, /\.js$/))
Flecks.provide(require.context('./things', false, /\.js$/)) ),
), '@flecks/core/test/invoke': () => 420,
'@flecks/core/test/invoke': () => 420, '@flecks/core/test/invoke-parallel': (O) => new Promise((resolve) => {
'@flecks/core/test/invoke-parallel': (O) => new Promise((resolve) => { setTimeout(() => {
setTimeout(() => { // eslint-disable-next-line no-param-reassign
// eslint-disable-next-line no-param-reassign O.foo += 2;
O.foo += 2; resolve();
resolve(); }, 0);
}, 0); }),
}), '@flecks/core/test/invoke-merge': () => ({bar: 420}),
'@flecks/core/test/invoke-merge': () => ({bar: 420}), '@flecks/core/test/invoke-merge-async': () => new Promise((resolve) => resolve({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();
}, },
}; };

View File

@ -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]: { * Decorate database models.
/** *
* Gather database models. * In the example below, your fleck would have a `models/decorators` subdirectory, and each
* * decorator would be defined in its own file.
* In the example below, your fleck would have a `models` subdirectory, and each model would be * See: https://github.com/cha0s/flecks/tree/master/packages/user/src/local/server/models/decorators
* defined in its own file. *
* See: https://github.com/cha0s/flecks/tree/master/packages/user/src/server/models * @param {constructor} Model The model to decorate.
*/ */
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), '@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$/))
),
},
}; };

View File

@ -1,5 +1,3 @@
import {Hooks} from '@flecks/core';
import {createDatabaseConnection} from './connection'; import {createDatabaseConnection} from './connection';
import containers from './containers'; import containers from './containers';
@ -9,49 +7,47 @@ export {default as Model} from './model';
export {createDatabaseConnection}; export {createDatabaseConnection};
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * The database to connect to.
* The database to connect to. */
*/ database: ':memory:',
database: ':memory:', /**
/** * SQL dialect.
* SQL dialect. *
* * See: https://sequelize.org/v5/manual/dialects.html
* See: https://sequelize.org/v5/manual/dialects.html */
*/ dialect: 'sqlite',
dialect: 'sqlite', /**
/** * Database server host.
* Database server host. */
*/ host: undefined,
host: undefined, /**
/** * Database server password.
* Database server password. */
*/ password: undefined,
password: undefined, /**
/** * Database server port.
* Database server port. */
*/ port: undefined,
port: undefined, /**
/** * Database server username.
* Database server username. */
*/ username: undefined,
username: undefined, }),
}), '@flecks/core.starting': (flecks) => {
'@flecks/core.starting': (flecks) => { flecks.set('$flecks/db.models', flecks.gather(
flecks.set('$flecks/db.models', flecks.gather( '@flecks/db/server.models',
'@flecks/db/server.models', {typeProperty: 'name'},
{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'),
}),
}, },
'@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'),
}),
}; };

View File

@ -1,27 +1,23 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define docker containers.
[Hooks]: { *
/** * Beware: the user running the server must have Docker privileges.
* Define docker containers. * See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
* */
* Beware: the user running the server must have Docker privileges. '@flecks/docker.containers': () => ({
* See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user someContainer: {
*/ // Environment variables.
'@flecks/docker.containers': () => ({ environment: {
someContainer: { SOME_CONTAINER_VAR: 'hello',
// 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},
}, },
}), // 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},
},
}),
}; };

View File

@ -1,26 +1,22 @@
import {Hooks} from '@flecks/core';
import commands from './commands'; import commands from './commands';
import startContainer from './start-container'; import startContainer from './start-container';
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Whether to run docker containers.
* Whether to run docker containers. */
*/ enabled: true,
enabled: true, }),
}), '@flecks/core.commands': commands,
'@flecks/core.commands': commands, '@flecks/server.up': async (flecks) => {
'@flecks/server.up': async (flecks) => { if (!flecks.get('@flecks/docker/server.enabled')) {
if (!flecks.get('@flecks/docker/server.enabled')) { return;
return; }
} const containers = await flecks.invokeMergeAsync('@flecks/docker.containers');
const containers = await flecks.invokeMergeAsync('@flecks/docker.containers'); await Promise.all(
await Promise.all( Object.entries(containers)
Object.entries(containers) .map(([key, config]) => startContainer(flecks, key, config)),
.map(([key, config]) => startContainer(flecks, key, config)), );
);
},
}, },
}; };

View File

@ -17,6 +17,7 @@ import {
isObjectExpression, isObjectExpression,
isStringLiteral, isStringLiteral,
isThisExpression, isThisExpression,
isVariableDeclaration,
} from '@babel/types'; } from '@babel/types';
import {require as R} from '@flecks/core/server'; import {require as R} from '@flecks/core/server';
import {parse as parseComment} from 'comment-parser'; import {parse as parseComment} from 'comment-parser';
@ -75,15 +76,14 @@ class ParserState {
} }
const implementationVisitor = (fn) => ({ const implementationVisitor = (fn) => ({
ExportDefaultDeclaration(path) { ExportNamedDeclaration(path) {
const {declaration} = path.node; const {declaration} = path.node;
if (isObjectExpression(declaration)) { if (isVariableDeclaration(declaration)) {
const {properties} = declaration; const {declarations} = declaration;
properties.forEach((property) => { declarations.forEach((declarator) => {
const {key, value} = property; if ('hooks' === declarator.id.name) {
if (isIdentifier(key) && key.name === 'Hooks') { if (isObjectExpression(declarator.init)) {
if (isObjectExpression(value)) { const {properties} = declarator.init;
const {properties} = value;
properties.forEach((property) => { properties.forEach((property) => {
const {key} = property; const {key} = property;
if (isLiteral(key)) { if (isLiteral(key)) {

View File

@ -1,17 +1,13 @@
import {Hooks} from '@flecks/core';
import commands from './commands'; import commands from './commands';
export default { export const hooks = {
[Hooks]: { '@flecks/core.commands': commands,
'@flecks/core.commands': commands, '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Rewrite the output filenames of source files.
* Rewrite the output filenames of source files. *
* * `filename.replace(new RegExp([key]), [value]);`
* `filename.replace(new RegExp([key]), [value]);` */
*/ filenameRewriters: {},
filenameRewriters: {}, }),
}),
},
}; };

View File

@ -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 a window is created
/** * @param {Electron.BrowserWindow} win The electron browser window. See: https://www.electronjs.org/docs/latest/api/browser-window
* Invoked when electron is initializing. */
* @param {Electron.App} app The electron app. See: https://www.electronjs.org/docs/latest/api/app '@flecks/electron/server.window': (win) => {
*/ win.maximize();
'@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();
},
}, },
}; };

View File

@ -1,7 +1,6 @@
import cluster from 'cluster'; import cluster from 'cluster';
import {join} from 'path'; import {join} from 'path';
import {Hooks} from '@flecks/core';
import {require as R} from '@flecks/core/server'; import {require as R} from '@flecks/core/server';
import { import {
app, app,
@ -21,119 +20,117 @@ async function createWindow(flecks) {
await flecks.invokeSequentialAsync('@flecks/electron/server.window', win); await flecks.invokeSequentialAsync('@flecks/electron/server.window', win);
} }
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Browser window options.
* Browser window options. *
* * See: https://www.electronjs.org/docs/latest/api/browser-window
* See: https://www.electronjs.org/docs/latest/api/browser-window */
*/ browserWindowOptions: {},
browserWindowOptions: {}, /**
/** * Install devtools extensions (by default).
* Install devtools extensions (by default). *
* * If `true`, will install some devtools extensions based on which flecks are enabled.
* 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.
* 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`
* Extensions will not be installed if `'production' === process.env.NODE_ENV` */
*/ installExtensions: true,
installExtensions: true, /**
/** * Quit the app when all windows are closed.
* Quit the app when all windows are closed. */
*/ quitOnClosed: true,
quitOnClosed: true, /**
/** * The URL to load in electron by default.
* The URL to load in electron by default. *
* * Defaults to `http://${flecks.get('@flecks/web/server.public')}`.
* Defaults to `http://${flecks.get('@flecks/web/server.public')}`. */
*/ url: undefined,
url: undefined, }),
}), '@flecks/core.webpack': (target, config) => {
'@flecks/core.webpack': (target, config) => { const StartServerWebpackPlugin = R('start-server-webpack-plugin');
const StartServerWebpackPlugin = R('start-server-webpack-plugin'); const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin);
const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin); // Extremely hackish, c'est la vie.
// Extremely hackish, c'est la vie. if (plugin) {
if (plugin) { /* eslint-disable no-underscore-dangle */
/* eslint-disable no-underscore-dangle */ plugin._startServer = function _startServerHacked(callback) {
plugin._startServer = function _startServerHacked(callback) { const execArgv = this._getArgs();
const execArgv = this._getArgs(); const inspectPort = this._getInspectPort(execArgv);
const inspectPort = this._getInspectPort(execArgv); const clusterOptions = {
const clusterOptions = { args: [this._entryPoint],
args: [this._entryPoint], exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'),
exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'), execArgv,
execArgv,
};
if (inspectPort) {
clusterOptions.inspectPort = inspectPort;
}
cluster.setupMaster(clusterOptions);
cluster.on('online', (worker) => {
callback(worker);
});
cluster.fork();
}; };
/* eslint-enable no-underscore-dangle */ if (inspectPort) {
} clusterOptions.inspectPort = inspectPort;
},
'@flecks/electron/server.initialize': async (app, flecks) => {
app.on('window-all-closed', () => {
const {quitOnClosed} = flecks.get('@flecks/electron/server');
if (!quitOnClosed) {
return;
} }
// Apple has to be *special*. cluster.setupMaster(clusterOptions);
if (process.platform === 'darwin') { cluster.on('online', (worker) => {
return; callback(worker);
} });
app.quit(); cluster.fork();
}); };
app.on('activate', async () => { /* eslint-enable no-underscore-dangle */
if (BrowserWindow.getAllWindows().length === 0) { }
createWindow(); },
} '@flecks/electron/server.initialize': async (app, flecks) => {
}); app.on('window-all-closed', () => {
await app.whenReady(); const {quitOnClosed} = flecks.get('@flecks/electron/server');
await createWindow(flecks); if (!quitOnClosed) {
},
'@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; 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);
}, },
}; };

View File

@ -1,21 +1,17 @@
import {Hooks} from '@flecks/core';
import commands from './commands'; import commands from './commands';
export default { export const hooks = {
[Hooks]: { '@flecks/core.commands': commands,
'@flecks/core.commands': commands, '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Webpack stats configuration when building fleck target.
* Webpack stats configuration when building fleck target. */
*/ stats: {
stats: { children: false,
children: false, chunks: false,
chunks: false, colors: true,
colors: true, modules: false,
modules: false, },
}, }),
}), '@flecks/core.targets': () => ['fleck'],
'@flecks/core.targets': () => ['fleck'],
},
}; };

View File

@ -1,135 +1,133 @@
import {ByType, Flecks, Hooks} from '@flecks/core'; import {ByType, Flecks} from '@flecks/core';
import LimitedPacket from './limited-packet'; import LimitedPacket from './limited-packet';
import createLimiter from './limiter'; import createLimiter from './limiter';
export {default as createLimiter} from './limiter'; export {default as createLimiter} from './limiter';
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * All keys used to determine fingerprint.
* All keys used to determine fingerprint. */
*/ keys: ['ip'],
web: {
keys: ['ip'], keys: ['ip'],
web: { points: 60,
keys: ['ip'], duration: 30,
points: 60, ttl: 30,
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(`<pre>${error.message}</pre>`);
return;
}
req.ban = async (keys, ttl = 0) => {
const ban = Ban.fromRequest(req, keys, ttl);
await Ban.create({...ban});
res.status(403).send(`<pre>${Ban.format([ban])}</pre>`);
};
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(`<pre>${Ban.format([ban])}</pre>`);
}
};
}, },
'@flecks/server.up': async (flecks) => { socket: {
if (flecks.fleck('@flecks/web/server')) { keys: ['ip'],
const {web} = flecks.get('@flecks/governor/server'); points: 60,
const limiter = await createLimiter( duration: 30,
flecks, ttl: 30,
{ },
keyPrefix: '@flecks/governor.web.request.route', }),
...web, '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
}, '@flecks/web/server.request.route': (flecks) => {
); const {web} = flecks.get('@flecks/governor/server');
flecks.set('$flecks/governor.web.limiter', limiter); 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')) { catch (error) {
const {[ByType]: Packets} = flecks.get('$flecks/socket.packets'); res.status(403).send(`<pre>${error.message}</pre>`);
const limiters = Object.fromEntries( return;
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);
} }
}, req.ban = async (keys, ttl = 0) => {
'@flecks/socket/server.request.socket': (flecks) => { const ban = Ban.fromRequest(req, keys, ttl);
const limiter = flecks.get('$flecks/governor.socket.limiter'); await Ban.create({...ban});
return async (socket, next) => { res.status(403).send(`<pre>${Ban.format([ban])}</pre>`);
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);
}
}; };
}, try {
'@flecks/socket.packets.decorate': (Packets, flecks) => ( await limiter.consume(req.ip);
Object.fromEntries( next();
Object.entries(Packets).map(([keyPrefix, Packet]) => [ }
keyPrefix, catch (error) {
!Packet.limit ? Packet : LimitedPacket(flecks, [keyPrefix, Packet]), const {ttl, keys} = web;
]), const ban = Ban.fromRequest(req, keys, ttl);
) await Ban.create({...ban});
), res.status(429).send(`<pre>${Ban.format([ban])}</pre>`);
}
};
}, },
'@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]),
]),
)
),
}; };

View File

@ -1,36 +1,32 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define React Providers.
[Hooks]: { *
/** * Note: `req` will be only be defined when server-side rendering.
* Define React Providers. * @param {http.ClientRequest} req The HTTP request object.
* */
* Note: `req` will be only be defined when server-side rendering. '@flecks/react.providers': (req) => {
* @param {http.ClientRequest} req The HTTP request object. // Generally it makes more sense to separate client and server concerns using platform
*/ // naming conventions, but this is just a small contrived example.
'@flecks/react.providers': (req) => { return req ? serverSideProvider(req) : clientSideProvider();
// 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.
/** *
* Define root-level React components that are mounted as siblings on `#main`. * Return either a React component or an array whose elements must either be a React component
* Note: `req` will be only be defined when server-side rendering. * or an array of two elements where the first element is the component and the second element
* * is the props passed to the component.
* Return either a React component or an array whose elements must either be a React component * @param {http.ClientRequest} req The HTTP request object.
* or an array of two elements where the first element is the component and the second element */
* is the props passed to the component. '@flecks/react.roots': (req) => {
* @param {http.ClientRequest} req The HTTP request object. // Note that we're not returning `<Component />`, but `Component`.
*/ return [
'@flecks/react.roots': (req) => { Component,
// Note that we're not returning `<Component />`, but `Component`. [SomeOtherComponent, {prop: 'value'}]
return [ ];
Component, // You can also just:
[SomeOtherComponent, {prop: 'value'}] return Component;
];
// You can also just:
return Component;
},
}, },
}; };

View File

@ -1,4 +1,4 @@
import {D, Hooks} from '@flecks/core'; import {D} from '@flecks/core';
import {hydrate, render} from '@hot-loader/react-dom'; import {hydrate, render} from '@hot-loader/react-dom';
import React from 'react'; import React from 'react';
@ -10,20 +10,18 @@ const debug = D('@flecks/react/client');
export {FlecksContext}; export {FlecksContext};
export default { export const hooks = {
[Hooks]: { '@flecks/web/client.up': async (flecks) => {
'@flecks/web/client.up': async (flecks) => { const {ssr} = flecks.get('@flecks/react');
const {ssr} = flecks.get('@flecks/react'); debug('%sing...', ssr ? 'hydrat' : 'render');
debug('%sing...', ssr ? 'hydrat' : 'render'); (ssr ? hydrate : render)(
(ssr ? hydrate : render)( React.createElement(
React.createElement( React.StrictMode,
React.StrictMode, {},
{}, [React.createElement(await root(flecks), {key: 'root'})],
[React.createElement(await root(flecks), {key: 'root'})], ),
), window.document.getElementById('root'),
window.document.getElementById('root'), );
); debug('rendered');
debug('rendered');
},
}, },
}; };

View File

@ -1,5 +1,3 @@
import {Hooks} from '@flecks/core';
export {default as ReactDom} from '@hot-loader/react-dom'; export {default as ReactDom} from '@hot-loader/react-dom';
export {default as classnames} from 'classnames'; export {default as classnames} from 'classnames';
export {default as PropTypes} from 'prop-types'; 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 useFlecks} from './hooks/use-flecks';
export {default as usePrevious} from './hooks/use-previous'; export {default as usePrevious} from './hooks/use-previous';
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Whether to enable server-side rendering.
* Whether to enable server-side rendering. */
*/ ssr: true,
ssr: true, }),
}),
},
}; };

View File

@ -1,15 +1,12 @@
import {Hooks} from '@flecks/core';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import {createReduxHistory, history} from '@flecks/react/router/context'; import {createReduxHistory, history} from '@flecks/react/router/context';
import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom'; import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom';
import {HistoryRouter as ReduxHistoryRouter} from 'redux-first-history/rr6'; import {HistoryRouter as ReduxHistoryRouter} from 'redux-first-history/rr6';
export default { export const hooks = {
[Hooks]: { '@flecks/react.providers': (req, flecks) => (
'@flecks/react.providers': (req, flecks) => ( flecks.fleck('@flecks/redux')
flecks.fleck('@flecks/redux') ? [ReduxHistoryRouter, {history: createReduxHistory(flecks.get('$flecks/redux.store'))}]
? [ReduxHistoryRouter, {history: createReduxHistory(flecks.get('$flecks/redux.store'))}] : [HistoryRouter, {history}]
: [HistoryRouter, {history}] ),
),
},
}; };

View File

@ -1,17 +1,14 @@
import {Hooks} from '@flecks/core';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import {routerMiddleware, routerReducer} from '@flecks/react/router/context'; import {routerMiddleware, routerReducer} from '@flecks/react/router/context';
export * from 'react-router-dom'; export * from 'react-router-dom';
export * from 'redux-first-history'; export * from 'redux-first-history';
export default { export const hooks = {
[Hooks]: { '@flecks/redux.slices': () => ({
'@flecks/redux.slices': () => ({ router: routerReducer,
router: routerReducer, }),
}), '@flecks/redux.store': (options) => {
'@flecks/redux.store': (options) => { options.middleware.push(routerMiddleware);
options.middleware.push(routerMiddleware);
},
}, },
}; };

View File

@ -1,10 +1,7 @@
import {Hooks} from '@flecks/core';
import {StaticRouter} from 'react-router-dom/server'; import {StaticRouter} from 'react-router-dom/server';
export default { export const hooks = {
[Hooks]: { '@flecks/react.providers': (req, flecks) => (
'@flecks/react.providers': (req, flecks) => ( flecks.get('@flecks/react.ssr') ? [StaticRouter, {location: req.url}] : []
flecks.get('@flecks/react.ssr') ? [StaticRouter, {location: req.url}] : [] ),
),
},
}; };

View File

@ -1,28 +1,25 @@
import {Hooks} from '@flecks/core';
import {augmentBuild} from '@flecks/web/server'; import {augmentBuild} from '@flecks/web/server';
import ssr from './ssr'; import ssr from './ssr';
export default { export const hooks = {
[Hooks]: { '@flecks/core.build': (target, config, flecks) => {
'@flecks/core.build': (target, config, flecks) => { // Resolution.
// Resolution. config.use.push(({config}) => {
config.use.push(({config}) => { config.resolve.alias
config.resolve.alias .set('react-native', 'react-native-web');
.set('react-native', 'react-native-web'); config.resolve.extensions
config.resolve.extensions .prepend('.web.js')
.prepend('.web.js') .prepend('.web.jsx');
.prepend('.web.jsx'); });
}); // Augment the build on behalf of a missing `@flecks/web`.
// Augment the build on behalf of a missing `@flecks/web`. if (!flecks.fleck('@flecks/web/server')) {
if (!flecks.fleck('@flecks/web/server')) { flecks.registerBuildConfig('postcss.config.js', {fleck: '@flecks/web/server'});
flecks.registerBuildConfig('postcss.config.js', {fleck: '@flecks/web/server'}); flecks.registerResolver('@flecks/web');
flecks.registerResolver('@flecks/web'); augmentBuild(target, config, flecks);
augmentBuild(target, config, flecks); }
}
},
'@flecks/web/server.stream.html': (stream, req, flecks) => (
flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream
),
}, },
'@flecks/web/server.stream.html': (stream, req, flecks) => (
flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream
),
}; };

View File

@ -1,5 +1,3 @@
import {Hooks} from '@flecks/core';
import containers from './containers'; import containers from './containers';
import createClient from './create-client'; 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 const keys = (client, pattern) => safeKeys(client, pattern, 0);
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Redis server host.
* Redis server host. */
*/ host: 'localhost',
host: 'localhost', /**
/** * Redis server port.
* Redis server port. */
*/ port: 6379,
port: 6379, }),
}), '@flecks/docker.containers': containers,
'@flecks/docker.containers': containers, '@flecks/repl.context': (flecks) => ({
'@flecks/repl.context': (flecks) => ({ redisClient: createClient(flecks),
redisClient: createClient(flecks), }),
}),
},
}; };

View File

@ -1,4 +1,4 @@
import {D, Hooks} from '@flecks/core'; import {D} from '@flecks/core';
import redisAdapter from '@socket.io/redis-adapter'; import redisAdapter from '@socket.io/redis-adapter';
import ConnectRedis from 'connect-redis'; import ConnectRedis from 'connect-redis';
import session from 'express-session'; import session from 'express-session';
@ -10,23 +10,21 @@ const debugSilly = debug.extend('silly');
const RedisStore = ConnectRedis(session); const RedisStore = ConnectRedis(session);
export default { export const hooks = {
[Hooks]: { '@flecks/user.session': async (flecks) => {
'@flecks/user.session': async (flecks) => { const client = createClient(flecks, {legacyMode: true});
const client = createClient(flecks, {legacyMode: true}); await client.connect();
await client.connect(); return {
return { store: new RedisStore({client}),
store: new RedisStore({client}), };
}; },
}, '@flecks/socket.server': async (flecks) => {
'@flecks/socket.server': async (flecks) => { const pubClient = createClient(flecks);
const pubClient = createClient(flecks); const subClient = createClient(flecks);
const subClient = createClient(flecks); await Promise.all([pubClient.connect(), subClient.connect()]);
await Promise.all([pubClient.connect(), subClient.connect()]); debugSilly('creating adapter');
debugSilly('creating adapter'); return {
return { adapter: redisAdapter(pubClient, subClient),
adapter: redisAdapter(pubClient, subClient), };
};
},
}, },
}; };

View File

@ -1,45 +1,41 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define side-effects to run against Redux actions.
[Hooks]: { */
/** '@flecks/redux.effects': () => ({
* Define side-effects to run against Redux actions. someActionName: (store, action) => {
*/ // Runs when `someActionName` actions are dispatched.
'@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);
}, },
}),
/**
* 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);
}, },
}; };

View File

@ -1,26 +1,24 @@
import {ensureUniqueReduction, Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import configureStore, {createReducer} from '../store'; import configureStore, {createReducer} from '../store';
import localStorageEnhancer from './local-storage'; import localStorageEnhancer from './local-storage';
export default { export const hooks = {
[Hooks]: { '@flecks/react.providers': async (req, flecks) => {
'@flecks/react.providers': async (req, flecks) => { const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
const slices = await ensureUniqueReduction(flecks, '@flecks/redux.slices'); const reducer = createReducer(flecks, slices);
const reducer = createReducer(flecks, slices); // Hydrate from server.
// Hydrate from server. const {preloadedState} = flecks.get('@flecks/redux/client');
const {preloadedState} = flecks.get('@flecks/redux/client'); const store = await configureStore(flecks, reducer, {preloadedState});
const store = await configureStore(flecks, reducer, {preloadedState}); flecks.set('$flecks/redux.store', store);
flecks.set('$flecks/redux.store', store); return [Provider, {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$/))
),
}, },
'@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$/))
),
}; };

View File

@ -1,12 +1,10 @@
import {Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
export * from '@reduxjs/toolkit'; export * from '@reduxjs/toolkit';
export * from 'react-redux'; export * from 'react-redux';
export * from './actions'; export * from './actions';
export default { export const hooks = {
[Hooks]: { '@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
},
}; };

View File

@ -1,4 +1,4 @@
import {D, ensureUniqueReduction, Hooks} from '@flecks/core'; import {D} from '@flecks/core';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import {hydrateServer} from './actions'; import {hydrateServer} from './actions';
@ -8,29 +8,27 @@ import configureStore from './store';
const debug = D('@flecks/redux/server'); const debug = D('@flecks/redux/server');
const debugSilly = debug.extend('silly'); const debugSilly = debug.extend('silly');
export default { export const hooks = {
[Hooks]: { '@flecks/web/server.request.route': (flecks) => async (req, res, next) => {
'@flecks/web/server.request.route': (flecks) => async (req, res, next) => { const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
const slices = await ensureUniqueReduction(flecks, '@flecks/redux.slices'); const reducer = createReducer(flecks, slices);
const reducer = createReducer(flecks, slices); // Let the slices have a(n async) chance to hydrate with server data.
// Let the slices have a(n async) chance to hydrate with server data. await Promise.all(
await Promise.all( Object.values(slices).map(({hydrateServer}) => hydrateServer?.(req, flecks)),
Object.values(slices).map(({hydrateServer}) => hydrateServer?.(req, flecks)), );
); const preloadedState = reducer(undefined, hydrateServer({flecks, req}));
const preloadedState = reducer(undefined, hydrateServer({flecks, req})); debugSilly(
debugSilly( 'creating redux store with slices(%O) and state(%O)',
'creating redux store with slices(%O) and state(%O)', Object.keys(slices),
Object.keys(slices), preloadedState,
preloadedState, );
); req.redux = await configureStore(flecks, reducer, {preloadedState});
req.redux = await configureStore(flecks, reducer, {preloadedState}); next();
next();
},
'@flecks/web.config': async (req) => ({
'@flecks/redux/client': {
preloadedState: req.redux.getState(),
},
}),
'@flecks/react.providers': (req) => [Provider, {store: req.redux}],
}, },
'@flecks/web.config': async (req) => ({
'@flecks/redux/client': {
preloadedState: req.redux.getState(),
},
}),
'@flecks/react.providers': (req) => [Provider, {store: req.redux}],
}; };

View File

@ -1,30 +1,26 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define REPL commands.
[Hooks]: { *
/** * Note: commands will be prefixed with a period in the Node REPL.
* Define REPL commands. */
* '@flecks/repl.commands': () => ({
* Note: commands will be prefixed with a period in the Node REPL. someCommand: (...args) => {
*/ // args are passed from the Node REPL. So, you could invoke it like:
'@flecks/repl.commands': () => ({ // .someCommand foo bar
someCommand: (...args) => { // and `args` would be `['foo', 'bar']`.
// 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',
};
}, },
}),
/**
* 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',
};
}, },
}; };

View File

@ -1,11 +1,7 @@
import {Hooks} from '@flecks/core';
import commands from './commands'; import commands from './commands';
import {createReplServer} from './repl'; import {createReplServer} from './repl';
export default { export const hooks = {
[Hooks]: { '@flecks/core.commands': commands,
'@flecks/core.commands': commands, '@flecks/server.up': (flecks) => createReplServer(flecks),
'@flecks/server.up': (flecks) => createReplServer(flecks),
},
}; };

View File

@ -1,12 +1,8 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define sequential actions to run when the server comes up.
[Hooks]: { */
/** '@flecks/server.up': async () => {
* Define sequential actions to run when the server comes up. await youCanDoAsyncThingsHere();
*/
'@flecks/server.up': async () => {
await youCanDoAsyncThingsHere();
},
}, },
}; };

View File

@ -33,7 +33,8 @@ const {version} = require('../package.json');
rcs, rcs,
}); });
try { 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!'); debug('up!');
} }
catch (error) { catch (error) {

View File

@ -1,28 +1,24 @@
import {Hooks} from '@flecks/core'; export const hooks = {
'@flecks/core.config': () => ({
export default { /**
[Hooks]: { * Whether HMR is enabled.
'@flecks/core.config': () => ({ */
/** hot: false,
* Whether HMR is enabled. /**
*/ * Arguments to pass along to node. See: https://nodejs.org/api/cli.html
hot: false, */
/** nodeArgs: [],
* Arguments to pass along to node. See: https://nodejs.org/api/cli.html /**
*/ * Whether to start the server after building.
nodeArgs: [], */
/** start: true,
* Whether to start the server after building. /**
*/ * Webpack stats configuration when building server target.
start: true, */
/** stats: {
* Webpack stats configuration when building server target. chunks: false,
*/ colors: true,
stats: { modules: false,
chunks: false, },
colors: true, }),
modules: false,
},
}),
},
}; };

View File

@ -1,7 +1,3 @@
import {Hooks} from '@flecks/core'; export const hooks = {
'@flecks/core.targets': () => ['server'],
export default {
[Hooks]: {
'@flecks/core.targets': () => ['server'],
},
}; };

View File

@ -1,65 +1,61 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Modify Socket.io client configuration.
[Hooks]: { *
/** * See: https://socket.io/docs/v4/client-options/
* Modify Socket.io client configuration. */
* '@flecks/socket.client': () => ({
* See: https://socket.io/docs/v4/client-options/ timeout: Infinity,
*/ }),
'@flecks/socket.client': () => ({ /**
timeout: Infinity, * Define server-side intercom channels.
}), */
/** '@flecks/socket.intercom': (req) => ({
* Define server-side intercom channels. // This would have been called like:
*/ // `const result = await req.intercom('someChannel', payload)`.
'@flecks/socket.intercom': (req) => ({ // `result` will be an `n`-length array, where `n` is the number of server instances. Each
// This would have been called like: // element in the array will be the result of `someServiceSpecificInformation()` running
// `const result = await req.intercom('someChannel', payload)`. // against that server instance.
// `result` will be an `n`-length array, where `n` is the number of server instances. Each someChannel: async (payload, server) => {
// element in the array will be the result of `someServiceSpecificInformation()` running return someServiceSpecificInformation();
// 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();
}, },
}),
/**
* 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();
}, },
}; };

View File

@ -1,20 +1,16 @@
import {Hooks} from '@flecks/core';
import SocketClient from './socket'; import SocketClient from './socket';
export default { export const hooks = {
[Hooks]: { '@flecks/web/client.up': (flecks) => {
'@flecks/web/client.up': (flecks) => { const socket = new SocketClient(flecks);
const socket = new SocketClient(flecks); flecks.set('$flecks/socket.socket', socket);
flecks.set('$flecks/socket.socket', socket); socket.connect();
socket.connect(); socket.listen();
socket.listen();
},
'@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({
cors: {
origin: false,
},
path: `/${id}/socket.io`,
}),
}, },
'@flecks/socket.client': ({config: {'@flecks/core': {id}}}) => ({
cors: {
origin: false,
},
path: `/${id}/socket.io`,
}),
}; };

View File

@ -1,5 +1,3 @@
import {Hooks} from '@flecks/core';
import badPacketsCheck from './packet/bad-packets-check'; import badPacketsCheck from './packet/bad-packets-check';
import Bundle from './packet/bundle'; import Bundle from './packet/bundle';
import Redirect from './packet/redirect'; import Redirect from './packet/redirect';
@ -9,28 +7,26 @@ export {default as normalize} from './normalize';
export * from './hooks'; export * from './hooks';
export {default as Packet, Packer, ValidationError} from './packet'; export {default as Packet, Packer, ValidationError} from './packet';
export default { export const hooks = {
[Hooks]: { '@flecks/core.starting': (flecks) => {
'@flecks/core.starting': (flecks) => { flecks.set('$flecks/socket.packets', flecks.gather(
flecks.set('$flecks/socket.packets', flecks.gather( '@flecks/socket.packets',
'@flecks/socket.packets', {check: badPacketsCheck},
{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,
}),
}, },
'@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,
}),
}; };

View File

@ -1,25 +1,21 @@
import {Hooks} from '@flecks/core';
import createIntercom from './create-intercom'; import createIntercom from './create-intercom';
import Sockets from './sockets'; import Sockets from './sockets';
export default { export const hooks = {
[Hooks]: { '@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => {
'@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => { req.intercom = createIntercom(sockets, 'web');
req.intercom = createIntercom(sockets, 'web'); next();
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`,
}),
}, },
'@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`,
}),
}; };

View File

@ -1,15 +1,10 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Modify express-session configuration.
[Hooks]: { *
/** * See: https://www.npmjs.com/package/express-session
* Modify express-session configuration. */
* '@flecks/user.session': () => ({
* See: https://www.npmjs.com/package/express-session saveUninitialized: true,
*/ }),
'@flecks/user.session': () => ({
saveUninitialized: true,
}),
},
}; };

View File

@ -1,19 +1,15 @@
import {Hooks} from '@flecks/core';
import {Logout} from './packets'; import {Logout} from './packets';
import {user, users} from './state'; import {user, users} from './state';
export * from './state'; export * from './state';
export default { export const hooks = {
[Hooks]: { '@flecks/redux.slices': () => ({
'@flecks/redux.slices': () => ({ user,
user, users,
users, }),
}), '@flecks/socket.packets': (flecks) => ({
'@flecks/socket.packets': (flecks) => ({ Logout: Logout(flecks),
Logout: Logout(flecks), }),
}),
},
}; };

View File

@ -1,70 +1,68 @@
import {randomBytes} from 'crypto'; import {randomBytes} from 'crypto';
import {Flecks, Hooks} from '@flecks/core'; import {Flecks} from '@flecks/core';
import passport from 'passport'; import passport from 'passport';
import LocalStrategy from 'passport-local'; import LocalStrategy from 'passport-local';
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Path to redirect to after failed login.
* Path to redirect to after failed login. */
*/ failureRedirect: '/',
failureRedirect: '/', /**
/** * Path to redirect to after successful login.
* Path to redirect to after successful login. */
*/ successRedirect: '/',
successRedirect: '/', }),
}), '@flecks/db/server.models.decorate': (
'@flecks/db/server.models.decorate': ( Flecks.decorate(require.context('./models/decorators', false, /\.js$/))
Flecks.decorate(require.context('./models/decorators', false, /\.js$/)) ),
), '@flecks/web.routes': (flecks) => {
'@flecks/web.routes': (flecks) => { const {failureRedirect, successRedirect} = flecks.get('@flecks/user/local/server');
const {failureRedirect, successRedirect} = flecks.get('@flecks/user/local/server'); return [
return [ {
{ method: 'post',
method: 'post', path: '/auth/local',
path: '/auth/local', middleware: passport.authenticate('local', {failureRedirect, successRedirect}),
middleware: passport.authenticate('local', {failureRedirect, successRedirect}), },
}, ];
]; },
}, '@flecks/repl.commands': (flecks) => {
'@flecks/repl.commands': (flecks) => { const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.get('$flecks/db.models'); return {
return { createUser: async (spec) => {
createUser: async (spec) => { const [email, maybePassword] = spec.split(' ', 2);
const [email, maybePassword] = spec.split(' ', 2); const password = maybePassword || randomBytes(8).toString('hex');
const password = maybePassword || randomBytes(8).toString('hex'); const user = User.build({email});
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.addHashedPassword(password);
await user.save(); await user.save();
}, return `\nNew password: ${password}\n\n`;
resetPassword: async (email) => { }
const password = randomBytes(8).toString('hex'); 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}}); const user = await User.findOne({where: {email}});
if (user) { fn(undefined, user && await user.validatePassword(password) && user);
await user.addHashedPassword(password); }
await user.save(); catch (error) {
return `\nNew password: ${password}\n\n`; fn(error);
} }
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);
}
},
));
},
}, },
}; };

View File

@ -1,85 +1,83 @@
import {D, Flecks, Hooks} from '@flecks/core'; import {D, Flecks} from '@flecks/core';
import passport from 'passport'; import passport from 'passport';
import LogOps from 'passport/lib/http/request'; import LogOps from 'passport/lib/http/request';
const debug = D('@flecks/user/passport'); const debug = D('@flecks/user/passport');
const debugSilly = debug.extend('silly'); const debugSilly = debug.extend('silly');
export default { export const hooks = {
[Hooks]: { '@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)), '@flecks/web/server.request.route': (flecks) => (req, res, next) => {
'@flecks/web/server.request.route': (flecks) => (req, res, next) => { debugSilly('@flecks/web/server.request.route: passport.initialize()');
debugSilly('@flecks/web/server.request.route: passport.initialize()'); passport.initialize()(req, res, () => {
passport.initialize()(req, res, () => { debugSilly('@flecks/web/server.request.route: passport.session()');
debugSilly('@flecks/web/server.request.route: passport.session()'); passport.session()(req, res, () => {
passport.session()(req, res, () => { if (!req.user) {
if (!req.user) { const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.get('$flecks/db.models'); req.user = new User();
req.user = new User(); req.user.id = 0;
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);
} }
next();
}); });
}, });
'@flecks/socket.intercom': () => ({ },
'@flecks/user/users': async (sids, server) => { '@flecks/web.routes': () => [
const sockets = await server.sockets(); {
return sids method: 'get',
.filter((sid) => sockets.has(sid)) path: '/auth/logout',
.reduce( middleware: (req, res) => {
(r, sid) => ({ req.logout();
...r, res.redirect('/');
[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();
});
});
}, },
],
'@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();
});
});
}, },
}; };

View File

@ -1,59 +1,57 @@
import {D, Hooks} from '@flecks/core'; import {D} from '@flecks/core';
import express from 'express'; import express from 'express';
import expressSession from 'express-session'; import expressSession from 'express-session';
const debug = D('@flecks/user/session'); const debug = D('@flecks/user/session');
const debugSilly = debug.extend('silly'); const debugSilly = debug.extend('silly');
export default { export const hooks = {
[Hooks]: { '@flecks/core.config': () => ({
'@flecks/core.config': () => ({ /**
/** * Set the cookie secret for session encryption.
* Set the cookie secret for session encryption. *
* * See: http://expressjs.com/en/resources/middleware/cookie-parser.html
* See: http://expressjs.com/en/resources/middleware/cookie-parser.html */
*/ cookieSecret: (
cookieSecret: ( 'Set the FLECKS_ENV_FLECKS_USER_SESSION_SERVER_cookieSecret environment variable!'
'Set the FLECKS_ENV_FLECKS_USER_SESSION_SERVER_cookieSecret environment variable!' ),
), }),
}), '@flecks/web/server.request.route': (flecks) => {
'@flecks/web/server.request.route': (flecks) => { const urle = express.urlencoded({extended: true});
const urle = express.urlencoded({extended: true}); return (req, res, next) => {
return (req, res, next) => { debugSilly('@flecks/web/server.request.route: express.urlencoded()');
debugSilly('@flecks/web/server.request.route: express.urlencoded()'); urle(req, res, (error) => {
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) { if (error) {
next(error); next(error);
return; return;
} }
debugSilly('@flecks/web/server.request.route: session()'); debugSilly('session ID: %s', req.session.id);
flecks.get('$flecks/user.session')(req, res, (error) => { next();
if (error) {
next(error);
return;
}
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();
});
}, },
}; };

View File

@ -1,62 +1,58 @@
import {Hooks} from '@flecks/core'; export const hooks = {
/**
export default { * Define sequential actions to run when the client comes up.
[Hooks]: { */
/** '@flecks/web/client.up': async () => {
* Define sequential actions to run when the client comes up. await youCanDoAsyncThingsHere();
*/ },
'@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. * Define HTTP routes.
*/ */
'@flecks/web.config': (req) => ({ '@flecks/web.routes': () => [
someClientFleck: { {
someConfig: req.someConfig, 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();
}, },
}; };

View File

@ -1,6 +1,6 @@
import {D, Flecks} from '@flecks/core'; 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'); const {version} = require('@flecks/web/package.json');
(async () => { (async () => {
@ -56,7 +56,8 @@ const {version} = require('@flecks/web/package.json');
const flecks = new Flecks(runtime); const flecks = new Flecks(runtime);
window.flecks = flecks; window.flecks = flecks;
try { 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'; window.document.querySelector('#root').style.display = 'block';
debug('up!'); debug('up!');
} }

View File

@ -1,7 +1,7 @@
import {stat, unlink} from 'fs/promises'; import {stat, unlink} from 'fs/promises';
import {join} from 'path'; import {join} from 'path';
import {D, Hooks} from '@flecks/core'; import {D} from '@flecks/core';
import {Flecks, spawnWith} from '@flecks/core/server'; import {Flecks, spawnWith} from '@flecks/core/server';
import augmentBuild from './augment-build'; import augmentBuild from './augment-build';
@ -16,199 +16,197 @@ const debug = D('@flecks/web/server');
export {augmentBuild}; export {augmentBuild};
export default { export const hooks = {
[Hooks]: { '@flecks/core.build': augmentBuild,
'@flecks/core.build': augmentBuild, '@flecks/core.build.alter': async (neutrinoConfigs, flecks) => {
'@flecks/core.build.alter': async (neutrinoConfigs, flecks) => { // Don't build if there's a fleck target.
// Don't build if there's a fleck target. if (neutrinoConfigs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) {
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 // eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.web; delete neutrinoConfigs['web-vendor'];
return;
} }
// Only build vendor in dev. // Only build if something actually changed.
if (neutrinoConfigs['web-vendor']) { const dll = flecks.get('@flecks/web/server.dll');
if (process.argv.find((arg) => 'production' === arg)) { if (dll.length > 0) {
// eslint-disable-next-line no-param-reassign const manifest = join(
delete neutrinoConfigs['web-vendor']; 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. // eslint-disable-next-line no-empty
const dll = flecks.get('@flecks/web/server.dll'); catch (error) {}
if (dll.length > 0) { let latest = 0;
const manifest = join( for (let i = 0; i < dll.length; ++i) {
FLECKS_CORE_ROOT, const path = dll[i];
'node_modules',
'.cache',
'flecks',
'web-vendor.manifest.json',
);
let timestamp = 0;
try { try {
const stats = await stat(manifest); // eslint-disable-next-line no-await-in-loop
timestamp = stats.mtime; const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path));
if (stats.mtime > latest) {
latest = stats.mtime;
}
} }
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
catch (error) {} catch (error) {}
let latest = 0; }
for (let i = 0; i < dll.length; ++i) { if (timestamp > latest) {
const path = dll[i]; // eslint-disable-next-line no-param-reassign
try { delete neutrinoConfigs['web-vendor'];
// eslint-disable-next-line no-await-in-loop }
const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path)); else if (timestamp > 0) {
if (stats.mtime > latest) { await unlink(manifest);
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);
}
} }
} }
// Bail if there's no web build. }
if (!neutrinoConfigs.web) { // Bail if there's no web build.
return; if (!neutrinoConfigs.web) {
} return;
// Bail if the build isn't watching. }
if (!process.argv.find((arg) => '--watch' === arg)) { // Bail if the build isn't watching.
return; if (!process.argv.find((arg) => '--watch' === arg)) {
} return;
// Otherwise, spawn `webpack-dev-server` (WDS). }
const cmd = [ // Otherwise, spawn `webpack-dev-server` (WDS).
'npx', 'webpack-dev-server', const cmd = [
'--mode', 'development', 'npx', 'webpack-dev-server',
'--hot', '--mode', 'development',
'--config', flecks.buildConfig('webpack.config.js'), '--hot',
]; '--config', flecks.buildConfig('webpack.config.js'),
spawnWith( ];
cmd, 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) => [
{ {
method: 'get', env: {
path: '/flecks.config.js', FLECKS_CORE_BUILD_LIST: 'web',
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, // Remove the build config since we're handing off to WDS.
'@flecks/server.up': (flecks) => createHttpServer(flecks), // eslint-disable-next-line no-param-reassign
'@flecks/repl.context': (flecks) => ({ delete neutrinoConfigs.web;
httpServer: flecks.get('$flecks/web/server.instance'),
}),
}, },
'@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'),
}),
}; };