feat(v3.4.3): cleanup, fix memory leak, arm support

This commit is contained in:
diced 2022-03-13 19:13:18 -07:00
parent 083040e300
commit aa611fa6ba
No known key found for this signature in database
GPG key ID: 85AB64C74535D76E
18 changed files with 315 additions and 241 deletions

View file

@ -1,4 +1,4 @@
FROM node:16-alpine AS deps
FROM node:17-alpine AS deps
WORKDIR /build
COPY package.json yarn.lock ./
@ -6,7 +6,7 @@ COPY package.json yarn.lock ./
RUN apk add --no-cache libc6-compat
RUN yarn install --frozen-lockfile
FROM node:16-alpine AS builder
FROM node:17-alpine AS builder
WORKDIR /build
COPY --from=deps /build/node_modules ./node_modules
@ -20,7 +20,7 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:16-alpine AS runner
FROM node:17-alpine AS runner
WORKDIR /zipline
ENV NODE_ENV production

44
Dockerfile-arm Normal file
View file

@ -0,0 +1,44 @@
FROM node:17 AS deps
WORKDIR /build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
FROM node:17 AS builder
WORKDIR /build
COPY --from=deps /build/node_modules ./node_modules
COPY src ./src
COPY scripts ./scripts
COPY prisma ./prisma
COPY package.json yarn.lock esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
ENV ZIPLINE_DOCKER_BUILD 1
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
FROM node:17 AS runner
WORKDIR /zipline
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 zipline
RUN adduser --system --uid 1001 zipline
COPY --from=builder --chown=zipline:zipline /build/.next ./.next
COPY --from=builder --chown=zipline:zipline /build/dist ./dist
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
COPY --from=builder /build/next.config.js ./next.config.js
COPY --from=builder /build/src ./src
COPY --from=builder /build/scripts ./scripts
COPY --from=builder /build/prisma ./prisma
COPY --from=builder /build/tsconfig.json ./tsconfig.json
COPY --from=builder /build/package.json ./package.json
USER zipline
CMD ["node", "dist/server"]

View file

