chore: initial

This commit is contained in:
cha0s 2022-02-25 04:58:08 -06:00
commit 702de1005d
224 changed files with 107367 additions and 0 deletions

119
.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# local
/build

164
README.md Normal file
View File

@ -0,0 +1,164 @@
<div align="center">
<h1>flecks</h1>
<p>
Flecks is a dynamic, configuration-driven, fullstack application production system. Its 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.
</p>
<p>For documentation, see <a href="ADDME">the documentation page</a>.</p>
## ⚠️ PROCEED AT YOUR OWN RISK ⚠️
This is alpha software. There are undoubtedly many bugs that haven't yet been found.
**You've been warned!**
</div>
## Table of Contents
1. [Install](#install)
2. [Introduction](#introduction)
3. [Concepts](#concepts)
## Install
Quickly scaffold a new application monorepo:
```
yarn create @flecks/app my-new-app
```
or with `npm`:
```
npx @flecks/create-app my-new-app
```
---
Quickly scaffold a new fleck:
```
yarn create @flecks/fleck my-new-fleck
```
or with `npm`:
```
npx @flecks/create-fleck my-new-fleck
```
## Introduction
At its core, flecks is a collection of modules that use [hooks](#hooks) to orchestrate everything
from building your project to handling the minutia of what happens when your application starts,
when a client connects, defining database models, and more.
All flecks projects, be they an application or another fleck, contain a `build` directory with a
[`flecks.yml`](#flecksyml) that defines the flecks use to compose the project, as well as
build-time configuration.
Modern features you expect &mdash; like [ESLint](https://eslint.org/),
[Mocha tests](https://mochajs.org/),
[Hot Module Replacement (HMR)](https://v4.webpack.js.org/guides/hot-module-replacement/),
[SSR](https://reactjs.org/docs/react-dom-server.html) &mdash; are baked in. Along with some you
may not expect &mdash; like *server-side* HMR, the ability to define [redux](https://redux.js.org/)
application state (and store enhancers/middleware) dynamically with hooks,
[REPL](https://nodejs.org/api/repl.html) support, and much more.
## Concepts
### `build` directory
The `build` directory contains build directives and run commands. Examples of these would be:
- `babel.config.js`
- `.eslint.defaults.js`
- `.neutrinorc.js`
- `webpack.config.js`
- etc, etc, depending on which flecks you have enabled. Support for the aforementioned
configuration comes stock in `@flecks/core`.
The `build` directory is a solution to the problem of "ejecting" that you run into with
e.g. Create React App. Flecks doesn't force you into an all-or-nothing approach. If your project
requires advanced configuration for one aspect, you can simply override that aspect of
configuration in your `build` directory on a case-by-case basis.
Of course, flecks strives to provide powerful defaults that minimize the need to override
configuration.
See [the build directory documentation page](packages/core/build/dox/build.md) for more details.
---
#### `flecks.yml`
The build directory also contains a special file, `flecks.yml`. This file is the heart of your
flecks project's configuration and is how you orchestrate your project.
The structure of the file is an object whose keys are the flecks composing your application and
whose values are the default configuration for those flecks.
```yml
# Specify configuration overrides for this fleck:
'my-fleck':
some_value: 69
some_other_value: 420
# Default configuration:
'some-other-fleck': {}
```
The simplest example of a flecks server application:
```yml
'@flecks/core': {}
'@flecks/server': {}
```
Yes, that's it! In fact, when you use `yarn create @flecks/app`, that's what is generated for you
by default. Obviously, this doesn't do much on its own. It simply bootstraps flecks and runs a
server application with no interesting work to do.
---
### Hooks
Documentation page: (ADDME)
Hooks are how everything happens in flecks. There are many hooks and they will not be treated
exhaustively here. See the documentation page above.
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:
```javascript
import {Hooks} from '@flecks/core';
export default {
[Hooks]: {
'@flecks/core/starting': () => {
console.log('hello, gorgeous');
},
},
};
```
Now add your newly-minted fleck to [`flecks.yml`](#flecksyml), and let your fledgling fleck treat
you the way you deserve to be treated.
Just to give you an idea of the power of hooks, some will be listed here:
- `@flecks/core/config`:
> Define default configuration.
- `@flecks/docker/containers`:
> Define [Docker](https://www.docker.com/) containers to run alongside your application to
develop e.g. DB models, redis commands, etc. without having to worry about installing stuff.
- `@flecks/http/server/request.route`:
> Define [Express](http://expressjs.com/) middleware that runs when an HTTP route is hit.
- `@flecks/server/up`:
> Do things when server comes up (e.g. DB connection, HTTP listener, make you coffee, etc).
...and so many more.
We didn't even touch on [gather hooks](ADDME), [provider hooks](ADDME), [decorator hooks](ADDME),
and so many more. Please see the [hook documentation page](ADDME) for the full rundown on all of
the wonderful things hooks can do for you.

15
TODO.md Normal file
View File

@ -0,0 +1,15 @@
- x multi-build lint is broken
- x @flecks/db
- x @flecks/governor
- x @flecks/react
- x Use aliasing for self-referential flecks context
- x @flecks/redis
- x @flecks/redux
- x @flecks/socket
- x @flecks/user
- x flecks aliasing must ensure webpack aliasing and de-externalization into bundles
- x `flecks.invokeMiddleware()` should not build every invocation
- x `flecks.invokeComposed()` and `flecks.invokeMiddleware()` should not fatal on a missed lookup
- x flecks should have a `platforms` setting, so auto-lookups of `/client`, `/server` are less
magical
- x `flecks.expandedFlecks()` should use `platforms`

6
lerna.json Normal file
View File

@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "1.0.0"
}

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "@flecks/monorepo",
"private": true,
"scripts": {
"build": "lerna run build",
"lint": "lerna run lint",
"test": "lerna run test --no-bail -- --silent"
},
"dependencies": {},
"devDependencies": {
"lerna": "^3.22.1"
}
}

2
packages/core/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/dist
/node_modules

View File

@ -0,0 +1,4 @@
const neutrino = require('neutrino');
// eslint-disable-next-line import/no-dynamic-require
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).eslintrc();

View File

@ -0,0 +1,77 @@
const {chmod} = require('fs');
const {join} = require('path');
const airbnb = require('@neutrinojs/airbnb');
const banner = require('@neutrinojs/banner');
const copy = require('@neutrinojs/copy');
const node = require('@neutrinojs/node');
const glob = require('glob');
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
module.exports = require('../src/bootstrap/fleck.neutrinorc');
// Dotfiles.
module.exports.use.push((neutrino) => {
['eslintrc', 'eslint.defaults'].forEach((filename) => {
neutrino.config
.entry(`build/.${filename}`)
.clear()
.add(`./src/build/${filename}`);
})
});
// Tests.
module.exports.use.push((neutrino) => {
// Test entrypoint.
const testPaths = glob.sync(join(FLECKS_ROOT, 'test/*.js'));
if (testPaths.length > 0) {
const testEntry = neutrino.config.entry('test').clear();
testPaths.forEach((path) => testEntry.add(path));
}
});
module.exports.use.unshift((neutrino) => {
neutrino.config.plugins.delete('start-server');
});
module.exports.use.unshift(node());
module.exports.use.unshift(
airbnb({
eslint: {
baseConfig: {
...require('../src/build/eslint.defaults'),
env: {
mocha: true,
},
},
},
}),
);
module.exports.use.push(banner({
banner: '#!/usr/bin/env node',
include: /^cli\.js$/,
pluginId: 'shebang',
raw: true,
}))
module.exports.use.push(({config}) => {
config
.plugin('executable')
.use(class Executable {
apply(compiler) {
compiler.hooks.afterEmit.tapAsync(
'Executable',
(compilation, callback) => {
chmod(join(__dirname, '..', 'dist', 'cli.js'), 0o755, callback);
},
)
}
});
});

View File

@ -0,0 +1,96 @@
# Build directory ⚡️
The `build` directory is where build-time configuration is specified.
The prime example of this for Flecks is `flecks.yml`, but it extends to other more general
configuration such as `.eslintrc.js`, `babel.config.js`, etc.
## `flecks.yml` ⛏️
`flecks.yml` specifies the flecks that compose your project.
Using `@flecks/create-fleck` creates the following `flecks.yml`:
```yml
'@flecks/core': {}
'@flecks/fleck': {}
```
This means that by default a new fleck will pull in the `@flecks/core` fleck, and the
`@flecks/fleck` fleck, both with default configuration.
### Overriding configuration 💪
`@flecks/core`'s configuration has an `id` key. Starting from the example above, overriding the
ID to, say, `'example'`, would look like this:
```yml
'@flecks/core':
id: 'example'
'@flecks/fleck': {}
```
### Aliasing 🕵️‍♂️
Flecks may be aliased to alternative paths.
Say you have an application structured as a monorepo with a `packages` directory. If you have a
subpackage named `@my-monorepo/foo`, you could alias your fleck, like so:
```yml
'@flecks/core': {}
'@flecks/server': {}
'@my-monorepo/foo:./packages/foo/src': {}
```
Within your application, the fleck will be referred to as `@my-monorepo/foo` even though
`./packages/foo/src` is where the package is actually located.
This way you can use package structure without having to worry about actually publishing them to
npm (or running verdaccio, for instance).
## On-the-fly compilation(!) 🤯
If your flecks are aliased (as above) or symlinked (e.g. `yarn link`), they will be treated
specially and will be compiled on-the-fly. The flecks are searched for a local `babel.config.js`,
which is used to compile the code if present.
This means you can e.g. develop your `packages` in a monorepo with full HMR support, on both the
server and the client, each with their own babel configuration!
Have fun!
## Resolution order 🤔
The flecks server provides an interface (`flecks.localConfig()`) for gathering configuration files
from the `build` directory. The resolution order is determined by a few variables:
- `filename` specifies the name of the configuration file, e.g. `babel.config.js`.
- `general` specifies a general variation of the given configuration. `@flecks/server` looks for
an overridden `server.neutrinorc.js` when building, however `general` is set to `.neutrinorc.js`,
so it will also accept overrides of that more general configuration file.
- `root` specifies an alternative location to search. Defaults to `FLECKS_ROOT`.
- `fleck` specifies the fleck owning the configuration. `@flecks/core` owns `babel.config.js`,
`@flecks/server` owns `server.neutrinorc.js`, etc. This only really matters if you are writing a
fleck that owns its configuration.
Given these considerations, and supposing we had the above variables set like:
```javascript
const filename = 'server.neutrinorc.js';
const general = '.neutrinorc.js';
const root = '/foo/bar/baz';
const fleck = '@flecks/server';
```
We would then expect flecks to search using the following resolution order:
- `/foo/bar/baz/build/server.neutrinorc.js`
- `/foo/bar/baz/build/.neutrinorc.js`
- `${FLECKS_ROOT}/build/server.neutrinorc.js`
- `${FLECKS_ROOT}/build/.neutrinorc.js`
- `@flecks/server/build/server.neutrinorc.js`
- `@flecks/server/build/.neutrinorc.js`

View File

@ -0,0 +1,59 @@
/**
* Hook into neutrino configuration.
* @param {string} target - The build target; e.g. `server`.
* @param {Object} config - The neutrino configuration.
*/
hooks['@flecks/core/build'] = (target, config) => {};
/**
* Alter build configurations after they have been hooked.
* @param {Object} configs - The neutrino configurations.
*/
hooks['@flecks/core/build/alter'] = (configs) => {};
/**
* Define CLI commands.
*/
hooks['@flecks/core/commands'] = (program) => {};
/**
* Define configuration.
*/
hooks['@flecks/core/config'] = () => {};
/**
* Alter configuration.
* @param {Object} config - The neutrino configuration.
*/
hooks['@flecks/core/config/alter'] = (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`.
*/
hooks['@flecks/core/gathered/hmr'] = (Class, hook) => {};
/**
* Invoked when a fleck is HMR'd
* @param {constructor} Class - The class.
* @param {string} hook - The gather hook; e.g. `@flecks/db/server/models`.
*/
hooks['@flecks/core/hmr'] = (Class, hook) => {};
/**
* Invoked when the application is starting. Use for order-independent initialization tasks.
*/
hooks['@flecks/core/starting'] = () => {};
/**
* Define neutrino build targets.
*/
hooks['@flecks/core/targets'] = () => {};
/**
* Hook into webpack configuration.
* @param {string} target - The build target; e.g. `server`.
* @param {Object} config - The neutrino configuration.
*/
hooks['@flecks/core/webpack'] = (target, config) => {};

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require('./.neutrinorc')).webpack();

View File

@ -0,0 +1,74 @@
{
"name": "@flecks/core",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"bin": {
"flecks": "./cli.js"
},
"scripts": {
"build": "NODE_PATH=./node_modules webpack --config ./build/webpack.config.js --mode production",
"clean": "rm -rf dist node_modules yarn.lock && yarn",
"lint": "NODE_PATH=./node_modules eslint --config ./build/.eslintrc.js --format codeframe --ext mjs,js .",
"test": "npm run-script build && mocha --reporter min --colors ./dist/test.js"
},
"files": [
"build",
"build/.eslint.defaults.js",
"build/.eslint.defaults.js.map",
"build/.eslintrc.js",
"build/.eslintrc.js.map",
"build/babel.config.js",
"build/babel.config.js.map",
"build/webpack.config.js",
"build/webpack.config.js.map",
"cli.js",
"cli.js.map",
"empty.js",
"empty.js.map",
"index.js",
"index.js.map",
"server.js",
"server.js.map",
"src",
"start.js",
"start.js.map",
"test.js",
"test.js.map"
],
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-proposal-optional-chaining": "^7.12.16",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/preset-env": "^7.12.11",
"@babel/register": "^7.12.10",
"@neutrinojs/airbnb": "^9.4.0",
"@neutrinojs/compile-loader": "^9.5.0",
"@neutrinojs/copy": "^9.4.0",
"@neutrinojs/node": "^9.1.0",
"babel-plugin-prepend": "^1.0.2",
"chai": "4.2.0",
"commander": "^8.3.0",
"debug": "4.3.1",
"eslint": "^7.0.0",
"eslint-import-resolver-webpack": "0.13.0",
"js-yaml": "3.14.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.intersection": "^4.4.0",
"lodash.set": "^4.3.2",
"lodash.without": "^4.4.0",
"neutrino": "^9.4.0",
"rimraf": "^3.0.2",
"source-map-support": "0.5.19",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
},
"devDependencies": {
"@neutrinojs/banner": "^9.4.0",
"glob": "^7.2.0",
"mocha": "^8.3.2"
}
}

View File

@ -0,0 +1,43 @@
const {
basename,
dirname,
extname,
join,
} = require('path');
const R = require('./require');
const resolver = (source) => (path) => {
// Does the file resolve as source?
try {
R.resolve(`${source}/${path}`);
return true;
}
catch (error) {
const ext = extname(path);
// Try the implicit [path]/index[.ext] variation.
try {
R.resolve(`${source}/${dirname(path)}/${basename(path, ext)}/index${ext}`);
return true;
}
catch (error) {
return false;
}
}
};
module.exports = () => (neutrino) => {
const {packageJson: {files = []}, source} = neutrino.options;
// index is not taken for granted.
neutrino.config.entryPoints.delete('index');
// Calculate entry points from `files`.
files
.filter(resolver(source))
.forEach((file) => {
const trimmed = join(dirname(file), basename(file, extname(file)));
neutrino.config
.entry(trimmed)
.clear()
.add(`./src/${trimmed}`);
});
};

View File

@ -0,0 +1,29 @@
const nodeExternals = require('webpack-node-externals');
module.exports = () => (neutrino) => {
const {name} = neutrino.options.packageJson;
/* eslint-disable indent */
neutrino.config
.devtool('source-map')
.target('node')
.optimization
.splitChunks(false)
.runtimeChunk(false)
.end()
.output
.filename('[name].js')
.library(name)
.libraryTarget('umd')
.umdNamedDefine(true)
.end()
.node
.set('__dirname', false)
.set('__filename', false);
/* eslint-enable indent */
const options = neutrino.config.module
.rule('compile')
.use('babel')
.get('options');
options.presets[0][1].targets = {esmodules: true};
neutrino.config.externals(nodeExternals({importType: 'umd'}));
};

View File

@ -0,0 +1,36 @@
const copy = require('@neutrinojs/copy');
const autoentry = require('./autoentry');
const fleck = require('./fleck');
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
module.exports = {
options: {
output: 'dist',
root: FLECKS_ROOT,
},
use: [
copy({
patterns: [
{
from: 'package.json',
to: '.',
},
{
from: 'build',
to: 'build',
},
{
from: 'src',
to: 'src',
},
],
pluginId: '@flecks/core/copy',
}),
autoentry(),
fleck(),
],
};

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line no-eval
module.exports = eval('"undefined" !== typeof require ? require : undefined');

View File

@ -0,0 +1,18 @@
module.exports = (api) => {
api.cache(true);
return {
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
presets: [
[
'@babel/preset-env',
{
exclude: [
'@babel/plugin-transform-regenerator',
],
},
],
],
};
};

View File

@ -0,0 +1,23 @@
module.exports = {
globals: {
__non_webpack_require__: true,
window: true,
},
ignorePatterns: [
'**/dist/**',
'/build/dox/hooks.js',
],
rules: {
'babel/object-curly-spacing': 'off',
'brace-style': ['error', 'stroustrup'],
'no-plusplus': 'off',
'no-shadow': 'off',
'padded-blocks': ['error', {classes: 'always'}],
yoda: 'off',
},
settings: {
'import/resolver': {
node: {},
},
},
};

View File

@ -0,0 +1,15 @@
const neutrino = require('neutrino');
const R = require('../bootstrap/require');
const {targetNeutrino} = require('../server/commands');
const {default: Flecks} = require('../server/flecks');
const {
FLECKS_BUILD_TARGET = 'fleck',
} = process.env;
const flecks = Flecks.bootstrap();
const config = R(process.env[targetNeutrino(FLECKS_BUILD_TARGET)]);
flecks.invokeFlat('@flecks/core/build', FLECKS_BUILD_TARGET, config);
module.exports = neutrino(config).eslintrc();

View File

@ -0,0 +1,69 @@
/* eslint-disable import/first */
require('source-map-support/register');
if ('production' !== process.env.NODE_ENV) {
try {
// eslint-disable-next-line global-require, import/no-unresolved
require('dotenv/config');
}
// eslint-disable-next-line no-empty
catch (error) {}
}
import D from 'debug';
import flatten from 'lodash.flatten';
import intersection from 'lodash.intersection';
import neutrino from 'neutrino';
import {targetNeutrino} from '../server/commands';
import Flecks from '../server/flecks';
const debug = D('@flecks/core/build/webpack.config.js');
const {
FLECKS_BUILD_LIST = '',
} = process.env;
const buildList = FLECKS_BUILD_LIST
.split(',')
.map((name) => name.trim())
.filter((e) => e);
const flecks = Flecks.bootstrap();
const buildConfigs = async () => {
debug('gathering configs');
let targets = flatten(flecks.invokeFlat('@flecks/core/targets'));
if (buildList.length > 0) {
targets = intersection(targets, buildList);
}
debug('building: %O', targets);
if (0 === targets.length) {
debug('no build configuration found! aborting...');
await new Promise(() => {});
}
const entries = await Promise.all(targets.map(
async (target) => [
target,
await __non_webpack_require__(process.env[targetNeutrino(target)]),
],
));
await Promise.all(
entries.map(async ([target, config]) => (
flecks.invokeFlat('@flecks/core/build', target, config)
)),
);
const neutrinoConfigs = Object.fromEntries(entries);
await Promise.all(flecks.invokeFlat('@flecks/core/build/alter', neutrinoConfigs));
const webpackConfigs = await Promise.all(
Object.entries(neutrinoConfigs)
.map(async ([target, config]) => {
const webpackConfig = neutrino(config).webpack();
await flecks.invokeFlat('@flecks/core/webpack', target, webpackConfig);
return webpackConfig;
}),
);
return webpackConfigs;
};
export default buildConfigs();

105
packages/core/src/cli.js Executable file
View File

@ -0,0 +1,105 @@
import {fork} from 'child_process';
import {join, resolve, sep} from 'path';
import {Command} from 'commander';
import D from 'debug';
import Flecks from './server/flecks';
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
const debug = D('@flecks/core/cli');
// Guarantee local node_modules path.
const defaultNodeModules = resolve(join(FLECKS_ROOT, 'node_modules'));
const nodePathSeparator = '/' === sep ? ':' : ';';
let updatedNodePath;
if (!process.env.NODE_PATH) {
updatedNodePath = defaultNodeModules;
}
else {
const parts = process.env.NODE_PATH.split(nodePathSeparator);
if (!parts.some((part) => resolve(part) === defaultNodeModules)) {
parts.push(defaultNodeModules);
updatedNodePath = parts.join(nodePathSeparator);
}
}
// Guarantee symlink preservation for linked flecks.
const updateSymlinkPreservation = !process.env.NODE_PRESERVE_SYMLINKS;
const environmentUpdates = (
updatedNodePath
|| updateSymlinkPreservation
)
? {
NODE_PATH: updatedNodePath,
NODE_PRESERVE_SYMLINKS: 1,
}
: undefined;
if (environmentUpdates) {
debug('updating environment, forking with %O...', environmentUpdates);
const forkOptions = {
env: {...process.env, ...environmentUpdates},
stdio: 'inherit',
};
fork(__filename, process.argv.slice(2), forkOptions)
.on('exit', (code, signal) => {
process.exitCode = code;
process.exit(signal);
});
}
else {
// Asynchronous command process code forwarding.
const forwardProcessCode = (fn) => async (...args) => {
const child = await fn(...args);
if ('object' !== typeof child) {
debug('action returned code %d', child);
process.exitCode = child;
return;
}
const reject = (error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exitCode = child.exitCode || 1;
};
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
debug('action exited with code %d', code);
process.exitCode = code;
});
};
// Initialize Commander.
const program = new Command();
program.enablePositionalOptions();
// Bootstrap.
debug('bootstrapping flecks...');
const flecks = Flecks.bootstrap();
debug('bootstrapped');
// Register commands.
const commands = flecks.invokeReduce('@flecks/core/commands', undefined, undefined, program);
const keys = Object.keys(commands);
for (let i = 0; i < keys.length; ++i) {
const {
action,
args = [],
description,
name = keys[i],
options = [],
} = commands[keys[i]];
debug('adding command %s...', name);
const cmd = program.command(name);
cmd.description(description);
for (let i = 0; i < args.length; ++i) {
cmd.addArgument(args[i]);
}
for (let i = 0; i < options.length; ++i) {
cmd.option(...options[i]);
}
cmd.action(forwardProcessCode(action));
}
// Parse commandline.
program.parse(process.argv);
}

View File

@ -0,0 +1,9 @@
export default function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

View File

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,126 @@
const createListener = (fn, that, type, once) => ({
fn,
that,
type,
once,
bound: that ? fn.bind(that) : fn,
});
export default function EventEmitterDecorator(Superclass) {
return class EventEmitter extends Superclass {
constructor(...args) {
super(...args);
this.$$events = Object.create(null);
}
addListener(typesOrType, fn, that) {
return this.on(typesOrType, fn, that);
}
// Notify ALL the listeners!
emit(type, ...args) {
const typeListeners = this.$$events[type];
if (typeListeners && typeListeners.length > 0) {
this.emitToListeners(typeListeners, args);
}
}
emitToListeners(listeners, args) {
for (let i = 0; i < listeners.length; ++i) {
const {
once,
type,
fn,
bound,
that,
} = listeners[i];
// Remove if only once.
if (once) {
this.offSingleEvent(type, fn);
}
// Fast path...
if (0 === args.length) {
bound();
}
else if (1 === args.length) {
bound(args[0]);
}
else if (2 === args.length) {
bound(args[0], args[1]);
}
else if (3 === args.length) {
bound(args[0], args[1], args[2]);
}
else if (4 === args.length) {
bound(args[0], args[1], args[2], args[3]);
}
else if (5 === args.length) {
bound(args[0], args[1], args[2], args[3], args[4]);
}
// Slow path...
else {
fn.apply(that, args);
}
}
}
off(typesOrType, fn) {
const types = Array.isArray(typesOrType) ? typesOrType : [typesOrType];
for (let i = 0; i < types.length; i++) {
this.offSingleEvent(types[i], fn);
}
return this;
}
offSingleEvent(type, fn) {
if ('function' !== typeof fn) {
// Only type.
if (type in this.$$events) {
this.$$events[type] = [];
}
return;
}
// Function.
if (!(type in this.$$events)) {
return;
}
this.$$events[type] = this.$$events[type].filter((listener) => listener.fn !== fn);
}
on(typesOrType, fn, that = undefined) {
this.$$on(typesOrType, fn, that, false);
return this;
}
$$on(typesOrType, fn, that, once) {
const types = Array.isArray(typesOrType) ? typesOrType : [typesOrType];
for (let i = 0; i < types.length; i++) {
this.onSingleEvent(types[i], fn, that, once);
}
}
once(types, fn, that = undefined) {
this.$$on(types, fn, that, true);
return this;
}
onSingleEvent(type, fn, that, once) {
if ('function' !== typeof fn) {
throw new TypeError('EventEmitter::onSingleEvent() requires function listener');
}
const listener = createListener(fn, that, type, once);
if (!(type in this.$$events)) {
this.$$events[type] = [];
}
this.$$events[type].push(listener);
}
removeListener(...args) {
return this.off(...args);
}
};
}

480
packages/core/src/flecks.js Normal file
View File

@ -0,0 +1,480 @@
// eslint-disable-next-line max-classes-per-file
import {
basename,
dirname,
extname,
join,
} from 'path';
import D from 'debug';
import get from 'lodash.get';
import set from 'lodash.set';
import without from 'lodash.without';
import Middleware from './middleware';
const debug = D('@flecks/core/flecks');
export const ById = Symbol.for('@flecks/core/byId');
export const ByType = Symbol.for('@flecks/core/byType');
export const Hooks = Symbol.for('@flecks/core/hooks');
const capitalize = (string) => string.substring(0, 1).toUpperCase() + string.substring(1);
const camelCase = (string) => string.split(/[_-]/).map(capitalize).join('');
const hotGathered = new Map();
const wrapperClass = (Class, id, idAttribute, type, typeAttribute) => {
class Subclass extends Class {
static get [idAttribute]() {
return id;
}
static get [typeAttribute]() {
return type;
}
}
return Subclass;
};
export default class Flecks {
constructor({
config = {},
flecks = {},
platforms = [],
} = {}) {
this.originalConfig = JSON.parse(JSON.stringify(config));
this.config = {
...Object.fromEntries(Object.keys(flecks).map((path) => [path, {}])),
...config,
};
this.hooks = {};
this.flecks = {};
this.platforms = platforms;
const entries = Object.entries(flecks);
debug('paths: %O', entries.map(([fleck]) => fleck));
for (let i = 0; i < entries.length; i++) {
const [fleck, M] = entries[i];
this.registerFleck(fleck, M);
}
this.introduceConfig();
}
static decorate(
context,
{
transformer = camelCase,
} = {},
) {
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));
// eslint-disable-next-line no-param-reassign
Gathered[key] = M(Gathered[key], flecks);
});
return Gathered;
};
}
expandedFlecks(hook) {
const flecks = this.lookupFlecks(hook);
let expanded = [];
for (let i = 0; i < flecks.length; ++i) {
const fleck = flecks[i];
expanded.push(fleck);
for (let j = 0; j < this.platforms.length; ++j) {
const platform = this.platforms[j];
const variant = join(fleck, platform);
if (this.fleck(variant)) {
expanded.push(variant);
}
}
}
const index = expanded.findIndex((fleck) => '...' === fleck);
if (-1 !== index) {
if (-1 !== expanded.slice(index + 1).findIndex((fleck) => '...' === fleck)) {
throw new Error(
`Illegal ordering specification: hook '${hook}' has multiple ellipses.`,
);
}
const before = expanded.slice(0, index);
const after = expanded.slice(index + 1);
const implementing = this.flecksImplementing(hook);
const all = [];
for (let i = 0; i < implementing.length; ++i) {
const fleck = implementing[i];
all.push(fleck);
for (let j = 0; j < this.platforms.length; ++j) {
const platform = this.platforms[j];
const variant = join(fleck, platform);
if (this.fleck(variant)) {
all.push(variant);
}
}
}
const rest = without(all, ...before.concat(after));
expanded = [...before, ...rest, ...after];
}
return expanded;
}
fleck(fleck) {
return this.flecks[fleck];
}
fleckImplements(fleck, hook) {
return !!this.hooks[hook].find(({fleck: candidate}) => fleck === candidate);
}
flecksImplementing(hook) {
return this.hooks[hook]?.map(({fleck}) => fleck) || [];
}
gather(
hook,
{
idAttribute = 'id',
typeAttribute = 'type',
check = () => {},
} = {},
) {
if (!hook || 'string' !== typeof hook) {
throw new TypeError('Flecks.gather(): Expects parameter 1 (hook) to be string');
}
const raw = this.invokeReduce(hook);
check(raw, hook);
const decorated = this.invokeComposed(`${hook}.decorate`, raw);
check(decorated, `${hook}.decorate`);
let uid = 1;
const ids = {};
const types = (
Object.fromEntries(
Object.entries(decorated)
.sort(([ltype], [rtype]) => (ltype < rtype ? -1 : 1))
.map(([type, Class]) => {
const id = uid++;
ids[id] = wrapperClass(Class, id, idAttribute, type, typeAttribute);
return [type, ids[id]];
}),
)
);
const gathered = {
...ids,
...types,
[ById]: ids,
[ByType]: types,
};
hotGathered.set(hook, {idAttribute, gathered, typeAttribute});
debug("gathered '%s': %O", hook, gathered);
return gathered;
}
get(path, defaultValue) {
return get(this.config, path, defaultValue);
}
introduceConfig() {
const defaultConfig = this.invoke('@flecks/core/config');
this.invokeFlat('@flecks/core/config/alter', defaultConfig);
const flecks = Object.keys(defaultConfig);
for (let i = 0; i < flecks.length; i++) {
const fleck = flecks[i];
this.config[fleck] = {
...defaultConfig[fleck],
...this.config[fleck],
};
}
debug('config: %O', this.config);
}
invoke(hook, ...args) {
if (!this.hooks[hook]) {
return [];
}
return this.flecksImplementing(hook)
.reduce((r, fleck) => ({
...r,
[fleck]: this.invokeFleck(hook, fleck, ...args),
}), {});
}
invokeComposed(hook, arg, ...args) {
if (!this.hooks[hook]) {
return arg;
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return arg;
}
return flecks
.filter((fleck) => this.fleckImplements(fleck, hook))
.reduce((r, fleck) => this.invokeFleck(hook, fleck, r, ...args), arg);
}
invokeFlat(hook, ...args) {
if (!this.hooks[hook]) {
return [];
}
return this.hooks[hook].map(({fleck}) => this.invokeFleck(hook, fleck, ...args));
}
invokeFleck(hook, fleck, ...args) {
debug('invokeFleck(%s, %s, ...)', hook, fleck);
if (!this.hooks[hook]) {
return undefined;
}
const candidate = this.hooks[hook]
.find(({fleck: candidate}) => candidate === fleck);
if (!candidate) {
return undefined;
}
return candidate.fn(...(args.concat(this)));
}
invokeParallel(hook, ...args) {
if (!this.hooks[hook]) {
return [];
}
const flecks = this.flecksImplementing(hook);
if (0 === flecks.length) {
return [];
}
const results = [];
for (let i = 0; i < flecks.length; ++i) {
results.push(this.invokeFleck(hook, flecks[i], ...(args.concat(this))));
}
return results;
}
invokeReduce(hook, initial = {}, reducer = (r, o) => ({...r, ...o}), ...args) {
if (!this.hooks[hook]) {
return initial;
}
return this.hooks[hook]
.reduce((r, {fleck}) => reducer(r, this.invokeFleck(hook, fleck, ...args)), initial);
}
async invokeReduceAsync(hook, initial = {}, reducer = (r, o) => ({...r, ...o}), ...args) {
if (!this.hooks[hook]) {
return initial;
}
return this.hooks[hook]
.reduce(
async (r, {fleck}) => reducer(await r, await this.invokeFleck(hook, fleck, ...args)),
initial,
);
}
invokeSequential(hook, ...args) {
if (!this.hooks[hook]) {
return [];
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return [];
}
const results = [];
while (flecks.length > 0) {
const fleck = flecks.shift();
if (this.fleckImplements(fleck, hook)) {
results.push(this.invokeFleck(hook, fleck, ...args));
}
}
return results;
}
async invokeSequentialAsync(hook, ...args) {
if (!this.hooks[hook]) {
return [];
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return [];
}
const results = [];
while (flecks.length > 0) {
const fleck = flecks.shift();
if (this.fleckImplements(fleck, hook)) {
// eslint-disable-next-line no-await-in-loop
results.push(await this.invokeFleck(hook, fleck, ...args));
}
}
return results;
}
isOnPlatform(platform) {
return -1 !== this.platforms.indexOf(platform);
}
lookupFlecks(hook) {
const parts = hook.split('/');
const key = parts.pop();
return this.config[parts.join('/')]?.[key]?.concat() || [];
}
makeMiddleware(hook) {
debug('makeMiddleware(...): %s', hook);
if (!this.hooks[hook]) {
return Promise.resolve();
}
const flecks = this.expandedFlecks(hook);
if (0 === flecks.length) {
return Promise.resolve();
}
const middleware = flecks
.filter((fleck) => this.fleckImplements(fleck, hook));
debug('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);
}
};
}
static provide(
context,
{
transformer = camelCase,
} = {},
) {
return (flecks) => (
Object.fromEntries(
context.keys()
.map((path) => {
const {default: M} = context(path);
if ('function' !== typeof M) {
throw new ReferenceError(
`Flecks.provide(): require(${
path
}).default is not a function (from: ${
context.id
})`,
);
}
return [
transformer(this.symbolizePath(path)),
M(flecks),
];
}),
)
);
}
refresh(fleck, M) {
debug('refreshing %s...', fleck);
// Remove old hook implementations.
const keys = Object.keys(this.hooks);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
if (this.hooks[key]) {
const index = this.hooks[key].findIndex(({fleck: hookPlugin}) => hookPlugin === fleck);
if (-1 !== index) {
this.hooks[key].splice(index, 1);
}
}
}
// Replace the fleck.
this.registerFleck(fleck, M);
// Write config.
const defaultConfig = this.invoke('@flecks/core/config');
this.config[fleck] = {
...defaultConfig[fleck],
...this.config[fleck],
};
this.invokeFlat('@flecks/core/config/alter', this.config);
// HMR.
this.updateHotGathered(fleck);
}
registerFleck(fleck, M) {
debug('registering %s...', fleck);
this.flecks[fleck] = M;
if (M.default) {
const {default: {[Hooks]: hooks}} = M;
if (hooks) {
const keys = Object.keys(hooks);
debug("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 {
debug("'%s' has no default export: %O", fleck, M);
}
}
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/gathered/hmr', Subclass, hook);
}
}
}
}
}

View File

@ -0,0 +1,22 @@
import {Hooks} from './flecks';
export {default as autoentry} from './bootstrap/autoentry';
export {default as fleck} from './bootstrap/fleck';
export {default as compose} from './compose';
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': () => ({
'eslint.exclude': [],
id: 'flecks',
}),
},
};

View File

@ -0,0 +1,64 @@
export default class Middleware {
constructor(middleware = []) {
this.middleware = [];
for (let i = 0; i < middleware.length; ++i) {
this.middleware.push(this.constructor.check(middleware[i]));
}
}
static check(middleware) {
if ('function' !== typeof middleware) {
if ('undefined' !== typeof middleware.then) {
throw new TypeError('middleware expected a function, looks like a promise');
}
throw new TypeError('middleware expected a function');
}
return middleware;
}
dispatch(...args) {
const fn = args.pop();
const middleware = this.middleware.concat();
const invoke = (error) => {
if (middleware.length > 0) {
const current = middleware.shift();
// Check mismatch.
if ((args.length + 2 === current.length) === !error) {
invoke(error);
}
// Invoke.
else {
try {
current(...args.concat(error ? [error] : []).concat(invoke));
}
catch (error) {
invoke(error);
}
}
}
// Finish...
else if (fn) {
setTimeout(() => fn(error), 0);
}
};
invoke();
}
promise(...args) {
return new Promise((resolve, reject) => {
this.dispatch(...(args.concat((error) => {
if (error) {
reject(error);
return;
}
resolve();
})));
});
}
use(fn) {
this.middleware.push(this.constructor.check(fn));
}
}

View File

@ -0,0 +1,171 @@
import {spawn} from 'child_process';
import {join, normalize} from 'path';
import {Argument} from 'commander';
import D from 'debug';
import flatten from 'lodash.flatten';
import rimraf from 'rimraf';
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
const debug = D('@flecks/core/commands');
const flecksRoot = normalize(FLECKS_ROOT);
export const spawnWith = (cmd, localEnv, spawnArgs) => {
debug('spawning:\n%s %s\nwith local environment: %O', cmd, spawnArgs.join(' '), localEnv);
const spawnOptions = {
env: {...localEnv, ...process.env},
stdio: 'inherit',
};
return spawn('npx', [cmd, ...spawnArgs], spawnOptions);
};
export const targetNeutrino = (target) => (
`FLECKS_CORE_BUILD_TARGET_${
target
.toUpperCase()
.replace(/[^A-Za-z0-9]/g, '_')
}_NEUTRINO`
);
export const targetNeutrinos = (flecks) => {
const entries = Object.entries(flecks.invoke('@flecks/core/targets'));
const targetNeutrinos = {};
for (let i = 0; i < entries.length; ++i) {
const [fleck, targets] = entries[i];
targets
.forEach((target) => {
targetNeutrinos[targetNeutrino(target)] = flecks.localConfig(
`${target}.neutrinorc.js`,
fleck,
{general: '.neutrinorc.js'},
);
});
}
return targetNeutrinos;
};
export default (program, flecks) => {
Object.entries(targetNeutrinos(flecks))
.forEach(([key, value]) => {
process.env[key] = value;
});
const commands = {
clean: {
description: 'remove node_modules, lock file, build artifacts, then reinstall',
action: (opts) => {
const {
noYarn,
} = opts;
rimraf.sync(join(flecksRoot, 'dist'));
rimraf.sync(join(flecksRoot, 'node_modules'));
if (noYarn) {
rimraf.sync(join(flecksRoot, 'package-lock.json'));
return spawn('npm', ['install'], {stdio: 'inherit'});
}
rimraf.sync(join(flecksRoot, 'yarn.lock'));
return spawn('yarn', [], {stdio: 'inherit'});
},
options: [
['--no-yarn', 'use npm instead of yarn'],
],
},
};
const targets = flatten(flecks.invokeFlat('@flecks/core/targets'));
if (targets.length > 0) {
commands.build = {
args: [
new Argument('[target]', 'target').choices(targets),
],
options: [
['-d, --no-production', 'dev build'],
['-h, --hot', 'build with hot module reloading'],
['-w, --watch', 'watch for changes'],
['-v, --verbose', 'verbose output'],
],
description: 'build',
action: (target, opts) => {
const {
hot,
production,
watch,
verbose,
} = opts;
debug('Building...', opts);
const webpackConfig = flecks.localConfig('webpack.config.js', '@flecks/core');
const localEnv = {
...targetNeutrinos(flecks),
...(target ? {FLECKS_BUILD_LIST: target} : {}),
...(hot ? {FLECKS_HOT: 1} : {}),
};
const spawnArgs = [
'--config', webpackConfig,
'--mode', (production && !hot) ? 'production' : 'development',
...(verbose ? ['--stats', 'verbose'] : []),
...((watch || hot) ? ['--watch'] : []),
];
return spawnWith('webpack', localEnv, spawnArgs);
},
};
commands.lint = {
description: 'run linter',
args: [
program.createArgument('[target]', 'target').choices(targets),
],
action: (targetArgument) => {
const promises = [];
for (let i = 0; i < targets.length; ++i) {
const target = targets[i];
if (targetArgument && targetArgument !== target) {
// eslint-disable-next-line no-continue
continue;
}
process.env.FLECKS_BUILD_TARGET = target;
const spawnArgs = [
'--config', flecks.localConfig(
`${target}.eslintrc.js`,
'@flecks/core',
{general: '.eslintrc.js'},
),
'--format', 'codeframe',
'--ext', 'js',
'.',
];
const localEnv = {
FLECKS_BUILD_TARGET: target,
...targetNeutrinos(flecks),
};
promises.push(new Promise((resolve, reject) => {
const child = spawnWith('eslint', localEnv, spawnArgs);
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
}));
}
const promise = Promise.all(promises)
.then(
(codes) => (
codes.every((code) => 0 === parseInt(code, 10))
? 0
: codes.find((code) => code !== 0)
),
);
return {
off: () => {},
on: (type, fn) => {
if ('error' === type) {
promise.catch(fn);
}
else if ('exit' === type) {
promise.then(fn);
}
},
};
},
};
}
return commands;
};

View File

@ -0,0 +1,463 @@
import {
readFileSync,
realpathSync,
statSync,
} from 'fs';
import {
basename,
dirname,
extname,
join,
} from 'path';
import compileLoader from '@neutrinojs/compile-loader';
import D from 'debug';
import R from '../bootstrap/require';
import Flecks from '../flecks';
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
const debug = D('@flecks/core/flecks/server');
export default class ServerFlecks extends Flecks {
constructor(options = {}) {
super(options);
const {
resolver = {},
rcs = {},
} = options;
this.resolver = resolver;
this.rcs = rcs;
}
aliases() {
return this.constructor.aliases(this.rcs);
}
static aliases(rcs) {
const keys = Object.keys(rcs);
let aliases = {};
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const config = rcs[key];
if (config.aliases && Object.keys(config.aliases).length > 0) {
aliases = {...aliases, ...config.aliases};
}
}
return aliases;
}
static bootstrap({platforms = ['server'], without = []} = {}) {
let initial;
let configType;
try {
const {safeLoad} = R('js-yaml');
const filename = join(FLECKS_ROOT, 'build', 'flecks.yml');
const buffer = readFileSync(filename, 'utf8');
debug('parsing configuration from YML...');
initial = safeLoad(buffer, {filename}) || {};
configType = 'YML';
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
initial = {'@flecks/core': {}};
configType = 'barebones';
}
debug('bootstrap configuration (%s): %O', configType, initial);
// Fleck discovery.
const aliased = {};
const config = {};
const resolver = {};
const keys = Object.keys(initial);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const index = key.lastIndexOf(':');
const [path, alias] = -1 === index ? [key, key] : [key.slice(0, index), key.slice(index + 1)];
if (-1 !== without.indexOf(path.split('/').pop())) {
// eslint-disable-next-line no-continue
continue;
}
const aliasPath = '.'.charCodeAt(0) === alias.charCodeAt(0)
? join(FLECKS_ROOT, alias)
: alias;
try {
config[path] = initial[key];
R.resolve(aliasPath);
if (path !== alias) {
aliased[path] = aliasPath;
}
resolver[path] = aliasPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
// Discover platform-specific variants.
if (platforms) {
platforms.forEach((platform) => {
try {
const platformAliasPath = join(aliasPath, platform);
const platformPath = join(path, platform);
R.resolve(platformAliasPath);
if (path !== alias) {
aliased[platformPath] = platformAliasPath;
}
config[platformPath] = config[platformPath] || {};
resolver[platformPath] = platformAliasPath;
}
// eslint-disable-next-line no-empty
catch (error) {}
});
}
}
const paths = Object.keys(resolver);
const rcs = {};
const roots = Array.from(new Set(
paths
.map((path) => this.root(resolver, path))
.filter((e) => !!e),
));
for (let i = 0; i < roots.length; ++i) {
const root = roots[i];
try {
rcs[root] = R(join(root, 'build', '.flecksrc'));
}
catch (error) {
if ('MODULE_NOT_FOUND' !== error.code) {
throw error;
}
}
}
// Stub platform-unfriendly modules.
const stubs = this.stubs(platforms, rcs);
if (stubs.length > 0) {
debug('stubbing: %O', stubs);
const regex = new RegExp(stubs.join('|'));
R('pirates').addHook(
() => '',
{
ignoreNodeModules: false,
matcher: (path) => !!path.match(regex),
},
);
}
// Flecks that are aliased or symlinked need compilation.
const flecks = {};
const needCompilation = paths
.filter((path) => (
this.fleckIsAliased(resolver, path) || this.fleckIsSymlinked(resolver, path)
));
// Lookups redirect require() requests.
const lookups = {
...Object.fromEntries(
needCompilation
.map((path) => [
R.resolve(this.fleckIsAliased(resolver, path) ? aliased[path] : path),
this.fleckIsAliased(resolver, path)
? aliased[path]
: this.sourcepath(R.resolve(this.resolve(resolver, path))),
]),
),
};
debug('lookups: %O', lookups);
R('pirates').addHook(
(code, path) => `module.exports = require('${lookups[path]}')`,
{
ignoreNodeModules: false,
// eslint-disable-next-line arrow-body-style
matcher: (path) => {
return !!lookups[path];
},
},
);
const {Module} = R('module');
const aliases = this.aliases(rcs);
debug('aliases: %O', aliases);
// Nasty hax to give us FULL CONTROL.
const {require: Mr} = Module.prototype;
const requirers = {
...aliased,
...aliases,
};
Module.prototype.require = function hackedRequire(request, options) {
if (requirers[request]) {
return Mr.call(this, requirers[request], options);
}
return Mr.call(this, request, options);
};
// Key flecks needing compilation by their roots, so we can compile all common roots with a
// single invocation of `@babel/register`.
const compilationRootMap = {};
needCompilation.forEach((fleck) => {
const root = this.root(resolver, fleck);
if (!compilationRootMap[root]) {
compilationRootMap[root] = [];
}
compilationRootMap[root].push(fleck);
});
// Register a compiler for each root and require() the flecks underneath.
Object.entries(compilationRootMap).forEach(([root, compiling]) => {
const resolved = dirname(R.resolve(join(root, 'package.json')));
const configFile = this.localConfig(
resolver,
'babel.config.js',
'@flecks/core',
{root: realpathSync(resolved)},
);
const register = R('@babel/register');
register({
cache: true,
configFile,
only: [this.sourcepath(resolved)],
// Make webpack goodies exist in node land.
plugins: [
[
'prepend',
{
prepend: [
'require.context = (',
' directory,',
' useSubdirectories = true,',
' regExp = /^\\.\\/.*$/,',
' mode = "sync",',
') => {',
' const glob = require("glob");',
' const {resolve, sep} = require("path");',
' const keys = glob.sync(',
' useSubdirectories ? "**/*" : "*",',
' {cwd: resolve(__dirname, directory)},',
' )',
' .filter((key) => key.match(regExp))',
' .map(',
' (key) => (',
' -1 !== [".".charCodeAt(0), "/".charCodeAt(0)].indexOf(key.charCodeAt(0))',
' ? key',
' : ("." + sep + key)',
' ),',
' );',
' const R = (request) => require(keys[request]);',
' R.id = __filename',
' R.keys = () => keys;',
' return R;',
'};',
].join('\n'),
},
'require.context',
],
[
'prepend',
{
prepend: 'const __non_webpack_require__ = require;',
},
'__non_webpack_require__',
],
],
});
compiling.forEach((fleck) => {
flecks[fleck] = R(this.resolve(resolver, fleck));
// Remove the required fleck from the list still needing require().
paths.splice(paths.indexOf(fleck), 1);
});
// Don't pollute, kids.
register.revert();
});
// Load the rest of the flecks.
paths.forEach((path) => {
flecks[path] = R(this.resolve(resolver, path));
});
return new ServerFlecks({
config,
flecks,
platforms,
rcs,
resolver,
});
}
fleckIsAliased(fleck) {
return this.constructor.fleckIsAliased(this.resolver, fleck);
}
static fleckIsAliased(resolver, fleck) {
return fleck !== this.resolve(resolver, fleck);
}
fleckIsSymlinked(fleck) {
return this.constructor.fleckIsSymlinked(this.resolver, fleck);
}
static fleckIsSymlinked(resolver, fleck) {
const resolved = R.resolve(this.resolve(resolver, fleck));
const realpath = realpathSync(resolved);
return realpath !== resolved;
}
localConfig(path, fleck, options) {
return this.constructor.localConfig(this.resolver, path, fleck, options);
}
static localConfig(resolver, path, fleck, {general = path, root = FLECKS_ROOT} = {}) {
let configFile;
try {
const localConfig = join(root, 'build', path);
statSync(localConfig);
configFile = localConfig;
}
catch (error) {
try {
const localConfig = join(root, 'build', general);
statSync(localConfig);
configFile = localConfig;
}
catch (error) {
try {
const localConfig = join(FLECKS_ROOT, 'build', path);
statSync(localConfig);
configFile = localConfig;
}
catch (error) {
try {
const localConfig = join(FLECKS_ROOT, 'build', general);
statSync(localConfig);
configFile = localConfig;
}
catch (error) {
const resolved = this.resolve(resolver, fleck);
try {
configFile = R.resolve(join(resolved, 'build', path));
}
catch (error) {
configFile = R.resolve(join(resolved, 'build', general));
}
}
}
}
}
return configFile;
}
rcs() {
return this.rcs;
}
sourcepath(fleck) {
return this.constructor.sourcepath(fleck);
}
static sourcepath(path) {
let sourcepath = realpathSync(path);
const parts = sourcepath.split('/');
const indexOf = parts.lastIndexOf('dist');
if (-1 !== indexOf) {
parts.splice(indexOf, 1, 'src');
sourcepath = parts.join('/');
sourcepath = join(dirname(sourcepath), basename(sourcepath, extname(sourcepath)));
}
else {
sourcepath = join(sourcepath, 'src');
}
return sourcepath;
}
resolve(path) {
return this.constructor.resolve(this.resolver, path);
}
static resolve(resolver, fleck) {
return resolver[fleck] || fleck;
}
root(fleck) {
return this.constructor.root(this.resolver, fleck);
}
static root(resolver, fleck) {
const parts = this.resolve(resolver, fleck).split('/');
try {
R.resolve(parts.join('/'));
}
catch (error) {
return undefined;
}
while (parts.length > 0) {
try {
R.resolve(join(parts.join('/'), 'package.json'));
return parts.join('/');
}
catch (error) {
parts.pop();
}
}
return undefined;
}
runtimeCompiler(runtime, neutrino, allowlist = []) {
const {config} = neutrino;
// Pull the default compiler.
if (config.module.rules.has('compile')) {
config.module.rules.delete('compile');
}
// Flecks that are aliased or symlinked need compilation.
const needCompilation = Object.entries(this.resolver)
.filter(([fleck]) => this.fleckIsAliased(fleck) || this.fleckIsSymlinked(fleck));
// Alias and de-externalize.
needCompilation
.forEach(([fleck, resolved]) => {
const alias = this.fleckIsAliased(fleck)
? resolved
: this.sourcepath(R.resolve(this.resolve(fleck)));
allowlist.push(`${fleck}$`);
config.resolve.alias
.set(`${fleck}$`, alias);
});
// Set up compilation at each root.
Array.from(new Set(
needCompilation
.map(([fleck]) => fleck)
.map((fleck) => this.root(fleck)),
))
.forEach((root) => {
const resolved = dirname(R.resolve(join(root, 'package.json')));
const sourcepath = this.sourcepath(resolved);
const configFile = this.localConfig(
'babel.config.js',
'@flecks/core',
{root: resolved},
);
compileLoader({
include: [sourcepath],
babel: {configFile},
ruleId: `@flecks/${runtime}/runtime/compile[${root}]`,
})(neutrino);
});
}
stubs() {
return this.constructor.stubs(this.platforms, this.rcs);
}
static stubs(platforms, rcs) {
const keys = Object.keys(rcs);
const stubs = {};
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const config = rcs[key];
if (config.stubs) {
Object.entries(config.stubs)
.forEach(([platform, paths]) => {
if (-1 !== platforms.indexOf(platform)) {
paths.forEach((path) => {
stubs[path] = true;
});
}
});
}
}
return Object.keys(stubs);
}
}

View File

@ -0,0 +1,46 @@
import airbnb from '@neutrinojs/airbnb';
import {Hooks} from '../flecks';
import commands from './commands';
import R from '../bootstrap/require';
export {
default as commands,
spawnWith,
targetNeutrino,
targetNeutrinos,
} from './commands';
export {default as Flecks} from './flecks';
export {default as require} from '../bootstrap/require';
export default {
[Hooks]: {
'@flecks/core/build': (target, config, flecks) => {
const {'eslint.exclude': exclude} = flecks.get('@flecks/core');
if (-1 !== exclude.indexOf(target)) {
return;
}
const baseConfig = R(
flecks.localConfig(
`${target}.eslint.defaults.js`,
'@flecks/core',
{general: '.eslint.defaults.js'},
),
);
config.use.unshift(
airbnb({
eslint: {
baseConfig: {
...baseConfig,
env: {
mocha: true,
},
},
},
}),
);
},
'@flecks/core/commands': commands,
},
};

View File

@ -0,0 +1,23 @@
import Flecks, {Hooks} from '../../src/flecks';
export default {
[Hooks]: {
'@flecks/core/config': () => ({
foo: 'bar',
'test-gather.decorate': ['...'],
}),
'./fleck-one/test-gather': (
Flecks.provide(require.context('./things', false, /\.js$/))
),
'./fleck-one/test-gather.decorate': (
Flecks.decorate(require.context('./things/decorators', false, /\.js$/))
),
'flecks-test-invoke': () => 69,
'flecks-test-invoke-parallel': (O) => {
// eslint-disable-next-line no-param-reassign
O.foo *= 2;
},
'flecks-test-invoke-reduce': () => ({foo: 69}),
'flecks-test-invoke-reduce-async': () => new Promise((resolve) => resolve({foo: 69})),
},
};

View File

@ -0,0 +1,6 @@
export default (Three) => class AnotherThree extends Three {
static bar() {
}
};

View File

@ -0,0 +1,7 @@
export default () => class One {
static get foo() {
return 'One';
}
};

View File

@ -0,0 +1,7 @@
export default () => class Two {
static get foo() {
return 'Two';
}
};

View File

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

View File

@ -0,0 +1,7 @@
export default () => class Three {
static get foo() {
return 'Three';
}
};

View File

@ -0,0 +1,26 @@
import {expect} from 'chai';
import Flecks, {ById, ByType} from '../src/flecks';
const testFleckOne = require('./fleck-one');
const testFleckTwo = require('./fleck-two');
it('can gather', () => {
const flecks = new Flecks({
flecks: {
'./fleck-one': testFleckOne,
'./fleck-two': testFleckTwo,
},
});
const Gathered = flecks.gather('./fleck-one/test-gather');
expect(Object.keys(Gathered[ByType]).length)
.to.equal(Object.keys(Gathered[ById]).length);
const typeKeys = Object.keys(Gathered[ByType]);
for (let i = 0; i < typeKeys.length; ++i) {
const type = typeKeys[i];
expect(Gathered[type].foo)
.to.equal(type);
}
expect(typeof Gathered.Three.bar)
.to.not.equal('undefined');
});

View File

@ -0,0 +1,32 @@
import {expect} from 'chai';
import Flecks from '../src/flecks';
const testFleckOne = require('./fleck-one');
it('can create an empty instance', () => {
const flecks = new Flecks();
expect(Object.keys(flecks.originalConfig).length)
.to.equal(0);
expect(Object.keys(flecks.config).length)
.to.equal(0);
expect(Object.keys(flecks.hooks).length)
.to.equal(0);
expect(Object.keys(flecks.flecks).length)
.to.equal(0);
});
it('can gather config', () => {
let flecks;
flecks = new Flecks({
flecks: {'./fleck-one': testFleckOne},
});
expect(flecks.get(['./fleck-one']))
.to.contain({foo: 'bar'});
flecks = new Flecks({
config: {'./fleck-one': {foo: 'baz'}},
flecks: {'./fleck-one': testFleckOne},
});
expect(flecks.get(['./fleck-one']))
.to.contain({foo: 'baz'});
});

View File

@ -0,0 +1,42 @@
import {expect} from 'chai';
import Flecks from '../src/flecks';
const testFleckOne = require('./fleck-one');
const testFleckTwo = require('./fleck-two');
let flecks;
beforeEach(() => {
flecks = new Flecks({
flecks: {
'./fleck-one': testFleckOne,
'./fleck-two': testFleckTwo,
},
});
});
it('can invoke', () => {
expect(flecks.invoke('flecks-test-invoke'))
.to.deep.equal({
'./fleck-one': 69,
'./fleck-two': 420,
});
});
it('can invoke parallel', async () => {
const O = {foo: 3};
await Promise.all(flecks.invokeParallel('flecks-test-invoke-parallel', O));
expect(O.foo)
.to.equal(8);
});
it('can invoke reduced', () => {
expect(flecks.invokeReduce('flecks-test-invoke-reduce'))
.to.deep.equal({foo: 69, bar: 420});
});
it('can invoke reduced async', async () => {
expect(await flecks.invokeReduce('flecks-test-invoke-reduce'))
.to.deep.equal({foo: 69, bar: 420});
});

5897
packages/core/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

116
packages/create-app/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,44 @@
/* eslint-disable import/no-extraneous-dependencies */
const {chmod} = require('fs');
const {join} = require('path');
const banner = require('@neutrinojs/banner');
const copy = require('@neutrinojs/copy');
module.exports = require('@flecks/fleck/server/build/fleck.neutrinorc');
module.exports.use.push(banner({
banner: '#!/usr/bin/env node',
include: /^cli\.js$/,
pluginId: 'shebang',
raw: true,
}));
module.exports.use.push(({config}) => {
config
.plugin('executable')
.use(class Executable {
// eslint-disable-next-line class-methods-use-this
apply(compiler) {
compiler.hooks.afterEmit.tapAsync(
'Executable',
(compilation, callback) => {
chmod(join(__dirname, '..', 'dist', 'cli.js'), 0o755, callback);
},
);
}
});
});
module.exports.use.push(
copy({
patterns: [
{
from: 'template',
to: 'template',
},
],
}),
);

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

View File

@ -0,0 +1,26 @@
{
"name": "@flecks/create-app",
"version": "1.0.0",
"bin": {
"create-app": "./cli.js"
},
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"cli.js",
"cli.js.map",
"src",
"template"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"fs-extra": "10.0.0"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1,50 @@
import {spawn} from 'child_process';
import {readFileSync, writeFileSync} from 'fs';
import {join, normalize} from 'path';
import {
copySync,
mkdirpSync,
moveSync,
} from 'fs-extra';
const cwd = normalize(process.cwd());
const forwardProcessCode = (fn) => async (...args) => {
process.exitCode = await fn(args.slice(0, -2));
};
const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
});
const create = () => async () => {
const name = process.argv[2];
const path = name.split('/').pop();
copySync(join(__dirname, 'template'), join(cwd, path), {recursive: true});
mkdirpSync(join(cwd, path, 'packages'));
moveSync(join(cwd, path, '.gitignore.extraneous'), join(cwd, path, '.gitignore'));
moveSync(join(cwd, path, 'package.json.extraneous'), join(cwd, path, 'package.json'));
writeFileSync(
join(cwd, path, 'package.json'),
JSON.stringify(
{
name: `@${name}/monorepo`,
...JSON.parse(readFileSync(join(cwd, path, 'package.json')).toString()),
},
null,
2,
),
);
const code = await processCode(spawn('yarn', [], {cwd: join(cwd, path), stdio: 'inherit'}));
if (0 !== code) {
return code;
}
return processCode(spawn('yarn', ['build'], {cwd: join(cwd, path), stdio: 'inherit'}));
};
forwardProcessCode(create())();

View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/server': {}

View File

@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "1.0.0"
}

View File

@ -0,0 +1,15 @@
{
"private": true,
"scripts": {
"build": "flecks build",
"dev": "FLECKS_START_SERVER=1 npm run -- build -h"
},
"dependencies": {
"@flecks/core": "^1.0.0",
"@flecks/server": "^1.0.0"
},
"devDependencies": {
"@flecks/create-fleck": "^1.0.0",
"lerna": "^3.22.1"
}
}

File diff suppressed because it is too large Load Diff

116
packages/create-fleck/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,44 @@
/* eslint-disable import/no-extraneous-dependencies */
const {chmod} = require('fs');
const {join} = require('path');
const banner = require('@neutrinojs/banner');
const copy = require('@neutrinojs/copy');
module.exports = require('@flecks/fleck/server/build/fleck.neutrinorc');
module.exports.use.push(banner({
banner: '#!/usr/bin/env node',
include: /^cli\.js$/,
pluginId: 'shebang',
raw: true,
}));
module.exports.use.push(({config}) => {
config
.plugin('executable')
.use(class Executable {
// eslint-disable-next-line class-methods-use-this
apply(compiler) {
compiler.hooks.afterEmit.tapAsync(
'Executable',
(compilation, callback) => {
chmod(join(__dirname, '..', 'dist', 'cli.js'), 0o755, callback);
},
);
}
});
});
module.exports.use.push(
copy({
patterns: [
{
from: 'template',
to: 'template',
},
],
}),
);

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

View File

@ -0,0 +1,27 @@
{
"name": "@flecks/create-fleck",
"version": "1.0.0",
"bin": {
"create-fleck": "./cli.js"
},
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"cli.js",
"cli.js.map",
"src",
"template"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"fs-extra": "10.0.0",
"validate-npm-package-name": "^3.0.0"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1,111 @@
import {spawn} from 'child_process';
import {
readFileSync,
statSync,
writeFileSync,
} from 'fs';
import {join, normalize} from 'path';
import {copySync, moveSync} from 'fs-extra';
import validate from 'validate-npm-package-name';
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
const cwd = normalize(FLECKS_ROOT);
const forwardProcessCode = (fn) => async (...args) => {
process.exitCode = await fn(args.slice(0, -2));
};
const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
});
const monorepoScope = () => {
try {
statSync(join(cwd, 'packages'));
const {name} = __non_webpack_require__(join(cwd, 'package.json'));
const [scope] = name.split('/');
return scope;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return undefined;
}
};
const testDestination = (destination) => {
try {
statSync(destination);
return false;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return true;
}
};
const create = () => async () => {
const rawname = process.argv[2];
const {errors} = validate(rawname);
if (errors) {
// eslint-disable-next-line no-console
console.error(`@flecks/create-fleck: invalid fleck name: ${errors.join(', ')}`);
return 128;
}
const parts = rawname.split('/');
let path = cwd;
let pkg;
let scope;
if (1 === parts.length) {
pkg = rawname;
}
else {
[scope, pkg] = parts;
}
if (!scope) {
scope = monorepoScope();
if (scope) {
path = join(path, 'packages');
}
}
const name = [scope, pkg].filter((e) => !!e).join('/');
const destination = join(path, pkg);
if (!testDestination(destination)) {
// eslint-disable-next-line no-console
console.error(`@flecks/create-fleck: destination '${destination} already exists: aborting`);
return 129;
}
// eslint-disable-next-line no-unreachable
copySync(join(__dirname, 'template'), destination, {recursive: true});
moveSync(join(destination, '.gitignore.extraneous'), join(destination, '.gitignore'));
moveSync(join(destination, 'package.json.extraneous'), join(destination, 'package.json'));
writeFileSync(
join(destination, 'package.json'),
JSON.stringify(
{
name,
...JSON.parse(readFileSync(join(destination, 'package.json')).toString()),
},
null,
2,
),
);
const code = await processCode(spawn('yarn', [], {cwd: destination, stdio: 'inherit'}));
if (0 !== code) {
return code;
}
return processCode(spawn('yarn', ['build'], {cwd: destination, stdio: 'inherit'}));
};
forwardProcessCode(create())();

View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

View File

@ -0,0 +1,21 @@
{
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"index.js",
"index.js.map",
"src"
],
"dependencies": {
"@flecks/core": "^1.0.0"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

File diff suppressed because it is too large Load Diff

116
packages/db/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

25
packages/db/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "@flecks/db",
"version": "1.0.0",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"server.js",
"server.js.map",
"src"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"debug": "4.3.1",
"sequelize": "^6.3.5",
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1,107 @@
import {ByType} from '@flecks/core';
import D from 'debug';
import Sequelize from 'sequelize';
import environment from './environment';
const debug = D('@flecks/db/server/connection');
export async function createDatabaseConnection(flecks) {
const env = environment();
debug('environment: %O', {...env, ...(env.password ? {password: '*** REDACTED ***'} : {})});
const sequelize = new Sequelize({
logging: false,
...env,
});
const Models = flecks.get('$flecks/db.models')[ByType];
Object.values(Models)
.filter((Model) => Model.attributes)
.forEach((Model) => {
Model.init(Model.attributes, {
sequelize,
underscored: true,
...(Model.modelOptions || {}),
});
});
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
await sequelize.authenticate();
break;
}
catch (error) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
const dependencies = {};
Object.values(Models).forEach((Model) => {
Model.associate(Models);
});
Object.values(Models).forEach((Model) => {
const associations = Object.entries(Model.associations);
for (let i = 0; i < associations.length; i++) {
if (associations[i][1].isSelfAssociation) {
// eslint-disable-next-line no-continue
continue;
}
if ('BelongsToMany' === associations[i][1].associationType) {
if (associations[i][1].through.model.isManagedByFlecks) {
const {name} = associations[i][1].through.model;
if (!dependencies[name]) {
dependencies[name] = new Set();
}
dependencies[name].add(Model.name);
}
}
if ('BelongsTo' === associations[i][1].associationType) {
if (!dependencies[Model.name]) {
dependencies[Model.name] = new Set();
}
dependencies[Model.name].add(associations[i][1].target.name);
}
}
});
const entries = Object.values(Models);
let lastLength = entries.length;
while (entries.length > 0) {
for (let i = 0; i < entries.length; i++) {
const Model = entries[i];
if (
!dependencies[Model.name]
|| 0 === dependencies[Model.name].length
) {
// eslint-disable-next-line no-await-in-loop
await Model.sync();
const dependents = Object.keys(dependencies);
for (let j = 0; j < dependents.length; j++) {
const dependent = dependents[j];
if (dependencies[dependent].has(Model.name)) {
dependencies[dependent].delete(Model.name);
if (0 === dependencies[dependent].size) {
delete dependencies[dependent];
}
}
}
entries.splice(i, 1);
break;
}
}
if (entries.length === lastLength) {
throw new TypeError(`@flecks/db circular dependencies: '${entries.join("', '")}'`);
}
lastLength = entries.length;
}
debug('synchronizing...');
await sequelize.sync();
debug('synchronized');
return sequelize;
}
export function destroyDatabaseConnection(databaseConnection) {
if (!databaseConnection) {
return undefined;
}
return databaseConnection.close();
}

View File

@ -0,0 +1,49 @@
import environment from './environment';
export default () => {
const {
dialect,
username,
password,
port,
database,
} = environment();
let args = [];
let image;
let mount;
switch (dialect) {
case 'mysql': {
args = [
'-e', `MYSQL_USER=${username}`,
'-e', `MYSQL_DATABASE=${database}`,
'-e', `MYSQL_ROOT_PASSWORD=${password}`,
'-p', `${port}:3306`,
];
image = 'mysql';
mount = '/var/lib/mysql';
break;
}
case 'postgres': {
args = [
'-e', `POSTGRES_USER=${username}`,
'-e', `POSTGRES_DB=${database}`,
'-e', `POSTGRES_PASSWORD=${password}`,
'-p', `${port}:5432`,
];
image = 'postgres';
mount = '/var/lib/postgresql/data';
break;
}
default:
}
if (!image) {
return {};
}
return {
sequelize: {
args,
image,
mount,
},
};
};

View File

@ -0,0 +1,25 @@
const {
SEQUELIZE_DIALECT = 'sqlite',
SEQUELIZE_USER = 'user',
SEQUELIZE_PASSWORD = 'Set_The_SEQUELIZE_PASSWORD_Environment_Variable',
SEQUELIZE_HOST = 'localhost',
SEQUELIZE_PORT,
SEQUELIZE_DATABASE = ':memory:',
} = process.env;
export default () => {
if ('sqlite' === SEQUELIZE_DIALECT) {
return ({
dialect: 'sqlite',
storage: SEQUELIZE_DATABASE,
});
}
return ({
dialect: SEQUELIZE_DIALECT,
username: SEQUELIZE_USER,
password: SEQUELIZE_PASSWORD,
host: SEQUELIZE_HOST,
port: SEQUELIZE_PORT,
database: SEQUELIZE_DATABASE,
});
};

17
packages/db/src/model.js Normal file
View File

@ -0,0 +1,17 @@
import {Model as SeqModel} from 'sequelize';
class Model extends SeqModel {
static associate() {}
static get attributes() {
return {};
}
static get isManagedByFlecks() {
return true;
}
}
export default Model;

32
packages/db/src/server.js Normal file
View File

@ -0,0 +1,32 @@
import {Hooks} from '@flecks/core';
import {createDatabaseConnection} from './connection';
import containers from './containers';
export {DataTypes as Types, Op, default as Sequelize} from 'sequelize';
export {default as Model} from './model';
export {createDatabaseConnection};
export default {
[Hooks]: {
'@flecks/core/config': () => ({
'models.decorate': ['...'],
}),
'@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'),
}),
},
};

6624
packages/db/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

116
packages/docker/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

View File

@ -0,0 +1,24 @@
{
"name": "@flecks/docker",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"server.js",
"server.js.map",
"src"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"debug": "^4.3.3"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1,15 @@
import {Hooks} from '@flecks/core';
import startContainer from './start-container';
export default {
[Hooks]: {
'@flecks/server/up': async (flecks) => {
const containers = await flecks.invokeReduceAsync('@flecks/docker/containers');
await Promise.all(
Object.entries(containers)
.map(([key, config]) => startContainer(flecks, key, config)),
);
},
},
};

View File

@ -0,0 +1,81 @@
import {execSync, spawn} from 'child_process';
import {mkdir} from 'fs/promises';
import {tmpdir} from 'os';
import {join} from 'path';
import D from 'debug';
const debug = D('@flecks/docker/container');
const containerIsRunning = (name) => {
try {
const output = execSync(
`docker container inspect -f '{{.State.Running}}' ${name}`,
{stdio: 'pipe'},
).toString();
if ('true\n' === output) {
return true;
}
}
catch (e) {
if (1 !== e.status) {
throw e;
}
}
return false;
};
export default async (flecks, key, config) => {
const {id} = flecks.get('@flecks/core');
if (!config.image) {
throw new Error(`@flecks/docker: ${key} container has no image specified`);
}
if (!config.mount) {
throw new Error(`@flecks/docker: ${key} container has no mount point specified`);
}
const name = `${id}_${key}`;
if (containerIsRunning(name)) {
debug("'%s' already running", key);
return;
}
const args = [
'run',
'--name', name,
'--rm',
...(config.args || []),
];
const datadir = join(tmpdir(), 'flecks', key, 'docker');
debug("creating datadir '%s'", datadir);
try {
await mkdir(datadir, {recursive: true});
}
catch (error) {
if ('EEXIST' !== error.code) {
throw error;
}
}
args.push('-v', `${datadir}:${config.mount}`);
args.push(config.image);
debug('launching: docker %s', args.join(' '));
spawn('docker', args, {
detached: true,
stdio: 'ignore',
}).unref();
while (!containerIsRunning(name)) {
debug("waiting for '%s' to start...", key);
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 10));
}
debug("'%s' started", key);
if (config.hasConnected) {
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
if (await config.hasConnected()) {
break;
}
debug("waiting for '%s' to connect...", key);
}
debug("'%s' connected", key);
}
};

5942
packages/docker/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

116
packages/dox/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

27
packages/dox/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "@flecks/dox",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"index.js",
"index.js.map",
"src"
],
"dependencies": {
"@babel/core": "^7.17.2",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@flecks/core": "^1.0.0",
"glob": "^7.2.0"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1 @@
export {parseCode, parseFleck} from './parser';

126
packages/dox/src/parser.js Normal file
View File

@ -0,0 +1,126 @@
import {readFile} from 'fs/promises';
import {dirname, join} from 'path';
import {transformAsync} from '@babel/core';
import traverse from '@babel/traverse';
import {
isIdentifier,
isMemberExpression,
isStringLiteral,
isThisExpression,
} from '@babel/types';
import glob from 'glob';
const flecksCorePath = dirname(__non_webpack_require__.resolve('@flecks/core/package.json'));
const FlecksInvocations = (state, filename) => ({
CallExpression(path) {
if (isMemberExpression(path.node.callee)) {
if (
(isIdentifier(path.node.callee.object) && 'flecks' === path.node.callee.object.name)
|| (
(
isThisExpression(path.node.callee.object)
&& (filename === join(flecksCorePath, 'src', 'flecks.js'))
)
)
) {
if (isIdentifier(path.node.callee.property)) {
if (path.node.callee.property.name.match(/^invoke.*/)) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.invocations.push({
filename,
hook: path.node.arguments[0].value,
line: path.node.loc.start.line,
type: path.node.callee.property.name,
});
}
}
}
if ('up' === path.node.callee.property.name) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.invocations.push({
filename,
hook: path.node.arguments[0].value,
line: path.node.loc.start.line,
type: 'invokeSequentialAsync',
});
state.invocations.push({
filename,
hook: '@flecks/core/starting',
line: path.node.loc.start.line,
type: 'invokeFlat',
});
}
}
}
if ('gather' === path.node.callee.property.name) {
if (path.node.arguments.length > 0) {
if (isStringLiteral(path.node.arguments[0])) {
state.invocations.push({
filename,
hook: path.node.arguments[0].value,
line: path.node.loc.start.line,
type: 'invokeReduce',
});
state.invocations.push({
filename,
hook: `${path.node.arguments[0].value}.decorate`,
line: path.node.loc.start.line,
type: 'invokeComposed',
});
}
}
}
}
}
}
},
});
export const parseCode = async (code, state, filename = '<inline>') => {
const config = {
ast: true,
code: false,
configFile: join(__dirname, '..', '..', 'core', 'src', 'build', 'babel.config.js'),
};
const {ast} = await transformAsync(code, config);
traverse(ast, FlecksInvocations(state, filename));
};
export const parseFile = async (filename, state) => {
const buffer = await readFile(filename);
return parseCode(buffer.toString('utf8'), state, filename);
};
const fleckSources = (path) => (
new Promise((r, e) => (
glob(
join(path, 'src', '**', '*.js'),
(error, result) => (error ? e(error) : r(result)),
)
))
);
export const parseFleck = async (path, state) => {
const sources = await fleckSources(path);
await Promise.all(sources.map((source) => parseFile(source, state)));
};
const state = {
invocations: [],
};
(async () => {
const path = flecksCorePath;
await parseFleck(path, state);
state.invocations.forEach(({
filename,
hook,
line,
type,
}) => {
console.log(`${type}('${hook}') in ${filename}:${line}`);
});
})();

