chore: initial

This commit is contained in:
cha0s 2020-06-17 11:01:01 -05:00
commit a16a54f4d7
34 changed files with 11165 additions and 0 deletions

33
.eslint.defaults.js Normal file
View File

@ -0,0 +1,33 @@
const side = require('./side');
const config = {
globals: {
process: true,
AVOCADO_CLIENT: true,
AVOCADO_SERVER: true,
},
rules: {
'babel/object-curly-spacing': 'off',
'brace-style': ['error', 'stroustrup'],
'no-bitwise': ['error', {int32Hint: true}],
'no-plusplus': 'off',
'no-underscore-dangle': 'off',
'padded-blocks': ['error', {classes: 'always'}],
yoda: 'off',
},
settings: {
'import/resolver': {
webpack: {
config: `${__dirname}/webpack.config.js`,
},
},
},
};
if ('client' === side) {
config.rules['jsx-a11y/label-has-associated-control'] = [2, {
'assert': 'either',
}];
}
module.exports = config;

8
.eslintrc.js Normal file
View File

@ -0,0 +1,8 @@
const neutrino = require('neutrino');
// Have to default side for IDE eslint.
process.env.SIDE = process.env.SIDE || 'client';
const side = require('./side');
module.exports = neutrino(require(`./.neutrinorc.${side}`)).eslintrc();

119
.gitignore vendored Normal file
View File

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

5
.mocharc.js Normal file
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
const side = require('./side');
module.exports = neutrino(require(`./.neutrinorc.${side}`)).mocha();

80
.neutrinorc.client.js Normal file
View File

@ -0,0 +1,80 @@
const path = require('path');
const react = require('@neutrinojs/react');
const styles = require('@neutrinojs/style-loader');
const globImporter = require('node-sass-glob-importer');
const {DefinePlugin} = require('webpack');
const {afterPlatform, initial} = require('./middleware');
module.exports = {
options: {
root: __dirname,
},
use: [
initial({
environmentDefines: ['FRONTEND_ORIGIN'],
scwpPaths: [/^@avocado/],
}),
react({
devServer: {
contentBase: path.resolve(__dirname, 'resource'),
},
style: {
test: /\.(css|sass|scss)$/,
modulesTest: /\.module\.(css|sass|scss)$/,
loaders: [
{
loader: 'postcss-loader',
useId: 'postcss',
options: {
config: {
path: __dirname,
},
},
},
{
loader: 'sass-loader',
useId: 'sass',
options: {
importer: globImporter(),
},
},
],
},
html: {
template: `${__dirname}/src/client/index.ejs`,
title: 'Persea',
},
}),
afterPlatform({
babelPaths: [/^@avocado/, /^contempo/, /^scwp/],
}),
(neutrino) => {
neutrino.config.module.rule('style')
.oneOf('raw')
.before('normal')
.test(/\.raw\.(css|sass|scss)$/)
.use('raw')
.loader('raw-loader')
.end()
.use('postcss')
.loader('postcss-loader')
.options({config: {path: __dirname}})
.end()
.use('sass')
.loader('sass-loader')
.options({importer: globImporter()})
.end()
neutrino.config.resolve.modules
.add(`${neutrino.options.source}/client/scss`);
neutrino.config
.plugin('avocado-define')
.use(DefinePlugin, [
{
AVOCADO_CLIENT: true,
AVOCADO_SERVER: false,
},
]);
}
],
};

38
.neutrinorc.server.js Normal file
View File

@ -0,0 +1,38 @@
const {spawn} = require('child_process');
const copy = require('@neutrinojs/copy');
const node = require('@neutrinojs/node');
const {DefinePlugin} = require('webpack');
const {afterPlatform, initial} = require('./middleware');
if (!process.argv.find((arg) => arg === 'production')) {
spawn('docker', ['run', '-p', '11346:6379', 'redis'], {stdio: 'inherit'});
}
module.exports = {
options: {
root: __dirname,
},
use: [
initial({
environmentDefines: ['REDIS_ORIGIN'],
scwpPaths: [/^@avocado/],
}),
node(),
afterPlatform({
babelPaths: [/^@avocado/],
externalMatcher: /(?:@avocado|@pixi|scwp|webpack)/,
}),
(neutrino) => {
neutrino.config
.plugin('avocado-define')
.use(DefinePlugin, [
{
AVOCADO_CLIENT: false,
AVOCADO_SERVER: true,
},
]);
},
],
};

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
// "preLaunchTask": "Both",
"request": "attach",
"name": "Terrible",
"address": "127.0.0.1",
"port": 43000,
"skipFiles": [
"<node_internals>/**"
]
},
],
}