@ -1,8 +1,11 @@
const esbuild = require('esbuild');
const { rm } = require('fs/promises');
(async () => {
const watch = process.argv[2] === '--watch';
await rm('./dist', { recursive: true });
await esbuild.build({
tsconfig: 'tsconfig.json',
outdir: 'dist',
@ -11,6 +14,7 @@ const esbuild = require('esbuild');
treeShaking: true,
entryPoints: [
'src/server/index.ts',
'src/server/server.ts',
'src/server/util.ts',
'src/server/validateConfig.ts',
'src/lib/logger.ts',

View file

@ -1,6 +1,6 @@
{
"name": "zip3",
"version": "3.4.2",
"version": "3.4.3",
"license": "MIT",
"scripts": {
"dev": "node esbuild.config.js && REACT_EDITOR=code-insiders NODE_ENV=development node dist/server",
@ -35,7 +35,8 @@
"colorette": "^1.2.2",
"cookie": "^0.4.1",
"fecha": "^4.2.1",
"multer": "^1.4.2",
"find-my-way": "^5.2.0",
"multer": "^1.4.4",
"next": "^12.1.0",
"prisma": "^3.9.2",
"react": "^17.0.2",

View file

@ -1,4 +1,5 @@
import { readdir, readFile, stat, writeFile } from 'fs/promises';
import { createReadStream, ReadStream } from 'fs';
import { readdir, stat, writeFile } from 'fs/promises';
import { join } from 'path';
import { Datasource } from './datasource';
@ -13,10 +14,9 @@ export class Local extends Datasource {
await writeFile(join(process.cwd(), this.path, file), data);
}
public async get(file: string): Promise<Buffer> {
public get(file: string): ReadStream {
try {
const data = await readFile(join(process.cwd(), this.path, file));
return data;
return createReadStream(join(process.cwd(), this.path, file));
} catch (e) {
return null;
}

View file

@ -1,5 +1,6 @@
import { Datasource } from './datasource';
import AWS from 'aws-sdk';
import { Readable } from 'stream';
export class S3 extends Datasource {
public name: string = 'S3';
@ -33,20 +34,12 @@ export class S3 extends Datasource {
});
}
public async get(file: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
this.s3.getObject({
public get(file: string): Readable {
// Unfortunately, aws-sdk is bad and the stream still loads everything into memory.
return this.s3.getObject({
Bucket: this.bucket,
Key: file,
}, (err, data) => {
if (err) {
reject(err);
} else {
// @ts-ignore
resolve(Buffer.from(data.Body));
}
});
});
}).createReadStream();
}
public async size(): Promise<number> {

View file

@ -1,7 +1,9 @@
import { Readable } from 'stream';
export abstract class Datasource {
public name: string;
public abstract save(file: string, data: Buffer): Promise<void>;
public abstract get(file: string): Promise<Buffer>;
public abstract get(file: string): Readable;
public abstract size(): Promise<number>;
}

View file

@ -5,16 +5,16 @@ import Logger from './logger';
if (!global.datasource) {
switch (config.datasource.type) {
case 's3':
Logger.get('datasource').info(`Using S3(${config.datasource.s3.bucket}) datasource`);
global.datasource = new S3(config.datasource.s3.access_key_id, config.datasource.s3.secret_access_key, config.datasource.s3.bucket);
Logger.get('datasource').info(`Using S3:${config.datasource.s3.bucket} datasource`);
break;
case 'local':
Logger.get('datasource').info(`Using local(${config.datasource.local.directory}) datasource`);
global.datasource = new Local(config.datasource.local.directory);
Logger.get('datasource').info(`Using local:${config.datasource.local.directory} datasource`);
break;
default:
throw new Error('Invalid datasource type');
}
};
}
export default global.datasource;

View file

@ -21,12 +21,12 @@ export default class Logger {
this.name = name;
}
info(message: string) {
console.log(this.formatMessage(LoggerLevel.INFO, this.name, message));
info(...args) {
console.log(this.formatMessage(LoggerLevel.INFO, this.name, args.join(' ')));
}
error(error: any) {
console.log(this.formatMessage(LoggerLevel.ERROR, this.name, error.stack ?? error));
error(...args: any[]) {
console.log(this.formatMessage(LoggerLevel.ERROR, this.name, args.map(error => error.stack ?? error).join(' ')));
}
formatMessage(level: LoggerLevel, name, message) {

View file

@ -127,10 +127,10 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
if (!image.mimetype.startsWith('image')) {
const { default: datasource } = await import('lib/ds');
const data = await datasource.get(image.file);
const data = datasource.get(image.file);
if (!data) return { notFound: true };
context.res.end(data);
data.pipe(context.res);
return { props: {} };
}

View file

@ -9,9 +9,7 @@ import { format as formatDate } from 'fecha';
import { v4 } from 'uuid';
import datasource from 'lib/ds';
const uploader = multer({
storage: multer.memoryStorage(),
});
const uploader = multer();
async function handler(req: NextApiReq, res: NextApiRes) {
if (req.method !== 'POST') return res.forbid('invalid method');
@ -25,13 +23,16 @@ async function handler(req: NextApiReq, res: NextApiRes) {
if (!user) return res.forbid('authorization incorect');
if (user.ratelimited) return res.ratelimited();
await run(uploader.array('file'))(req, res);
if (!req.files) return res.error('no files');
if (req.files && req.files.length === 0) return res.error('no files');
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
const files = [];
for (let i = 0; i !== req.files.length; ++i) {
const file = req.files[i];
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error(`file[${i}] size too big`);
@ -117,8 +118,6 @@ function run(middleware: any) {
}
export default async function handlers(req, res) {
await run(uploader.array('file'))(req, res);
return withZipline(handler)(req, res);
};

View file

@ -36,14 +36,14 @@ export default function Login() {
icon: <Cross1Icon />,
});
} else {
router.push(router.query.url as string || '/dashboard');
await router.push(router.query.url as string || '/dashboard');
}
};
useEffect(() => {
(async () => {
const a = await fetch('/api/user');
if (a.ok) router.push('/dashboard');
if (a.ok) await router.push('/dashboard');
else {
const v = await useFetch('/api/version');
setVersions(v);

View file

@ -4,8 +4,6 @@ import { LoadingOverlay } from '@mantine/core';
export default function Logout() {
const router = useRouter();
const [visible, setVisible] = useState(true);
useEffect(() => {
(async () => {
@ -20,7 +18,7 @@ export default function Logout() {
}, []);
return (
<LoadingOverlay visible={visible} />
<LoadingOverlay visible={true} />
);
}

View file

@ -11,7 +11,7 @@ export default function Code() {
useEffect(() => {
(async () => {
const res = await fetch('/r/' + id);
if (id && !res.ok) router.push('/404');
if (id && !res.ok) await router.push('/404');
const data = await res.text();
if (id) setPrismRenderCode(data);
})();

View file

@ -1,164 +1,3 @@
import next from 'next';
import { createServer } from 'http';
import { extname } from 'path';
import Logger from '../lib/logger';
import mimes from '../../scripts/mimes';
import { log, getStats, migrations } from './util';
import { PrismaClient } from '@prisma/client';
import { version } from '../../package.json';
import exts from '../../scripts/exts';
import datasource from '../lib/ds';
import config from '../lib/config';
import { mkdir } from 'fs/promises';
const serverLog = Logger.get('server');
import Server from './server';
serverLog.info(`starting zipline@${version} server`);
const dev = process.env.NODE_ENV === 'development';
(async () => {
try {
await run();
} catch (e) {
serverLog.error(e);
process.exit(1);
}
})();
async function run() {
process.env.DATABASE_URL = config.core.database_url;
await migrations();
if (config.datasource.type === 'local') {
await mkdir(config.datasource.local.directory, { recursive: true });
}
const app = next({
dir: '.',
dev,
quiet: !dev,
hostname: config.core.host,
port: config.core.port,
});
await app.prepare();
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
const srv = createServer(async (req, res) => {
if (req.url.startsWith('/r')) {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
let image = await prisma.image.findFirst({
where: {
OR: [
{ file: parts[2] },
{ invisible:{ invis: decodeURI(parts[2]) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
},
});
if (!image) {
const data = await datasource.get(parts[2]);
if (!data) return app.render404(req, res);
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
res.end(data);
} else {
const data = await datasource.get(parts[2]);
if (!data) return app.render404(req, res);
await prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
res.setHeader('Content-Type', image.mimetype);
res.end(data);
}
} else if (req.url.startsWith(config.uploader.route)) {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
let image = await prisma.image.findFirst({
where: {
OR: [
{ file: parts[2] },
{ invisible:{ invis: decodeURI(parts[2]) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
embed: true,
},
});
if (!image) {
const data = await datasource.get(parts[2]);
if (!data) return app.render404(req, res);
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
res.end(data);
} else if (image.embed) {
handle(req, res);
} else {
const ext = image.file.split('.').pop();
if (Object.keys(exts).includes(ext)) return handle(req, res);
const data = await datasource.get(parts[2]);
if (!data) return app.render404(req, res);
await prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
res.setHeader('Content-Type', image.mimetype);
res.end(data);
}
} else {
handle(req, res);
}
if (config.core.logger) log(req.url);
});
srv.on('error', (e) => {
serverLog.error(e);
process.exit(1);
});
srv.on('listening', () => {
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
});
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
const stats = await getStats(prisma, datasource);
await prisma.stats.create({
data: {
data: stats,
},
});
setInterval(async () => {
const stats = await getStats(prisma, datasource);
await prisma.stats.create({
data: {
data: stats,
},
});
if (config.core.logger) serverLog.info('stats updated');
}, config.core.stats_interval * 1000);
}
new Server();

186
src/server/server.ts Normal file
View file

@ -0,0 +1,186 @@
import Router from 'find-my-way';
import { NextServer, RequestHandler } from 'next/dist/server/next';
import { Image, PrismaClient } from '@prisma/client';
import { createServer, IncomingMessage, OutgoingMessage, Server as HttpServer, ServerResponse } from 'http';
import next from 'next';
import config from '../lib/config';
import datasource from '../lib/ds';
import { getStats, log, migrations } from './util';
import { mkdir } from 'fs/promises';
import Logger from '../lib/logger';
import mimes from '../../scripts/mimes';
import { extname } from 'path';
import exts from '../../scripts/exts';
import { version } from '../../package.json';
const serverLog = Logger.get('server');
export default class Server {
public router: Router.Instance<Router.HTTPVersion.V1>;
public nextServer: NextServer;
public handle: RequestHandler;
public prisma: PrismaClient;
private http: HttpServer;
public constructor() {
serverLog.info(`starting zipline@${version} server`);
this.start();
}
private async start() {
const dev = process.env.NODE_ENV === 'development';
process.env.DATABASE_URL = config.core.database_url;
await migrations();
this.prisma = new PrismaClient();
if (config.datasource.type === 'local') {
await mkdir(config.datasource.local.directory, { recursive: true });
}
this.nextServer = next({
dir: '.',
dev,
quiet: !dev,
hostname: config.core.host,
port: config.core.port,
});
this.handle = this.nextServer.getRequestHandler();
this.router = Router({
defaultRoute: (req, res) => {
this.handle(req, res);
},
});
this.router.on('GET', `${config.uploader.route}/:id`, async (req, res, params) => {
const image = await this.prisma.image.findFirst({
where: {
OR: [
{ file: params.id },
{ invisible: { invis: decodeURI(params.id) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
embed: true,
},
});
if (!image) await this.rawFile(req, res, params.id);
else if (image.embed) await this.handle(req, res);
else await this.fileDb(req, res, image);
});
this.router.on('GET', '/r/:id', async (req, res, params) => {
const image = await this.prisma.image.findFirst({
where: {
OR: [
{ file: params.id },
{ invisible: { invis: decodeURI(params.id) } },
],
},
select: {
mimetype: true,
id: true,
file: true,
invisible: true,
embed: true,
},
});
if (!image) await this.rawFile(req, res, params.id);
else await this.rawFileDb(req, res, image);
});
await this.nextServer.prepare();
this.http = createServer((req, res) => {
this.router.lookup(req, res);
if (config.core.logger) log(req.url);
});
this.http.on('error', (e) => {
serverLog.error(e);
process.exit(1);
});
this.http.on('listening', () => {
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
});
this.http.listen(config.core.port, config.core.host ?? '0.0.0.0');
this.stats();
}
private async rawFile(req: IncomingMessage, res: OutgoingMessage, id: string) {
const data = datasource.get(id);
if (!data) return this.nextServer.render404(req, res as ServerResponse);
const mimetype = mimes[extname(id)] ?? 'application/octet-stream';
res.setHeader('Content-Type', mimetype);
data.pipe(res);
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
data.on('end', () => res.end());
}
private async rawFileDb(req: IncomingMessage, res: OutgoingMessage, image: any) {
const data = datasource.get(image.file);
if (!data) return this.nextServer.render404(req, res as ServerResponse);
res.setHeader('Content-Type', image.mimetype);
data.pipe(res);
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
data.on('end', () => res.end());
await this.prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
}
private async fileDb(req: IncomingMessage, res: OutgoingMessage, image: any) {
const ext = image.file.split('.').pop();
if (Object.keys(exts).includes(ext)) return this.handle(req, res as ServerResponse);
const data = datasource.get(image.file);
if (!data) return this.nextServer.render404(req, res as ServerResponse);
res.setHeader('Content-Type', image.mimetype);
data.pipe(res);
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
data.on('end', () => res.end());
await this.prisma.image.update({
where: { id: image.id },
data: { views: { increment: 1 } },
});
}
private async stats() {
const stats = await getStats(this.prisma, datasource);
await this.prisma.stats.create({
data: {
data: stats,
},
});
setInterval(async () => {
const stats = await getStats(this.prisma, datasource);
await this.prisma.stats.create({
data: {
data: stats,
},
});
if (config.core.logger) serverLog.info('stats updated');
}, config.core.stats_interval * 1000);
}
}

View file

@ -1,9 +1,6 @@
import { readFile, readdir, stat } from 'fs/promises';
import { join } from 'path';
import { Migrate } from '@prisma/migrate/dist/Migrate';
import Logger from '../lib/logger';
import { execSync } from 'child_process';
import { Datasource } from '../lib/datasource/index';
import { Datasource } from 'lib/datasource';
export async function migrations() {
const migrate = new Migrate('./prisma/schema.prisma');
@ -25,27 +22,6 @@ export function log(url) {
return Logger.get('url').info(url);
}
export function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
export async function sizeOfDir(directory) {
const files = await readdir(directory);
let size = 0;
for (let i = 0, L = files.length; i !== L; ++i) {
const sta = await stat(join(directory, files[i]));
size += sta.size;
}
return size;
}
export function bytesToRead(bytes) {
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;

View file

@ -2450,6 +2450,11 @@ exit-on-epipe@~1.0.1:
resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
fast-decode-uri-component@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@ -2535,6 +2540,16 @@ find-cache-dir@3.3.2:
make-dir "^3.0.2"
pkg-dir "^4.1.0"
find-my-way@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-5.2.0.tgz#3e8b6d55471c9eac52164d268db108ff64849ae0"
integrity sha512-YLocRSSJJ1PCnrupBmaw2pUcFkU125QTWfFfpdLq11h7bQ+r+Ke4D1/4v7UkERlw0037VkYMd+1RMGTbhFCbPw==
dependencies:
fast-decode-uri-component "^1.0.1"
fast-deep-equal "^3.1.3"
safe-regex2 "^2.0.0"
semver-store "^0.3.0"
find-root@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz"
@ -3546,9 +3561,9 @@ minizlib@^2.1.1:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp@^0.5.1:
mkdirp@^0.5.4:
version "0.5.5"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
@ -3596,15 +3611,15 @@ mssql@8.0.1:
tarn "^3.0.2"
tedious "^14.0.0"
multer@^1.4.2:
version "1.4.2"
resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz"
integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==
multer@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
dependencies:
append-field "^1.0.0"
busboy "^0.2.11"
concat-stream "^1.5.2"
mkdirp "^0.5.1"
mkdirp "^0.5.4"
object-assign "^4.1.1"
on-finished "^2.3.0"
type-is "^1.6.4"
@ -4511,6 +4526,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
retry@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
@ -4557,6 +4577,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-regex2@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9"
integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==
dependencies:
ret "~0.2.0"
"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
@ -4585,6 +4612,11 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver-store@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9"
integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"