5942
packages/dox/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck:./src': {}

View File

@ -0,0 +1,35 @@
{
"name": "@flecks/fleck",
"version": "1.0.0",
"author": "cha0s",
"license": "MIT",
"bin": {
"flecks": "./cli.js"
},
"scripts": {
"build": "flecks build",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"server/build/fleck.neutrinorc.js",
"server/build/fleck.neutrinorc.js.map",
"server.js",
"server.js.map",
"src",
"test.js",
"test.js.map"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"@neutrinojs/node": "^9.4.0",
"chokidar": "^3.5.3",
"debug": "^4.3.3",
"glob": "^7.2.0",
"mocha": "^8.3.2"
},
"devDependencies": {
"chai": "4.2.0"
}
}

View File

@ -0,0 +1,55 @@
const {join} = require('path');
const {Flecks} = require('@flecks/core/server');
const node = require('@neutrinojs/node');
const D = require('debug');
const glob = require('glob');
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
const debug = D('@flecks/fleck/fleck.neutrino.js');
debug('bootstrapping flecks...');
const flecks = Flecks.bootstrap();
debug('bootstrapped');
const config = require('../../../../core/src/bootstrap/fleck.neutrinorc');
config.use.push((neutrino) => {
// Test entrypoint.
const testPaths = glob.sync(join(FLECKS_ROOT, 'test/*.js'));
if (testPaths.length > 0) {
const testEntry = neutrino.config.entry('test').clear();
testPaths.forEach((path) => testEntry.add(path));
}
});
const compiler = flecks.invokeFleck(
'@flecks/fleck/compiler',
flecks.get('@flecks/fleck.compiler'),
);
if (compiler) {
config.use.unshift(compiler);
}
else {
config.use.unshift((neutrino) => {
neutrino.config.plugins.delete('start-server');
});
const configFile = flecks.localConfig('babel.config.js', '@flecks/core');
config.use.unshift(node({
babel: {configFile},
}));
}
config.use.unshift((neutrino) => {
// Test entrypoint.
const testPaths = glob.sync(join(FLECKS_ROOT, 'test/*.js'));
if (testPaths.length > 0) {
const testEntry = neutrino.config.entry('test').clear();
testPaths.forEach((path) => testEntry.add(path));
}
});
module.exports = config;