79
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,79 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Client",
"type": "shell",
"isBackground": true,
"command": "yarn",
"args": [
"run",
"client"
],
"problemMatcher": [
{
"owner": "custom",
"pattern": {
"regexp": "__________"
},
"background": {
"activeOnStart": true,
"beginsPattern": "(Compiling|Project is running)",
"endsPattern": "(?:Compiled successfully|Failed to compile)"
}
}
]
},
{
"label": "Server",
"type": "shell",
"isBackground": true,
"command": "yarn",
"args": [
"run",
"server",
"--inspect=127.0.0.1:43000"
],
"problemMatcher": [
{
"owner": "custom",
"pattern": {
"regexp": "You need to restart the application"
}
},
{
"owner": "custom",
"pattern": {
"regexp": "__________"
},
"background": {
"activeOnStart": true,
"beginsPattern": "(Hash:|Updated modules)",
"endsPattern": "(Built at:|Update applied)"
}
}
]
},
{
"label": "Both",
"isBackground": true,
"dependsOn": [
"Client",
"Server"
],
"problemMatcher": []
},
{
"type": "shell",
"command": "yarn",
"args": [
"run",
"lint"
],
"problemMatcher": [
"$eslint-stylish"
],
"label": "yarn run lint"
}
]
}

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM node:14
EXPOSE 11344
COPY ./build /var/www
COPY ./node_modules /var/www/server/node_modules
CMD ["node", "/var/www/server/index.js"]

View File

@ -0,0 +1 @@
{"traits":{"alive":{},"animated":{"params":{"animations":{"idle":{"offset":[0,-3],"uri":"/kitty.animation.json"}}}},"audible":{"params":{"sounds":{"deathSound":{"uri":"/ded.sound.json"}}}},"behaved":{"params":{"routines":{"type":"routines","routines":{"initial":{"type":"routine","routine":{"type":"actions","traversals":[{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"direction"}],"value":{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"floor"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"randomNumber"},{"type":"invoke","args":[{"type":"literal","value":0},{"type":"literal","value":4}]}]}]}]}},{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"moveFor"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"Vector"},{"type":"key","key":"fromDirection"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"direction"}]}]}]},{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"randomNumber"},{"type":"invoke","args":[{"type":"literal","value":0.25},{"type":"literal","value":2.5}]}]}]}]},{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"isAnimating"}],"value":{"type":"literal","value":false}},{"type":"traversal","steps":[{"type":"key","key":"Timing"},{"type":"key","key":"wait"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"randomNumber"},{"type":"invoke","args":[{"type":"literal","value":1},{"type":"literal","value":4}]}]}]}]},{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"direction"}],"value":{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"floor"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"randomNumber"},{"type":"invoke","args":[{"type":"literal","value":0},{"type":"literal","value":4}]}]}]}]}},{"type":"traversal","steps":[{"type":"key","key":"Timing"},{"type":"key","key":"wait"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"Math"},{"type":"key","key":"randomNumber"},{"type":"invoke","args":[{"type":"literal","value":0.5},{"type":"literal","value":3}]}]}]}]},{"type":"traversal","steps":[{"type":"key","key":"entity"},{"type":"key","key":"isAnimating"}],"value":{"type":"literal","value":true}}]}}}}}},"collider":{"params":{"collidesWithGroups":["default","environmental","projectile"]}},"directional":{"params":{"directionCount":4},"state":{"direction":2}},"emitter":{},"existent":{"state":{"name":"Kitty"}},"layered":{},"listed":{},"lootable":{"params":{"table":[{"perc":70,"json":{"uri":"/yarn-ball.entity.json"}},{"perc":80,"json":{"uri":"/yarn-ball.entity.json"}},{"perc":90,"json":{"uri":"/yarn-ball.entity.json"}}]}},"mobile":{"state":{"speed":40}},"physical":{},"positioned":{},"roomed":{},"shaped":{"params":{"shape":{"type":"rectangle","position":[0,0],"size":[8,4]}}},"visible":{"state":{"visibleScale":[1,1]}},"vulnerable":{"params":{"types":["bio"]}}}}

