chore: initial
This commit is contained in:
commit
702de1005d
119
.gitignore
vendored
Normal file
119
.gitignore
vendored
Normal 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
164
README.md
Normal 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 — 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) — are baked in. Along with some you
|
||||
may not expect — 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
15
TODO.md
Normal 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
6
lerna.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "1.0.0"
|
||||
}
|
13
package.json
Normal file
13
package.json
Normal 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
2
packages/core/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/dist
|
||||
/node_modules
|
4
packages/core/build/.eslintrc.js
Normal file
4
packages/core/build/.eslintrc.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
const neutrino = require('neutrino');
|
||||
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).eslintrc();
|
77
packages/core/build/.neutrinorc.js
Normal file
77
packages/core/build/.neutrinorc.js
Normal 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);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
});
|
||||
});
|
96
packages/core/build/dox/build.md
Normal file
96
packages/core/build/dox/build.md
Normal 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`
|
59
packages/core/build/dox/hooks.js
Normal file
59
packages/core/build/dox/hooks.js
Normal 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) => {};
|
3
packages/core/build/webpack.config.js
Normal file
3
packages/core/build/webpack.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const neutrino = require('neutrino');
|
||||
|
||||
module.exports = neutrino(require('./.neutrinorc')).webpack();
|
74
packages/core/package.json
Normal file
74
packages/core/package.json
Normal 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"
|
||||
}
|
||||
}
|
43
packages/core/src/bootstrap/autoentry.js
Normal file
43
packages/core/src/bootstrap/autoentry.js
Normal 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}`);
|
||||
});
|
||||
};
|
29
packages/core/src/bootstrap/fleck.js
Normal file
29
packages/core/src/bootstrap/fleck.js
Normal 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'}));
|
||||
};
|
36
packages/core/src/bootstrap/fleck.neutrinorc.js
Normal file
36
packages/core/src/bootstrap/fleck.neutrinorc.js
Normal 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(),
|
||||
],
|
||||
};
|
2
packages/core/src/bootstrap/require.js
Normal file
2
packages/core/src/bootstrap/require.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// eslint-disable-next-line no-eval
|
||||
module.exports = eval('"undefined" !== typeof require ? require : undefined');
|
18
packages/core/src/build/babel.config.js
Normal file
18
packages/core/src/build/babel.config.js
Normal 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',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
};
|
23
packages/core/src/build/eslint.defaults.js
Normal file
23
packages/core/src/build/eslint.defaults.js
Normal 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: {},
|
||||
},
|
||||
},
|
||||
};
|
15
packages/core/src/build/eslintrc.js
Normal file
15
packages/core/src/build/eslintrc.js
Normal 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();
|
69
packages/core/src/build/webpack.config.js
Normal file
69
packages/core/src/build/webpack.config.js
Normal 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
105
packages/core/src/cli.js
Executable 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);
|
||||
}
|
9
packages/core/src/compose.js
Normal file
9
packages/core/src/compose.js
Normal 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)));
|
||||
}
|
0
packages/core/src/empty.js
Normal file
0
packages/core/src/empty.js
Normal file
16
packages/core/src/ensure-unique-reduction.js
Normal file
16
packages/core/src/ensure-unique-reduction.js
Normal 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};
|
||||
}, {});
|
||||
};
|
126
packages/core/src/event-emitter.js
Normal file
126
packages/core/src/event-emitter.js
Normal 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
480
packages/core/src/flecks.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
22
packages/core/src/index.js
Normal file
22
packages/core/src/index.js
Normal 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',
|
||||
}),
|
||||
},
|
||||
};
|
64
packages/core/src/middleware.js
Normal file
64
packages/core/src/middleware.js
Normal 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));
|
||||
}
|
||||
|
||||
}
|
171
packages/core/src/server/commands.js
Normal file
171
packages/core/src/server/commands.js
Normal 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;
|
||||
};
|
463
packages/core/src/server/flecks.js
Normal file
463
packages/core/src/server/flecks.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
46
packages/core/src/server/index.js
Normal file
46
packages/core/src/server/index.js
Normal 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,
|
||||
},
|
||||
};
|
23
packages/core/test/fleck-one/index.js
Normal file
23
packages/core/test/fleck-one/index.js
Normal 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})),
|
||||
},
|
||||
};
|
6
packages/core/test/fleck-one/things/decorators/three.js
Normal file
6
packages/core/test/fleck-one/things/decorators/three.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default (Three) => class AnotherThree extends Three {
|
||||
|
||||
static bar() {
|
||||
}
|
||||
|
||||
};
|
7
packages/core/test/fleck-one/things/one.js
Normal file
7
packages/core/test/fleck-one/things/one.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default () => class One {
|
||||
|
||||
static get foo() {
|
||||
return 'One';
|
||||
}
|
||||
|
||||
};
|
7
packages/core/test/fleck-one/things/two.js
Normal file
7
packages/core/test/fleck-one/things/two.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default () => class Two {
|
||||
|
||||
static get foo() {
|
||||
return 'Two';
|
||||
}
|
||||
|
||||
};
|
19
packages/core/test/fleck-two/index.js
Normal file
19
packages/core/test/fleck-two/index.js
Normal 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})),
|
||||
},
|
||||
};
|
7
packages/core/test/fleck-two/things/three.js
Normal file
7
packages/core/test/fleck-two/things/three.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default () => class Three {
|
||||
|
||||
static get foo() {
|
||||
return 'Three';
|
||||
}
|
||||
|
||||
};
|
26
packages/core/test/gather.js
Normal file
26
packages/core/test/gather.js
Normal 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');
|
||||
});
|
32
packages/core/test/instance.js
Normal file
32
packages/core/test/instance.js
Normal 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'});
|
||||
});
|
42
packages/core/test/invoke.js
Normal file
42
packages/core/test/invoke.js
Normal 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
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
116
packages/create-app/.gitignore
vendored
Normal 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.*
|
44
packages/create-app/build/fleck.neutrinorc.js
Normal file
44
packages/create-app/build/fleck.neutrinorc.js
Normal 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',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
2
packages/create-app/build/flecks.yml
Normal file
2
packages/create-app/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
26
packages/create-app/package.json
Normal file
26
packages/create-app/package.json
Normal 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"
|
||||
}
|
||||
}
|
50
packages/create-app/src/cli.js
Normal file
50
packages/create-app/src/cli.js
Normal 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())();
|
116
packages/create-app/template/.gitignore.extraneous
Normal file
116
packages/create-app/template/.gitignore.extraneous
Normal 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.*
|
2
packages/create-app/template/build/flecks.yml
Normal file
2
packages/create-app/template/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/server': {}
|
6
packages/create-app/template/lerna.json
Normal file
6
packages/create-app/template/lerna.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "1.0.0"
|
||||
}
|
15
packages/create-app/template/package.json.extraneous
Normal file
15
packages/create-app/template/package.json.extraneous
Normal 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"
|
||||
}
|
||||
}
|
5965
packages/create-app/yarn.lock
Normal file
5965
packages/create-app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/create-fleck/.gitignore
vendored
Normal file
116
packages/create-fleck/.gitignore
vendored
Normal 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.*
|
44
packages/create-fleck/build/fleck.neutrinorc.js
Normal file
44
packages/create-fleck/build/fleck.neutrinorc.js
Normal 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',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
2
packages/create-fleck/build/flecks.yml
Normal file
2
packages/create-fleck/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
27
packages/create-fleck/package.json
Normal file
27
packages/create-fleck/package.json
Normal 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"
|
||||
}
|
||||
}
|
111
packages/create-fleck/src/cli.js
Normal file
111
packages/create-fleck/src/cli.js
Normal 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())();
|
116
packages/create-fleck/template/.gitignore.extraneous
Normal file
116
packages/create-fleck/template/.gitignore.extraneous
Normal 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.*
|
2
packages/create-fleck/template/build/flecks.yml
Normal file
2
packages/create-fleck/template/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
21
packages/create-fleck/template/package.json.extraneous
Normal file
21
packages/create-fleck/template/package.json.extraneous
Normal 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"
|
||||
}
|
||||
}
|
0
packages/create-fleck/template/src/index.js
Normal file
0
packages/create-fleck/template/src/index.js
Normal file
5977
packages/create-fleck/yarn.lock
Normal file
5977
packages/create-fleck/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/db/.gitignore
vendored
Normal file
116
packages/db/.gitignore
vendored
Normal 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.*
|
2
packages/db/build/flecks.yml
Normal file
2
packages/db/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
25
packages/db/package.json
Normal file
25
packages/db/package.json
Normal 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"
|
||||
}
|
||||
}
|
107
packages/db/src/connection.js
Normal file
107
packages/db/src/connection.js
Normal 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();
|
||||
}
|
49
packages/db/src/containers.js
Normal file
49
packages/db/src/containers.js
Normal 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,
|
||||
},
|
||||
};
|
||||
};
|
25
packages/db/src/environment.js
Normal file
25
packages/db/src/environment.js
Normal 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
17
packages/db/src/model.js
Normal 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
32
packages/db/src/server.js
Normal 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
6624
packages/db/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/docker/.gitignore
vendored
Normal file
116
packages/docker/.gitignore
vendored
Normal 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.*
|
2
packages/docker/build/flecks.yml
Normal file
2
packages/docker/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
24
packages/docker/package.json
Normal file
24
packages/docker/package.json
Normal 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"
|
||||
}
|
||||
}
|
15
packages/docker/src/server.js
Normal file
15
packages/docker/src/server.js
Normal 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)),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
81
packages/docker/src/start-container.js
Normal file
81
packages/docker/src/start-container.js
Normal 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
5942
packages/docker/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/dox/.gitignore
vendored
Normal file
116
packages/dox/.gitignore
vendored
Normal 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.*
|
2
packages/dox/build/flecks.yml
Normal file
2
packages/dox/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
27
packages/dox/package.json
Normal file
27
packages/dox/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
packages/dox/src/index.js
Normal file
1
packages/dox/src/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export {parseCode, parseFleck} from './parser';
|
126
packages/dox/src/parser.js
Normal file
126
packages/dox/src/parser.js
Normal 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
5942
packages/dox/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
2
packages/fleck/build/flecks.yml
Normal file
2
packages/fleck/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck:./src': {}
|
35
packages/fleck/package.json
Normal file
35
packages/fleck/package.json
Normal 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"
|
||||
}
|
||||
}
|
55
packages/fleck/src/server/build/fleck.neutrinorc.js
Normal file
55
packages/fleck/src/server/build/fleck.neutrinorc.js
Normal 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;
|
91
packages/fleck/src/server/commands.js
Normal file
91
packages/fleck/src/server/commands.js
Normal 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;
|
||||
};
|
10
packages/fleck/src/server/index.js
Normal file
10
packages/fleck/src/server/index.js
Normal 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
5930
packages/fleck/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/governor/.gitignore
vendored
Normal file
116
packages/governor/.gitignore
vendored
Normal 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.*
|
2
packages/governor/build/flecks.yml
Normal file
2
packages/governor/build/flecks.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
'@flecks/core': {}
|
||||
'@flecks/fleck': {}
|
28
packages/governor/package.json
Normal file
28
packages/governor/package.json
Normal 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"
|
||||
}
|
||||
}
|
2
packages/governor/src/client/index.js
Normal file
2
packages/governor/src/client/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export {default as createLimiter} from './limiter';
|
3
packages/governor/src/client/limiter.js
Normal file
3
packages/governor/src/client/limiter.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import RateLimiterMemory from 'rate-limiter-flexible/lib/RateLimiterMemory';
|
||||
|
||||
export default (flecks, options) => new RateLimiterMemory(options);
|
30
packages/governor/src/limited-packet.js
Normal file
30
packages/governor/src/limited-packet.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
12
packages/governor/src/limiter.js
Normal file
12
packages/governor/src/limiter.js
Normal 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,
|
||||
});
|
||||
};
|
73
packages/governor/src/models/ban.js
Normal file
73
packages/governor/src/models/ban.js
Normal 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} : {});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
123
packages/governor/src/server.js
Normal file
123
packages/governor/src/server.js
Normal 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
6671
packages/governor/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/http/.gitignore
vendored
Normal file
116
packages/http/.gitignore
vendored
Normal 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.*
|
5
packages/http/build/.eslint.defaults.js
Normal file
5
packages/http/build/.eslint.defaults.js
Normal 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
Loading…
Reference in New Issue
Block a user