View File

@ -0,0 +1,91 @@
import {stat, unlink} from 'fs/promises';
import {join} from 'path';
import chokidar from 'chokidar';
import D from 'debug';
import glob from 'glob';
import {
commands as coreCommands,
spawnWith,
} from '@flecks/core/server';
const debug = D('@flecks/core/commands');
const {
FLECKS_ROOT = process.cwd(),
} = process.env;
export default (program, flecks) => {
const commands = {};
commands.test = {
options: [
['-d, --no-production', 'dev build'],
['-w, --watch', 'watch for changes'],
['-v, --verbose', 'verbose output'],
],
description: 'run tests',
action: async (opts) => {
const {
watch,
} = opts;
const testPaths = glob.sync(join(FLECKS_ROOT, 'test/*.js'));
if (0 === testPaths.length) {
// eslint-disable-next-line no-console
console.log('No fleck tests found.');
return 0;
}
const testLocation = join(FLECKS_ROOT, 'dist', 'test.js');
if (watch) {
await unlink(testLocation);
}
const {build} = coreCommands(program, flecks);
const child = build.action(undefined, opts);
debug('Testing...', opts);
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
await stat(testLocation);
break;
}
catch (error) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
const spawnMocha = () => {
const localEnv = {};
const spawnArgs = [
'--colors',
'--reporter', 'min',
testLocation,
];
return spawnWith('mocha', localEnv, spawnArgs);
};
if (!watch) {
await new Promise((resolve, reject) => {
child.on('exit', (code) => {
if (code !== 0) {
reject(code);
return;
}
resolve();
});
child.on('error', reject);
});
return spawnMocha();
}
let tester;
chokidar.watch(testLocation)
.on('all', () => {
if (tester) {
tester.kill();
}
tester = spawnMocha();
});
return new Promise(() => {});
},
};
return commands;
};