5
inspect.js Normal file
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
const side = require('./side');
neutrino(require(`./.neutrinorc.${side}`)).inspect();

132
middleware.js Normal file
View File

@ -0,0 +1,132 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-param-reassign */
const path = require('path');
const airbnb = require('@neutrinojs/airbnb');
const airbnbBase = require('@neutrinojs/airbnb-base');
const mocha = require('@neutrinojs/mocha');
const scwp = require('scwp/neutrino');
const {DefinePlugin} = require('webpack');
const nodeExternals = require('webpack-node-externals');
const side = require('./side');
const avocadoPackages = {};
const pkg = require(`${__dirname}/package.json`);
const gatherPackagePaths = (root, packageMatchers) => {
return packageMatchers.reduce((r, packageMatcher) => (
r.concat([
(pkg.dependencies || {}),
(pkg.devDependencies || {}),
].reduce((r, deps) => {
const packageNames = Object.keys(deps);
const packages = [];
for (let i = 0; i < packageNames.length; i++) {
const packageName = packageNames[i];
if (packageName.match(packageMatcher)) {
packages.push(path.relative(root, path.dirname(require.resolve(packageName))));
}
}
return r.concat(packages);
}, []))
), []);
}
exports.initial = (options) => (neutrino) => {
const {
environmentDefines = [],
scwpPaths = [],
} = options;
neutrino.options.mains.index = `${side}/index`;
neutrino.options.output = `build/${side}`;
neutrino.config.resolve.modules
.add(`${neutrino.options.root}/node_modules`);
neutrino.config.resolveLoader.modules
.add(`${neutrino.options.root}/node_modules`);
neutrino.config.resolve.alias
.set('~', neutrino.options.source);
('server' === side ? airbnbBase : airbnb)({
eslint: {
cache: false,
// eslint-disable-next-line global-require
baseConfig: require('./.eslint.defaults'),
},
})(neutrino);
mocha({
spec: `src/+(${side}|common)/**/*.spec.js`,
})(neutrino);
scwp({
paths: [
'./src/common',
`./src/${side}`,
].concat(
gatherPackagePaths(neutrino.options.root, scwpPaths.concat(/^scwp/))
),
})(neutrino);
neutrino.config
.plugin('environment-define')
.use(DefinePlugin, [
environmentDefines.reduce((r, k) => ({...r, [k]: JSON.stringify(process.env[k])}), {})
]);
};
exports.afterPlatform = (options) => (neutrino) => {
const {
babelPaths = [],
externalMatcher = [],
} = options;
const allBabelPaths = gatherPackagePaths(neutrino.options.root, babelPaths);
neutrino.config.module
.rule('compile')
.use('babel')
.tap((options) => {
options.only = [
neutrino.options.source,
].concat(allBabelPaths);
options.ignore = [];
return options;
});
allBabelPaths.forEach((babelPath) => {
neutrino.config.module
.rule('compile')
.include
.add(path.resolve(neutrino.options.root, babelPath));
});
neutrino.config.module
.rule('compile')
.use('babel')
.get('options').plugins.push(
[
'babel-plugin-webpack-alias',
{
config: `${__dirname}/webpack.config.js`,
},
],
);
if ('client' === side) {
neutrino.config.node.delete('Buffer');
}
else /* if ('server' === side) */ {
neutrino.config.stats('normal');
if ('production' !== process.env.NODE_ENV) {
neutrino.config
.plugin('start-server')
.tap((args) => {
const options = args[0];
const inspectArg = process.argv.find((arg) => -1 !== arg.indexOf('--inspect'));
if (inspectArg) {
options.nodeArgs.push(inspectArg);
}
const profArg = process.argv.find((arg) => -1 !== arg.indexOf('--prof'));
if (profArg) {
options.nodeArgs.push(profArg);
}
return args;
});
}
neutrino.config
.externals(nodeExternals({
whitelist: externalMatcher,
}));
}
}

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "persea",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"build:client": "SIDE=CLIENT webpack --mode production --config webpack.config.js",
"build:docker": "yarn run build:client && yarn run build:server && docker build",
"build:server": "SIDE=SERVER webpack --mode production --config webpack.config.js",
"client": "SIDE=CLIENT webpack-dev-server --verbose --disable-host-check --host persea.cha0sdev --port 11345 --mode development --config webpack.config.js",
"inspect": "node ./inspect.js",
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src",
"server": "SIDE=SERVER webpack --watch --mode development --config webpack.config.js",
"test:client": "NODE_PRESERVE_SYMLINKS=1 SIDE=client mocha --watch",
"test:server": "NODE_PRESERVE_SYMLINKS=1 SIDE=server mocha --watch"
},
"dependencies": {
"@avocado/behavior": "1.x",
"@avocado/core": "1.x",
"@avocado/entity": "1.x",
"@avocado/graphics": "1.x",
"@avocado/input": "1.x",
"@avocado/math": "1.x",
"@avocado/net": "1.x",
"@avocado/physics": "1.x",
"@avocado/resource": "1.x",
"@avocado/sound": "1.x",
"@avocado/timing": "1.x",
"@avocado/topdown": "1.x",
"@reduxjs/toolkit": "1.3.6",
"ansi-html": "0.0.7",
"classnames": "2.2.6",
"contempo": "1.x",
"express": "^4.17.1",
"express-session": "^1.17.1",
"express-socket.io-session": "^1.3.5",
"html-entities": "1.3.1",
"memorystore": "^1.6.2",
"prop-types": "^15",
"react": "16.8.6",
"react-dom": "16.8.6",
"react-hot-loader": "^4.12.21",
"react-markdown": "^4.3.1",
"react-tabs": "^3.1.1",
"scwp": "1.x",
"socket.io-redis": "^5.3.0",
"source-map-support": "^0.5.11"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.1.0",
"@neutrinojs/airbnb-base": "^9.1.0",
"@neutrinojs/copy": "^9.2.0",
"@neutrinojs/mocha": "^9.1.0",
"@neutrinojs/node": "^9.1.0",
"@neutrinojs/react": "^9.1.0",
"autoprefixer": "9.8.0",
"babel-plugin-webpack-alias": "^2.1.2",
"eslint": "^6",
"eslint-import-resolver-webpack": "^0.12.1",
"mocha": "^7",
"neutrino": "^9.1.0",
"node-sass": "4.12.0",
"node-sass-glob-importer": "5.3.2",
"postcss-loader": "3.0.0",
"raw-loader": "1.x",
"sass-loader": "7.1.0",
"v8-natives": "^1.1.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-dev-server": "^3"
}
}

