diff --git a/.gitignore b/.gitignore index 8ce4ef8e..1c1d2582 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist +build node_modules/* server/scripts .env diff --git a/Dockerfile b/Dockerfile index 5f61043c..c43fb0e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12-alpine +FROM node:14-alpine ENV PATH /opt/outline/node_modules/.bin:/opt/node_modules/.bin:$PATH ENV NODE_PATH /opt/outline/node_modules:/opt/node_modules @@ -11,6 +11,7 @@ COPY . $APP_PATH RUN yarn install --pure-lockfile RUN yarn build RUN cp -r /opt/outline/node_modules /opt/node_modules +ENV NODE_ENV production CMD yarn start diff --git a/Procfile b/Procfile index 5ec9cc2c..f1093445 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node index.js \ No newline at end of file +web: node ./build/server/index.js \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 43a19c80..00000000 --- a/index.js +++ /dev/null @@ -1,73 +0,0 @@ -// @flow -require("./init"); - -if ( - !process.env.SECRET_KEY || - process.env.SECRET_KEY === "generate_a_new_key" -) { - console.error( - "The SECRET_KEY env variable must be set with the output of `openssl rand -hex 32`" - ); - // $FlowFixMe - process.exit(1); -} - -if (process.env.AWS_ACCESS_KEY_ID) { - [ - "AWS_REGION", - "AWS_SECRET_ACCESS_KEY", - "AWS_S3_UPLOAD_BUCKET_URL", - "AWS_S3_UPLOAD_MAX_SIZE", - ].forEach((key) => { - if (!process.env[key]) { - console.error(`The ${key} env variable must be set when using AWS`); - // $FlowFixMe - process.exit(1); - } - }); -} - -if (process.env.SLACK_KEY) { - if (!process.env.SLACK_SECRET) { - console.error( - `The SLACK_SECRET env variable must be set when using Slack Sign In` - ); - // $FlowFixMe - process.exit(1); - } -} - -if (!process.env.URL) { - console.error( - "The URL env variable must be set to the externally accessible URL, e.g (https://www.getoutline.com)" - ); - // $FlowFixMe - process.exit(1); -} - -if (!process.env.DATABASE_URL) { - console.error( - "The DATABASE_URL env variable must be set to the location of your postgres server, including authentication and port" - ); - // $FlowFixMe - process.exit(1); -} - -if (!process.env.REDIS_URL) { - console.error( - "The REDIS_URL env variable must be set to the location of your redis server, including authentication and port" - ); - // $FlowFixMe - process.exit(1); -} - -if (process.env.NODE_ENV === "production") { - console.log("\n\x1b[33m%s\x1b[0m", "Running Outline in production mode."); -} else if (process.env.NODE_ENV === "development") { - console.log( - "\n\x1b[33m%s\x1b[0m", - 'Running Outline in development mode with hot reloading. To run Outline in production mode set the NODE_ENV env variable to "production"' - ); -} - -require("./server"); diff --git a/init.js b/init.js deleted file mode 100644 index 8692b829..00000000 --- a/init.js +++ /dev/null @@ -1,4 +0,0 @@ -// @flow -require("@babel/register"); -require("@babel/polyfill"); -require("dotenv").config({ silent: true }); diff --git a/package.json b/package.json index dd557932..be798b97 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "outline", "private": true, + "license": "Business Source License 1.1", "main": "index.js", "scripts": { - "clean": "rimraf dist", - "build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js", - "build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json > stats.json", - "build": "yarn clean && yarn build:webpack", - "start": "NODE_ENV=production node index.js", - "dev": "NODE_ENV=development nodemon --watch server index.js", + "clean": "rimraf build", + "build:server": "babel -d ./build/server ./server && babel -d ./build/shared ./shared && cp package.json ./build && ln -sf \"$(pwd)/webpack.config.dev.js\" ./build", + "build:webpack": "webpack --config webpack.config.prod.js", + "build": "yarn clean && yarn build:webpack && yarn build:server", + "start": "node ./build/server/index.js", + "dev": "nodemon --exec \"yarn build:server && node build/server/index.js\" -e js --ignore build/", "lint": "eslint app server shared", "flow": "flow", "deploy": "git push heroku master", @@ -64,7 +65,6 @@ "@babel/preset-env": "^7.11.0", "@babel/preset-flow": "^7.10.4", "@babel/preset-react": "^7.10.4", - "@babel/register": "^7.10.5", "@rehooks/window-scroll-position": "^1.0.1", "@sentry/node": "^5.23.0", "@tippy.js/react": "^2.2.2", @@ -160,6 +160,7 @@ "validator": "5.2.0" }, "devDependencies": { + "@babel/cli": "^7.10.5", "@relative-ci/agent": "^1.3.0", "babel-eslint": "^10.1.0", "babel-jest": "^26.2.2", diff --git a/server/index.js b/server/index.js index 840625e8..f8986a38 100644 --- a/server/index.js +++ b/server/index.js @@ -1,189 +1,73 @@ // @flow -import http from "http"; -import IO from "socket.io"; -import socketRedisAdapter from "socket.io-redis"; -import SocketAuth from "socketio-auth"; -import app from "./app"; -import { Document, Collection, View } from "./models"; -import policy from "./policies"; -import { client, subscriber } from "./redis"; -import { getUserForJWT } from "./utils/jwt"; +require("dotenv").config({ silent: true }); -const server = http.createServer(app.callback()); -let io; +if ( + !process.env.SECRET_KEY || + process.env.SECRET_KEY === "generate_a_new_key" +) { + console.error( + "The SECRET_KEY env variable must be set with the output of `openssl rand -hex 32`" + ); + // $FlowFixMe + process.exit(1); +} -const { can } = policy; - -io = IO(server, { - path: "/realtime", - serveClient: false, - cookie: false, -}); - -io.adapter( - socketRedisAdapter({ - pubClient: client, - subClient: subscriber, - }) -); - -SocketAuth(io, { - authenticate: async (socket, data, callback) => { - const { token } = data; - - try { - const user = await getUserForJWT(token); - socket.client.user = user; - - // store the mapping between socket id and user id in redis - // so that it is accessible across multiple server nodes - await client.hset(socket.id, "userId", user.id); - - return callback(null, true); - } catch (err) { - return callback(err); +if (process.env.AWS_ACCESS_KEY_ID) { + [ + "AWS_REGION", + "AWS_SECRET_ACCESS_KEY", + "AWS_S3_UPLOAD_BUCKET_URL", + "AWS_S3_UPLOAD_MAX_SIZE", + ].forEach((key) => { + if (!process.env[key]) { + console.error(`The ${key} env variable must be set when using AWS`); + // $FlowFixMe + process.exit(1); } - }, - postAuthenticate: async (socket, data) => { - const { user } = socket.client; + }); +} - // the rooms associated with the current team - // and user so we can send authenticated events - let rooms = [`team-${user.teamId}`, `user-${user.id}`]; - - // the rooms associated with collections this user - // has access to on connection. New collection subscriptions - // are managed from the client as needed through the 'join' event - const collectionIds = await user.collectionIds(); - collectionIds.forEach((collectionId) => - rooms.push(`collection-${collectionId}`) +if (process.env.SLACK_KEY) { + if (!process.env.SLACK_SECRET) { + console.error( + `The SLACK_SECRET env variable must be set when using Slack Sign In` ); + // $FlowFixMe + process.exit(1); + } +} - // join all of the rooms at once - socket.join(rooms); +if (!process.env.URL) { + console.error( + "The URL env variable must be set to the externally accessible URL, e.g (https://www.getoutline.com)" + ); + // $FlowFixMe + process.exit(1); +} - // allow the client to request to join rooms - socket.on("join", async (event) => { - // user is joining a collection channel, because their permissions have - // changed, granting them access. - if (event.collectionId) { - const collection = await Collection.scope({ - method: ["withMembership", user.id], - }).findByPk(event.collectionId); +if (!process.env.DATABASE_URL) { + console.error( + "The DATABASE_URL env variable must be set to the location of your postgres server, including authentication and port" + ); + // $FlowFixMe + process.exit(1); +} - if (can(user, "read", collection)) { - socket.join(`collection-${event.collectionId}`); - } - } +if (!process.env.REDIS_URL) { + console.error( + "The REDIS_URL env variable must be set to the location of your redis server, including authentication and port" + ); + // $FlowFixMe + process.exit(1); +} - // user is joining a document channel, because they have navigated to - // view a document. - if (event.documentId) { - const document = await Document.findByPk(event.documentId, { - userId: user.id, - }); +if (process.env.NODE_ENV === "production") { + console.log("\n\x1b[33m%s\x1b[0m", "Running Outline in production mode."); +} else if (process.env.NODE_ENV === "development") { + console.log( + "\n\x1b[33m%s\x1b[0m", + 'Running Outline in development mode with hot reloading. To run Outline in production mode set the NODE_ENV env variable to "production"' + ); +} - if (can(user, "read", document)) { - const room = `document-${event.documentId}`; - - await View.touch(event.documentId, user.id, event.isEditing); - const editing = await View.findRecentlyEditingByDocument( - event.documentId - ); - - socket.join(room, () => { - // let everyone else in the room know that a new user joined - io.to(room).emit("user.join", { - userId: user.id, - documentId: event.documentId, - isEditing: event.isEditing, - }); - - // let this user know who else is already present in the room - io.in(room).clients(async (err, sockets) => { - if (err) throw err; - - // because a single user can have multiple socket connections we - // need to make sure that only unique userIds are returned. A Map - // makes this easy. - let userIds = new Map(); - for (const socketId of sockets) { - const userId = await client.hget(socketId, "userId"); - userIds.set(userId, userId); - } - socket.emit("document.presence", { - documentId: event.documentId, - userIds: Array.from(userIds.keys()), - editingIds: editing.map((view) => view.userId), - }); - }); - }); - } - } - }); - - // allow the client to request to leave rooms - socket.on("leave", (event) => { - if (event.collectionId) { - socket.leave(`collection-${event.collectionId}`); - } - if (event.documentId) { - const room = `document-${event.documentId}`; - socket.leave(room, () => { - io.to(room).emit("user.leave", { - userId: user.id, - documentId: event.documentId, - }); - }); - } - }); - - socket.on("disconnecting", () => { - const rooms = Object.keys(socket.rooms); - - rooms.forEach((room) => { - if (room.startsWith("document-")) { - const documentId = room.replace("document-", ""); - io.to(room).emit("user.leave", { - userId: user.id, - documentId, - }); - } - }); - }); - - socket.on("presence", async (event) => { - const room = `document-${event.documentId}`; - - if (event.documentId && socket.rooms[room]) { - const view = await View.touch( - event.documentId, - user.id, - event.isEditing - ); - view.user = user; - - io.to(room).emit("user.presence", { - userId: user.id, - documentId: event.documentId, - isEditing: event.isEditing, - }); - } - }); - }, -}); - -server.on("error", (err) => { - throw err; -}); - -server.on("listening", () => { - const address = server.address(); - console.log(`\n> Listening on http://localhost:${address.port}\n`); -}); - -server.listen(process.env.PORT || "3000"); - -export const socketio = io; - -export default server; +require("./main"); diff --git a/server/main.js b/server/main.js new file mode 100644 index 00000000..840625e8 --- /dev/null +++ b/server/main.js @@ -0,0 +1,189 @@ +// @flow +import http from "http"; +import IO from "socket.io"; +import socketRedisAdapter from "socket.io-redis"; +import SocketAuth from "socketio-auth"; +import app from "./app"; +import { Document, Collection, View } from "./models"; +import policy from "./policies"; +import { client, subscriber } from "./redis"; +import { getUserForJWT } from "./utils/jwt"; + +const server = http.createServer(app.callback()); +let io; + +const { can } = policy; + +io = IO(server, { + path: "/realtime", + serveClient: false, + cookie: false, +}); + +io.adapter( + socketRedisAdapter({ + pubClient: client, + subClient: subscriber, + }) +); + +SocketAuth(io, { + authenticate: async (socket, data, callback) => { + const { token } = data; + + try { + const user = await getUserForJWT(token); + socket.client.user = user; + + // store the mapping between socket id and user id in redis + // so that it is accessible across multiple server nodes + await client.hset(socket.id, "userId", user.id); + + return callback(null, true); + } catch (err) { + return callback(err); + } + }, + postAuthenticate: async (socket, data) => { + const { user } = socket.client; + + // the rooms associated with the current team + // and user so we can send authenticated events + let rooms = [`team-${user.teamId}`, `user-${user.id}`]; + + // the rooms associated with collections this user + // has access to on connection. New collection subscriptions + // are managed from the client as needed through the 'join' event + const collectionIds = await user.collectionIds(); + collectionIds.forEach((collectionId) => + rooms.push(`collection-${collectionId}`) + ); + + // join all of the rooms at once + socket.join(rooms); + + // allow the client to request to join rooms + socket.on("join", async (event) => { + // user is joining a collection channel, because their permissions have + // changed, granting them access. + if (event.collectionId) { + const collection = await Collection.scope({ + method: ["withMembership", user.id], + }).findByPk(event.collectionId); + + if (can(user, "read", collection)) { + socket.join(`collection-${event.collectionId}`); + } + } + + // user is joining a document channel, because they have navigated to + // view a document. + if (event.documentId) { + const document = await Document.findByPk(event.documentId, { + userId: user.id, + }); + + if (can(user, "read", document)) { + const room = `document-${event.documentId}`; + + await View.touch(event.documentId, user.id, event.isEditing); + const editing = await View.findRecentlyEditingByDocument( + event.documentId + ); + + socket.join(room, () => { + // let everyone else in the room know that a new user joined + io.to(room).emit("user.join", { + userId: user.id, + documentId: event.documentId, + isEditing: event.isEditing, + }); + + // let this user know who else is already present in the room + io.in(room).clients(async (err, sockets) => { + if (err) throw err; + + // because a single user can have multiple socket connections we + // need to make sure that only unique userIds are returned. A Map + // makes this easy. + let userIds = new Map(); + for (const socketId of sockets) { + const userId = await client.hget(socketId, "userId"); + userIds.set(userId, userId); + } + socket.emit("document.presence", { + documentId: event.documentId, + userIds: Array.from(userIds.keys()), + editingIds: editing.map((view) => view.userId), + }); + }); + }); + } + } + }); + + // allow the client to request to leave rooms + socket.on("leave", (event) => { + if (event.collectionId) { + socket.leave(`collection-${event.collectionId}`); + } + if (event.documentId) { + const room = `document-${event.documentId}`; + socket.leave(room, () => { + io.to(room).emit("user.leave", { + userId: user.id, + documentId: event.documentId, + }); + }); + } + }); + + socket.on("disconnecting", () => { + const rooms = Object.keys(socket.rooms); + + rooms.forEach((room) => { + if (room.startsWith("document-")) { + const documentId = room.replace("document-", ""); + io.to(room).emit("user.leave", { + userId: user.id, + documentId, + }); + } + }); + }); + + socket.on("presence", async (event) => { + const room = `document-${event.documentId}`; + + if (event.documentId && socket.rooms[room]) { + const view = await View.touch( + event.documentId, + user.id, + event.isEditing + ); + view.user = user; + + io.to(room).emit("user.presence", { + userId: user.id, + documentId: event.documentId, + isEditing: event.isEditing, + }); + } + }); + }, +}); + +server.on("error", (err) => { + throw err; +}); + +server.on("listening", () => { + const address = server.address(); + console.log(`\n> Listening on http://localhost:${address.port}\n`); +}); + +server.listen(process.env.PORT || "3000"); + +export const socketio = io; + +export default server; diff --git a/server/routes.js b/server/routes.js index fe281a33..ae634a94 100644 --- a/server/routes.js +++ b/server/routes.js @@ -18,7 +18,7 @@ const readFile = util.promisify(fs.readFile); const readIndexFile = async (ctx) => { if (isProduction) { - return readFile(path.join(__dirname, "../dist/index.html")); + return readFile(path.join(__dirname, "../app/index.html")); } const middleware = ctx.devMiddleware; @@ -39,7 +39,7 @@ const readIndexFile = async (ctx) => { // serve static assets koa.use( - serve(path.resolve(__dirname, "../public"), { + serve(path.resolve(__dirname, "../../public"), { maxage: 60 * 60 * 24 * 30 * 1000, }) ); @@ -52,10 +52,7 @@ if (process.env.NODE_ENV === "production") { "Cache-Control": `max-age=${356 * 24 * 60 * 60}`, }); - await sendfile( - ctx, - path.join(__dirname, "../dist/", ctx.path.substring(8)) - ); + await sendfile(ctx, path.join(__dirname, "../app/", ctx.path.substring(8))); }); } diff --git a/server/services/websockets.js b/server/services/websockets.js index 2ad97e30..1e777198 100644 --- a/server/services/websockets.js +++ b/server/services/websockets.js @@ -1,7 +1,7 @@ // @flow import subHours from "date-fns/sub_hours"; -import { socketio } from "../"; import type { Event } from "../events"; +import { socketio } from "../main"; import { Document, Collection, diff --git a/server/utils/prefetchTags.js b/server/utils/prefetchTags.js index d002ea04..36fa0ddf 100644 --- a/server/utils/prefetchTags.js +++ b/server/utils/prefetchTags.js @@ -16,7 +16,7 @@ const prefetchTags = [ try { const manifest = fs.readFileSync( - path.join(__dirname, "../../dist/manifest.json"), + path.join(__dirname, "../../app/manifest.json"), "utf8" ); const manifestData = JSON.parse(manifest); diff --git a/webpack.config.js b/webpack.config.js index ef129882..8b2eca63 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,7 @@ require('dotenv').config({ silent: true }); module.exports = { output: { - path: path.join(__dirname, 'dist'), + path: path.join(__dirname, 'build/app'), filename: '[name].[hash].js', publicPath: '/static/', }, diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 8ed106c0..2a6ccfc1 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -8,7 +8,7 @@ commonWebpackConfig = require('./webpack.config'); productionWebpackConfig = Object.assign(commonWebpackConfig, { output: { - path: path.join(__dirname, 'dist'), + path: path.join(__dirname, 'build/app'), filename: '[name].[contenthash].js', publicPath: '/static/', }, diff --git a/yarn.lock b/yarn.lock index d5429303..0d9839a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,22 @@ # yarn lockfile v1 +"@babel/cli@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.10.5.tgz#57df2987c8cf89d0fc7d4b157ec59d7619f1b77a" + integrity sha512-j9H9qSf3kLdM0Ao3aGPbGZ73mEA9XazuupcS6cDGWuiyAcANoguhP0r2Lx32H5JGw4sSSoHG3x/mxVnHgvOoyA== + dependencies: + commander "^4.0.1" + convert-source-map "^1.1.0" + fs-readdir-recursive "^1.1.0" + glob "^7.0.0" + lodash "^4.17.19" + make-dir "^2.1.0" + slash "^2.0.0" + source-map "^0.5.0" + optionalDependencies: + chokidar "^2.1.8" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -938,17 +954,6 @@ "@babel/plugin-transform-react-jsx-source" "^7.10.4" "@babel/plugin-transform-react-pure-annotations" "^7.10.4" -"@babel/register@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.10.5.tgz#354f3574895f1307f79efe37a51525e52fd38d89" - integrity sha512-eYHdLv43nyvmPn9bfNfrcC4+iYNwdQ8Pxk1MFJuU/U5LpSYl/PH4dFMazCYZDFVi8ueG3shvO+AQfLrxpYulQw== - dependencies: - find-cache-dir "^2.0.0" - lodash "^4.17.19" - make-dir "^2.1.0" - pirates "^4.0.0" - source-map-support "^0.5.16" - "@babel/runtime-corejs3@^7.10.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz#02c3029743150188edeb66541195f54600278419" @@ -3263,6 +3268,11 @@ commander@^2.11.0, commander@^2.19.0, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -3387,7 +3397,7 @@ content-type@^1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -4792,7 +4802,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: +find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -4999,6 +5009,11 @@ fs-minipass@^2.0.0: dependencies: minipass "^3.0.0" +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -8782,7 +8797,7 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pirates@^4.0.0, pirates@^4.0.1: +pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== @@ -10212,6 +10227,11 @@ sisteransi@^1.0.4: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -10405,7 +10425,7 @@ source-map-support@^0.4.0: dependencies: source-map "^0.5.6" -source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@~0.5.12: +source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==