View File

@ -0,0 +1,10 @@
import {Hooks} from '@flecks/core';
import commands from './commands';
export default {
[Hooks]: {
'@flecks/core/commands': commands,
'@flecks/core/targets': () => ['fleck'],
},
};

5930
packages/fleck/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

116
packages/governor/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,2 @@
'@flecks/core': {}
'@flecks/fleck': {}

View File

@ -0,0 +1,28 @@
{
"name": "@flecks/governor",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "flecks build",
"clean": "flecks clean",
"lint": "flecks lint",
"test": "flecks test"
},
"files": [
"build",
"client.js",
"client.js.map",
"server.js",
"server.js.map",
"src"
],
"dependencies": {
"@flecks/core": "^1.0.0",
"@flecks/db": "^1.0.0",
"rate-limiter-flexible": "^2.1.13",
"redis": "^3.1.2"
},
"devDependencies": {
"@flecks/fleck": "^1.0.0"
}
}

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export {default as createLimiter} from './limiter';

View File

@ -0,0 +1,3 @@
import RateLimiterMemory from 'rate-limiter-flexible/lib/RateLimiterMemory';
export default (flecks, options) => new RateLimiterMemory(options);

View File

@ -0,0 +1,30 @@
export default (flecks, [name, Packet]) => {
const {ValidationError} = flecks.fleck('@flecks/socket');
return class LimitedPacket extends Packet {
constructor(...args) {
super(...args);
const {[name]: limiter} = flecks.get('$flecks/governor.packet.limiters');
this.limit = limiter;
}
static async validate(packet, socket) {
try {
await packet.limit.consume(socket.id);
}
catch (error) {
if (error.msBeforeNext) {
throw new ValidationError({
code: 429,
ttr: Math.round(error.msBeforeNext / 1000) || 1,
});
}
throw error;
}
if (super.validate) {
await super.validate(packet, socket);
}
}
};
};