5
postcss.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

8
side.js Normal file
View File

@ -0,0 +1,8 @@
const side = (process.env.SIDE || '').toLowerCase();
if (-1 === ['client', 'server'].indexOf(side)) {
throw new Error(
"You must define the SIDE environment variable as 'server' or 'client' when building!",
);
}
module.exports = side;

View File

@ -0,0 +1,23 @@
import {compose} from '@avocado/core';
import contempo from 'contempo';
import React from 'react';
import Traits from './traits';
const decorate = compose(
contempo(require('./entity.raw.scss')),
);
const kitty = require('~/../fixtures/kitty.entity.json');
const Entity = () => {
const entity = kitty;
const {traits} = entity;
return (
<div className="entity">
<Traits traits={traits} />
</div>
);
};
export default decorate(Entity);

View File

View File

@ -0,0 +1,10 @@
import {hot} from 'react-hot-loader';
import React from 'react';
import Entity from './entity';
const Persea = () => (
<Entity />
);
export default hot(module)(Persea);

View File

@ -0,0 +1,89 @@
import PropTypes from 'prop-types';
import React from 'react';
const propertyPropTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
options: PropTypes.shape({}),
};
const bool = ({
name,
label,
value,
}) => (
<label>
<span className="text">{label}</span>
<div className="invisible-separator" />
<input name={name} type="checkbox" checked={value} />
</label>
);
bool.propTypes = {
...propertyPropTypes,
value: PropTypes.bool.isRequired,
};
const number = ({
name,
label,
options,
value,
}) => (
<label>
<span className="text">{label}</span>
<div className="invisible-separator" />
{
options
? (
<select name={name} value={value}>
{Object.entries(options).map(([optionValue, optionLabel]) => (
<option value={optionValue}>{optionLabel}</option>
))}
</select>
)
: <input name={name} size={5} type="text" value={value} />
}
</label>
);
number.propTypes = {
...propertyPropTypes,
value: PropTypes.number.isRequired,
};
const string = ({
name,
label,
options,
value,
}) => (
<label>
<span className="text">{label}</span>
<div className="invisible-separator" />
{
options
? (
<select>
{ /* eslint-disable-next-line jsx-a11y/control-has-associated-label */ }
{options.map((option) => <option value={option} />)}
</select>
)
: <input name={name} size={5} type="text" value={value} />
}
</label>
);
string.propTypes = {
...propertyPropTypes,
value: PropTypes.string.isRequired,
};
// eslint-disable-next-line import/prefer-default-export
export function propertyComponents() {
return {
bool,
number,
string,
};
}

