Refactor node packages to yarn workspaces & ditch next.js for CRA. (#161)

* chore: restructure project into yarn workspaces, remove next

* fix tests, remove webapp from terraform

* remove more ui deployment bits

* remove pages, fix FUNDING.yml

* remove isomorphism

* remove next providers

* fix linting issues

* feat: start basis of new web ui system on CRA

* chore: move types to @roleypoly/types package

* chore: move src/common/utils to @roleypoly/misc-utils

* chore: remove roleypoly/ path remappers

* chore: renmove vercel config

* chore: re-add worker-types to api package

* chore: fix type linting scope for api

* fix(web): craco should include all of packages dir

* fix(ci): change api webpack path for wrangler

* chore: remove GAR actions from CI

* chore: update codeql job

* chore: test better github dar matcher in lint-staged
This commit is contained in:
41666 2021-03-12 18:04:49 -05:00 committed by GitHub
parent 49e308507e
commit 2ff6588030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
328 changed files with 16624 additions and 3525 deletions

View file

@ -1,13 +0,0 @@
export enum CategoryType {
Single = 0,
Multi,
}
export type Category = {
id: string;
name: string;
roles: string[];
hidden: boolean;
type: CategoryType;
position: number;
};

View file

@ -1,47 +0,0 @@
import { Category } from './Category';
import { Role } from './Role';
import { Member } from './User';
export type Guild = {
id: string;
name: string;
icon: string;
roles: Role[];
};
export enum Features {
None,
Preview = None,
}
export type GuildData = {
id: string;
message: string;
categories: Category[];
features: Features;
};
export type PresentableGuild = {
id: string;
guild: GuildSlug;
member: Member;
data: GuildData;
roles: Role[];
};
export type GuildEnumeration = {
guilds: PresentableGuild[];
};
export enum UserGuildPermissions {
User,
Manager = 1 << 1,
Admin = 1 << 2,
}
export type GuildSlug = {
id: string;
name: string;
icon: string;
permissionLevel: UserGuildPermissions;
};

View file

@ -1,37 +0,0 @@
export enum RoleSafety {
Safe = 0,
HigherThanBot = 1 << 1,
DangerousPermissions = 1 << 2,
ManagedRole = 1 << 3,
}
export type Role = {
id: string;
name: string;
color: number;
managed: boolean;
position: number;
safety: RoleSafety;
/** Permissions is should be used as a BigInt, NOT a number. */
permissions: string;
};
export type OwnRoleInfo = {
highestRolePosition: number;
};
export enum TransactionType {
None = 0,
Remove = 1 << 1,
Add = 1 << 2,
}
export type RoleTransaction = {
id: string;
action: TransactionType;
};
export type RoleUpdate = {
knownState: Role['id'][];
transactions: RoleTransaction[];
};

View file

@ -1,18 +0,0 @@
import { GuildSlug } from './Guild';
import { DiscordUser } from './User';
export type AuthTokenResponse = {
access_token: string;
token_type: 'Bearer';
expires_in: number;
refresh_token: string;
scope: string;
};
export type SessionData = {
/** sessionID is a KSUID */
sessionID: string;
tokens: AuthTokenResponse;
user: DiscordUser;
guilds: GuildSlug[];
};

View file

@ -1,18 +0,0 @@
export type DiscordUser = {
id: string;
username: string;
discriminator: string;
avatar: string;
bot: boolean;
};
export type Member = {
guildid?: string;
roles: string[];
nick?: string;
user?: DiscordUser;
};
export type RoleypolyUser = {
discorduser: DiscordUser;
};

View file

@ -1,58 +0,0 @@
import { Role } from '.';
export const demoData: Role[] = [
{
id: '557812805546541066',
name: 'a cute role ♡',
color: 0xd19494,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
{
id: '557812901717737472',
name: 'a vanity role ♡',
color: 0xd1d194,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
{
id: '557812915386843170',
name: 'a brave role ♡',
color: 0x94d194,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
{
id: '557824893241131029',
name: 'a proud role ♡',
color: 0x94d1d1,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
{
id: '557824994269200384',
name: 'a wonderful role ♡',
color: 0x9494d1,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
{
id: '557825026406088717',
name: 'a 日本語 role ♡',
color: 0xd194d1,
permissions: '0',
safety: 0,
managed: false,
position: 0,
},
];

View file

@ -1,5 +0,0 @@
export * from './Category';
export * from './Guild';
export * from './Role';
export * from './Session';
export * from './User';

View file

@ -1,241 +0,0 @@
import {
Category,
CategoryType,
DiscordUser,
Features,
Guild,
GuildData,
GuildEnumeration,
GuildSlug,
Member,
Role,
RoleSafety,
RoleypolyUser,
} from '.';
export const roleCategory: Role[] = [
{
id: 'aaa',
permissions: '0',
name: 'She/Her',
color: 0xffc0cb,
position: 1,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'bbb',
permissions: '0',
name: 'He/Him',
color: 0xc0ebff,
position: 2,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'ccc',
permissions: '0',
name: 'They/Them',
color: 0xc0ffd5,
position: 3,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'ddd',
permissions: '0',
name: 'Reee',
color: 0xff0000,
position: 4,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'eee',
permissions: '0',
name: 'black but actually bravely default',
color: 0x000000,
position: 5,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'fff',
permissions: '0',
name: 'b̻͌̆̽ͣ̃ͭ̊l͚̥͙̔ͨ̊aͥć͕k͎̟͍͕ͥ̋ͯ̓̈̉̋i͛̄̔͂̚̚҉̳͈͔̖̼̮ṣ̤̗̝͊̌͆h͈̭̰͔̥̯ͅ',
color: 0x1,
position: 6,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'unsafe1',
permissions: '0',
name: 'too high',
color: 0xff0088,
position: 7,
managed: false,
safety: RoleSafety.HigherThanBot,
},
{
id: 'unsafe2',
permissions: String(0x00000008 | 0x10000000),
name: 'too strong',
color: 0x00ff88,
position: 8,
managed: false,
safety: RoleSafety.DangerousPermissions,
},
];
export const mockCategory: Category = {
id: 'aaa',
name: 'Mock',
roles: roleCategory.map((x) => x.id),
hidden: false,
type: CategoryType.Multi,
position: 0,
};
export const roleCategory2: Role[] = [
{
id: 'ddd2',
permissions: '0',
name: 'red',
color: 0xff0000,
position: 9,
managed: false,
safety: RoleSafety.Safe,
},
{
id: 'eee2',
permissions: '0',
name: 'green',
color: 0x00ff00,
position: 10,
managed: false,
safety: RoleSafety.Safe,
},
];
export const mockCategorySingle: Category = {
id: 'bbb',
name: 'Mock Single 岡野',
roles: roleCategory2.map((x) => x.id),
hidden: false,
type: CategoryType.Single,
position: 0,
};
export const roleWikiData = {
aaa: 'Typically used by feminine-identifying people',
bbb: 'Typically used by masculine-identifying people',
ccc: 'Typically used to refer to all people as a singular neutral.',
};
export const guild: Guild = {
name: 'emoji megaporium',
id: '421896162539470888',
icon: '3372fd895ed913b55616c5e49cd50e60',
roles: [],
};
export const roleypolyGuild: GuildSlug = {
name: 'Roleypoly',
id: '386659935687147521',
permissionLevel: 0,
icon: 'ffee638c73ff9c972554f64ca34d67ee',
};
export const guildMap: { [x: string]: GuildSlug } = {
'emoji megaporium': {
name: guild.name,
id: guild.id,
permissionLevel: 0,
icon: guild.icon,
},
Roleypoly: roleypolyGuild,
'chamber of secrets': {
name: 'chamber of secrets',
id: 'aaa',
permissionLevel: 0,
icon: '',
},
Eclipse: {
name: 'Eclipse',
id: '408821059161423873',
permissionLevel: 0,
icon: '49dfdd8b2456e2977e80a8b577b19c0d',
},
};
export const guildData: GuildData = {
id: 'aaa',
message: 'henlo worl!!',
categories: [mockCategory, mockCategorySingle],
features: Features.None,
};
export const user: DiscordUser = {
id: '62601275618889728',
username: 'okano',
discriminator: '0001',
avatar: 'ca2028bab0fe30e1af4392f3fa3576e2',
bot: false,
};
export const member: Member = {
guildid: 'aaa',
roles: ['aaa', 'eee', 'unsafe2', 'ddd2'],
nick: 'okano cat',
user: user,
};
export const rpUser: RoleypolyUser = {
discorduser: user,
};
export const guildEnum: GuildEnumeration = {
guilds: [
{
id: 'aaa',
guild: guildMap['emoji megaporium'],
member,
data: guildData,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'bbb',
guild: guildMap['Roleypoly'],
member: {
...member,
roles: ['unsafe2'],
},
data: guildData,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'ccc',
guild: guildMap['chamber of secrets'],
member,
data: guildData,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'ddd',
guild: guildMap['Eclipse'],
member,
data: guildData,
roles: [...roleCategory, ...roleCategory2],
},
],
};
export const mastheadSlugs: GuildSlug[] = guildEnum.guilds.map<GuildSlug>(
(guild, idx) => ({
id: guild.guild.id,
name: guild.guild.name,
icon: guild.guild.icon,
permissionLevel: 1 << idx % 3,
})
);

View file

@ -1,9 +0,0 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { ReactifyNewlines } from './ReactifyNewlines';
it('renders a correct number of divs per newlines', () => {
const view = shallow(<ReactifyNewlines>{`1\n2\n3`}</ReactifyNewlines>);
expect(view.find('div').length).toBe(3);
});

View file

@ -1,12 +0,0 @@
import * as React from 'react';
export const ReactifyNewlines = (props: { children: string }) => {
const textArray = props.children.split('\n');
return (
<>
{textArray.map((part, idx) => (
<div key={`rifynl${idx}`}>{part || <>&nbsp;</>}</div>
))}
</>
);
};

View file

@ -1,19 +0,0 @@
import * as React from 'react';
export enum FeatureFlag {
AllowListsBlockLists = 'AllowListsBlockLists',
}
export class FeatureFlagProvider {
activeFlags: FeatureFlag[] = [];
constructor(flags: FeatureFlag[] = []) {
this.activeFlags = flags;
}
has(flag: FeatureFlag) {
return this.activeFlags.includes(flag);
}
}
export const FeatureFlagsContext = React.createContext(new FeatureFlagProvider());

View file

@ -1 +0,0 @@
export * from './FeatureFlags';

View file

@ -1,12 +0,0 @@
import * as React from 'react';
import { FeatureFlag, FeatureFlagProvider, FeatureFlagsContext } from './FeatureFlags';
export const FeatureFlagDecorator = (flags: FeatureFlag[]) => (
storyFn: () => React.ReactNode
) => {
return (
<FeatureFlagsContext.Provider value={new FeatureFlagProvider(flags)}>
{storyFn()}
</FeatureFlagsContext.Provider>
);
};

View file

@ -1,47 +0,0 @@
import { Role } from 'roleypoly/common/types';
import { roleCategory } from 'roleypoly/common/types/storyData';
import { hasPermission, hasPermissionOrAdmin } from './hasPermission';
export const permissions = {
KICK_MEMBERS: BigInt(0x2),
BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8),
SPEAK: BigInt(0x200000),
CHANGE_NICKNAME: BigInt(0x4000000),
MANAGE_ROLES: BigInt(0x10000000),
};
const roles: Role[] = [
{
...roleCategory[0],
permissions: String(permissions.ADMINISTRATOR),
},
{
...roleCategory[0],
permissions: String(
permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME
),
},
{
...roleCategory[0],
permissions: String(permissions.BAN_MEMBERS),
},
];
it('finds a permission within a list of roles', () => {
const result = hasPermission(roles, permissions.CHANGE_NICKNAME);
expect(result).toBe(true);
});
it('finds admin within a list of roles', () => {
const result = hasPermissionOrAdmin(roles, permissions.BAN_MEMBERS);
expect(result).toBe(true);
});
it('does not find a permission within a list of roles without one', () => {
const result = hasPermission(roles, permissions.KICK_MEMBERS);
expect(result).toBe(false);
});

View file

@ -1,55 +0,0 @@
import { Role } from '../types';
export const evaluatePermission = <T extends number | bigint>(
haystack: T,
needle: T
): boolean => {
return (haystack & needle) === needle;
};
export const hasPermission = (roles: Role[], permission: bigint): boolean => {
const aggregateRoles = roles.reduce(
(acc, role) => acc | BigInt(role.permissions),
BigInt(0)
);
return evaluatePermission(aggregateRoles, permission);
};
export const hasPermissionOrAdmin = (roles: Role[], permission: bigint): boolean =>
hasPermission(roles, permission | permissions.ADMINISTRATOR);
export const permissions = {
// IMPORTANT: Only uncomment what's actually used. All are left for convenience.
// CREATE_INSTANT_INVITE: BigInt(0x1),
// KICK_MEMBERS: BigInt(0x2),
// BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8),
// MANAGE_CHANNELS: BigInt(0x10),
// MANAGE_GUILD: BigInt(0x20),
// ADD_REACTIONS: BigInt(0x40),
// VIEW_AUDIT_LOG: BigInt(0x80),
// VIEW_CHANNEL: BigInt(0x400),
// SEND_MESSAGES: BigInt(0x800),
// SEND_TTS_MESSAGES: BigInt(0x1000),
// MANAGE_MESSAGES: BigInt(0x2000),
// EMBED_LINKS: BigInt(0x4000),
// ATTACH_FILES: BigInt(0x8000),
// READ_MESSAGE_HISTORY: BigInt(0x10000),
// MENTION_EVERYONE: BigInt(0x20000),
// USE_EXTERNAL_EMOJIS: BigInt(0x40000),
// VIEW_GUILD_INSIGHTS: BigInt(0x80000),
// CONNECT: BigInt(0x100000),
// SPEAK: BigInt(0x200000),
// MUTE_MEMBERS: BigInt(0x400000),
// DEAFEN_MEMBERS: BigInt(0x800000),
// MOVE_MEMBERS: BigInt(0x1000000),
// USE_VAD: BigInt(0x2000000),
// PRIORITY_SPEAKER: BigInt(0x100),
// STREAM: BigInt(0x200),
// CHANGE_NICKNAME: BigInt(0x4000000),
// MANAGE_NICKNAMES: BigInt(0x8000000),
MANAGE_ROLES: BigInt(0x10000000),
// MANAGE_WEBHOOKS: BigInt(0x20000000),
// MANAGE_EMOJIS: BigInt(0x40000000),
};

View file

@ -1 +0,0 @@
export const isBrowser = () => typeof window !== 'undefined';

View file

@ -1,53 +0,0 @@
import { NextPageContext } from 'next';
import getConfig from 'next/config';
import nookies from 'nookies';
import useSWR from 'swr';
export const getPublicURI = (context?: NextPageContext) => {
if (context?.req) {
const { publicRuntimeConfig } = getConfig();
return publicRuntimeConfig.apiPublicURI;
} else {
return typeof localStorage !== 'undefined' && localStorage.getItem('api_uri');
}
};
export const getSessionKey = (context?: NextPageContext) => {
if (context?.req) {
return nookies.get(context)['rp_session_key'];
} else {
return (
typeof sessionStorage !== 'undefined' && sessionStorage.getItem('session_key')
);
}
};
export const apiFetch = async <T>(
path: string,
init?: RequestInit,
context?: NextPageContext
): Promise<T | null> => {
const sessionKey = getSessionKey(context);
const authorizedInit: RequestInit = {
...(init || {}),
headers: {
...(init?.headers || {}),
authorization: sessionKey ? `Bearer ${sessionKey}` : '',
},
};
const response = await fetch(`${getPublicURI(context)}${path}`, authorizedInit);
if (response.status >= 400) {
const reason = (await response.json())['error'];
throw new Error(`Fetch failed: ${reason}`);
}
return response.json() as Promise<T>;
};
export const swrFetch = <T>(path: string, context?: NextPageContext) =>
useSWR<T>(path, (url: string): Promise<any> => apiFetch<T>(url, undefined, context), {
revalidateOnFocus: false,
});

View file

@ -1,48 +0,0 @@
import { sortBy } from './sortBy';
it('sorts an array of objects by its key', () => {
const output = sortBy(
[
{
name: 'bbb',
},
{
name: 'aaa',
},
{
name: 'ddd',
},
{
name: 'ccc',
},
],
'name'
);
expect(output.map((v) => v.name)).toEqual(['aaa', 'bbb', 'ccc', 'ddd']);
});
it('sorts an array of objects by its key with a predicate', () => {
const output = sortBy(
[
{
name: 'cc',
},
{
name: 'bbb',
},
{
name: 'aaaa',
},
{
name: 'd',
},
],
'name',
(a, b) => {
return a.length > b.length ? 1 : -1;
}
);
expect(output.map((v) => v.name)).toEqual(['d', 'cc', 'bbb', 'aaaa']);
});

View file

@ -1,21 +0,0 @@
export const sortBy = <T, Key extends keyof T>(
array: T[],
key: Key,
predicate?: (a: T[typeof key], b: T[typeof key]) => number
) => {
return array.sort((a, b) => {
if (predicate) {
return predicate(a[key], b[key]);
}
if (a[key] === b[key]) {
return 0;
}
if (a[key] > b[key]) {
return 1;
}
return -1;
});
};

View file

@ -1,11 +0,0 @@
import * as React from 'react';
export type ContextShimProps<T> = {
context: React.Context<T>;
children: (data: T) => any;
};
export function ContextShim<T>(props: ContextShimProps<T>) {
const context = React.useContext(props.context);
return <>{props.children(context)}</>;
}

View file

@ -1,2 +0,0 @@
export * as testHelpers from './contextTestHelpers';
export * from './withContext';

View file

@ -1,10 +0,0 @@
import * as React from 'react';
export const withContext = <T, K extends T>(
Context: React.Context<T>,
Component: React.ComponentType<K>
): React.FunctionComponent<K> => (props) => (
<Context.Consumer>
{(context) => <Component {...props} {...context} />}
</Context.Consumer>
);