View File

@ -0,0 +1,12 @@
import {createClient} from 'redis';
import {RateLimiterRedis} from 'rate-limiter-flexible';
export default async (options) => {
const storeClient = createClient();
// @todo node-redis@4
// await storeClient.connect();
return new RateLimiterRedis({
...options,
storeClient,
});
};

View File

@ -0,0 +1,73 @@
export default (flecks) => {
const {Model, Op, Types} = flecks.fleck('@flecks/db/server');
const {config: {'@flecks/governor/server': {keys}}} = flecks;
return class Ban extends Model {
static get attributes() {
return {
ttl: {
type: Types.INTEGER,
defaultValue: 0,
},
...Object.fromEntries(keys.map((key) => ([key, {type: Types.STRING}]))),
};
}
static async check(req) {
const ban = this.fromRequest(req, keys);
const candidates = Object.entries(ban)
.reduce((r, [key, value]) => [...r, {[key]: value}], []);
const where = {
where: {
[Op.or]: candidates,
},
};
const bans = await this.findAll(where);
const pruned = bans
.reduce((r, ban) => {
if (ban && ban.ttl > 0) {
const expiresAt = new Date(ban.createdAt);
expiresAt.setSeconds(expiresAt.getSeconds() + ban.ttl);
if (Date.now() >= expiresAt.getTime()) {
this.destroy({where: {id: ban.id}});
return [...r, null];
}
// eslint-disable-next-line no-param-reassign
ban.ttl = Math.ceil((expiresAt.getTime() - Date.now()) / 1000);
}
return [...r, ban];
}, [])
.filter((ban) => !!ban)
.map((ban) => {
const {ttl, ...json} = ban.toJSON();
return json;
});
if (0 === pruned.length) {
return;
}
throw new Error(this.format(pruned));
}
static format(bans) {
return [
'bans = [',
bans.map((ban) => {
const entries = Object.entries(ban)
.filter(([key]) => -1 === ['id', 'createdAt', 'updatedAt'].indexOf(key));
return [
' {',
entries.map(([key, value]) => ` ${key}: ${value},`).join('\n'),
' },',
].join('\n');
}).join('\n'),
'];',
].join('\n');
}
static fromRequest(req, keys, ttl = 0) {
return keys.reduce((r, key) => ({...r, [key]: req[key]}), ttl ? {ttl} : {});
}
};
};

View File

@ -0,0 +1,123 @@
import {ByType, Flecks, Hooks} from '@flecks/core';
import LimitedPacket from './limited-packet';
import createLimiter from './limiter';
export {default as createLimiter} from './limiter';
export default {
[Hooks]: {
'@flecks/core/config': () => ({
keys: ['ip'],
http: {
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/http/server/request.route': (flecks) => {
const {http} = flecks.get('@flecks/governor/server');
const limiter = flecks.get('$flecks/governor.http.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} = http;
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/http/server')) {
const {http} = flecks.get('@flecks/governor/server');
const limiter = await createLimiter({
keyPrefix: '@flecks/governor.http.request.route',
...http,
});
flecks.set('$flecks/governor.http.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({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({
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]),
]),
)
),
},
};

6671
packages/governor/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

116
packages/http/.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1,5 @@
const config = require('@flecks/core/build/.eslint.defaults.js');
config.globals.window = true;
module.exports = config;

Some files were not shown because too many files have changed in this diff Show More