View File

@ -0,0 +1,139 @@
import {compose, mapObject} from '@avocado/core';
import {all} from '@avocado/entity/trait/trait-components.scwp';
import {lookupTrait} from '@avocado/entity';
import contempo from 'contempo';
import PropTypes from 'prop-types';
import React, {useState} from 'react';
// import ReactMarkdown from 'react-markdown';
import {
Tab,
Tabs,
TabList,
TabPanel,
} from 'react-tabs';
import {createSelector} from '@reduxjs/toolkit';
import {invokeHookFlat} from 'scwp';
const SCROLL_MAG = 80;
const decorate = compose(
contempo(require('./traits.raw.scss')),
);
// const fence = (code) => `\`\`\`json\n\n${code}\n\`\`\``;
// const markdown = (object) => (
// object && (
// <ReactMarkdown
// source={fence(JSON.stringify(object, null, 2))}
// escapeHtml={false}
// />
// )
// );
let TraitComponents;
const ensureTraitComponents = () => {
if (!TraitComponents) {
TraitComponents = Object.values(all()).reduce((r, M) => {
const {default: TraitComponent} = M;
return {...r, [TraitComponent.type]: TraitComponent};
}, {});
}
};
let PropertyComponents;
const ensurePropertyComponents = () => {
if (!PropertyComponents) {
const results = invokeHookFlat('propertyComponents');
PropertyComponents = results.reduce((r, map) => ({...r, ...map}), {});
}
};
const makeTabSelector = (type) => createSelector(
(_) => _,
(trait) => {
ensurePropertyComponents();
ensureTraitComponents();
const {params: paramsRaw, state: stateRaw} = trait;
const {[type]: TraitComponent} = TraitComponents;
const Trait = lookupTrait(type);
const params = Trait ? {...Trait.defaultParams(), ...paramsRaw} : paramsRaw;
const state = Trait ? {...Trait.defaultState(), ...stateRaw} : stateRaw;
const stateDescription = Trait?.describeState() || {};
const Components = Object.values(mapObject(stateDescription, (description, key) => {
const {label, options, type: componentType} = description;
const Component = PropertyComponents[componentType];
return Component
? (
<Component
key={key}
label={label}
name={key}
options={options}
value={state[key]}
/>
)
: null;
}));
return TraitComponent
? <TraitComponent params={params} state={state} />
: <form>{Components}</form>;
},
);
const tabSelectorMap = new WeakMap();
const Traits = (props) => {
const {traits} = props;
if (!tabSelectorMap.has(traits)) {
tabSelectorMap.set(traits, {});
}
const selectors = tabSelectorMap.get(traits);
const [tabIndex, setTabIndex] = useState(0);
let listRef;
const entries = Object.entries(traits);
const tabs = [];
for (let i = 0; i < entries.length; i++) {
const [type, trait] = entries[i];
trait.type = type;
if (!selectors[type]) {
selectors[type] = makeTabSelector(type);
}
tabs.push([type, selectors[type](trait)]);
}
return (
<div className="traits">
<Tabs
domRef={(node) => {
listRef = node && node.querySelector('ul');
}}
selectedIndex={tabIndex}
onSelect={(index) => setTabIndex(index)}
>
<TabList
onWheel={(event) => {
const {deltaY} = event;
if (listRef) {
listRef.scrollLeft += SCROLL_MAG * (deltaY / Math.abs(deltaY));
}
}}
>
{tabs.map(([type]) => <Tab key={type}>{type}</Tab>)}
</TabList>
{tabs.map(([type, content]) => <TabPanel key={type}>{content}</TabPanel>)}
</Tabs>
</div>
);
};
Traits.propTypes = {
traits: PropTypes.shape({}).isRequired,
};
export default decorate(Traits);
if (module.hot) {
module.hot.accept(['@avocado/entity/trait/trait-components.scwp', () => {
TraitComponents = undefined;
PropertyComponents = undefined;
}]);
}

