diff --git a/.eslintrc.json b/.eslintrc.json index cf8022cc92..4d3b4b384d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -36,7 +36,9 @@ { "files": [ "src/utils/**/*.?(js|ts)", - "+(middleware|scripts)/**/*.+(js|ts)" + "scripts/**/*.js", + "src/server/**/*.js", + "gatsby-*.js" ], "rules": { "@typescript-eslint/no-var-requires": "off" diff --git a/gatsby-config.js b/gatsby-config.js index 383c4fbbc8..4780c89fd6 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -5,8 +5,8 @@ const path = require('path') require('./config/prismjs/dvc') require('./config/prismjs/usage') -const apiMiddleware = require('./middleware/api') -const redirectsMiddleware = require('./middleware/redirects') +const apiMiddleware = require('./src/server/middleware/api') +const redirectsMiddleware = require('./src/server/middleware/redirects') const { BLOG } = require('./src/consts') const title = 'Data Version Control ยท DVC' diff --git a/package.json b/package.json index 23535bb899..316479f009 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "develop": "gatsby develop", "debug": "node --inspect-brk server.js", "build": "gatsby build", + "heroku-postbuild": "./scripts/deploy-with-s3.js", "test": "jest", - "start": "./scripts/clear-cloudflare-cache.js; NODE_ENV=production node server.js", + "start": "./scripts/clear-cloudflare-cache.js; node ./src/server/index.js", "format-staged": "pretty-quick --staged --no-restage --bail", "format-check": "prettier --check '{.,pages/**,content/docs/**,src/**}/*.{js,md,json}'", "format-all": "prettier --write './**/*.{js,jsx,md,tsx,ts,json}'", - "lint-ts": "tsc --noEmit --skipLibCheck && eslint --ext .json,.js,.ts,.tsx src middleware scripts", + "lint-ts": "tsc --noEmit --skipLibCheck && eslint --ext .json,.js,.ts,.tsx src scripts", "lint-css": "stylelint \"src/**/*.css\"", "link-check": "scripts/link-check-git-all.sh", "link-check-diff": "scripts/link-check-git-diff.sh" @@ -28,9 +29,10 @@ }, "homepage": "https://github.com/iterative/dvc.org#readme", "engines": { - "node": "^12.0.0" + "node": ">=12.0.0" }, "dependencies": { + "@hapi/wreck": "^17.0.0", "@octokit/graphql": "^4.3.1", "@reach/portal": "^0.9.0", "@reach/router": "^1.3.1", @@ -43,6 +45,7 @@ "date-fns": "^2.8.1", "docsearch.js": "^2.6.3", "express": "^4.17.1", + "fs-extra": "^9.0.0", "gatsby": "^2.20.2", "gatsby-image": "^2.3.0", "gatsby-link": "^2.3.0", @@ -57,7 +60,6 @@ "lodash.startcase": "^4.4.0", "lodash.throttle": "^4.1.1", "lodash.topairs": "^4.3.0", - "micro-cors": "^0.1.1", "node-cache": "^5.1.0", "perfect-scrollbar": "^1.4.0", "prismjs": "^1.19.0", @@ -76,6 +78,7 @@ "react-use": "^13.27.0", "rehype-react": "^4.0.1", "request": "^2.88.0", + "s3-client": "^4.4.2", "serve-handler": "^6.1.2", "slick-carousel": "^1.8.1", "styled-components": "^4.4.1", diff --git a/redirects-list.json b/redirects-list.json index 9c777662cc..23cff914db 100644 --- a/redirects-list.json +++ b/redirects-list.json @@ -1,21 +1,29 @@ [ - "^https://blog\\.dvc\\.org/blog/(.*?)/?$ https://dvc.org/blog/$1", - "^https://blog\\.dvc\\.org/(.*?)/?$ https://dvc.org/blog/$1", - "^https://(?:www\\.)dvc\\.org/(.+)? https://dvc.org/$1", - "^https://man\\.dvc\\.org/(.+)? https://dvc.org/doc/command-reference/$1 303", - "^https://error\\.dvc\\.org/(.+)? https://dvc.org/doc/user-guide/troubleshooting#$1 303", - "^https://(code|data|remote)\\.dvc\\.org/(.+) https://s3-us-east-2.amazonaws.com/dvc-public/$1/$2 303", - "^/((?:deb|rpm)/.+) https://s3-us-east-2.amazonaws.com/dvc-s3-repo/$1 303", - "^/(?:help|chat)/?$ https://discordapp.com/invite/dvwXA2N 303", - "^/(?:docs|documentation)(/.*)?$ /doc$1", - "^/doc/?$ /doc/home 307", - "^/doc/get-started(/.*)?$ /doc/tutorials/get-started$1", - "^/doc/tutorials/get-started/?$ /doc/tutorials/get-started/agenda 307", - "^/doc/tutorial/?$ /doc/tutorials", - "^/doc/tutorial/(.*)? /doc/tutorials/deep/$1", - "^/doc/commands-reference(/.*)?$ /doc/command-reference$1", - "^/doc/use-cases/data-and-model-files-versioning/?$ /doc/use-cases/versioning-data-and-model-files", - "^/doc/user-guide/contributing/?$ /doc/user-guide/contributing/core 307", - "^/doc/understanding-dvc/?$ /doc/understanding-dvc/collaboration-issues 307", - "^/doc/changelog/?$ /doc/changelog/0.18 307" + "^https://(?:www\\.)dataversioncontrol\\.com(.*)? https://dataversioncontrol.com$1 301", + "^https://blog\\.dataversioncontrol\\.com/data-version-control-tutorial-9146715eda46 https://dvc.org/doc/tutorials 301", + "^https://blog\\.dataversioncontrol\\.com/data-version-control-beta-release-iterative-machine-learning-a7faf7c8be67 https://dvc.org/doc/tutorials 301", + "^https://blog\\.dataversioncontrol\\.com/dvc-heartbeat-6301aebf5c96 https://dvc.org/blog/march-19-dvc-heartbeat 301", + "^https://blog\\.dataversioncontrol\\.com/april19-dvc-heartbeat-296c71a59be4 https://dvc.org/blog/april-19-dvc-heartbeat 301", + "^https://blog\\.dataversioncontrol\\.com/dvc-0-8-5-release-f66ef3b10684 https://github.com/iterative/dvc/releases 301", + "^https://blog\\.dataversioncontrol\\.com(.+)-[a-z0-9]{12}$ https://dvc.org/blog$1 301", + "^https://blog\\.dvc\\.org/blog/(.*?)/?$ https://dvc.org/blog/$1", + "^https://blog\\.dvc\\.org/(.*?)/?$ https://dvc.org/blog/$1", + "^https://(?:www\\.)dvc\\.org(.*)? https://dvc.org$1", + "^https://man\\.dvc\\.org(.*)? https://dvc.org/doc/command-reference$1 303", + "^https://error\\.dvc\\.org/(.+)? https://dvc.org/doc/user-guide/troubleshooting#$1 303", + "^https://(code|data|remote)\\.dvc\\.org/(.+) https://s3-us-east-2.amazonaws.com/dvc-public/$1/$2 303", + "^/((?:deb|rpm)/.+) https://s3-us-east-2.amazonaws.com/dvc-s3-repo/$1 303", + "^/(?:help|chat)/?$ https://discordapp.com/invite/dvwXA2N 303", + "^/(?:docs|documentation)(/.*)?$ /doc$1", + "^/doc/?$ /doc/home 307", + "^/doc/get-started(/.*)?$ /doc/tutorials/get-started$1", + "^/doc/tutorials/get-started/?$ /doc/tutorials/get-started/agenda 307", + "^/doc/tutorial/?$ /doc/tutorials", + "^/doc/tutorial/(.*)? /doc/tutorials/deep/$1", + "^/doc/commands-reference(/.*)?$ /doc/command-reference$1", + "^/doc/use-cases/data-and-model-files-versioning/?$ /doc/use-cases/versioning-data-and-model-files", + "^/doc/user-guide/contributing/?$ /doc/user-guide/contributing/core 307", + "^/doc/understanding-dvc/?$ /doc/understanding-dvc/collaboration-issues 307", + "^/doc/changelog/?$ /doc/changelog/0.18 307", + "^/(.+)/$ /$1 301" ] diff --git a/scripts/deploy-with-s3.js b/scripts/deploy-with-s3.js new file mode 100755 index 0000000000..af3ab7f580 --- /dev/null +++ b/scripts/deploy-with-s3.js @@ -0,0 +1,130 @@ +#!/usr/bin/env node +'use strict' + +/** + * Build gatsby site and deploy public/ to s3. + * + * The S3 path of the deployment depends on the HEROKU_APP_NAME variable, + * which is passed to PRs by heroku, but you can set locally too. + * + * With HEROKU_APP_NAME: /dvc-org-pulls/$HEROKU_APP_NAME + * Without HEROKU_APP_NAME: /dvc-org-prod + * + * Needs following environment variables: + * + * - S3_BUCKET: name of the bucket + * - AWS_REGION: region of the bucket + * - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: auth token to access the bucket. + * - HEROKU_APP_NAME: (optional) app name to specify the ID of the PR if any. + **/ + +const path = require('path') +const { execSync } = require('child_process') +const { remove, move, ensureDir } = require('fs-extra') +const { s3Prefix, s3Bucket, s3Client } = require('./s3-utils') + +const rootDir = path.join(__dirname, '..') +const cacheDir = path.join(rootDir, '.cache') +const publicDir = path.join(rootDir, 'public') + +function run(command) { + execSync(command, { + stdio: ['pipe', process.stdout, process.stderr] + }) +} + +function syncCall(method, ...args) { + return new Promise((resolve, reject) => { + const synchroniser = s3Client[method](...args) + synchroniser.on('error', reject) + synchroniser.on('end', resolve) + }) +} + +async function prefixIsEmpty(prefix) { + try { + await s3Client.s3 + .headObject({ + Bucket: s3Bucket, + Prefix: prefix + '/index.html' + }) + .promise() + return false + } catch (e) { + return true + } +} + +async function downloadFromS3(prefix) { + try { + const staticDir = path.join(publicDir, 'static') + const staticPrefix = prefix + '/static' + await ensureDir(staticDir) + + console.log( + `downloading public/static from s3://${s3Bucket}/${staticPrefix}` + ) + console.time('download from s3') + await syncCall('downloadDir', { + localDir: staticDir, + s3Params: { + Bucket: s3Bucket, + Prefix: staticPrefix + } + }) + console.timeEnd('download from s3') + } catch (downloadError) { + console.error('Error downloading initial data', downloadError) + // Don't propagate. It's just a cache warming step + } +} + +async function uploadToS3() { + console.log(`Uploading public/ to s3://${s3Bucket}/${s3Prefix}`) + console.time('upload to s3') + await syncCall('uploadDir', { + localDir: publicDir, + deleteRemoved: true, + s3Params: { + Bucket: s3Bucket, + Prefix: s3Prefix + } + }) + console.timeEnd('upload to s3') +} + +async function main() { + const emptyPrefix = await prefixIsEmpty(s3Prefix) + + // First build of a PR is slow because it can't reuse cache. + // But we can download from prod to warm cache up. + const cacheWarmPrefix = emptyPrefix ? 'dvc-org-prod' : s3Prefix + + await downloadFromS3(cacheWarmPrefix) + + try { + run('yarn build') + } catch (buildError) { + // Sometimes gatsby build fails because of bad cache. + // Clear it and try again. + + console.error('------------------------\n\n') + console.error(buildError) + console.error('\nAssuming bad cache and retrying:\n') + + await remove(cacheDir) + await remove(publicDir) + run('yarn build') + } + + await move(path.join(publicDir, '404.html'), path.join(rootDir, '404.html'), { + overwrite: true + }) + await uploadToS3() + await remove(publicDir) +} + +main().catch(e => { + console.error(e) + process.exit(1) +}) diff --git a/scripts/link-check-git-diff.sh b/scripts/link-check-git-diff.sh index 9b7b0f2dd0..1356a762ca 100755 --- a/scripts/link-check-git-diff.sh +++ b/scripts/link-check-git-diff.sh @@ -3,7 +3,7 @@ set -euo pipefail exclude="${CHECK_LINKS_EXCLUDE_LIST:-$(dirname $0)/exclude-links.txt}" differ="git diff $(git merge-base HEAD origin/master)" -changed="$($differ --name-only)" +changed="$differ --name-only :^redirects-list.json" [ -z "$changed" ] && exit 0 diff --git a/scripts/s3-utils.js b/scripts/s3-utils.js new file mode 100644 index 0000000000..a534cf32a5 --- /dev/null +++ b/scripts/s3-utils.js @@ -0,0 +1,32 @@ +'use strict' + +const { s3Prefix, s3Bucket } = require('../src/server/config') +const s3 = require('s3-client') + +const { + AWS_REGION, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + HEROKU_APP_NAME +} = process.env + +const s3Client = s3.createClient({ + maxAsyncS3: 50, + region: AWS_REGION, + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY +}) + +console.log({ + AWS_REGION, + HEROKU_APP_NAME, + s3Bucket, + s3Prefix, + hasCreds: Boolean(AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) +}) + +module.exports = { + s3Bucket, + s3Prefix, + s3Client +} diff --git a/src/consts.js b/src/consts.js index 7bc94f5d82..0cdc99dd0e 100644 --- a/src/consts.js +++ b/src/consts.js @@ -5,7 +5,6 @@ const HEADER = 'header' const WEBSITE_HOST = 'dvc.org' const FORUM_URL = `https://discuss.${WEBSITE_HOST}` -const BLOG_URL = `https://blog.${WEBSITE_HOST}` const PAGE_DOC = '/doc' @@ -24,7 +23,6 @@ const BLOG = { module.exports = { HEADER, FORUM_URL, - BLOG_URL, BLOG, PAGE_DOC } diff --git a/src/server/config.js b/src/server/config.js new file mode 100644 index 0000000000..c47f2e0627 --- /dev/null +++ b/src/server/config.js @@ -0,0 +1,11 @@ +'use strict' + +const { AWS_REGION, S3_BUCKET, HEROKU_APP_NAME } = process.env + +const s3Prefix = HEROKU_APP_NAME + ? `dvc-org-pulls/${HEROKU_APP_NAME}` + : 'dvc-org-prod' +const s3Bucket = S3_BUCKET +const s3Url = `http://${s3Bucket}.s3-website.${AWS_REGION}.amazonaws.com/${s3Prefix}` + +module.exports = { s3Prefix, s3Bucket, s3Url } diff --git a/src/server/index.js b/src/server/index.js new file mode 100644 index 0000000000..d72355a76b --- /dev/null +++ b/src/server/index.js @@ -0,0 +1,43 @@ +/* + * Production server. Proxies to S3 depending on HEROKU_APP_NAME (see + * scripts/deploy-with-s3.js) + * + * NOTE: This file doesn't go through babel or webpack. Make sure the syntax and + * sources this file requires are compatible with the current node version you + * are running. + * + * Required environment variables: + * + * - S3_BUCKET: name of the bucket + * - AWS_REGION: region of the bucket + * - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: IAM token to access bucket + * - HEROKU_APP_NAME: If this is a PR, an ID of the PR. Don't add this for + * production. + */ + +const express = require('express') +const compression = require('compression') +const { s3Url } = require('./config') +const { isProduction } = require('./utils') + +const port = process.env.PORT || 3000 +const app = express() + +const apiMiddleware = require('./middleware/api') +const redirectsMiddleware = require('./middleware/redirects') +const serveMiddleware = require('./middleware/serve') + +app.use(compression()) +app.use(redirectsMiddleware) +app.use('/api', apiMiddleware) +app.use(serveMiddleware) + +app.listen(port, () => { + console.log(`Listening on http://0.0.0.0:${port}/`) + + if (isProduction) { + console.log(`Proxying to ${s3Url}`) + } else { + console.log('Serving static files from local') + } +}) diff --git a/middleware/api/comments.js b/src/server/middleware/api/comments.js similarity index 76% rename from middleware/api/comments.js rename to src/server/middleware/api/comments.js index f177aff6d2..c2f9bc40d7 100644 --- a/middleware/api/comments.js +++ b/src/server/middleware/api/comments.js @@ -1,5 +1,3 @@ -/* eslint-env node */ - /* * This API endpoint is used by our blog to get comments count for the post, it * gets discuss.dvc.org topic URL as a param and returns comments count or @@ -10,20 +8,13 @@ */ const fetch = require('isomorphic-fetch') -const Cors = require('micro-cors') const NodeCache = require('node-cache') +const { isProduction } = require('../../utils') -const { BLOG_URL, FORUM_URL } = require('../../src/consts') +const { FORUM_URL } = require('../../../../src/consts') const cache = new NodeCache({ stdTTL: 900 }) -const dev = process.env.NODE_ENV === 'development' - -const cors = Cors({ - allowedMethods: ['GET', 'HEAD'], - origin: BLOG_URL -}) - const getCommentCount = async (req, res) => { const { query: { url } @@ -36,13 +27,13 @@ const getCommentCount = async (req, res) => { } if (cache.get(url) !== undefined) { - if (dev) console.log(`Using cache for ${url}`) + if (!isProduction) console.log(`Using cache for ${url}`) res.status(200).json({ count: cache.get(url) }) return } else { - if (dev) console.log(`Not using cache for ${url}`) + if (!isProduction) console.log(`Not using cache for ${url}`) } try { @@ -73,4 +64,4 @@ const getCommentCount = async (req, res) => { } } -module.exports = cors(getCommentCount) +module.exports = getCommentCount diff --git a/middleware/api/discourse.js b/src/server/middleware/api/discourse.js similarity index 79% rename from middleware/api/discourse.js rename to src/server/middleware/api/discourse.js index 46278dc14d..bbb5e773a7 100644 --- a/middleware/api/discourse.js +++ b/src/server/middleware/api/discourse.js @@ -1,23 +1,20 @@ -/* eslint-env node */ - const fetch = require('isomorphic-fetch') const NodeCache = require('node-cache') -const { FORUM_URL } = require('../../src/consts') +const { isProduction } = require('../../utils') +const { FORUM_URL } = require('../../../../src/consts') const cache = new NodeCache({ stdTTL: 900 }) -const dev = process.env.NODE_ENV === 'development' - module.exports = async (_, res) => { if (cache.get('topics')) { - if (dev) console.log('Using cache for "topics"') + if (!isProduction) console.log('Using cache for "topics"') res.status(200).json(cache.get('topics')) return } else { - if (dev) console.log('Not using cache for "topics"') + if (!isProduction) console.log('Not using cache for "topics"') } try { diff --git a/middleware/api/github.js b/src/server/middleware/api/github.js similarity index 92% rename from middleware/api/github.js rename to src/server/middleware/api/github.js index cc239a8ab6..7eeb94f328 100644 --- a/middleware/api/github.js +++ b/src/server/middleware/api/github.js @@ -1,18 +1,16 @@ -/* eslint-env node */ - const { graphql } = require('@octokit/graphql') const NodeCache = require('node-cache') -const cache = new NodeCache({ stdTTL: 900 }) +const { isProduction } = require('../../utils') -const dev = process.env.NODE_ENV === 'development' +const cache = new NodeCache({ stdTTL: 900 }) module.exports = async (_, res) => { if (!process.env.GITHUB_TOKEN) { res.status(200).json({ issues: [] }) } else { if (cache.get('issues')) { - if (dev) console.log('Using cache for "issues"') + if (!isProduction) console.log('Using cache for "issues"') res.status(200).json({ issues: cache.get('issues') }) } else { diff --git a/middleware/api/index.js b/src/server/middleware/api/index.js similarity index 92% rename from middleware/api/index.js rename to src/server/middleware/api/index.js index f96ffa01d5..76cc293bf1 100644 --- a/middleware/api/index.js +++ b/src/server/middleware/api/index.js @@ -1,5 +1,3 @@ -/* eslint-env node */ - const routes = require('express').Router() const comments = require('./comments') diff --git a/middleware/redirects/index.js b/src/server/middleware/redirects/index.js similarity index 54% rename from middleware/redirects/index.js rename to src/server/middleware/redirects/index.js index b607709e32..a2ef46c992 100644 --- a/middleware/redirects/index.js +++ b/src/server/middleware/redirects/index.js @@ -1,26 +1,30 @@ -/* eslint-env node */ - -const { getRedirect } = require('../../src/utils/redirects') +const { getRedirect } = require('../../../../src/utils/redirects') const { parse } = require('url') const { stringify } = require('querystring') +const { isProduction } = require('../../utils') -const dev = process.env.NODE_ENV !== 'production' +const OLD_BLOG_URL_REGEXP = /dataversioncontrol\.com/ module.exports = (req, res, next) => { const parsedUrl = parse(req.url, true) - const { pathname, query } = parsedUrl const host = req.headers.host + let pathname = parsedUrl.pathname + + // Remove invisible emoji from URL. It's was an old medium blog + if (OLD_BLOG_URL_REGEXP.test(host)) { + pathname = pathname.replace(/%EF%B8%8F/, '') + } const [code, location] = getRedirect(host, pathname, { req, - dev + dev: !isProduction }) if (location) { // HTTP redirects let redirectLocation = location - const queryStr = stringify(query) + const queryStr = stringify(parsedUrl.query) if (queryStr) { redirectLocation += '?' + queryStr } diff --git a/src/server/middleware/serve/index.js b/src/server/middleware/serve/index.js new file mode 100644 index 0000000000..fcd1679b87 --- /dev/null +++ b/src/server/middleware/serve/index.js @@ -0,0 +1,5 @@ +const { isProduction } = require('../../utils') +const localServeMiddleware = require('./local') +const s3ServeMiddleware = require('./s3') + +module.exports = isProduction ? s3ServeMiddleware : localServeMiddleware diff --git a/server.js b/src/server/middleware/serve/local.js similarity index 55% rename from server.js rename to src/server/middleware/serve/local.js index 341f0c1f69..111d903ee1 100644 --- a/server.js +++ b/src/server/middleware/serve/local.js @@ -1,21 +1,6 @@ -/* eslint-env node */ - -const express = require('express') -const compression = require('compression') const serveHandler = require('serve-handler') -const app = express() - -const apiMiddleware = require('./middleware/api') -const redirectsMiddleware = require('./middleware/redirects') - -const port = process.env.PORT || 3000 - -app.use(compression()) -app.use(redirectsMiddleware) -app.use('/api', apiMiddleware) - -app.use((req, res) => { +module.exports = (req, res) => { serveHandler(req, res, { public: 'public', cleanUrls: true, @@ -42,6 +27,4 @@ app.use((req, res) => { } ] }) -}) - -app.listen(port, () => console.log(`Ready on localhost:${port}!`)) +} diff --git a/src/server/middleware/serve/s3.js b/src/server/middleware/serve/s3.js new file mode 100644 index 0000000000..2c239a65c0 --- /dev/null +++ b/src/server/middleware/serve/s3.js @@ -0,0 +1,63 @@ +'use strict' + +/** + * Middleware to serve static files from S3 + */ + +const fs = require('fs') +const url = require('url') +const path = require('path') +const mime = require('mime-types') +const Wreck = require('@hapi/wreck') +const { s3Url } = require('../../config') + +const cacheControl = 'public, max-age=0, s-maxage=999999' +const htmlType = 'text/html; charset=utf-8' + +let notFoundPage +try { + // in production there's no public folder + notFoundPage = fs.readFileSync(path.join(__dirname, '../../../../404.html')) +} catch (e) { + notFoundPage = fs.readFileSync( + path.join(__dirname, '../../../../public/404.html') + ) +} + +async function serveFile(pathname, res) { + const target = s3Url + pathname + + const proxyRes = await Wreck.request('GET', target, { + redirects: 2, + timeout: 5000 + }) + + const { statusCode, headers: { etag } = {} } = proxyRes + + if (statusCode !== 200) { + throw new Error('Response not successful: ' + statusCode) + } + + res.writeHead(200, { + 'cache-control': cacheControl, + 'content-type': mime.lookup(pathname) || htmlType, + etag + }) + + proxyRes.pipe(res) +} + +module.exports = async (req, res) => { + const { pathname } = url.parse(req.url) + + try { + await serveFile(pathname, res) + } catch (e) { + res + .writeHead(404, { + 'cache-control': cacheControl, + 'content-type': htmlType + }) + .end(notFoundPage) + } +} diff --git a/src/server/utils.js b/src/server/utils.js new file mode 100644 index 0000000000..26b9c3f34e --- /dev/null +++ b/src/server/utils.js @@ -0,0 +1 @@ +module.exports.isProduction = process.env.NODE_ENV === 'production' diff --git a/src/utils/redirects.test.js b/src/utils/redirects.test.js index 072375cede..872e3f680f 100644 --- a/src/utils/redirects.test.js +++ b/src/utils/redirects.test.js @@ -60,7 +60,16 @@ describe('getRedirects', () => { // Detect redirect loops. const secondUrl = url.parse(addHost(rLocation)) const secondRedirect = getRedirect(secondUrl.hostname, secondUrl.pathname) - expect(secondRedirect).toEqual([]) + + // allow second redirect only if it removes trailing slash + if (secondRedirect.length) { + const thirdUrl = url.parse(addHost(secondRedirect[1])) + expect(secondUrl.host).toEqual(thirdUrl.host) + expect(secondUrl.pathname.replace(/\/$/, '')).toEqual(secondRedirect[1]) + + const thirdRedirect = getRedirect(thirdUrl.hostname, thirdUrl.pathname) + expect(thirdRedirect).toEqual([]) + } }) } @@ -91,6 +100,24 @@ describe('getRedirects', () => { 'https://dvc.org/doc/user-guide/troubleshooting#foo', 303 ) + + itRedirects( + 'https://www.dataversioncontrol.com/some-random', + 'https://dataversioncontrol.com/some-random', + 301 + ) + + itRedirects( + 'https://blog.dataversioncontrol.com/september-19-dvc-heartbeat-0123456789ab', + 'https://dvc.org/blog/september-19-dvc-heartbeat', + 301 + ) + + itRedirects( + 'https://blog.dvc.org/september-19-dvc-heartbeat', + 'https://dvc.org/blog/september-19-dvc-heartbeat', + 301 + ) }) describe('toS3', () => { diff --git a/tsconfig.json b/tsconfig.json index c68db1c3d7..d3c6e776be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "allowSyntheticDefaultImports": true, "allowJs": true }, - "include": ["./src/**/*", "./middleware/**/*", "./*.js"] + "include": ["./src/**/*", "./*.js"] } diff --git a/yarn.lock b/yarn.lock index f94c09999c..e1d492e8a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -934,16 +934,33 @@ resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== +"@hapi/boom@9.x.x": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.0.tgz#0d9517657a56ff1e0b42d0aca9da1b37706fec56" + integrity sha512-4nZmpp4tXbm162LaZT45P7F7sgiem8dwAh2vHWT6XX24dozNjGMg6BvKCRvtCUcmcXqeMIUqWN8Rc5X8yKuROQ== + dependencies: + "@hapi/hoek" "9.x.x" + "@hapi/bourne@1.x.x": version "1.3.2" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== +"@hapi/bourne@2.x.x": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" + integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== + "@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": version "8.5.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== +"@hapi/hoek@9.x.x": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010" + integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw== + "@hapi/joi@^15.1.1": version "15.1.1" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" @@ -961,6 +978,15 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hapi/wreck@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.0.0.tgz#8ab0ca286e937c3f7a82f67e4be4348c824b743c" + integrity sha512-d8lqCinbKyDByn7GzJDRDbitddhIEydNm44UcAMejfhEH3o4IYvKYq6K8cAqXbilXPuvZc0ErlUOg9SDdgRtMw== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/bourne" "2.x.x" + "@hapi/hoek" "9.x.x" + "@jest/console@^24.7.1", "@jest/console@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" @@ -2700,6 +2726,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -2730,6 +2761,21 @@ autoprefixer@^9.7.4: postcss "^7.0.27" postcss-value-parser "^4.0.3" +aws-sdk@^2.328.0: + version "2.650.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.650.0.tgz#edf995cf2805c918d7470a652f1316ae902c5aa4" + integrity sha512-MlTKXeRSe4IXXqnulAiXZccpTgDafs3ofYIQv/7ApR+oQUFsq96RHwe8MdW9N1cXn7fz302jLXUAykj4boR3DA== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -3373,6 +3419,15 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^4.3.0: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -5809,7 +5864,7 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== -events@^1.1.0: +events@1.1.1, events@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= @@ -6197,7 +6252,7 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fd-slicer@~1.1.0: +fd-slicer@^1.1.0, fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= @@ -6407,6 +6462,11 @@ find-versions@^3.0.0, find-versions@^3.2.0: dependencies: semver-regex "^2.0.0" +findit2@~2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/findit2/-/findit2-2.2.3.tgz#58a466697df8a6205cdfdbf395536b8bd777a5f6" + integrity sha1-WKRmaX34piBc39vzlVNri9d3pfY= + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -6544,6 +6604,16 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" + integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -7544,6 +7614,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@~4.1.11: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -8225,7 +8300,7 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -ieee754@^1.1.4: +ieee754@1.1.13, ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== @@ -9601,6 +9676,11 @@ jimp@^0.6.4: core-js "^2.5.7" regenerator-runtime "^0.13.3" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + jpeg-js@^0.3.4: version "0.3.7" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.7.tgz#471a89d06011640592d314158608690172b1028d" @@ -9749,6 +9829,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -10679,11 +10768,6 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -micro-cors@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/micro-cors/-/micro-cors-0.1.1.tgz#af7a480182c114ffd1ada84ad9dffc52bb4f4054" - integrity sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw== - microevent.ts@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" @@ -10765,7 +10849,7 @@ mime@1.6.0, mime@^1.3.4: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3, mime@^2.4.4: +mime@^2.0.3, mime@^2.3.1, mime@^2.4.4: version "2.4.4" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== @@ -14048,7 +14132,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2.6.3: +rimraf@2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -14143,6 +14227,21 @@ rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3: dependencies: tslib "^1.9.0" +s3-client@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/s3-client/-/s3-client-4.4.2.tgz#bb41f25abe1342a4f61189063f473e407208da47" + integrity sha512-XVL6vR9W6Rg3/pyWxKt2Gt+EiLEceKUFvjv69GmED6K8zta0/+kCYIpWtgOXtfYGHzZ8Cr55D2SLAEYHay3kTw== + dependencies: + aws-sdk "^2.328.0" + fd-slicer "^1.1.0" + findit2 "~2.2.3" + graceful-fs "~4.1.11" + mime "^2.3.1" + mkdirp "~0.5.1" + pend "~1.2.0" + rimraf "~2.6.2" + streamsink "~1.2.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -14196,6 +14295,11 @@ sanitize-html@^1.22.1: srcset "^2.0.1" xtend "^4.0.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -15029,6 +15133,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamsink@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/streamsink/-/streamsink-1.2.0.tgz#efafee9f1e22d3591ed7de3dcaa95c3f5e79f73c" + integrity sha1-76/unx4i01ke1949yqlcP1559zw= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -16241,6 +16350,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -16343,6 +16457,14 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -16402,6 +16524,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@3.4.0, uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -17000,6 +17127,14 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml2js@^0.4.5: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -17018,6 +17153,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"