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">
<h1>flecks</h1>
<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
a highly dynamic structure encourage consistency while allowing you to easily express your own
opinions.

View File

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

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());
// AirBnb linting.
config.use.unshift(
airbnb({
eslint: {
@ -45,13 +47,13 @@ config.use.unshift(
}),
);
// Include a shebang and set the executable bit..
config.use.push(banner({
banner: '#!/usr/bin/env node',
include: /^cli\.js$/,
pluginId: 'shebang',
raw: true,
}))
config.use.push(({config}) => {
config
.plugin('executable')

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).
To define hooks (and turn your plain ol' boring JS modules into beautiful interesting flecks), you only have to import the `Hooks` symbol and key your default export:
To define hooks (and turn your plain ol' boring JS modules into beautiful interesting flecks), you only have to export a `hooks` object:
```javascript
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
'@flecks/core.starting': () => {
console.log('hello, gorgeous');
},
export const hooks = {
'@flecks/core.starting': () => {
console.log('hello, gorgeous');
},
};
```
@ -133,15 +129,15 @@ assert(foo.type === 'Foo');
```javascript
{
// The property added when extending the class to return the numeric ID.
idAttribute = 'id',
idProperty = 'id',
// The property added when extending the class to return the type.
typeAttribute = 'type',
typeProperty = 'type',
// A function called with the `Gathered` object to allow checking validity.
check = () => {},
}
```
As an example, when `@flecks/db/server` gathers models, `typeAttribute` is set to `name`, because Sequelize requires its model classes to have a unique `name` property.
As an example, when `@flecks/db/server` gathers models, `typeProperty` is set to `name`, because Sequelize requires its model classes to have a unique `name` property.
**Note:** the numeric IDs are useful for efficient serialization between the client and server, but **if you are using this property, ensure that `flecks.gather()` is called equivalently on both the client and the server**. As a rule of thumb, if you have serializable `Gathered`s, they should be invoked and defined in `your-fleck`, and not in `your-fleck/[platform]`, so that they are invoked for every platform.
@ -152,19 +148,15 @@ Complementary to gather hooks above, `Flecks.provide()` allows you to ergonomica
Here's an example of how you could manually provide `@flecks/db/server.models` in your own fleck:
```javascript
import {Hooks} foom '@flecks/core';
import SomeModel from './models/some-model';
import AnotherModel from './models/another-model';
export default {
[Hooks]: {
'@flecks/db/server.models': () => ({
SomeModel,
AnotherModel,
}),
},
};
export const hooks = {
'@flecks/db/server.models': () => ({
SomeModel,
AnotherModel,
}),
}
```
If you think about the example above, you might realize that it will become a lot of typing to keep adding new models over time. Provider hooks exist to reduce this maintenance burden for you.
@ -183,12 +175,10 @@ models/
then, this `index.js`:
```javascript
import {Flecks, Hooks} from '@flecks/core';
import {Flecks} from '@flecks/core';
export default {
[Hooks]: {
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
},
export const hooks = {
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
};
```
@ -212,31 +202,27 @@ is *exactly equivalent* to the gather example above. By default, `Flecks.provide
When a Model (or any other) is gathered as above, an implicit hook is called: `${hook}.decorate`. This allows other flecks to decorate whatever has been gathered:
```javascript
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
'@flecks/db/server.models.decorate': (Models) => {
return {
...Models,
User: class extends Models.User {
// Let's mix in some logging...
constructor(...args) {
super(...args);
console.log ('Another user decorated!');
}
},
};
},
export const hooks = {
'@flecks/db/server.models.decorate': (Models) => {
return {
...Models,
User: class extends Models.User {
// Let's mix in some logging...
constructor(...args) {
super(...args);
console.log ('Another user decorated!');
}
},
};
},
};
```
#### `Flecks.decorate(context, options)`
As with above, there exists an API for making the maintenance of decorators more ergonomic.
As with above, there exists an API for making the maintenance of decorators even more ergonomic.
Supposing our fleck is structured like so:
@ -266,12 +252,12 @@ export default (User) => {
then, this `index.js`:
```javascript
import {Flecks, Hooks} from '@flecks/core';
import {Flecks} from '@flecks/core';
export default {
[Hooks]: {
'@flecks/db/server.models.decorate': Flecks.decorate(require.context('./models/decorators', false, /\.js$/)),
},
export const hooks = {
'@flecks/db/server.models.decorate': (
Flecks.decorate(require.context('./models/decorators', false, /\.js$/))
),
};
```
@ -307,7 +293,7 @@ Our `flecks.yml` could be configured like so:
In this application, when `@flecks/http/server.request.route` is invoked, `@flecks/user/session`'s implementation is invoked (which reifies the user's session from cookies), followed by `my-cool-fleck`'s (which, we assume, does some kind of very cool dark mode check).
### Ellipses
### Ellipses/elision
It may not always be ergonomic to configure the order of every single implementation, but enough to specify which implementations must run first (or last).

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

View File

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

View File

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

View File

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

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 debugSilly = debug.extend('silly');
// Symbols for Gathered classes.
export const ById = Symbol.for('@flecks/core.byId');
export const ByType = Symbol.for('@flecks/core.byType');
export const Hooks = Symbol.for('@flecks/core.hooks');
/**
* Capitalize a string.
*
* @param {string} string
* @returns {string}
*/
const capitalize = (string) => string.substring(0, 1).toUpperCase() + string.substring(1);
/**
* CamelCase a string.
*
* @param {string} string
* @returns {string}
*/
const camelCase = (string) => string.split(/[_-]/).map(capitalize).join('');
// Track gathered for HMR.
const hotGathered = new Map();
const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => {
// Wrap classes to expose their flecks ID and type.
const wrapGathered = (Class, id, idProperty, type, typeProperty) => {
class Subclass extends Class {
static get [idAttribute]() {
static get [idProperty]() {
return id;
}
static get [typeAttribute]() {
static get [typeProperty]() {
return type;
}
@ -43,72 +57,121 @@ const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => {
export default class Flecks {
config = {};
flecks = {};
hooks = {};
platforms = {};
/**
* @param {object} init
* @param {object} init.config The Flecks configuration (e.g. loaded from `flecks.yml`).
* @param {string[]} init.platforms Platforms this instance is running on.
*/
constructor({
config = {},
flecks = {},
platforms = [],
} = {}) {
this.config = {
...Object.fromEntries(Object.keys(flecks).map((path) => [path, {}])),
...config,
};
this.hooks = {};
this.flecks = {};
const emptyConfigForAllFlecks = Object.fromEntries(
Object.keys(flecks).map((path) => [path, {}]),
);
this.config = {...emptyConfigForAllFlecks, ...config};
this.platforms = platforms;
const entries = Object.entries(flecks);
debugSilly('paths: %O', entries.map(([fleck]) => fleck));
for (let i = 0; i < entries.length; i++) {
const [fleck, M] = entries[i];
this.registerFleck(fleck, M);
this.registerFleckHooks(fleck, M);
this.invoke('@flecks/core.registered', fleck, M);
}
this.configureFlecks();
this.configureFlecksDefaults();
debugSilly('config: %O', this.config);
}
configureFleck(fleck) {
/**
* Configure defaults for a fleck.
*
* @param {string} fleck
* @protected
*/
configureFleckDefaults(fleck) {
this.config[fleck] = {
...this.invokeFleck('@flecks/core.config', fleck),
...this.config[fleck],
};
}
configureFlecks() {
const defaultConfig = this.invoke('@flecks/core.config');
const flecks = Object.keys(defaultConfig);
/**
* Configure defaults for all flecks.
*
* @protected
*/
configureFlecksDefaults() {
const flecks = this.flecksImplementing('@flecks/core.config');
for (let i = 0; i < flecks.length; i++) {
this.configureFleck(flecks[i]);
this.configureFleckDefaults(flecks[i]);
}
}
/**
* [Dasherize]{@link https://en.wiktionary.org/wiki/dasherize} a fleck path.
*
* @param {string} path The path to dasherize.
* @returns {string}
*/
static dasherizePath(path) {
const parts = dirname(path).split('/');
if ('.' === parts[0]) {
parts.shift();
}
if ('index' === parts[parts.length - 1]) {
parts.pop();
}
return join(parts.join('-'), basename(path, extname(path)));
}
/**
* Generate a decorator from a require context.
*
* @param {*} context @see {@link https://webpack.js.org/guides/dependency-management/#requirecontext}
* @param {object} config
* @param {function} [config.transformer = {@link camelCase}]
* Function to run on each context path.
* @returns {function} The decorator.
*/
static decorate(
context,
{
transformer = camelCase,
} = {},
) {
return (Gathered, flecks) => {
return (Gathered, flecks) => (
context.keys()
.forEach((path) => {
const {default: M} = context(path);
if ('function' !== typeof M) {
throw new ReferenceError(
`Flecks.decorate(): require(${
path
}).default is not a function (from: ${
context.id
})`,
);
}
const key = transformer(this.symbolizePath(path));
if (Gathered[key]) {
// eslint-disable-next-line no-param-reassign
Gathered[key] = M(Gathered[key], flecks);
}
});
return Gathered;
};
.reduce(
(Gathered, path) => {
const key = transformer(this.dasherizePath(path));
if (!Gathered[key]) {
return Gathered;
}
const {default: M} = context(path);
if ('function' !== typeof M) {
throw new ReferenceError(
`Flecks.decorate(): require(${path}).default is not a function (from: ${context.id})`,
);
}
return {...Gathered, [key]: M(Gathered[key], flecks)};
},
Gathered,
)
);
}
/**
* Destroy this instance.
*/
destroy() {
this.config = {};
this.hooks = {};
@ -116,12 +179,20 @@ export default class Flecks {
this.platforms = [];
}
/**
* Lists all flecks implementing a hook, including platform-specific and elided variants.
*
* @param {string} hook
* @returns {string[]} The expanded list of flecks.
*/
expandedFlecks(hook) {
const flecks = this.lookupFlecks(hook);
let expanded = [];
for (let i = 0; i < flecks.length; ++i) {
const fleck = flecks[i];
// Just the fleck.
expanded.push(fleck);
// Platform-specific variants.
for (let j = 0; j < this.platforms.length; ++j) {
const platform = this.platforms[j];
const variant = join(fleck, platform);
@ -130,6 +201,7 @@ export default class Flecks {
}
}
}
// Expand elided flecks.
const index = expanded.findIndex((fleck) => '...' === fleck);
if (-1 !== index) {
if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) {
@ -158,33 +230,66 @@ export default class Flecks {
return expanded;
}
/**
* Get the module for a fleck.
*
* @param {*} fleck
*
* @returns {*}
*/
fleck(fleck) {
return this.flecks[fleck];
}
/**
* Test whether a fleck implements a hook.
*
* @param {*} fleck
* @param {string} hook
* @returns {boolean}
*/
fleckImplements(fleck, hook) {
return !!this.hooks[hook].find(({fleck: candidate}) => fleck === candidate);
}
/**
* Get a list of flecks implementing a hook.
*
* @param {string} hook
* @returns {string[]}
*/
flecksImplementing(hook) {
return this.hooks[hook]?.map(({fleck}) => fleck) || [];
}
/**
* Gather and register class types.
*
* @param {string} hook
* @param {object} config
* @param {string} [config.idProperty='id'] The property used to get/set the class ID.
* @param {string} [config.typeProperty='type'] The property used to get/set the class type.
* @param {function} [config.check=() => {}] Check the validity of the gathered classes.
* @returns {object} An object with keys for ID, type, {@link ById}, and {@link ByType}.
*/
gather(
hook,
{
idAttribute = 'id',
typeAttribute = 'type',
idProperty = 'id',
typeProperty = 'type',
check = () => {},
} = {},
) {
if (!hook || 'string' !== typeof hook) {
throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string');
}
// Gather classes and check.
const raw = this.invokeMerge(hook);
check(raw, hook);
// Decorate and check.
const decorated = this.invokeComposed(`${hook}.decorate`, raw);
check(decorated, `${hook}.decorate`);
// Assign unique IDs to each class and sort by type.
let uid = 1;
const ids = {};
const types = (
@ -193,50 +298,78 @@ export default class Flecks {
.sort(([ltype], [rtype]) => (ltype < rtype ? -1 : 1))
.map(([type, Class]) => {
const id = uid++;
ids[id] = wrapperClass(Class, id, idAttribute, type, typeAttribute);
ids[id] = wrapGathered(Class, id, idProperty, type, typeProperty);
return [type, ids[id]];
}),
)
);
// Conglomerate all ID and type keys along with Symbols for accessing either/or.
const gathered = {
...ids,
...types,
[ById]: ids,
[ByType]: types,
};
hotGathered.set(hook, {idAttribute, gathered, typeAttribute});
// Register for HMR?
if (module.hot) {
hotGathered.set(hook, {idProperty, gathered, typeProperty});
}
debug("gathered '%s': %O", hook, Object.keys(gathered[ByType]));
return gathered;
}
/**
* Get a configuration value.
*
* @param {string} path The configuration path e.g. `@flecks/example.config`.
* @param {*} defaultValue The default value if no configuration value is found.
* @returns {*}
*/
get(path, defaultValue) {
return get(this.config, path, defaultValue);
}
/**
* Return an object whose keys are fleck paths and values are the result of invoking the hook.
* @param {string} hook
* @param {...any} args Arguments passed to each implementation.
* @returns {*}
*/
invoke(hook, ...args) {
if (!this.hooks[hook]) {
return {};
}
return this.flecksImplementing(hook)
.reduce((r, fleck) => ({
...r,
[fleck]: this.invokeFleck(hook, fleck, ...args),
}), {});
.reduce((r, fleck) => ({...r, [fleck]: this.invokeFleck(hook, fleck, ...args)}), {});
}
invokeComposed(hook, arg, ...args) {
/**
* See: [function composition](https://www.educative.io/edpresso/function-composition-in-javascript).
*
* @configurable
* @param {string} hook
* @param {*} initial The initial value passed to the composition chain.
* @param {...any} args The arguments passed after the accumulator to each implementation.
* @returns {*} The final composed value.
*/
invokeComposed(hook, initial, ...args) {
if (!this.hooks[hook]) {
return arg;
return initial;
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return arg;
return initial;
}
return flecks
.filter((fleck) => this.fleckImplements(fleck, hook))
.reduce((r, fleck) => this.invokeFleck(hook, fleck, r, ...args), arg);
.reduce((r, fleck) => this.invokeFleck(hook, fleck, r, ...args), initial);
}
/**
* An async version of `invokeComposed`.
*
* @see {@link Flecks#invokeComposed}
*/
async invokeComposedAsync(hook, arg, ...args) {
if (!this.hooks[hook]) {
return arg;
@ -250,6 +383,13 @@ export default class Flecks {
.reduce(async (r, fleck) => this.invokeFleck(hook, fleck, await r, ...args), arg);
}
/**
* Invokes a hook and returns a flat array of results.
*
* @param {string} hook
* @param {...any} args The arguments passed to each implementation.
* @returns {any[]}
*/
invokeFlat(hook, ...args) {
if (!this.hooks[hook]) {
return [];
@ -257,6 +397,14 @@ export default class Flecks {
return this.hooks[hook].map(({fleck}) => this.invokeFleck(hook, fleck, ...args));
}
/**
* Invokes a hook on a single fleck.
*
* @param {string} hook
* @param {*} fleck
* @param {...any} args
* @returns {*}
*/
invokeFleck(hook, fleck, ...args) {
debugSilly('invokeFleck(%s, %s, ...)', hook, fleck);
if (!this.hooks[hook]) {
@ -270,33 +418,116 @@ export default class Flecks {
return candidate.fn(...(args.concat(this)));
}
static $$invokeMerge(r, o) {
return {...r, ...o};
}
/**
* Specialization of `invokeReduce`. Invokes a hook and reduces an object from all the resulting
* objects.
*
* @param {string} hook
* @param {...any} args
* @returns {object}
*/
invokeMerge(hook, ...args) {
return this.invokeReduce(hook, (r, o) => ({...r, ...o}), {}, ...args);
return this.invokeReduce(hook, this.constructor.$$invokeMerge, {}, ...args);
}
/**
* An async version of `invokeMerge`.
*
* @see {@link Flecks#invokeMerge}
*/
async invokeMergeAsync(hook, ...args) {
return this.invokeReduceAsync(hook, (r, o) => ({...r, ...o}), {}, ...args);
return this.invokeReduceAsync(hook, this.constructor.$$invokeMerge, {}, ...args);
}
static $$invokeMergeUnique() {
const track = {};
return (r, o, fleck, hook) => {
const keys = Object.keys(o);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
if (track[key]) {
throw new ReferenceError(
`Conflict in ${hook}: '${track[key]}' implemented '${key}', followed by '${fleck}'`,
);
}
track[key] = fleck;
}
return ({...r, ...o});
};
}
/**
* Specialization of `invokeMerge`. Invokes a hook and reduces an object from all the resulting
* objects.
*
* @param {string} hook
* @param {...any} args
* @returns {object}
*/
invokeMergeUnique(hook, ...args) {
return this.invokeReduce(hook, this.constructor.$$invokeMergeUnique(), {}, ...args);
}
/**
* An async version of `invokeMergeUnique`.
*
* @see {@link Flecks#invokeMergeUnique}
*/
async invokeMergeUniqueAsync(hook, ...args) {
return this.invokeReduceAsync(hook, this.constructor.$$invokeMergeUnique(), {}, ...args);
}
/**
* See: [Array.prototype.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)
*
* @param {string} hook
* @param {*} reducer
* @param {*} initial
* @param {...any} args The arguments passed after the accumulator to each implementation.
* @returns {*}
*/
invokeReduce(hook, reducer, initial, ...args) {
if (!this.hooks[hook]) {
return initial;
}
return this.hooks[hook]
.reduce((r, {fleck}) => reducer(r, this.invokeFleck(hook, fleck, ...args)), initial);
.reduce(
(r, {fleck}) => reducer(r, this.invokeFleck(hook, fleck, ...args), fleck, hook),
initial,
);
}
/**
* An async version of `invokeReduce`.
*
* @see {@link Flecks#invokeReduce}
*/
async invokeReduceAsync(hook, reducer, initial, ...args) {
if (!this.hooks[hook]) {
return initial;
}
return this.hooks[hook]
.reduce(
async (r, {fleck}) => reducer(await r, await this.invokeFleck(hook, fleck, ...args)),
async (r, {fleck}) => (
reducer(await r, await this.invokeFleck(hook, fleck, ...args), fleck, hook)
),
initial,
);
}
/**
* Invokes hooks on a fleck one after another. This is effectively a configurable version of
* {@link Flecks#invokeFlat}.
*
* @configurable
* @param {string} hook
* @param {...any} args The arguments passed to each implementation.
* @returns {any[]}
*/
invokeSequential(hook, ...args) {
if (!this.hooks[hook]) {
return [];
@ -315,6 +546,11 @@ export default class Flecks {
return results;
}
/**
* An async version of `invokeSequential`.
*
* @see {@link Flecks#invokeSequential}
*/
async invokeSequentialAsync(hook, ...args) {
if (!this.hooks[hook]) {
return [];
@ -334,10 +570,18 @@ export default class Flecks {
return results;
}
isOnPlatform(platform) {
return -1 !== this.platforms.indexOf(platform);
}
/**
* Lookup flecks configured for a hook.
*
* If no configuration is found, defaults to ellipses.
*
* @param {string} hook
* @example
* # Given hook @flecks/example.hook, `flecks.yml` could be configured as such:
* '@flecks/example':
* hook: ['...']
* @returns {string[]}
*/
lookupFlecks(hook) {
const index = hook.indexOf('.');
if (-1 === index) {
@ -346,31 +590,37 @@ export default class Flecks {
return this.get([hook.slice(0, index), hook.slice(index + 1)], ['...']);
}
/**
* Make a middleware function from configured middleware.
* @param {string} hook
* @returns {function}
*/
makeMiddleware(hook) {
debugSilly('makeMiddleware(...): %s', hook);
if (!this.hooks[hook]) {
return Promise.resolve();
return (...args) => args.pop()();
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return Promise.resolve();
return (...args) => args.pop()();
}
const middleware = flecks
.filter((fleck) => this.fleckImplements(fleck, hook));
debugSilly('middleware: %O', middleware);
const instance = new Middleware(middleware.map((fleck) => this.invokeFleck(hook, fleck)));
return async (...args) => {
const next = args.pop();
try {
await instance.promise(...args);
next();
}
catch (error) {
next(error);
}
};
return instance.dispatch.bind(instance);
}
/**
* Provide classes for e.g. {@link Flecks#gather}
*
* @param {*} context @see {@link https://webpack.js.org/guides/dependency-management/#requirecontext}
* @param {object} config
* @param {function} [config.invoke = true] Invoke the default exports as a function?
* @param {function} [config.transformer = {@link camelCase}]
* Function to run on each context path.
* @returns {object}
*/
static provide(
context,
{
@ -393,7 +643,7 @@ export default class Flecks {
);
}
return [
transformer(this.symbolizePath(path)),
transformer(this.dasherizePath(path)),
invoke ? M(flecks) : M,
];
}),
@ -401,9 +651,103 @@ export default class Flecks {
);
}
/**
* Refresh a fleck's hooks, configuration, and any gathered classes.
*
* @example
* module.hot.accept('@flecks/example', async () => {
* flecks.refresh('@flecks/example', require('@flecks/example'));
* });
* @param {string} fleck
* @param {object} M The fleck module
* @protected
*/
refresh(fleck, M) {
debug('refreshing %s...', fleck);
// Remove old hook implementations.
this.unregisterFleckHooks(fleck);
// Replace the fleck.
this.registerFleckHooks(fleck, M);
// Write config.
this.configureFleckDefaults(fleck);
// HMR.
if (module.hot) {
this.refreshGathered(fleck);
}
}
/**
* Refresh gathered classes for a fleck.
*
* @param {string} fleck
*/
refreshGathered(fleck) {
const it = hotGathered.entries();
for (let current = it.next(); current.done !== true; current = it.next()) {
const {
value: [
hook,
{
idProperty,
gathered,
typeProperty,
},
],
} = current;
const updates = this.invokeFleck(hook, fleck);
if (updates) {
debug('updating gathered %s from %s...', hook, fleck);
const entries = Object.entries(updates);
for (let i = 0, [type, Class] = entries[i]; i < entries.length; ++i) {
const {[type]: {[idProperty]: id}} = gathered;
const Subclass = wrapGathered(Class, id, idProperty, type, typeProperty);
// eslint-disable-next-line no-multi-assign
gathered[type] = gathered[id] = gathered[ById][id] = gathered[ByType][type] = Subclass;
this.invoke('@flecks/core.hmr.gathered', Subclass, hook);
}
}
}
}
/**
* Register hooks for a fleck.
*
* @param {string} fleck
* @param {object} M The fleck module
* @protected
*/
registerFleckHooks(fleck, M) {
debugSilly('registering %s...', fleck);
this.flecks[fleck] = M;
if (M.hooks) {
const keys = Object.keys(M.hooks);
debugSilly("hooks for '%s': %O", fleck, keys);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
if (!this.hooks[key]) {
this.hooks[key] = [];
}
this.hooks[key].push({fleck, fn: M.hooks[key]});
}
}
}
/**
* Set a configuration value.
*
* @param {string} path The configuration path e.g. `@flecks/example.config`.
* @param {*} value The value to set.
* @returns {*} The value that was set.
*/
set(path, value) {
return set(this.config, path, value);
}
/**
* Unregister hooks for a fleck.
* @param {*} fleck
*/
unregisterFleckHooks(fleck) {
const keys = Object.keys(this.hooks);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
@ -414,82 +758,6 @@ export default class Flecks {
}
}
}
// Replace the fleck.
this.registerFleck(fleck, M);
// Write config.
this.configureFleck(fleck);
// HMR.
this.updateHotGathered(fleck);
}
registerFleck(fleck, M) {
debugSilly('registering %s...', fleck);
this.flecks[fleck] = M;
if (M.default) {
const {default: {[Hooks]: hooks}} = M;
if (hooks) {
const keys = Object.keys(hooks);
debugSilly("hooks for '%s': %O", fleck, keys);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
if (!this.hooks[key]) {
this.hooks[key] = [];
}
this.hooks[key].push({fleck, fn: hooks[key]});
}
}
}
else {
debugSilly("'%s' has no default export", fleck);
}
}
set(path, value) {
return set(this.config, path, value);
}
static symbolizePath(path) {
const parts = dirname(path).split('/');
if ('.' === parts[0]) {
parts.shift();
}
if ('index' === parts[parts.length - 1]) {
parts.pop();
}
return join(parts.join('-'), basename(path, extname(path)));
}
async up(hook) {
await Promise.all(this.invokeFlat('@flecks/core.starting'));
await this.invokeSequentialAsync(hook);
}
updateHotGathered(fleck) {
const it = hotGathered.entries();
for (let current = it.next(); current.done !== true; current = it.next()) {
const {
value: [
hook,
{
idAttribute,
gathered,
typeAttribute,
},
],
} = current;
const updates = this.invokeFleck(hook, fleck);
if (updates) {
debug('updating gathered %s from %s...', hook, fleck);
const entries = Object.entries(updates);
for (let i = 0, [type, Class] = entries[i]; i < entries.length; ++i) {
const {[type]: {[idAttribute]: id}} = gathered;
const Subclass = wrapperClass(Class, id, idAttribute, type, typeAttribute);
// eslint-disable-next-line no-multi-assign
gathered[type] = gathered[id] = gathered[ById][id] = gathered[ByType][type] = Subclass;
this.invoke('@flecks/core.hmr.gathered', Subclass, hook);
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import {inspect} from 'util';
import airbnb from '@neutrinojs/airbnb';
import neutrino from 'neutrino';
import {Hooks} from '../flecks';
import commands from './commands';
import R from '../bootstrap/require';
@ -31,81 +30,79 @@ export {default as fleck} from '../bootstrap/fleck';
export {default as require} from '../bootstrap/require';
export {JsonStream, transform} from './stream';
export default {
[Hooks]: {
'@flecks/core.build': (target, config, flecks) => {
const {
'eslint.exclude': exclude,
profile,
} = flecks.get('@flecks/core/server');
if (-1 !== profile.indexOf(target)) {
config.use.push(({config}) => {
config
.plugin('profiler')
.use(
R.resolve('webpack/lib/debug/ProfilingPlugin'),
[{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}],
);
});
}
if (-1 === exclude.indexOf(target)) {
const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target));
const webpackConfig = neutrino(config).webpack();
config.use.unshift(
airbnb({
eslint: {
baseConfig: {
...baseConfig,
settings: {
...(baseConfig.settings || {}),
'import/resolver': {
...(baseConfig.settings['import/resolver'] || {}),
webpack: {
config: {
resolve: webpackConfig.resolve,
},
export const hooks = {
'@flecks/core.build': (target, config, flecks) => {
const {
'eslint.exclude': exclude,
profile,
} = flecks.get('@flecks/core/server');
if (-1 !== profile.indexOf(target)) {
config.use.push(({config}) => {
config
.plugin('profiler')
.use(
R.resolve('webpack/lib/debug/ProfilingPlugin'),
[{outputPath: join(FLECKS_CORE_ROOT, `profile.build-${target}.json`)}],
);
});
}
if (-1 === exclude.indexOf(target)) {
const baseConfig = R(flecks.buildConfig('.eslint.defaults.js', target));
const webpackConfig = neutrino(config).webpack();
config.use.unshift(
airbnb({
eslint: {
baseConfig: {
...baseConfig,
settings: {
...(baseConfig.settings || {}),
'import/resolver': {
...(baseConfig.settings['import/resolver'] || {}),
webpack: {
config: {
resolve: webpackConfig.resolve,
},
},
},
},
},
}),
);
}
},
'@flecks/core.build.config': () => [
/**
* Babel configuration. See: https://babeljs.io/docs/en/config-files
*/
'babel.config.js',
/**
* ESLint defaults. The default .eslintrc.js just reads from this file so that the build
* process can dynamically configure parts of ESLint.
*/
['.eslint.defaults.js', {specifier: (specific) => `${specific}.eslint.defaults.js`}],
/**
* ESLint configuration. See: https://eslint.org/docs/user-guide/configuring/
*/
['.eslintrc.js', {specifier: (specific) => `${specific}.eslintrc.js`}],
/**
* Neutrino build configuration. See: https://neutrinojs.org/usage/
*/
['.neutrinorc.js', {specifier: (specific) => `${specific}.neutrinorc.js`}],
/**
* Webpack (v4) configuration. See: https://v4.webpack.js.org/configuration/
*/
'webpack.config.js',
],
'@flecks/core.commands': commands,
'@flecks/core.config': () => ({
/**
* Build targets to exclude from ESLint.
*/
'eslint.exclude': [],
/**
* Build targets to profile with `webpack.debug.ProfilingPlugin`.
*/
profile: [],
}),
},
}),
);
}
},
'@flecks/core.build.config': () => [
/**
* Babel configuration. See: https://babeljs.io/docs/en/config-files
*/
'babel.config.js',
/**
* ESLint defaults. The default .eslintrc.js just reads from this file so that the build
* process can dynamically configure parts of ESLint.
*/
['.eslint.defaults.js', {specifier: (specific) => `${specific}.eslint.defaults.js`}],
/**
* ESLint configuration. See: https://eslint.org/docs/user-guide/configuring/
*/
['.eslintrc.js', {specifier: (specific) => `${specific}.eslintrc.js`}],
/**
* Neutrino build configuration. See: https://neutrinojs.org/usage/
*/
['.neutrinorc.js', {specifier: (specific) => `${specific}.neutrinorc.js`}],
/**
* Webpack (v4) configuration. See: https://v4.webpack.js.org/configuration/
*/
'webpack.config.js',
],
'@flecks/core.commands': commands,
'@flecks/core.config': () => ({
/**
* Build targets to exclude from ESLint.
*/
'eslint.exclude': [],
/**
* Build targets to profile with `webpack.debug.ProfilingPlugin`.
*/
profile: [],
}),
};

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';
chai.use(chaiAsPromised);
const {expect} = chai;
const testOne = require('./one');
const testTwo = require('./two');
@ -33,3 +38,13 @@ it('can invoke merge async', async () => {
expect(await flecks.invokeMergeAsync('@flecks/core/test/invoke-merge-async'))
.to.deep.equal({foo: 69, bar: 420});
});
it('can enforce uniqueness', () => {
expect(() => flecks.invokeMergeUnique('@flecks/core/test/invoke-merge-unique'))
.to.throw(ReferenceError);
});
it('can enforce uniqueness async', async () => {
expect(flecks.invokeMergeUniqueAsync('@flecks/core/test/invoke-merge-unique-async'))
.to.be.rejectedWith(ReferenceError);
});

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

View File

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

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

View File

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

View File

@ -1,27 +1,23 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define docker containers.
*
* Beware: the user running the server must have Docker privileges.
* See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
*/
'@flecks/docker.containers': () => ({
someContainer: {
// Environment variables.
environment: {
SOME_CONTAINER_VAR: 'hello',
},
// The docker image.
image: 'some-image:latest',
// Some container path you'd like to persist. Flecks handles the host path.
mount: '/some/container/path',
// Expose ports.
ports: {3000: 3000},
export const hooks = {
/**
* Define docker containers.
*
* Beware: the user running the server must have Docker privileges.
* See: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
*/
'@flecks/docker.containers': () => ({
someContainer: {
// Environment variables.
environment: {
SOME_CONTAINER_VAR: 'hello',
},
}),
},
// The docker image.
image: 'some-image:latest',
// Some container path you'd like to persist. Flecks handles the host path.
mount: '/some/container/path',
// Expose ports.
ports: {3000: 3000},
},
}),
};

View File

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

View File

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

View File

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

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

View File

@ -1,7 +1,6 @@
import cluster from 'cluster';
import {join} from 'path';
import {Hooks} from '@flecks/core';
import {require as R} from '@flecks/core/server';
import {
app,
@ -21,119 +20,117 @@ async function createWindow(flecks) {
await flecks.invokeSequentialAsync('@flecks/electron/server.window', win);
}
export default {
[Hooks]: {
'@flecks/core.config': () => ({
/**
* Browser window options.
*
* See: https://www.electronjs.org/docs/latest/api/browser-window
*/
browserWindowOptions: {},
/**
* Install devtools extensions (by default).
*
* If `true`, will install some devtools extensions based on which flecks are enabled.
*
* You can pass an array of Chrome store IDs to install a list of custom extensions.
*
* Extensions will not be installed if `'production' === process.env.NODE_ENV`
*/
installExtensions: true,
/**
* Quit the app when all windows are closed.
*/
quitOnClosed: true,
/**
* The URL to load in electron by default.
*
* Defaults to `http://${flecks.get('@flecks/web/server.public')}`.
*/
url: undefined,
}),
'@flecks/core.webpack': (target, config) => {
const StartServerWebpackPlugin = R('start-server-webpack-plugin');
const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin);
// Extremely hackish, c'est la vie.
if (plugin) {
/* eslint-disable no-underscore-dangle */
plugin._startServer = function _startServerHacked(callback) {
const execArgv = this._getArgs();
const inspectPort = this._getInspectPort(execArgv);
const clusterOptions = {
args: [this._entryPoint],
exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'),
execArgv,
};
if (inspectPort) {
clusterOptions.inspectPort = inspectPort;
}
cluster.setupMaster(clusterOptions);
cluster.on('online', (worker) => {
callback(worker);
});
cluster.fork();
export const hooks = {
'@flecks/core.config': () => ({
/**
* Browser window options.
*
* See: https://www.electronjs.org/docs/latest/api/browser-window
*/
browserWindowOptions: {},
/**
* Install devtools extensions (by default).
*
* If `true`, will install some devtools extensions based on which flecks are enabled.
*
* You can pass an array of Chrome store IDs to install a list of custom extensions.
*
* Extensions will not be installed if `'production' === process.env.NODE_ENV`
*/
installExtensions: true,
/**
* Quit the app when all windows are closed.
*/
quitOnClosed: true,
/**
* The URL to load in electron by default.
*
* Defaults to `http://${flecks.get('@flecks/web/server.public')}`.
*/
url: undefined,
}),
'@flecks/core.webpack': (target, config) => {
const StartServerWebpackPlugin = R('start-server-webpack-plugin');
const plugin = config.plugins.find((plugin) => plugin instanceof StartServerWebpackPlugin);
// Extremely hackish, c'est la vie.
if (plugin) {
/* eslint-disable no-underscore-dangle */
plugin._startServer = function _startServerHacked(callback) {
const execArgv = this._getArgs();
const inspectPort = this._getInspectPort(execArgv);
const clusterOptions = {
args: [this._entryPoint],
exec: join(FLECKS_CORE_ROOT, 'node_modules', '.bin', 'electron'),
execArgv,
};
/* eslint-enable no-underscore-dangle */
}
},
'@flecks/electron/server.initialize': async (app, flecks) => {
app.on('window-all-closed', () => {
const {quitOnClosed} = flecks.get('@flecks/electron/server');
if (!quitOnClosed) {
return;
if (inspectPort) {
clusterOptions.inspectPort = inspectPort;
}
// Apple has to be *special*.
if (process.platform === 'darwin') {
return;
}
app.quit();
});
app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
await app.whenReady();
await createWindow(flecks);
},
'@flecks/electron/server.window': async (win, flecks) => {
const {public: $$public} = flecks.get('@flecks/web/server');
const {
installExtensions,
url = `http://${$$public}`,
} = flecks.get('@flecks/electron/server');
if (installExtensions && 'production' !== NODE_ENV) {
const {
default: installExtension,
REDUX_DEVTOOLS,
REACT_DEVELOPER_TOOLS,
} = __non_webpack_require__('electron-devtools-installer');
let extensions = installExtensions;
if (!Array.isArray(extensions)) {
extensions = [];
if (flecks.fleck('@flecks/react')) {
extensions.push(REACT_DEVELOPER_TOOLS);
}
if (flecks.fleck('@flecks/redux')) {
extensions.push(REDUX_DEVTOOLS);
}
}
await installExtension(extensions);
}
await win.loadURL(url);
},
'@flecks/repl.context': (flecks) => ({
electron: {
createWindow: () => createWindow(flecks),
},
}),
'@flecks/server.up': async (flecks) => {
// `app` will be undefined if we aren't running in an electron environment. Just bail.
if (!app) {
cluster.setupMaster(clusterOptions);
cluster.on('online', (worker) => {
callback(worker);
});
cluster.fork();
};
/* eslint-enable no-underscore-dangle */
}
},
'@flecks/electron/server.initialize': async (app, flecks) => {
app.on('window-all-closed', () => {
const {quitOnClosed} = flecks.get('@flecks/electron/server');
if (!quitOnClosed) {
return;
}
await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', app);
// Apple has to be *special*.
if (process.platform === 'darwin') {
return;
}
app.quit();
});
app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
await app.whenReady();
await createWindow(flecks);
},
'@flecks/electron/server.window': async (win, flecks) => {
const {public: $$public} = flecks.get('@flecks/web/server');
const {
installExtensions,
url = `http://${$$public}`,
} = flecks.get('@flecks/electron/server');
if (installExtensions && 'production' !== NODE_ENV) {
const {
default: installExtension,
REDUX_DEVTOOLS,
REACT_DEVELOPER_TOOLS,
} = __non_webpack_require__('electron-devtools-installer');
let extensions = installExtensions;
if (!Array.isArray(extensions)) {
extensions = [];
if (flecks.fleck('@flecks/react')) {
extensions.push(REACT_DEVELOPER_TOOLS);
}
if (flecks.fleck('@flecks/redux')) {
extensions.push(REDUX_DEVTOOLS);
}
}
await installExtension(extensions);
}
await win.loadURL(url);
},
'@flecks/repl.context': (flecks) => ({
electron: {
createWindow: () => createWindow(flecks),
},
}),
'@flecks/server.up': async (flecks) => {
// `app` will be undefined if we aren't running in an electron environment. Just bail.
if (!app) {
return;
}
await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', app);
},
};

View File

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

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 createLimiter from './limiter';
export {default as createLimiter} from './limiter';
export default {
[Hooks]: {
'@flecks/core.config': () => ({
/**
* All keys used to determine fingerprint.
*/
export const hooks = {
'@flecks/core.config': () => ({
/**
* All keys used to determine fingerprint.
*/
keys: ['ip'],
web: {
keys: ['ip'],
web: {
keys: ['ip'],
points: 60,
duration: 30,
ttl: 30,
},
socket: {
keys: ['ip'],
points: 60,
duration: 30,
ttl: 30,
},
}),
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/web/server.request.route': (flecks) => {
const {web} = flecks.get('@flecks/governor/server');
const limiter = flecks.get('$flecks/governor.web.limiter');
return async (req, res, next) => {
const {Ban} = flecks.get('$flecks/db.models');
try {
await Ban.check(req);
}
catch (error) {
res.status(403).send(`<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>`);
}
};
points: 60,
duration: 30,
ttl: 30,
},
'@flecks/server.up': async (flecks) => {
if (flecks.fleck('@flecks/web/server')) {
const {web} = flecks.get('@flecks/governor/server');
const limiter = await createLimiter(
flecks,
{
keyPrefix: '@flecks/governor.web.request.route',
...web,
},
);
flecks.set('$flecks/governor.web.limiter', limiter);
socket: {
keys: ['ip'],
points: 60,
duration: 30,
ttl: 30,
},
}),
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/web/server.request.route': (flecks) => {
const {web} = flecks.get('@flecks/governor/server');
const limiter = flecks.get('$flecks/governor.web.limiter');
return async (req, res, next) => {
const {Ban} = flecks.get('$flecks/db.models');
try {
await Ban.check(req);
}
if (flecks.fleck('@flecks/socket/server')) {
const {[ByType]: Packets} = flecks.get('$flecks/socket.packets');
const limiters = Object.fromEntries(
await Promise.all(
Object.entries(Packets)
.filter(([, Packet]) => Packet.limit)
.map(async ([name, Packet]) => (
[
name,
await createLimiter(
flecks,
{keyPrefix: `@flecks/governor.packet.${name}`, ...Packet.limit},
),
]
)),
),
);
flecks.set('$flecks/governor.packet.limiters', limiters);
const {socket} = flecks.get('@flecks/governor/server');
const limiter = await createLimiter(
flecks,
{
keyPrefix: '@flecks/governor.socket.request.socket',
...socket,
},
);
flecks.set('$flecks/governor.socket.limiter', limiter);
catch (error) {
res.status(403).send(`<pre>${error.message}</pre>`);
return;
}
},
'@flecks/socket/server.request.socket': (flecks) => {
const limiter = flecks.get('$flecks/governor.socket.limiter');
return async (socket, next) => {
const {handshake: req} = socket;
const {Ban} = flecks.get('$flecks/db.models');
try {
await Ban.check(req);
}
catch (error) {
next(error);
return;
}
req.ban = async (keys, ttl) => {
await Ban.create(Ban.fromRequest(req, keys, ttl));
socket.disconnect();
};
try {
await limiter.consume(req.ip);
next();
}
catch (error) {
const {ttl, keys} = socket;
await Ban.create(Ban.fromRequest(req, keys, ttl));
next(error);
}
req.ban = async (keys, ttl = 0) => {
const ban = Ban.fromRequest(req, keys, ttl);
await Ban.create({...ban});
res.status(403).send(`<pre>${Ban.format([ban])}</pre>`);
};
},
'@flecks/socket.packets.decorate': (Packets, flecks) => (
Object.fromEntries(
Object.entries(Packets).map(([keyPrefix, Packet]) => [
keyPrefix,
!Packet.limit ? Packet : LimitedPacket(flecks, [keyPrefix, Packet]),
]),
)
),
try {
await limiter.consume(req.ip);
next();
}
catch (error) {
const {ttl, keys} = web;
const ban = Ban.fromRequest(req, keys, ttl);
await Ban.create({...ban});
res.status(429).send(`<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 default {
[Hooks]: {
/**
* Define React Providers.
*
* Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react.providers': (req) => {
// Generally it makes more sense to separate client and server concerns using platform
// naming conventions, but this is just a small contrived example.
return req ? serverSideProvider(req) : clientSideProvider();
},
/**
* Define root-level React components that are mounted as siblings on `#main`.
* Note: `req` will be only be defined when server-side rendering.
*
* Return either a React component or an array whose elements must either be a React component
* or an array of two elements where the first element is the component and the second element
* is the props passed to the component.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react.roots': (req) => {
// Note that we're not returning `<Component />`, but `Component`.
return [
Component,
[SomeOtherComponent, {prop: 'value'}]
];
// You can also just:
return Component;
},
export const hooks = {
/**
* Define React Providers.
*
* Note: `req` will be only be defined when server-side rendering.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react.providers': (req) => {
// Generally it makes more sense to separate client and server concerns using platform
// naming conventions, but this is just a small contrived example.
return req ? serverSideProvider(req) : clientSideProvider();
},
/**
* Define root-level React components that are mounted as siblings on `#main`.
* Note: `req` will be only be defined when server-side rendering.
*
* Return either a React component or an array whose elements must either be a React component
* or an array of two elements where the first element is the component and the second element
* is the props passed to the component.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/react.roots': (req) => {
// Note that we're not returning `<Component />`, but `Component`.
return [
Component,
[SomeOtherComponent, {prop: 'value'}]
];
// 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 React from 'react';
@ -10,20 +10,18 @@ const debug = D('@flecks/react/client');
export {FlecksContext};
export default {
[Hooks]: {
'@flecks/web/client.up': async (flecks) => {
const {ssr} = flecks.get('@flecks/react');
debug('%sing...', ssr ? 'hydrat' : 'render');
(ssr ? hydrate : render)(
React.createElement(
React.StrictMode,
{},
[React.createElement(await root(flecks), {key: 'root'})],
),
window.document.getElementById('root'),
);
debug('rendered');
},
export const hooks = {
'@flecks/web/client.up': async (flecks) => {
const {ssr} = flecks.get('@flecks/react');
debug('%sing...', ssr ? 'hydrat' : 'render');
(ssr ? hydrate : render)(
React.createElement(
React.StrictMode,
{},
[React.createElement(await root(flecks), {key: 'root'})],
),
window.document.getElementById('root'),
);
debug('rendered');
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,45 +1,41 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define side-effects to run against Redux actions.
*/
'@flecks/redux.effects': () => ({
someActionName: (store, action) => {
// Runs when `someActionName` actions are dispatched.
},
}),
/**
* Define root-level reducers for the Redux store.
*/
'@flecks/redux.reducers': () => {
return (state, action) => {
// Whatever you'd like.
return state;
};
},
/**
* Define Redux slices.
*
* See: https://redux-toolkit.js.org/api/createSlice
*/
'@flecks/redux.slices': () => {
const something = createSlice(
// ...
);
return {
something: something.reducer,
};
},
/**
* Modify Redux store configuration.
* @param {Object} options A mutable object with keys for enhancers and middleware.
*/
'@flecks/redux.store': (options) => {
options.enhancers.splice(someIndex, 1);
options.middleware.push(mySpecialMiddleware);
export const hooks = {
/**
* Define side-effects to run against Redux actions.
*/
'@flecks/redux.effects': () => ({
someActionName: (store, action) => {
// Runs when `someActionName` actions are dispatched.
},
}),
/**
* Define root-level reducers for the Redux store.
*/
'@flecks/redux.reducers': () => {
return (state, action) => {
// Whatever you'd like.
return state;
};
},
/**
* Define Redux slices.
*
* See: https://redux-toolkit.js.org/api/createSlice
*/
'@flecks/redux.slices': () => {
const something = createSlice(
// ...
);
return {
something: something.reducer,
};
},
/**
* Modify Redux store configuration.
* @param {Object} options A mutable object with keys for enhancers and middleware.
*/
'@flecks/redux.store': (options) => {
options.enhancers.splice(someIndex, 1);
options.middleware.push(mySpecialMiddleware);
},
};

View File

@ -1,26 +1,24 @@
import {ensureUniqueReduction, Flecks, Hooks} from '@flecks/core';
import {Flecks} from '@flecks/core';
import {Provider} from 'react-redux';
import configureStore, {createReducer} from '../store';
import localStorageEnhancer from './local-storage';
export default {
[Hooks]: {
'@flecks/react.providers': async (req, flecks) => {
const slices = await ensureUniqueReduction(flecks, '@flecks/redux.slices');
const reducer = createReducer(flecks, slices);
// Hydrate from server.
const {preloadedState} = flecks.get('@flecks/redux/client');
const store = await configureStore(flecks, reducer, {preloadedState});
flecks.set('$flecks/redux.store', store);
return [Provider, {store}];
},
'@flecks/redux.store': ({enhancers}) => {
// Hydrate from and subscribe to localStorage.
enhancers.push(localStorageEnhancer);
},
'@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
),
export const hooks = {
'@flecks/react.providers': async (req, flecks) => {
const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
const reducer = createReducer(flecks, slices);
// Hydrate from server.
const {preloadedState} = flecks.get('@flecks/redux/client');
const store = await configureStore(flecks, reducer, {preloadedState});
flecks.set('$flecks/redux.store', store);
return [Provider, {store}];
},
'@flecks/redux.store': ({enhancers}) => {
// Hydrate from and subscribe to localStorage.
enhancers.push(localStorageEnhancer);
},
'@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
),
};

View File

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

View File

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

View File

@ -1,30 +1,26 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define REPL commands.
*
* Note: commands will be prefixed with a period in the Node REPL.
*/
'@flecks/repl.commands': () => ({
someCommand: (...args) => {
// args are passed from the Node REPL. So, you could invoke it like:
// .someCommand foo bar
// and `args` would be `['foo', 'bar']`.
},
}),
/**
* Provide global context to the REPL.
*/
'@flecks/repl.context': () => {
// Now you'd be able to do like:
// `node> someValue;`
// and the REPL would evaluate it to `'foobar'`.
return {
someValue: 'foobar',
};
export const hooks = {
/**
* Define REPL commands.
*
* Note: commands will be prefixed with a period in the Node REPL.
*/
'@flecks/repl.commands': () => ({
someCommand: (...args) => {
// args are passed from the Node REPL. So, you could invoke it like:
// .someCommand foo bar
// and `args` would be `['foo', 'bar']`.
},
}),
/**
* Provide global context to the REPL.
*/
'@flecks/repl.context': () => {
// Now you'd be able to do like:
// `node> someValue;`
// and the REPL would evaluate it to `'foobar'`.
return {
someValue: 'foobar',
};
},
};

View File

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

View File

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

View File

@ -33,7 +33,8 @@ const {version} = require('../package.json');
rcs,
});
try {
await global.flecks.up('@flecks/server.up');
await Promise.all(global.flecks.invokeFlat('@flecks/core.starting'));
await global.flecks.invokeSequentialAsync('@flecks/server.up');
debug('up!');
}
catch (error) {

View File

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

View File

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

View File

@ -1,65 +1,61 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Modify Socket.io client configuration.
*
* See: https://socket.io/docs/v4/client-options/
*/
'@flecks/socket.client': () => ({
timeout: Infinity,
}),
/**
* Define server-side intercom channels.
*/
'@flecks/socket.intercom': (req) => ({
// This would have been called like:
// `const result = await req.intercom('someChannel', payload)`.
// `result` will be an `n`-length array, where `n` is the number of server instances. Each
// element in the array will be the result of `someServiceSpecificInformation()` running
// against that server instance.
someChannel: async (payload, server) => {
return someServiceSpecificInformation();
},
}),
/**
* Define socket packets.
*
* In the example below, your fleck would have a `packets` subdirectory, and each
* decorator would be defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/redux/src/packets
*
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/packet.js
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/redirect.js
*/
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
/**
* Decorate database models.
*
* In the example below, your fleck would have a `packets/decorators` subdirectory, and each
* decorator would be defined in its own file.
* @param {constructor} Packet The packet to decorate.
*/
'@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
),
/**
* Modify Socket.io server configuration.
*
* See: https://socket.io/docs/v4/server-options/
*/
'@flecks/socket.server': () => ({
pingTimeout: Infinity,
}),
/**
* Define middleware to run when a socket connection is established.
*/
'@flecks/socket/server.request.socket': () => (socket, next) => {
// Express-style route middleware...
next();
export const hooks = {
/**
* Modify Socket.io client configuration.
*
* See: https://socket.io/docs/v4/client-options/
*/
'@flecks/socket.client': () => ({
timeout: Infinity,
}),
/**
* Define server-side intercom channels.
*/
'@flecks/socket.intercom': (req) => ({
// This would have been called like:
// `const result = await req.intercom('someChannel', payload)`.
// `result` will be an `n`-length array, where `n` is the number of server instances. Each
// element in the array will be the result of `someServiceSpecificInformation()` running
// against that server instance.
someChannel: async (payload, server) => {
return someServiceSpecificInformation();
},
}),
/**
* Define socket packets.
*
* In the example below, your fleck would have a `packets` subdirectory, and each
* decorator would be defined in its own file.
* See: https://github.com/cha0s/flecks/tree/master/packages/redux/src/packets
*
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/packet.js
* See: https://github.com/cha0s/flecks/tree/master/packages/socket/src/packet/redirect.js
*/
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
/**
* Decorate database models.
*
* In the example below, your fleck would have a `packets/decorators` subdirectory, and each
* decorator would be defined in its own file.
* @param {constructor} Packet The packet to decorate.
*/
'@flecks/socket.packets.decorate': (
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
),
/**
* Modify Socket.io server configuration.
*
* See: https://socket.io/docs/v4/server-options/
*/
'@flecks/socket.server': () => ({
pingTimeout: Infinity,
}),
/**
* Define middleware to run when a socket connection is established.
*/
'@flecks/socket/server.request.socket': () => (socket, next) => {
// Express-style route middleware...
next();
},
};

View File

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

View File

@ -1,5 +1,3 @@
import {Hooks} from '@flecks/core';
import badPacketsCheck from './packet/bad-packets-check';
import Bundle from './packet/bundle';
import Redirect from './packet/redirect';
@ -9,28 +7,26 @@ export {default as normalize} from './normalize';
export * from './hooks';
export {default as Packet, Packer, ValidationError} from './packet';
export default {
[Hooks]: {
'@flecks/core.starting': (flecks) => {
flecks.set('$flecks/socket.packets', flecks.gather(
'@flecks/socket.packets',
{check: badPacketsCheck},
));
},
'@flecks/web.config': async (
req,
{config: {'@flecks/socket': {'packets.decorate': decorators = ['...']}}},
) => ({
'@flecks/socket': {
'packets.decorate': decorators.filter(
(decorator) => 'server' !== decorator.split('/').pop(),
),
},
}),
'@flecks/socket.packets': (flecks) => ({
Bundle: Bundle(flecks),
Redirect,
Refresh,
}),
export const hooks = {
'@flecks/core.starting': (flecks) => {
flecks.set('$flecks/socket.packets', flecks.gather(
'@flecks/socket.packets',
{check: badPacketsCheck},
));
},
'@flecks/web.config': async (
req,
{config: {'@flecks/socket': {'packets.decorate': decorators = ['...']}}},
) => ({
'@flecks/socket': {
'packets.decorate': decorators.filter(
(decorator) => 'server' !== decorator.split('/').pop(),
),
},
}),
'@flecks/socket.packets': (flecks) => ({
Bundle: Bundle(flecks),
Redirect,
Refresh,
}),
};

View File

@ -1,25 +1,21 @@
import {Hooks} from '@flecks/core';
import createIntercom from './create-intercom';
import Sockets from './sockets';
export default {
[Hooks]: {
'@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => {
req.intercom = createIntercom(sockets, 'web');
next();
},
'@flecks/web/server.up': async (httpServer, flecks) => {
const sockets = new Sockets(httpServer, flecks);
await sockets.connect();
flecks.set('$flecks/socket.sockets', sockets);
},
'@flecks/repl.context': (flecks) => ({
Packets: flecks.get('$flecks/socket.packets'),
sockets: flecks.get('$flecks/socket.sockets'),
}),
'@flecks/socket.server': ({config: {'@flecks/core': {id}}}) => ({
path: `/${id}/socket.io`,
}),
export const hooks = {
'@flecks/web/server.request.socket': ({config: {'$flecks/socket.sockets': sockets}}) => (req, res, next) => {
req.intercom = createIntercom(sockets, 'web');
next();
},
'@flecks/web/server.up': async (httpServer, flecks) => {
const sockets = new Sockets(httpServer, flecks);
await sockets.connect();
flecks.set('$flecks/socket.sockets', sockets);
},
'@flecks/repl.context': (flecks) => ({
Packets: flecks.get('$flecks/socket.packets'),
sockets: flecks.get('$flecks/socket.sockets'),
}),
'@flecks/socket.server': ({config: {'@flecks/core': {id}}}) => ({
path: `/${id}/socket.io`,
}),
};

View File

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

View File

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

View File

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

View File

@ -1,85 +1,83 @@
import {D, Flecks, Hooks} from '@flecks/core';
import {D, Flecks} from '@flecks/core';
import passport from 'passport';
import LogOps from 'passport/lib/http/request';
const debug = D('@flecks/user/passport');
const debugSilly = debug.extend('silly');
export default {
[Hooks]: {
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/web/server.request.route': (flecks) => (req, res, next) => {
debugSilly('@flecks/web/server.request.route: passport.initialize()');
passport.initialize()(req, res, () => {
debugSilly('@flecks/web/server.request.route: passport.session()');
passport.session()(req, res, () => {
if (!req.user) {
const {User} = flecks.get('$flecks/db.models');
req.user = new User();
req.user.id = 0;
}
next();
});
});
},
'@flecks/web.routes': () => [
{
method: 'get',
path: '/auth/logout',
middleware: (req, res) => {
req.logout();
res.redirect('/');
},
},
],
'@flecks/server.up': (flecks) => {
passport.serializeUser((user, fn) => fn(null, user.id));
passport.deserializeUser(async (id, fn) => {
const {User} = flecks.get('$flecks/db.models');
try {
fn(undefined, await User.findByPk(id));
}
catch (error) {
fn(error);
export const hooks = {
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/web/server.request.route': (flecks) => (req, res, next) => {
debugSilly('@flecks/web/server.request.route: passport.initialize()');
passport.initialize()(req, res, () => {
debugSilly('@flecks/web/server.request.route: passport.session()');
passport.session()(req, res, () => {
if (!req.user) {
const {User} = flecks.get('$flecks/db.models');
req.user = new User();
req.user.id = 0;
}
next();
});
},
'@flecks/socket.intercom': () => ({
'@flecks/user/users': async (sids, server) => {
const sockets = await server.sockets();
return sids
.filter((sid) => sockets.has(sid))
.reduce(
(r, sid) => ({
...r,
[sid]: sockets.get(sid).handshake.user.id,
}),
{},
);
});
},
'@flecks/web.routes': () => [
{
method: 'get',
path: '/auth/logout',
middleware: (req, res) => {
req.logout();
res.redirect('/');
},
}),
'@flecks/socket/server.request.socket': (flecks) => (socket, next) => {
debugSilly('@flecks/socket/server.request.socket: passport.initialize()');
passport.initialize()(socket.handshake, undefined, () => {
debugSilly('@flecks/socket/server.request.socket: passport.session()');
passport.session()(socket.handshake, undefined, async () => {
/* eslint-disable no-param-reassign */
if (!socket.handshake.user) {
const {User} = flecks.get('$flecks/db.models');
socket.handshake.user = new User();
socket.handshake.user.id = 0;
}
socket.handshake.login = LogOps.logIn;
socket.handshake.logIn = LogOps.logIn;
socket.handshake.logout = LogOps.logOut;
socket.handshake.logOut = LogOps.logOut;
socket.handshake.isAuthenticated = LogOps.isAuthenticated;
socket.handshake.isUnauthenticated = LogOps.isUnauthenticated;
/* eslint-enable no-param-reassign */
await socket.join(`/u/${socket.handshake.user.id}`);
next();
});
});
},
],
'@flecks/server.up': (flecks) => {
passport.serializeUser((user, fn) => fn(null, user.id));
passport.deserializeUser(async (id, fn) => {
const {User} = flecks.get('$flecks/db.models');
try {
fn(undefined, await User.findByPk(id));
}
catch (error) {
fn(error);
}
});
},
'@flecks/socket.intercom': () => ({
'@flecks/user/users': async (sids, server) => {
const sockets = await server.sockets();
return sids
.filter((sid) => sockets.has(sid))
.reduce(
(r, sid) => ({
...r,
[sid]: sockets.get(sid).handshake.user.id,
}),
{},
);
},
}),
'@flecks/socket/server.request.socket': (flecks) => (socket, next) => {
debugSilly('@flecks/socket/server.request.socket: passport.initialize()');
passport.initialize()(socket.handshake, undefined, () => {
debugSilly('@flecks/socket/server.request.socket: passport.session()');
passport.session()(socket.handshake, undefined, async () => {
/* eslint-disable no-param-reassign */
if (!socket.handshake.user) {
const {User} = flecks.get('$flecks/db.models');
socket.handshake.user = new User();
socket.handshake.user.id = 0;
}
socket.handshake.login = LogOps.logIn;
socket.handshake.logIn = LogOps.logIn;
socket.handshake.logout = LogOps.logOut;
socket.handshake.logOut = LogOps.logOut;
socket.handshake.isAuthenticated = LogOps.isAuthenticated;
socket.handshake.isUnauthenticated = LogOps.isUnauthenticated;
/* eslint-enable no-param-reassign */
await socket.join(`/u/${socket.handshake.user.id}`);
next();
});
});
},
};

View File

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

View File

@ -1,62 +1,58 @@
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
/**
* Define sequential actions to run when the client comes up.
*/
'@flecks/web/client.up': async () => {
await youCanDoAsyncThingsHere();
export const hooks = {
/**
* Define sequential actions to run when the client comes up.
*/
'@flecks/web/client.up': async () => {
await youCanDoAsyncThingsHere();
},
/**
* Override flecks configuration sent to client flecks.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/web.config': (req) => ({
someClientFleck: {
someConfig: req.someConfig,
},
/**
* Override flecks configuration sent to client flecks.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/web.config': (req) => ({
someClientFleck: {
someConfig: req.someConfig,
}),
/**
* Define HTTP routes.
*/
'@flecks/web.routes': () => [
{
method: 'get',
path: '/some-path',
middleware: (req, res, next) => {
// Express-style route middleware...
next();
},
}),
/**
* Define HTTP routes.
*/
'@flecks/web.routes': () => [
{
method: 'get',
path: '/some-path',
middleware: (req, res, next) => {
// Express-style route middleware...
next();
},
},
],
/**
* Define middleware to run when a route is matched.
*/
'@flecks/web/server.request.route': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define middleware to run when an HTTP socket connection is established.
*/
'@flecks/web/server.request.socket': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define composition functions to run over the HTML stream prepared for the client.
* @param {stream.Readable} stream The HTML stream.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/web/server.stream.html': (stream, req) => {
return stream.pipe(myTransformStream);
},
/**
* Define sequential actions to run when the HTTP server comes up.
*/
'@flecks/web/server.up': async () => {
await youCanDoAsyncThingsHere();
},
],
/**
* Define middleware to run when a route is matched.
*/
'@flecks/web/server.request.route': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define middleware to run when an HTTP socket connection is established.
*/
'@flecks/web/server.request.socket': () => (req, res, next) => {
// Express-style route middleware...
next();
},
/**
* Define composition functions to run over the HTML stream prepared for the client.
* @param {stream.Readable} stream The HTML stream.
* @param {http.ClientRequest} req The HTTP request object.
*/
'@flecks/web/server.stream.html': (stream, req) => {
return stream.pipe(myTransformStream);
},
/**
* Define sequential actions to run when the HTTP server comes up.
*/
'@flecks/web/server.up': async () => {
await youCanDoAsyncThingsHere();
},
};

View File

@ -1,6 +1,6 @@
import {D, Flecks} from '@flecks/core';
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
const {version} = require('@flecks/web/package.json');
(async () => {
@ -56,7 +56,8 @@ const {version} = require('@flecks/web/package.json');
const flecks = new Flecks(runtime);
window.flecks = flecks;
try {
await flecks.up('@flecks/web/client.up');
await Promise.all(flecks.invokeFlat('@flecks/core.starting'));
await flecks.invokeSequentialAsync('@flecks/web/client.up');
window.document.querySelector('#root').style.display = 'block';
debug('up!');
}

View File

@ -1,7 +1,7 @@
import {stat, unlink} from 'fs/promises';
import {join} from 'path';
import {D, Hooks} from '@flecks/core';
import {D} from '@flecks/core';
import {Flecks, spawnWith} from '@flecks/core/server';
import augmentBuild from './augment-build';
@ -16,199 +16,197 @@ const debug = D('@flecks/web/server');
export {augmentBuild};
export default {
[Hooks]: {
'@flecks/core.build': augmentBuild,
'@flecks/core.build.alter': async (neutrinoConfigs, flecks) => {
// Don't build if there's a fleck target.
if (neutrinoConfigs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) {
export const hooks = {
'@flecks/core.build': augmentBuild,
'@flecks/core.build.alter': async (neutrinoConfigs, flecks) => {
// Don't build if there's a fleck target.
if (neutrinoConfigs.fleck && !flecks.get('@flecks/web/server.forceBuildWithFleck')) {
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.web;
return;
}
// Only build vendor in dev.
if (neutrinoConfigs['web-vendor']) {
if (process.argv.find((arg) => 'production' === arg)) {
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.web;
return;
delete neutrinoConfigs['web-vendor'];
}
// Only build vendor in dev.
if (neutrinoConfigs['web-vendor']) {
if (process.argv.find((arg) => 'production' === arg)) {
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs['web-vendor'];
// Only build if something actually changed.
const dll = flecks.get('@flecks/web/server.dll');
if (dll.length > 0) {
const manifest = join(
FLECKS_CORE_ROOT,
'node_modules',
'.cache',
'flecks',
'web-vendor.manifest.json',
);
let timestamp = 0;
try {
const stats = await stat(manifest);
timestamp = stats.mtime;
}
// Only build if something actually changed.
const dll = flecks.get('@flecks/web/server.dll');
if (dll.length > 0) {
const manifest = join(
FLECKS_CORE_ROOT,
'node_modules',
'.cache',
'flecks',
'web-vendor.manifest.json',
);
let timestamp = 0;
// eslint-disable-next-line no-empty
catch (error) {}
let latest = 0;
for (let i = 0; i < dll.length; ++i) {
const path = dll[i];
try {
const stats = await stat(manifest);
timestamp = stats.mtime;
// eslint-disable-next-line no-await-in-loop
const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path));
if (stats.mtime > latest) {
latest = stats.mtime;
}
}
// eslint-disable-next-line no-empty
catch (error) {}
let latest = 0;
for (let i = 0; i < dll.length; ++i) {
const path = dll[i];
try {
// eslint-disable-next-line no-await-in-loop
const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path));
if (stats.mtime > latest) {
latest = stats.mtime;
}
}
// eslint-disable-next-line no-empty
catch (error) {}
}
if (timestamp > latest) {
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs['web-vendor'];
}
else if (timestamp > 0) {
await unlink(manifest);
}
}
if (timestamp > latest) {
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs['web-vendor'];
}
else if (timestamp > 0) {
await unlink(manifest);
}
}
// Bail if there's no web build.
if (!neutrinoConfigs.web) {
return;
}
// Bail if the build isn't watching.
if (!process.argv.find((arg) => '--watch' === arg)) {
return;
}
// Otherwise, spawn `webpack-dev-server` (WDS).
const cmd = [
'npx', 'webpack-dev-server',
'--mode', 'development',
'--hot',
'--config', flecks.buildConfig('webpack.config.js'),
];
spawnWith(
cmd,
{
env: {
FLECKS_CORE_BUILD_LIST: 'web',
},
},
);
// Remove the build config since we're handing off to WDS.
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.web;
},
'@flecks/core.build.config': () => [
/**
* Template file used to generate the client HTML.
*
* See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md
*/
'template.ejs',
/**
* PostCSS config file.
*
* See: https://github.com/postcss/postcss#usage
*/
'postcss.config.js',
],
'@flecks/core.config': () => ({
/**
* (webpack-dev-server) Disable the host check.
*
* See: https://github.com/webpack/webpack-dev-server/issues/887
*/
devDisableHostCheck: false,
/**
* (webpack-dev-server) Host to bind.
*/
devHost: 'localhost',
/**
* (webpack-dev-server) Port to bind.
*/
devPort: undefined,
/**
* (webpack-dev-server) Public path to serve.
*
* Defaults to `flecks.get('@flecks/web/server.public')`.
*/
devPublic: undefined,
/**
* (webpack-dev-server) Webpack stats output.
*/
devStats: {
assets: false,
chunks: false,
colors: true,
modules: false,
},
/**
* Modules to externalize using `webpack.DllPlugin`.
*/
dll: [],
/**
* Force building http target even if there's a fleck target.
*/
forceBuildWithFleck: false,
/**
* Host to bind.
*/
host: '0.0.0.0',
/**
* Build path.
*/
output: 'web',
/**
* Port to bind.
*/
port: 32340,
/**
* Public path to server.
*/
public: 'localhost:32340',
/**
* Webpack stats configuration when building HTTP target.
*/
stats: {
children: false,
chunks: false,
colors: true,
modules: false,
},
/**
* Proxies to trust.
*
* See: https://www.npmjs.com/package/proxy-addr
*/
trust: false,
}),
'@flecks/core.starting': (flecks) => {
debug('bootstrapping flecks...');
const webFlecks = Flecks.bootstrap({
config: flecks.config,
platforms: ['client', '!server'],
});
debug('bootstrapped');
flecks.set('$flecks/web.flecks', webFlecks);
},
'@flecks/core.targets': (flecks) => [
'web',
...(flecks.get('@flecks/web/server.dll').length > 0 ? ['web-vendor'] : []),
],
'@flecks/web.routes': (flecks) => [
}
// Bail if there's no web build.
if (!neutrinoConfigs.web) {
return;
}
// Bail if the build isn't watching.
if (!process.argv.find((arg) => '--watch' === arg)) {
return;
}
// Otherwise, spawn `webpack-dev-server` (WDS).
const cmd = [
'npx', 'webpack-dev-server',
'--mode', 'development',
'--hot',
'--config', flecks.buildConfig('webpack.config.js'),
];
spawnWith(
cmd,
{
method: 'get',
path: '/flecks.config.js',
middleware: async (req, res) => {
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
res.send(await configSource(flecks, req));
env: {
FLECKS_CORE_BUILD_LIST: 'web',
},
},
],
'@flecks/web/server.stream.html': inlineConfig,
'@flecks/server.up': (flecks) => createHttpServer(flecks),
'@flecks/repl.context': (flecks) => ({
httpServer: flecks.get('$flecks/web/server.instance'),
}),
);
// Remove the build config since we're handing off to WDS.
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.web;
},
'@flecks/core.build.config': () => [
/**
* Template file used to generate the client HTML.
*
* See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md
*/
'template.ejs',
/**
* PostCSS config file.
*
* See: https://github.com/postcss/postcss#usage
*/
'postcss.config.js',
],
'@flecks/core.config': () => ({
/**
* (webpack-dev-server) Disable the host check.
*
* See: https://github.com/webpack/webpack-dev-server/issues/887
*/
devDisableHostCheck: false,
/**
* (webpack-dev-server) Host to bind.
*/
devHost: 'localhost',
/**
* (webpack-dev-server) Port to bind.
*/
devPort: undefined,
/**
* (webpack-dev-server) Public path to serve.
*
* Defaults to `flecks.get('@flecks/web/server.public')`.
*/
devPublic: undefined,
/**
* (webpack-dev-server) Webpack stats output.
*/
devStats: {
assets: false,
chunks: false,
colors: true,
modules: false,
},
/**
* Modules to externalize using `webpack.DllPlugin`.
*/
dll: [],
/**
* Force building http target even if there's a fleck target.
*/
forceBuildWithFleck: false,
/**
* Host to bind.
*/
host: '0.0.0.0',
/**
* Build path.
*/
output: 'web',
/**
* Port to bind.
*/
port: 32340,
/**
* Public path to server.
*/
public: 'localhost:32340',
/**
* Webpack stats configuration when building HTTP target.
*/
stats: {
children: false,
chunks: false,
colors: true,
modules: false,
},
/**
* Proxies to trust.
*
* See: https://www.npmjs.com/package/proxy-addr
*/
trust: false,
}),
'@flecks/core.starting': (flecks) => {
debug('bootstrapping flecks...');
const webFlecks = Flecks.bootstrap({
config: flecks.config,
platforms: ['client', '!server'],
});
debug('bootstrapped');
flecks.set('$flecks/web.flecks', webFlecks);
},
'@flecks/core.targets': (flecks) => [
'web',
...(flecks.get('@flecks/web/server.dll').length > 0 ? ['web-vendor'] : []),
],
'@flecks/web.routes': (flecks) => [
{
method: 'get',
path: '/flecks.config.js',
middleware: async (req, res) => {
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
res.send(await configSource(flecks, req));
},
},
],
'@flecks/web/server.stream.html': inlineConfig,
'@flecks/server.up': (flecks) => createHttpServer(flecks),
'@flecks/repl.context': (flecks) => ({
httpServer: flecks.get('$flecks/web/server.instance'),
}),
};