View File

@ -0,0 +1,27 @@
@import '~/client/scss/tabs.scss';
@import '~/client/scss/trait.scss';
.react-tabs {
@extend %tabs;
}
.react-tabs__tab-panel--selected {
height: calc(100vh - 2.7em);
overflow-y: auto;
}
.react-tabs__tab-panel input {
text-align: right;
}
form {
@extend %trait;
}
.invisible-separator {
margin: 1em 0;
}
.text {
margin-right: 0.5em;
}

13
src/client/index.ejs Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="<%= htmlWebpackPlugin.options.lang %>">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="<%= htmlWebpackPlugin.options.appMountId %>">
<div class="debug-container"></div>
</div>
</body>
</html>

13
src/client/index.jsx Normal file
View File

@ -0,0 +1,13 @@
import './index.scss';
import React from 'react';
import {render} from 'react-dom';
import Persea from '~/client/components/persea';
render(
(
<Persea />
),
document.getElementById('root'),
);

142
src/client/index.scss Normal file
View File

@ -0,0 +1,142 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
ins {
text-decoration: none;
}
* {
box-sizing: border-box;
}
html {
background-color: #212121;
color: #FFFFFF;
}
body {
scrollbar-width: thin;
scrollbar-color: #777 #333;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: #333;
}
::-webkit-scrollbar-thumb {
background-color: #777;
border-radius: 20px;
border: 3px solid #333;
}
code {
font-family: monospace;
}
label {
align-items: center;
background-color: #272727;
color: #ffffff;
display: flex;
flex-wrap: wrap;
font-family: "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
font-size: 0.6em;
min-height: 3em;
justify-content: space-between;
padding: 0.5em 0.5em 0.5em 1em;
text-transform: uppercase;
user-select: none;
}
@media(min-width: 32em) {
label {
font-size: 0.8em;
}
}
@media(min-width: 64em) {
label {
font-size: 1em;
}
}
label:nth-of-type(2n+1) {
background-color: #1b1b1b;
}
input {
background: #333;
border: 1px solid black;
color: #ffffff;
font-size: 0.75em;
padding: 0.5em;
}
fieldset {
background-color: #151515;
border: 1px solid #000000;
display: inline-block;
margin: 0 0 1em 0;
padding: 0.5em;
position: relative;
top: -0.5em;
width: 100%;
}
button, input[type="checkbox"], input[type="checkbox"] + label {
cursor: pointer;
}
select {
background: #222222;
border: 1px solid #000000;
color: #ffffff;
font-size: 0.75em;
line-height: 1em;
-moz-appearance: none;
-webkit-appearance: none;
padding: 0.5em;
}
*:focus {
box-shadow: 0 0 2px 0 #d68030ff;
outline: none;
}

44
src/client/scss/tabs.scss Normal file
View File

@ -0,0 +1,44 @@
%tabs {
.react-tabs__tab-list {
background-color: #272727;
font-family: "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
font-size: 0.9em;
overflow-x: hidden;
scrollbar-width: thin;
white-space: nowrap;
width: 100%;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: #212121;
}
&::-webkit-scrollbar-thumb {
background-color: #777;
border-radius: 0;
border-width: 1px;
}
}
.react-tabs__tab {
background-color: #333333;
color: #aaaaaa;
cursor: pointer;
display: inline-block;
padding: 1em 3em;
&:not(:last-of-type) {
border-right: 1px solid #282828;
}
&:hover {
color: #ddd;
}
}
.react-tabs__tab--selected[class] {
background-color: #212121;
color: #ffffff;
}
}

View File

@ -0,0 +1,4 @@
%trait {
font-size: 1.5em;
padding: 2rem;
}

View File

@ -0,0 +1,6 @@
/* eslint-disable no-undef */
const withDefault = (variable, value) => ('undefined' !== typeof variable ? variable : value);
// eslint-disable-next-line import/prefer-default-export
export const redisOrigin = withDefault(REDIS_ORIGIN, 'localhost:11346');

27
src/server/app.js Normal file
View File

@ -0,0 +1,27 @@
import express from 'express';
import httpSession from 'express-session';
const MemoryStore = require('memorystore')(httpSession);
let insideSession;
export default function createApp() {
const app = express();
app.use(express.urlencoded({
extended: false,
}));
insideSession = httpSession({
resave: true,
saveUninitialized: true,
secret: process.env.COOKIE_SECRET || 'foobar',
store: new MemoryStore({
checkPeriod: 86400000,
}),
});
app.use(insideSession);
return app;
}
export function session() {
return insideSession;
}

57
src/server/http.js Normal file
View File

@ -0,0 +1,57 @@
/* eslint-disable import/no-extraneous-dependencies */
import express from 'express';
import http, {ServerResponse} from 'http';
import {join} from 'path';
import httpProxy from 'http-proxy';
import createApp from './app';
let app;
export function createHttpServer() {
app = createApp();
const httpServer = http.createServer(app);
httpServer.listen(11344, '0.0.0.0');
if ('production' !== process.env.NODE_ENV) {
const proxy = httpProxy.createProxyServer({
secure: false,
target: 'http://192.168.0.190:11345',
});
proxy.on('error', (err, req, res) => {
if (!(res instanceof ServerResponse)) {
return;
}
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end('Bad Proxy');
});
httpServer.proxy = proxy;
}
return httpServer;
}
export function destroyHttpServer(httpServer) {
if (!httpServer) {
return;
}
httpServer.close();
if ('production' !== process.env.NODE_ENV) {
httpServer.proxy.close();
}
}
export function proxyHttpServer(httpServer) {
if ('production' === process.env.NODE_ENV) {
app.use(express.static(join(__dirname, '..', 'client')));
app.get('*', (req, res) => {
res.sendFile(join(__dirname, '..', 'client', 'index.html'));
});
}
else {
app.get('*', (req, res) => {
httpServer.proxy.web(req, res);
});
}
}

42
src/server/index.js Normal file
View File

@ -0,0 +1,42 @@
import {createHttpServer, destroyHttpServer, proxyHttpServer} from './http';
import {createSocketServer, destroySocketServer} from './sockets';
let httpServer;
let socketServer;
const connectionSet = new Set();
function trackConnections() {
httpServer.on('connection', (connection) => {
connectionSet.add(connection);
connection.once('close', () => {
connectionSet.delete(connection);
});
});
}
function releaseConnections() {
const connections = Array.from(connectionSet.values());
for (let i = 0; i < connections.length; i++) {
connections[i].destroy();
}
connectionSet.clear();
}
function restartListening() {
// Destroy...
destroySocketServer(socketServer);
destroyHttpServer(httpServer);
releaseConnections();
// Create...
httpServer = createHttpServer();
trackConnections();
socketServer = createSocketServer(httpServer);
proxyHttpServer(httpServer);
}
restartListening();
if (module.hot) {
module.hot?.accept([
'./src/server/http/index.js',
'./src/server/sockets/index.js',
], restartListening);
}

9
src/server/repl.js Normal file
View File

@ -0,0 +1,9 @@
import repl from 'repl';
export default function createReplServer() {
return repl.start({
prompt: 'terrible> ',
input: process.stdin,
output: process.stdout,
});
}

View File

@ -0,0 +1,31 @@
/* eslint-disable import/no-extraneous-dependencies */
import redisAdapter from 'socket.io-redis';
import {SocketServer} from '@avocado/net/server/socket';
import socketSession from 'express-socket.io-session';
import {redisOrigin} from '~/common/environment';
import {session} from '~/server/app';
// const debug = require('debug')('persea:server:sockets');
export function createSocketServer(httpServer) {
const [redisHost, redisPort] = redisOrigin.split(':');
const socketServer = new SocketServer(httpServer, {
adapter: redisAdapter({
host: redisHost || 'localhost',
port: redisPort || 11346,
}),
});
socketServer.io.use(socketSession(session()));
return socketServer;
}
export function destroySocketServer(socketServer) {
if (!socketServer) {
return;
}
socketServer.removeTicker();
socketServer.close();
}

9
webpack.config.js Normal file
View File

@ -0,0 +1,9 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const side = require('./side');
// eslint-disable-next-line import/no-dynamic-require
module.exports = neutrino(require(`./.neutrinorc.${side}`)).webpack();

9872
yarn.lock Normal file

File diff suppressed because it is too large Load Diff