From 23586633c2c463e2e0328da9465e632421607e2d Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Mon, 3 Oct 2022 09:01:17 -0700 Subject: [PATCH 001/115] upgrade superstatic (#4935) * upgrade superstatic to use GH branch * package-lock update * superstatic 9 --- npm-shrinkwrap.json | 1156 ++++++++++++++------------------- package.json | 2 +- src/hosting/initMiddleware.ts | 19 +- src/serve/hosting.ts | 19 +- 4 files changed, 514 insertions(+), 682 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a33b403c1b5c..45841fad3e88 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -57,7 +57,7 @@ "stream-chain": "^2.2.4", "stream-json": "^1.7.3", "strip-ansi": "^6.0.1", - "superstatic": "^8.0.0", + "superstatic": "^9.0.0", "tar": "^6.1.11", "tcp-port-used": "^1.0.2", "tmp": "^0.2.1", @@ -1423,9 +1423,9 @@ "dev": true }, "node_modules/@gar/promisify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", - "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, "node_modules/@google-cloud/common": { @@ -2187,22 +2187,22 @@ } }, "node_modules/@npmcli/fs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz", - "integrity": "sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "optional": true, "dependencies": { - "@gar/promisify": "^1.0.1", + "@gar/promisify": "^1.1.3", "semver": "^7.3.5" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "optional": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2215,16 +2215,16 @@ } }, "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "optional": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" }, "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { @@ -2531,7 +2531,8 @@ "node_modules/@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true }, "node_modules/@types/configstore": { "version": "4.0.0", @@ -3469,9 +3470,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/agentkeepalive": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.0.tgz", - "integrity": "sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", "optional": true, "dependencies": { "debug": "^4.1.0", @@ -3483,9 +3484,9 @@ } }, "node_modules/agentkeepalive/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "optional": true, "dependencies": { "ms": "2.1.2" @@ -3611,14 +3612,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -3729,16 +3722,16 @@ "dev": true }, "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" }, "engines": { - "node": ">=10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/arg": { @@ -4059,6 +4052,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", @@ -4080,6 +4074,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, "dependencies": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -4095,6 +4090,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4107,6 +4103,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4117,12 +4114,14 @@ "node_modules/boxen/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/boxen/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -4131,6 +4130,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4259,32 +4259,41 @@ } }, "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", "optional": true, "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" }, "engines": { - "node": ">= 10" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/cacache/node_modules/chownr": { @@ -4296,6 +4305,46 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4405,6 +4454,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "engines": { "node": ">=6" } @@ -4796,14 +4846,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "node_modules/compare-semver": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", - "integrity": "sha1-fAp5onu4C2xplERfgpWCWdPQIVM=", - "dependencies": { - "semver": "^5.0.1" - } - }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -4928,7 +4970,7 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "optional": true }, "node_modules/content-disposition": { @@ -5287,7 +5329,7 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, "node_modules/depd": { @@ -7041,32 +7083,22 @@ "dev": true }, "node_modules/gauge": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz", - "integrity": "sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "optional": true, "dependencies": { - "ansi-regex": "^5.0.1", "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", - "signal-exit": "^3.0.0", + "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" + "wide-align": "^1.1.5" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/gaxios": { @@ -7400,17 +7432,6 @@ "toxic": "^1.0.0" } }, - "node_modules/global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "dependencies": { - "ini": "^1.3.5" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7961,17 +7982,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7996,7 +8006,7 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "optional": true }, "node_modules/has-yarn": { @@ -8180,7 +8190,7 @@ "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "optional": true, "dependencies": { "ms": "^2.0.0" @@ -8411,9 +8421,9 @@ } }, "node_modules/install-artifact-from-github": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz", - "integrity": "sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", "optional": true, "bin": { "install-from-cache": "bin/install-from-cache.js", @@ -8508,21 +8518,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -8534,17 +8529,9 @@ "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "optional": true }, - "node_modules/is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9583,36 +9570,45 @@ "dev": true }, "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "optional": true, "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", + "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", + "negotiator": "^0.6.3", "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, "engines": { "node": ">= 10" } }, "node_modules/make-fetch-happen/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "optional": true, "dependencies": { "ms": "2.1.2" @@ -9626,21 +9622,53 @@ } } }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/make-fetch-happen/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", - "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "optional": true, "dependencies": { "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" + "debug": "^4.3.3", + "socks": "^2.6.2" }, "engines": { "node": ">= 10" @@ -9913,20 +9941,20 @@ } }, "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "optional": true, "dependencies": { - "minipass": "^3.1.0", + "minipass": "^3.1.6", "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" + "minizlib": "^2.1.2" }, "engines": { - "node": ">=8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" }, "optionalDependencies": { - "encoding": "^0.1.12" + "encoding": "^0.1.13" } }, "node_modules/minipass-flush": { @@ -10190,9 +10218,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "node_modules/nanoid": { @@ -10402,15 +10430,15 @@ } }, "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", "optional": true, "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", + "make-fetch-happen": "^10.0.3", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", @@ -10422,13 +10450,13 @@ "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": ">= 10.12.0" + "node": "^12.22 || ^14.13 || >=16" } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "optional": true, "dependencies": { "lru-cache": "^6.0.0" @@ -10559,18 +10587,18 @@ } }, "node_modules/npmlog": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.0.tgz", - "integrity": "sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "optional": true, "dependencies": { - "are-we-there-yet": "^2.0.0", + "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", - "gauge": "^4.0.0", + "gauge": "^4.0.3", "set-blocking": "^2.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/nyc": { @@ -11551,7 +11579,7 @@ "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "optional": true }, "node_modules/promise-polyfill": { @@ -11576,7 +11604,7 @@ "node_modules/promise-retry/node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "optional": true, "engines": { "node": ">= 4" @@ -11918,15 +11946,15 @@ } }, "node_modules/re2": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.3.tgz", - "integrity": "sha512-Dp5iWVR8W3C7Nm9DziMY4BleMPRb/pe6kvfbzLv80dVYaXRc9jRnwwNqU0oE/taRm0qYR1+Qrtzk9rPjS9ecaQ==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", "hasInstallScript": true, "optional": true, "dependencies": { - "install-artifact-from-github": "^1.3.0", - "nan": "^2.15.0", - "node-gyp": "^8.4.1" + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" } }, "node_modules/react": { @@ -12747,12 +12775,12 @@ "optional": true }, "node_modules/socks": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", - "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", "dependencies": { - "ip": "^1.1.5", - "smart-buffer": "^4.1.0" + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" }, "engines": { "node": ">= 10.13.0", @@ -12793,6 +12821,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12915,15 +12948,15 @@ } }, "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "optional": true, "dependencies": { "minipass": "^3.1.1" }, "engines": { - "node": ">= 8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/stack-trace": { @@ -12996,28 +13029,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "dependencies": { - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13182,190 +13193,77 @@ } }, "node_modules/superstatic": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-8.0.0.tgz", - "integrity": "sha512-PqlA2xuEwOlRZsknl58A/rZEmgCUcfWIFec0bn10wYE5/tbMhEbMXGHCYDppiXLXcuhGHyOp1IimM2hLqkLLuw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.0.tgz", + "integrity": "sha512-4rvzTZdqBPtCjeo/V4YkbBeDnHxI2+3jP1FHGzvTeDswq+HQFB7l3JTjq31BfyJFTogn8JmbDW9sKOeBUGDAhg==", "dependencies": { "basic-auth-connect": "^1.0.0", - "chalk": "^1.1.3", - "commander": "^9.2.0", - "compare-semver": "^1.0.0", + "commander": "^9.4.0", "compression": "^1.7.0", - "connect": "^3.6.2", + "connect": "^3.7.0", "destroy": "^1.0.4", "fast-url-parser": "^1.1.3", "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "lodash": "^4.17.19", - "mime-types": "^2.1.16", - "minimatch": "^3.0.4", + "mime-types": "^2.1.35", + "minimatch": "^5.1.0", "morgan": "^1.8.2", "on-finished": "^2.2.0", "on-headers": "^1.0.0", "path-to-regexp": "^1.8.0", "router": "^1.3.1", - "string-length": "^1.0.0", - "update-notifier": "^4.1.1" + "update-notifier": "^5.1.0" }, "bin": { - "superstatic": "bin/server" + "superstatic": "lib/bin/server.js" }, "engines": { - "node": ">= 12.20" + "node": "^14.18.0 || >=16.4.0" }, "optionalDependencies": { - "re2": "^1.15.8" + "re2": "^1.17.7" } }, - "node_modules/superstatic/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/superstatic/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/superstatic/node_modules/color-convert": { + "node_modules/superstatic/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "balanced-match": "^1.0.0" } }, - "node_modules/superstatic/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/superstatic/node_modules/commander": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", - "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", + "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==", "engines": { "node": "^12.20.0 || >=14" } }, - "node_modules/superstatic/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/superstatic/node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "node_modules/superstatic/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/superstatic/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/superstatic/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/superstatic/node_modules/update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "dependencies": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/superstatic/node_modules/update-notifier/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/superstatic/node_modules/update-notifier/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/superstatic/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/superstatic/node_modules/update-notifier/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "isarray": "0.0.1" } }, "node_modules/supertest": { @@ -13707,6 +13605,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "dev": true, "engines": { "node": ">=8" }, @@ -13983,6 +13882,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, "engines": { "node": ">=8" } @@ -14097,21 +13997,27 @@ } }, "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", "optional": true, "dependencies": { - "unique-slug": "^2.0.0" + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", "optional": true, "dependencies": { "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/universal-analytics": { @@ -16074,9 +15980,9 @@ "dev": true }, "@gar/promisify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", - "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, "@google-cloud/common": { @@ -16670,19 +16576,19 @@ } }, "@npmcli/fs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz", - "integrity": "sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "optional": true, "requires": { - "@gar/promisify": "^1.0.1", + "@gar/promisify": "^1.1.3", "semver": "^7.3.5" }, "dependencies": { "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "optional": true, "requires": { "lru-cache": "^6.0.0" @@ -16691,9 +16597,9 @@ } }, "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "optional": true, "requires": { "mkdirp": "^1.0.4", @@ -16964,7 +16870,8 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true }, "@types/configstore": { "version": "4.0.0", @@ -17751,9 +17658,9 @@ } }, "agentkeepalive": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.0.tgz", - "integrity": "sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", "optional": true, "requires": { "debug": "^4.1.0", @@ -17762,9 +17669,9 @@ }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "optional": true, "requires": { "ms": "2.1.2" @@ -17854,11 +17761,6 @@ } } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -17958,9 +17860,9 @@ "dev": true }, "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -18226,6 +18128,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, "requires": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", @@ -18241,6 +18144,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -18250,6 +18154,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -18259,6 +18164,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -18266,17 +18172,20 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -18359,37 +18268,74 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", "optional": true, "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" }, "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "optional": true }, + "glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -18472,7 +18418,8 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, "camelcase-keys": { "version": "6.2.2", @@ -18759,14 +18706,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "compare-semver": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", - "integrity": "sha1-fAp5onu4C2xplERfgpWCWdPQIVM=", - "requires": { - "semver": "^5.0.1" - } - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -18868,7 +18807,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "optional": true }, "content-disposition": { @@ -19146,7 +19085,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, "depd": { @@ -20498,28 +20437,19 @@ "dev": true }, "gauge": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz", - "integrity": "sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "optional": true, "requires": { - "ansi-regex": "^5.0.1", "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", - "signal-exit": "^3.0.0", + "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true - } + "wide-align": "^1.1.5" } }, "gaxios": { @@ -20789,14 +20719,6 @@ "toxic": "^1.0.0" } }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "requires": { - "ini": "^1.3.5" - } - }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -21243,14 +21165,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -21266,7 +21180,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "optional": true }, "has-yarn": { @@ -21421,7 +21335,7 @@ "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "optional": true, "requires": { "ms": "^2.0.0" @@ -21583,9 +21497,9 @@ } }, "install-artifact-from-github": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz", - "integrity": "sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", "optional": true }, "ip": { @@ -21652,15 +21566,6 @@ "is-extglob": "^2.1.1" } }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -21669,14 +21574,9 @@ "is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "optional": true }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -22544,53 +22444,82 @@ "dev": true }, "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "optional": true, "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", + "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", + "negotiator": "^0.6.3", "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" }, "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "optional": true, "requires": { "ms": "2.1.2" } }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true + }, "socks-proxy-agent": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", - "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "optional": true, "requires": { "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" + "debug": "^4.3.3", + "socks": "^2.6.2" } } } @@ -22784,15 +22713,15 @@ } }, "minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "optional": true, "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", + "encoding": "^0.1.13", + "minipass": "^3.1.6", "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" + "minizlib": "^2.1.2" } }, "minipass-flush": { @@ -22989,9 +22918,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "nanoid": { @@ -23139,15 +23068,15 @@ "dev": true }, "node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", "optional": true, "requires": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", + "make-fetch-happen": "^10.0.3", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", @@ -23157,9 +23086,9 @@ }, "dependencies": { "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "optional": true, "requires": { "lru-cache": "^6.0.0" @@ -23258,14 +23187,14 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" }, "npmlog": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.0.tgz", - "integrity": "sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "optional": true, "requires": { - "are-we-there-yet": "^2.0.0", + "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", - "gauge": "^4.0.0", + "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, @@ -24015,7 +23944,7 @@ "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "optional": true }, "promise-polyfill": { @@ -24037,7 +23966,7 @@ "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "optional": true } } @@ -24306,14 +24235,14 @@ } }, "re2": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.3.tgz", - "integrity": "sha512-Dp5iWVR8W3C7Nm9DziMY4BleMPRb/pe6kvfbzLv80dVYaXRc9jRnwwNqU0oE/taRm0qYR1+Qrtzk9rPjS9ecaQ==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", "optional": true, "requires": { - "install-artifact-from-github": "^1.3.0", - "nan": "^2.15.0", - "node-gyp": "^8.4.1" + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" } }, "react": { @@ -24965,12 +24894,19 @@ "optional": true }, "socks": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", - "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", "requires": { - "ip": "^1.1.5", - "smart-buffer": "^4.1.0" + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "dependencies": { + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + } } }, "socks-proxy-agent": { @@ -25099,9 +25035,9 @@ } }, "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "optional": true, "requires": { "minipass": "^3.1.1" @@ -25168,24 +25104,6 @@ "safe-buffer": "~5.1.0" } }, - "string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "requires": { - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -25300,79 +25218,57 @@ } }, "superstatic": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-8.0.0.tgz", - "integrity": "sha512-PqlA2xuEwOlRZsknl58A/rZEmgCUcfWIFec0bn10wYE5/tbMhEbMXGHCYDppiXLXcuhGHyOp1IimM2hLqkLLuw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.0.tgz", + "integrity": "sha512-4rvzTZdqBPtCjeo/V4YkbBeDnHxI2+3jP1FHGzvTeDswq+HQFB7l3JTjq31BfyJFTogn8JmbDW9sKOeBUGDAhg==", "requires": { "basic-auth-connect": "^1.0.0", - "chalk": "^1.1.3", - "commander": "^9.2.0", - "compare-semver": "^1.0.0", + "commander": "^9.4.0", "compression": "^1.7.0", - "connect": "^3.6.2", + "connect": "^3.7.0", "destroy": "^1.0.4", "fast-url-parser": "^1.1.3", "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "lodash": "^4.17.19", - "mime-types": "^2.1.16", - "minimatch": "^3.0.4", + "mime-types": "^2.1.35", + "minimatch": "^5.1.0", "morgan": "^1.8.2", "on-finished": "^2.2.0", "on-headers": "^1.0.0", "path-to-regexp": "^1.8.0", - "re2": "^1.15.8", + "re2": "^1.17.7", "router": "^1.3.1", - "string-length": "^1.0.0", - "update-notifier": "^4.1.1" + "update-notifier": "^5.1.0" }, "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "color-convert": { + "brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "requires": { - "color-name": "~1.1.4" + "balanced-match": "^1.0.0" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "commander": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", - "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", + "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==" }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -25380,66 +25276,6 @@ "requires": { "isarray": "0.0.1" } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } } } }, @@ -25707,7 +25543,8 @@ "term-size": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "dev": true }, "terser": { "version": "5.15.0", @@ -25909,7 +25746,8 @@ "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true }, "type-is": { "version": "1.6.18", @@ -25988,18 +25826,18 @@ } }, "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", "optional": true, "requires": { - "unique-slug": "^2.0.0" + "unique-slug": "^3.0.0" } }, "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", "optional": true, "requires": { "imurmurhash": "^0.1.4" diff --git a/package.json b/package.json index 0e38df9e97b2..8ca79c6f01f4 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "stream-chain": "^2.2.4", "stream-json": "^1.7.3", "strip-ansi": "^6.0.1", - "superstatic": "^8.0.0", + "superstatic": "^9.0.0", "tar": "^6.1.11", "tcp-port-used": "^1.0.2", "tmp": "^0.2.1", diff --git a/src/hosting/initMiddleware.ts b/src/hosting/initMiddleware.ts index b70a3cf1bec9..6a555838d644 100644 --- a/src/hosting/initMiddleware.ts +++ b/src/hosting/initMiddleware.ts @@ -1,6 +1,4 @@ -import * as url from "url"; -import * as qs from "querystring"; -import { RequestHandler } from "express"; +import { IncomingMessage, ServerResponse } from "http"; import { Client } from "../apiv2"; import { TemplateServerResponse } from "./implicitInit"; @@ -15,14 +13,16 @@ const SDK_PATH_REGEXP = /^\/__\/firebase\/([^/]+)\/([^/]+)$/; * @param init template server response. * @return the middleware function. */ -export function initMiddleware(init: TemplateServerResponse): RequestHandler { +export function initMiddleware( + init: TemplateServerResponse +): (req: IncomingMessage, res: ServerResponse, next: () => void) => void { return (req, res, next) => { - const parsedUrl = url.parse(req.url); - const match = RegExp(SDK_PATH_REGEXP).exec(req.url); + const parsedUrl = new URL(req.url || "", `http://${req.headers.host}`); + const match = RegExp(SDK_PATH_REGEXP).exec(parsedUrl.pathname); if (match) { const version = match[1]; const sdkName = match[2]; - const u = new url.URL(`https://www.gstatic.com/firebasejs/${version}/${sdkName}`); + const u = new URL(`https://www.gstatic.com/firebasejs/${version}/${sdkName}`); const c = new Client({ urlPrefix: u.origin, auth: false }); const headers: { [key: string]: string } = {}; const acceptEncoding = req.headers["accept-encoding"]; @@ -57,10 +57,9 @@ export function initMiddleware(init: TemplateServerResponse): RequestHandler { // In theory we should be able to get this from req.query but for some // when testing this functionality, req.query and req.params were always // empty or undefined. - const query = qs.parse(parsedUrl.query || ""); - + const query = parsedUrl.searchParams; res.setHeader("Content-Type", "application/javascript"); - if (query["useEmulator"] === "true") { + if (query.get("useEmulator") === "true") { res.end(init.emulatorsJs); } else { res.end(init.js); diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index 4175e4ca238d..abddfe3c177f 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -1,7 +1,7 @@ -import * as clc from "colorette"; - -const superstatic = require("superstatic").server; // Superstatic has no types, requires odd importing. const morgan = require("morgan"); +import { IncomingMessage, ServerResponse } from "http"; +import { server as superstatic } from "superstatic"; +import * as clc from "colorette"; import { detectProjectRoot } from "../detectProjectRoot"; import { FirebaseError } from "../error"; @@ -10,7 +10,6 @@ import { initMiddleware } from "../hosting/initMiddleware"; import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; import cloudRunProxy from "../hosting/cloudRunProxy"; import { functionsProxy } from "../hosting/functionsProxy"; -import { NextFunction, Request, Response } from "express"; import { Writable } from "stream"; import { EmulatorLogger } from "../emulator/emulatorLogger"; import { Emulators } from "../emulator/types"; @@ -80,19 +79,15 @@ function startServer(options: any, config: any, port: number, init: TemplateServ const server = superstatic({ debug: false, port: port, - host: options.host, + hostname: options.host, config: config, compression: true, - cwd: detectProjectRoot(options), + cwd: detectProjectRoot(options) || undefined, stack: "strict", before: { - files: (req: Request, res: Response, next: NextFunction) => { + files: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { // We do these in a single method to ensure order of operations - morganMiddleware(req, res, () => { - /* - NoOp next function - */ - }); + morganMiddleware(req, res, () => null); firebaseMiddleware(req, res, next); }, }, From 6e3eff7068a68ed0dc8b403cece845ea5d31dd30 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 3 Oct 2022 22:27:09 -0700 Subject: [PATCH 002/115] Consolidate config utilities and add better typing (#5017) Consolidate and refactor utilities for getting, normalizing, and materializing hosting configuration as described in firebase.json. --- schema/firebase-config.json | 12 + scripts/npm-link.sh | 2 + src/commands/hosting-channel-deploy.ts | 26 +- src/deploy/hosting/context.ts | 17 + src/deploy/hosting/deploy.ts | 21 +- src/deploy/hosting/hostingDeploy.ts | 21 -- src/deploy/hosting/prepare.ts | 11 +- src/deploy/hosting/release.ts | 13 +- src/deploy/hosting/validate.ts | 53 --- src/firebaseConfig.ts | 49 ++- src/frameworks/index.ts | 24 +- src/hosting/config.ts | 245 +++++++++++++ src/hosting/expireUtils.ts | 3 +- src/hosting/normalizedHostingConfigs.ts | 152 -------- src/hosting/options.ts | 30 ++ src/metaprogramming.ts | 20 ++ src/serve/hosting.ts | 10 +- src/test/deploy/hosting/convertConfig.spec.ts | 4 +- src/test/deploy/hosting/validate.spec.ts | 74 ---- src/test/hosting/config.spec.ts | 338 ++++++++++++++++++ src/test/hosting/expireUtils.spec.ts | 4 +- .../hosting/normalizedHostingConfigs.spec.ts | 287 --------------- 22 files changed, 765 insertions(+), 651 deletions(-) create mode 100644 src/deploy/hosting/context.ts delete mode 100644 src/deploy/hosting/hostingDeploy.ts delete mode 100644 src/deploy/hosting/validate.ts create mode 100644 src/hosting/config.ts delete mode 100644 src/hosting/normalizedHostingConfigs.ts create mode 100644 src/hosting/options.ts delete mode 100644 src/test/deploy/hosting/validate.spec.ts create mode 100644 src/test/hosting/config.spec.ts delete mode 100644 src/test/hosting/normalizedHostingConfigs.spec.ts diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 2c0c9ef6261a..96d65e516cbf 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -459,6 +459,10 @@ "additionalProperties": false, "properties": { "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], "type": "string" }, "cleanUrls": { @@ -944,6 +948,10 @@ "additionalProperties": false, "properties": { "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], "type": "string" }, "cleanUrls": { @@ -1429,6 +1437,10 @@ "additionalProperties": false, "properties": { "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], "type": "string" }, "cleanUrls": { diff --git a/scripts/npm-link.sh b/scripts/npm-link.sh index 3a565581766f..dc3674b34947 100755 --- a/scripts/npm-link.sh +++ b/scripts/npm-link.sh @@ -3,3 +3,5 @@ set -e echo "Running npm link..." npm link + +chmod u+rx ./lib/bin/firebase.js diff --git a/src/commands/hosting-channel-deploy.ts b/src/commands/hosting-channel-deploy.ts index de7abce37bf5..78471c55c764 100644 --- a/src/commands/hosting-channel-deploy.ts +++ b/src/commands/hosting-channel-deploy.ts @@ -11,7 +11,6 @@ import { cleanAuthState, normalizeName, } from "../hosting/api"; -import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; import { requirePermissions } from "../requirePermissions"; import { deploy } from "../deploy"; import { needProjectId } from "../projectUtils"; @@ -19,14 +18,17 @@ import { logger } from "../logger"; import { requireConfig } from "../requireConfig"; import { DEFAULT_DURATION, calculateChannelExpireTTL } from "../hosting/expireUtils"; import { logLabeledSuccess, datetimeString, logLabeledWarning, consoleUrl } from "../utils"; +import { hostingConfig } from "../hosting/config"; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const { marked } = require("marked"); import { requireHostingSite } from "../requireHostingSite"; +import { HostingOptions } from "../hosting/options"; +import { Options } from "../options"; const LOG_TAG = "hosting:channel"; interface ChannelInfo { - target: string | null; + target?: string; site: string; url: string; version: string; @@ -48,7 +50,7 @@ export const command = new Command("hosting:channel:deploy [channelId]") .action( async ( channelId: string, - options: any // eslint-disable-line @typescript-eslint/no-explicit-any + options: Options & HostingOptions ): Promise<{ [targetOrSite: string]: ChannelInfo }> => { const projectId = needProjectId(options); @@ -87,15 +89,15 @@ export const command = new Command("hosting:channel:deploy [channelId]") .join(","); } - const sites: ChannelInfo[] = normalizedHostingConfigs(options, { - resolveTargets: true, - }).map((cfg) => ({ - site: cfg.site, - target: cfg.target, - url: "", - version: "", - expireTime: "", - })); + const sites: ChannelInfo[] = hostingConfig(options).map((config) => { + return { + target: config.target, + site: config.site, + url: "", + version: "", + expireTime: "", + }; + }); await Promise.all( sites.map(async (siteInfo) => { diff --git a/src/deploy/hosting/context.ts b/src/deploy/hosting/context.ts new file mode 100644 index 000000000000..05ee0769988f --- /dev/null +++ b/src/deploy/hosting/context.ts @@ -0,0 +1,17 @@ +import { HostingResolved } from "../../firebaseConfig"; +import { Context as FunctionsContext } from "../functions/args"; + +export interface HostingDeploy { + config: HostingResolved; + site: string; + version?: string; +} + +export interface Context extends FunctionsContext { + hosting?: { + deploys: HostingDeploy[]; + }; + + // Set as a global in hosting-channel-deploy.ts + hostingChannel?: string; +} diff --git a/src/deploy/hosting/deploy.ts b/src/deploy/hosting/deploy.ts index 740630067791..02fe79c5de00 100644 --- a/src/deploy/hosting/deploy.ts +++ b/src/deploy/hosting/deploy.ts @@ -4,20 +4,15 @@ import { listFiles } from "../../listFiles"; import { logger } from "../../logger"; import { track } from "../../track"; import { envOverride, logLabeledBullet, logLabeledSuccess } from "../../utils"; -import { HostingDeploy } from "./hostingDeploy"; import { bold, cyan } from "colorette"; import * as ora from "ora"; +import { Context, HostingDeploy } from "./context"; +import { Options } from "../../options"; -export async function deploy( - context: { hosting?: { deploys?: HostingDeploy[] } }, - options: { - cwd?: string; - configPath?: string; - debug?: boolean; - nonInteractive?: boolean; - config: { path: (path: string) => string }; - } -): Promise { +/** + * Uploads static assets to the upcoming Hosting versions. + */ +export async function deploy(context: Context, options: Options): Promise { if (!context.hosting?.deploys) { return; } @@ -100,9 +95,9 @@ export async function deploy( spinner.stop(); } - logLabeledSuccess("hosting[" + deploy.site + "]", "file upload complete"); + logLabeledSuccess(`hosting[${deploy.site}]`, "file upload complete"); const dt = Date.now() - t0; - logger.debug("[hosting] deploy completed after " + dt + "ms"); + logger.debug(`[hosting] deploy completed after ${dt}ms`); void track("Hosting Deploy", "success", dt); return runDeploys(deploys, debugging); diff --git a/src/deploy/hosting/hostingDeploy.ts b/src/deploy/hosting/hostingDeploy.ts deleted file mode 100644 index 21aefb41017e..000000000000 --- a/src/deploy/hosting/hostingDeploy.ts +++ /dev/null @@ -1,21 +0,0 @@ -// NOTE: This type is incomplete and contains only config necessary for validation. -export interface HostingConfig { - ignore?: string[]; - public?: string; - rewrites?: { - source: string; - destination?: string; - function?: string; - run?: { serviceId: string; location?: string }; - }[]; - redirects?: { - source: string; - }[]; - i18n?: { root: string }; -} - -export interface HostingDeploy { - site: string; - version?: string; - config: HostingConfig; -} diff --git a/src/deploy/hosting/prepare.ts b/src/deploy/hosting/prepare.ts index b502635d86f5..c628798cd4dc 100644 --- a/src/deploy/hosting/prepare.ts +++ b/src/deploy/hosting/prepare.ts @@ -1,16 +1,17 @@ import { FirebaseError } from "../../error"; import { client } from "./client"; import { needProjectNumber } from "../../projectUtils"; -import { normalizedHostingConfigs } from "../../hosting/normalizedHostingConfigs"; -import { validateDeploy } from "./validate"; +import * as config from "../../hosting/config"; import { convertConfig } from "./convertConfig"; import * as deploymentTool from "../../deploymentTool"; import { Payload } from "./args"; +import { Context } from "./context"; +import { Options } from "../../options"; /** * Prepare creates versions for each Hosting site to be deployed. */ -export async function prepare(context: any, options: any, payload: Payload): Promise { +export async function prepare(context: Context, options: Options, payload: Payload): Promise { // Allow the public directory to be overridden by the --public flag if (options.public) { if (Array.isArray(options.config.get("hosting"))) { @@ -22,7 +23,7 @@ export async function prepare(context: any, options: any, payload: Payload): Pro const projectNumber = await needProjectNumber(options); - const configs = normalizedHostingConfigs(options, { resolveTargets: true }); + const configs = config.hostingConfig(options); if (configs.length === 0) { return Promise.resolve(); } @@ -38,8 +39,6 @@ export async function prepare(context: any, options: any, payload: Payload): Pro for (const deploy of context.hosting.deploys) { const cfg = deploy.config; - validateDeploy(deploy, options); - const data = { config: await convertConfig(context, payload, cfg, false), labels: deploymentTool.labels(), diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts index 5bc7c27b862c..83c555da550c 100644 --- a/src/deploy/hosting/release.ts +++ b/src/deploy/hosting/release.ts @@ -4,11 +4,14 @@ import { needProjectNumber } from "../../projectUtils"; import * as utils from "../../utils"; import { convertConfig } from "./convertConfig"; import { Payload } from "./args"; +import { Context } from "./context"; +import { Options } from "../../options"; +import { FirebaseError } from "../../error"; /** * Release finalized a Hosting release. */ -export async function release(context: any, options: any, payload: Payload): Promise { +export async function release(context: Context, options: Options, payload: Payload): Promise { if (!context.hosting || !context.hosting.deploys) { return; } @@ -17,7 +20,13 @@ export async function release(context: any, options: any, payload: Payload): Pro logger.debug(JSON.stringify(context.hosting.deploys, null, 2)); await Promise.all( - context.hosting.deploys.map(async (deploy: any) => { + context.hosting.deploys.map(async (deploy) => { + if (!deploy.version) { + throw new FirebaseError( + "Assertion failed: Hosting version should have been set in the prepare phase", + { exit: 2 } + ); + } utils.logLabeledBullet(`hosting[${deploy.site}]`, "finalizing version..."); const config = await convertConfig(context, payload, deploy.config, true); diff --git a/src/deploy/hosting/validate.ts b/src/deploy/hosting/validate.ts deleted file mode 100644 index 5a2c60a99823..000000000000 --- a/src/deploy/hosting/validate.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as path from "path"; -import * as clc from "colorette"; - -import { FirebaseError } from "../../error"; -import { resolveProjectPath } from "../../projectPath"; -import { dirExistsSync } from "../../fsutils"; -import { logLabeledWarning } from "../../utils"; -import { HostingDeploy } from "./hostingDeploy"; - -export function validateDeploy(deploy: HostingDeploy, options: any) { - const cfg = deploy.config; - - const hasPublicDir = !!cfg.public; - const hasAnyStaticRewrites = !!(cfg.rewrites || []).filter((rw) => rw.destination)?.length; - const hasAnyDynamicRewrites = !!(cfg.rewrites || []).filter((rw) => !rw.destination)?.length; - const hasAnyRedirects = !!cfg.redirects?.length; - - if (!hasPublicDir && hasAnyStaticRewrites) { - throw new FirebaseError('Must supply a "public" directory when using "destination" rewrites.'); - } - - if (!hasPublicDir && !hasAnyDynamicRewrites && !hasAnyRedirects) { - throw new FirebaseError( - 'Must supply a "public" directory or at least one rewrite or redirect in each "hosting" config.' - ); - } - - if (hasPublicDir && !dirExistsSync(resolveProjectPath(options, cfg.public!))) { - throw new FirebaseError( - `Specified "public" directory "${cfg.public}" does not exist, can't deploy hosting to site "${deploy.site}"` - ); - } - - if (cfg.i18n) { - if (!hasPublicDir) { - throw new FirebaseError('Must supply a "public" directory when using "i18n" configuration.'); - } - - if (!cfg.i18n.root) { - throw new FirebaseError('Must supply a "root" in "i18n" config.'); - } else { - const i18nPath = path.join(cfg.public!, cfg.i18n.root); - if (!dirExistsSync(resolveProjectPath(options, i18nPath))) { - logLabeledWarning( - "hosting", - `Couldn't find specified i18n root directory ${clc.bold( - cfg.i18n.root - )} in public directory ${clc.bold(cfg.public || "")}.` - ); - } - } - } -} diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index fc2e16e68dfd..2e38c8fa5de5 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -5,10 +5,7 @@ // 'npm run generate:json-schema' to regenerate the schema files. // -// Sourced from - https://docs.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest -type RequireAtLeastOne = { - [K in keyof T]-?: Required> & Partial>>; -}[keyof T]; +import { RequireAtLeastOne } from "./metaprogramming"; // should be sourced from - https://github.com/firebase/firebase-tools/blob/master/src/deploy/functions/runtimes/index.ts#L15 type CloudFunctionRuntimes = "nodejs10" | "nodejs12" | "nodejs14" | "nodejs16"; @@ -30,25 +27,25 @@ type DatabaseMultiple = ({ }> & Deployable)[]; -type HostingSource = { glob: string } | { source: string } | { regex: string }; +export type HostingSource = { glob: string } | { source: string } | { regex: string }; type HostingRedirects = HostingSource & { destination: string; type?: number; }; +export type DestinationRewrite = { destination: string }; +export type LegacyFunctionsRewrite = { function: string; region?: string }; +// TODO: add new format for FunctionsRewrite that looks like RunRewrite +export type RunRewrite = { + run: { + serviceId: string; + region?: string; + }; +}; +export type DynamicLinksRewrite = { dynamicLinks: boolean }; export type HostingRewrites = HostingSource & - ( - | { destination: string } - | { function: string; region?: string } - | { - run: { - serviceId: string; - region?: string; - }; - } - | { dynamicLinks: boolean } - ); + (DestinationRewrite | LegacyFunctionsRewrite | RunRewrite | DynamicLinksRewrite); export type HostingHeaders = HostingSource & { headers: { @@ -61,7 +58,7 @@ type HostingBase = { public?: string; source?: string; ignore?: string[]; - appAssociation?: string; + appAssociation?: "AUTO" | "NONE"; cleanUrls?: boolean; trailingSlash?: boolean; redirects?: HostingRedirects[]; @@ -72,18 +69,32 @@ type HostingBase = { }; }; -type HostingSingle = HostingBase & { +export type HostingSingle = HostingBase & { site?: string; target?: string; } & Deployable; -type HostingMultiple = (HostingBase & +// N.B. You would expect that a HostingMultiple is a HostingSingle[], but not +// quite. When you only have one hosting object you can omit both `site` and +// `target` because the default site will be looked up and provided for you. +// When you have a list of hosting targets, though, we require all configs +// to specify which site is being targeted. +// If you can assume we've resolved targets, you probably want to use +// HostingResolved, which says you must have site and may have target. +export type HostingMultiple = (HostingBase & RequireAtLeastOne<{ site: string; target: string; }> & Deployable)[]; +// After validating a HostingMultiple and resolving targets, we will instead +// have a HostingResolved. +export type HostingResolved = HostingBase & { + site: string; + target?: string; +} & Deployable; + type StorageSingle = { rules: string; target?: string; diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 372f9ef8dfc5..7e9b900dd649 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -11,7 +11,7 @@ import * as process from "node:process"; import * as semver from "semver"; import { needProjectId } from "../projectUtils"; -import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; +import { hostingConfig } from "../hosting/config"; import { listSites } from "../hosting/api"; import { getAppConfig, AppPlatform } from "../management/apps"; import { promptOnce } from "../prompt"; @@ -152,6 +152,9 @@ export function relativeRequire(dir: string, mod: "vite"): typeof import("vite") export function relativeRequire(dir: string, mod: "jsonc-parser"): typeof import("jsonc-parser"); // TODO the types for @nuxt/kit are causing a lot of troubles, need to do something other than any export function relativeRequire(dir: string, mod: "@nuxt/kit"): Promise; +/** + * + */ export function relativeRequire(dir: string, mod: string) { try { const path = require.resolve(mod, { paths: [dir] }); @@ -171,7 +174,10 @@ export function relativeRequire(dir: string, mod: string) { } } -export async function discover(dir: string, warn: boolean = true) { +/** + * + */ +export async function discover(dir: string, warn = true) { const allFrameworkTypes = [ ...new Set(Object.values(WebFrameworks).map(({ type }) => type)), ].sort(); @@ -206,6 +212,9 @@ function scanDependencyTree(searchingFor: string, dependencies = {}): any { return; } +/** + * + */ export function findDependency(name: string, options: Partial = {}) { const { cwd, depth, omitDev } = { ...DEFAULT_FIND_DEP_OPTIONS, ...options }; const result = spawnSync( @@ -224,6 +233,9 @@ export function findDependency(name: string, options: Partial = return scanDependencyTree(name, json.dependencies); } +/** + * + */ export async function prepareFrameworks( targetNames: string[], context: any, @@ -245,8 +257,7 @@ export async function prepareFrameworks( // been booted up (at this point) and we may be offline, so just use projectId. Most of the time // the default site is named the same as the project & for frameworks this is only used for naming the // function... unless you're using authenticated server-context TODO explore the implication here. - const configs = normalizedHostingConfigs({ site: project, ...options }, { resolveTargets: true }); - options.normalizedHostingConfigs = configs; + const configs = hostingConfig({ site: project, ...options }); let firebaseDefaults: FirebaseDefaults | undefined = undefined; if (configs.length === 0) return; for (const config of configs) { @@ -286,7 +297,7 @@ export async function prepareFrameworks( if (usesFirebaseJsSdk) { firebaseDefaults ||= {}; firebaseDefaults.emulatorHosts ||= {}; - firebaseDefaults.emulatorHosts![info.name] = formatHost(info); + firebaseDefaults.emulatorHosts[info.name] = formatHost(info); } }); let firebaseConfig = null; @@ -494,6 +505,9 @@ function codegenDevModeFunctionsDirectory() { return Promise.resolve({ packageJson, frameworksEntry: "_devMode" }); } +/** + * + */ export function createServerResponseProxy( req: IncomingMessage, res: ServerResponse, diff --git a/src/hosting/config.ts b/src/hosting/config.ts new file mode 100644 index 000000000000..f0db92833a23 --- /dev/null +++ b/src/hosting/config.ts @@ -0,0 +1,245 @@ +import { bold } from "colorette"; +import { cloneDeep, logLabeledWarning } from "../utils"; + +import { FirebaseError } from "../error"; +import { HostingMultiple, HostingSingle, HostingResolved } from "../firebaseConfig"; +import { partition } from "../functional"; +import { RequireAtLeastOne } from "../metaprogramming"; +import { dirExistsSync } from "../fsutils"; +import { resolveProjectPath } from "../projectPath"; +import { HostingOptions } from "./options"; +import path from "path"; + +// assertMatches allows us to throw when an --only flag doesn't match a target +// but an --except flag doesn't. Is this desirable behavior? +function matchingConfigs( + configs: HostingMultiple, + targets: string[], + assertMatches: boolean +): HostingMultiple { + const matches: HostingMultiple = []; + const [hasSite, hasTarget] = partition(configs, (c) => "site" in c); + for (const target of targets) { + const siteMatch = hasSite.find((c) => c.site === target); + const targetMatch = hasTarget.find((c) => c.target === target); + if (siteMatch) { + matches.push(siteMatch); + } else if (targetMatch) { + matches.push(targetMatch); + } else if (assertMatches) { + throw new FirebaseError( + `Hosting site or target ${bold(target)} not detected in firebase.json` + ); + } + } + return matches; +} + +/** + * Returns a subset of configs that match the only string + */ +export function filterOnly(configs: HostingMultiple, onlyString?: string): HostingMultiple { + if (!onlyString) { + return configs; + } + + let onlyTargets = onlyString.split(","); + // If an unqualified "hosting" is in the --only, + // all hosting sites should be deployed. + if (onlyTargets.includes("hosting")) { + return configs; + } + + // Strip out Hosting deploy targets from onlyTarget + onlyTargets = onlyTargets + .filter((target) => target.startsWith("hosting:")) + .map((target) => target.replace("hosting:", "")); + + return matchingConfigs(configs, onlyTargets, /* assertMatch= */ true); +} + +/** + * Returns a subset of configs that match the except string; + */ +export function filterExcept(configs: HostingMultiple, exceptOption?: string): HostingMultiple { + if (!exceptOption) { + return configs; + } + + const exceptTargets = exceptOption.split(","); + if (exceptTargets.includes("hosting")) { + return []; + } + + const exceptValues = exceptTargets + .filter((t) => t.startsWith("hosting:")) + .map((t) => t.replace("hosting:", "")); + const toReject = matchingConfigs(configs, exceptValues, /* assertMatch= */ false); + + return configs.filter((c) => !toReject.find((r) => c.site === r.site && c.target === r.target)); +} + +/** + * Verifies that input in firebase.json is sane + * @param options options from the command library + * @return a deep copy of validated configs + */ +export function extract(options: HostingOptions): HostingMultiple { + const config = options.config.src; + if (!config.hosting) { + return []; + } + const assertOneTarget = (config: HostingSingle): void => { + if (config.target && config.site) { + throw new FirebaseError( + `Hosting configs should only include either "site" or "target", not both.` + ); + } + }; + + if (!Array.isArray(config.hosting)) { + // Upgrade the type because we pinky swear to ensure site exists as a backup. + const res = cloneDeep(config.hosting) as unknown as RequireAtLeastOne<{ + site: string; + target: string; + }>; + // earlier the default RTDB instance was used as the hosting site + // because it used to be created along with the Firebase project. + // RTDB instance creation is now deferred and decoupled from project creation. + // the fallback hosting site is now filled in through requireHostingSite. + if (!res.target && !res.site) { + // Fun fact. Site can be the empty string if someone just downloads code + // and launches the emulator before configuring a project. + res.site = options.site; + } + assertOneTarget(res); + return [res]; + } else { + config.hosting.forEach(assertOneTarget); + return cloneDeep(config.hosting); + } +} + +/** Validates hosting configs for semantic correctness. */ +export function validate(configs: HostingMultiple, options: HostingOptions): void { + for (const config of configs) { + validateOne(config, options); + } +} + +function validateOne(config: HostingMultiple[number], options: HostingOptions): void { + // NOTE: a possible validation is to make sure site and target are not both + // specified, but this expectation is broken after calling resolveTargets. + // Thus that one validation is tucked into extract() where we know we haven't + // resolved targets yet. + + const hasAnyStaticRewrites = !!config.rewrites?.find((rw) => "destination" in rw); + const hasAnyDynamicRewrites = !!config.rewrites?.find((rw) => !("destination" in rw)); + const hasAnyRedirects = !!config.redirects?.length; + + if (!config.public && hasAnyStaticRewrites) { + throw new FirebaseError('Must supply a "public" directory when using "destination" rewrites.'); + } + + if (!config.public && !hasAnyDynamicRewrites && !hasAnyRedirects) { + throw new FirebaseError( + 'Must supply a "public" directory or at least one rewrite or redirect in each "hosting" config.' + ); + } + + if (config.public && !dirExistsSync(resolveProjectPath(options, config.public))) { + throw new FirebaseError( + `Specified "public" directory "${ + config.public + }" does not exist, can't deploy hosting to site "${config.site || config.target || ""}"` + ); + } + + // Using stupid types because type unions are painful sometimes + const regionWithoutFunction = (rewrite: Record): boolean => + typeof rewrite.region === "string" && typeof rewrite.function !== "string"; + const violation = config.rewrites?.find(regionWithoutFunction); + if (violation) { + throw new FirebaseError( + "Rewrites only support 'region' as a top-level field when 'function' is set as a string" + ); + } + + if (config.i18n) { + if (!config.public) { + throw new FirebaseError('Must supply a "public" directory when using "i18n" configuration.'); + } + + if (!config.i18n.root) { + throw new FirebaseError('Must supply a "root" in "i18n" config.'); + } + + const i18nPath = path.join(config.public, config.i18n.root); + if (!dirExistsSync(resolveProjectPath(options, i18nPath))) { + logLabeledWarning( + "hosting", + `Couldn't find specified i18n root directory ${bold( + config.i18n.root + )} in public directory ${bold(config.public)}` + ); + } + } +} + +/** + * Converts all configs from having a target to having a source + */ +export function resolveTargets( + configs: HostingMultiple, + options: HostingOptions +): HostingResolved[] { + return configs.map((config) => { + const newConfig = cloneDeep(config); + if (config.site) { + return newConfig as HostingResolved; + } + if (!config.target) { + throw new FirebaseError( + "Assertion failed: resolving hosting target of a site with no site name " + + "or target name. This should have caused an error earlier", + { exit: 2 } + ); + } + if (!options.project) { + throw new FirebaseError( + "Assertion failed: options.project is not set. Commands depending on hosting.config should use requireProject", + { exit: 2 } + ); + } + const matchingTargets = options.rc.requireTarget(options.project, "hosting", config.target); + if (matchingTargets.length > 1) { + throw new FirebaseError( + `Hosting target ${bold(config.target)} is linked to multiple sites, ` + + `but only one is permitted. ` + + `To clear, run:\n\n ${bold(`firebase target:clear hosting ${config.target}`)}` + ); + } + newConfig.site = matchingTargets[0]; + return newConfig as HostingResolved; + }); +} + +/** + * Extract a validated normalized set of Hosting configs from the command options. + * This also resolves targets, so it is not suitable for the emulator. + */ +export function hostingConfig(options: HostingOptions): HostingResolved[] { + if (!options.normalizedHostingConfig) { + let configs: HostingMultiple = extract(options); + configs = filterOnly(configs, options.only); + configs = filterExcept(configs, options.except); + + // N.B. We're calling resolveTargets after filterOnly/except, which means + // we won't recognize a --only when the config has a target. + // This is the way I found this code and should bring up to others whether + // we should change the behavior. + const resolved = resolveTargets(configs, options); + options.normalizedHostingConfig = resolved; + } + return options.normalizedHostingConfig; +} diff --git a/src/hosting/expireUtils.ts b/src/hosting/expireUtils.ts index 1d41a6e11b23..c54c271fd484 100644 --- a/src/hosting/expireUtils.ts +++ b/src/hosting/expireUtils.ts @@ -1,4 +1,5 @@ import { FirebaseError } from "../error"; +import { HostingOptions } from "./options"; /** * A regex to test for valid duration strings. @@ -36,7 +37,7 @@ export const DEFAULT_DURATION = 7 * Duration.DAY; * @param flag string duration (e.g. "1d"). * @return a duration in milliseconds. */ -export function calculateChannelExpireTTL(flag = ""): number { +export function calculateChannelExpireTTL(flag: NonNullable): number { const match = DURATION_REGEX.exec(flag); if (!match) { throw new FirebaseError( diff --git a/src/hosting/normalizedHostingConfigs.ts b/src/hosting/normalizedHostingConfigs.ts deleted file mode 100644 index b9bb0a720f96..000000000000 --- a/src/hosting/normalizedHostingConfigs.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { bold } from "colorette"; -import { cloneDeep } from "lodash"; - -import { FirebaseError } from "../error"; - -interface HostingConfig { - source?: string; - public?: string; - site: string; - target: string; - rewrites?: any[]; - redirects?: any[]; - headers?: any[]; - cleanUrls?: boolean; -} - -function filterOnly(configs: HostingConfig[], onlyString: string): HostingConfig[] { - if (!onlyString) { - return configs; - } - - let onlyTargets = onlyString.split(","); - // If an unqualified "hosting" is in the --only, - // all hosting sites should be deployed. - if (onlyTargets.includes("hosting")) { - return configs; - } - - // Strip out Hosting deploy targets from onlyTarget - onlyTargets = onlyTargets - .filter((target) => target.startsWith("hosting:")) - .map((target) => target.replace("hosting:", "")); - - const configsBySite = new Map(); - const configsByTarget = new Map(); - for (const c of configs) { - if (c.site) { - configsBySite.set(c.site, c); - } - if (c.target) { - configsByTarget.set(c.target, c); - } - } - - const filteredConfigs: HostingConfig[] = []; - // Check to see that all the hosting deploy targets exist in the hosting - // config as either `site`s or `target`s. - for (const onlyTarget of onlyTargets) { - if (configsBySite.has(onlyTarget)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - filteredConfigs.push(configsBySite.get(onlyTarget)!); - } else if (configsByTarget.has(onlyTarget)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - filteredConfigs.push(configsByTarget.get(onlyTarget)!); - } else { - throw new FirebaseError( - `Hosting site or target ${bold(onlyTarget)} not detected in firebase.json` - ); - } - } - - return filteredConfigs; -} - -function filterExcept(configs: HostingConfig[], exceptOption: string): HostingConfig[] { - if (!exceptOption) { - return configs; - } - - const exceptTargets = exceptOption.split(","); - if (exceptTargets.includes("hosting")) { - return []; - } - - const exceptValues = new Set( - exceptTargets.filter((t) => t.startsWith("hosting:")).map((t) => t.replace("hosting:", "")) - ); - - const filteredConfigs: HostingConfig[] = []; - for (const c of configs) { - if (!(exceptValues.has(c.site) || exceptValues.has(c.target))) { - filteredConfigs.push(c); - } - } - - return filteredConfigs; -} - -/** - * Normalize options to HostingConfig array. - * @param cmdOptions the Firebase CLI options object. - * @param options options for normalizing configs. - * @return normalized hosting config array. - */ -export function normalizedHostingConfigs( - cmdOptions: any, // eslint-disable-line @typescript-eslint/no-explicit-any - options: { resolveTargets?: boolean } = {} -): HostingConfig[] { - // First see if there's a momoized copy on the options, from frameworks - const normalizedHostingConfigs = cmdOptions.normalizedHostingConfigs; - if (normalizedHostingConfigs) return normalizedHostingConfigs; - let configs = cloneDeep(cmdOptions.config.get("hosting")); - if (!configs) { - return []; - } - if (!Array.isArray(configs)) { - if (!configs.target && !configs.site) { - // earlier the default RTDB instance was used as the hosting site - // because it used to be created along with the Firebase project. - // RTDB instance creation is now deferred and decoupled from project creation. - // the fallback hosting site is now filled in through requireHostingSite. - configs.site = cmdOptions.site; - } - configs = [configs]; - } - - for (const c of configs) { - if (c.target && c.site) { - throw new FirebaseError( - `Hosting configs should only include either "site" or "target", not both.` - ); - } - } - - // filter* functions check if the strings are empty for us. - let hostingConfigs: HostingConfig[] = filterOnly(configs, cmdOptions.only); - hostingConfigs = filterExcept(hostingConfigs, cmdOptions.except); - - if (options.resolveTargets) { - for (const cfg of hostingConfigs) { - if (cfg.target) { - const matchingTargets = cmdOptions.rc.requireTarget( - cmdOptions.project, - "hosting", - cfg.target - ); - if (matchingTargets.length > 1) { - throw new FirebaseError( - `Hosting target ${bold(cfg.target)} is linked to multiple sites, ` + - `but only one is permitted. ` + - `To clear, run:\n\n firebase target:clear hosting ${cfg.target}` - ); - } - cfg.site = matchingTargets[0]; - } else if (!cfg.site) { - throw new FirebaseError('Must supply either "site" or "target" in each "hosting" config.'); - } - } - } - - return hostingConfigs; -} diff --git a/src/hosting/options.ts b/src/hosting/options.ts new file mode 100644 index 000000000000..9cee6436858f --- /dev/null +++ b/src/hosting/options.ts @@ -0,0 +1,30 @@ +import { FirebaseConfig, HostingResolved } from "../firebaseConfig"; +import { Implements } from "../metaprogramming"; +import { Options } from "../options"; + +/** + * The set of fields that the Hosting codebase needs from Options. + * It is preferable that all codebases use this technique so that they keep + * strong typing in their codebase but limit the codebase to have less to mock. + */ +export interface HostingOptions { + project?: string; + site?: string; + config: { + src: FirebaseConfig; + }; + rc: { + requireTarget(project: string, type: string, name: string): string[]; + }; + cwd?: string; + configPath?: string; + only?: string; + except?: string; + normalizedHostingConfig?: Array; + expires?: `${number}${"h" | "d" | "m"}`; +} + +// This line caues a compile-time error if HostingOptions has a field that is +// missing in Options or incompatible with the type in Options. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const optionsAreHostingOptions: Implements = true; diff --git a/src/metaprogramming.ts b/src/metaprogramming.ts index 448bf577eda8..fb86756c8040 100644 --- a/src/metaprogramming.ts +++ b/src/metaprogramming.ts @@ -1,5 +1,25 @@ type Primitive = string | number | boolean | Function; +/** + * Assert that one implementation conforms to another in a static type assertion. + * This is useful because unlike trying to cast a value from one type + * to another, this will exhaustively check all fields as they are added. + * Usage: const test: Implements = true; + * This line will fail to compile with "true cannot be assigned to never" if + * A does not implement B. + */ +export type Implements = Test extends MaybeBase ? true : never; + +/** + * Creates a type that requires at least one key to be present in an interface + * type. For example, RequireAtLeastOne<{ foo: string; bar: string }> can hold + * a value of { foo: "a" }, { bar: "b" }, or { foo: "a", bar: "b" } but not {} + * Sourced from - https://docs.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest + */ +export type RequireAtLeastOne = { + [K in keyof T]-?: Required> & Partial>>; +}[keyof T]; + /** * RecursiveKeyOf is a type for keys of an objet usind dots for subfields. * For a given object: {a: {b: {c: number}}, d } the RecursiveKeysOf are diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index abddfe3c177f..3447a6b2753b 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -7,7 +7,7 @@ import { detectProjectRoot } from "../detectProjectRoot"; import { FirebaseError } from "../error"; import { implicitInit, TemplateServerResponse } from "../hosting/implicitInit"; import { initMiddleware } from "../hosting/initMiddleware"; -import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; +import * as config from "../hosting/config"; import cloudRunProxy from "../hosting/cloudRunProxy"; import { functionsProxy } from "../hosting/functionsProxy"; import { Writable } from "stream"; @@ -139,7 +139,13 @@ export function stop(): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function start(options: any): Promise { const init = await implicitInit(options); - const configs = normalizedHostingConfigs(options); + // Note: we cannot use the hostingConfig() method because it would resolve + // targets and we don't want to crash the emulator just because the target + // doesn't exist (nor do we want to depend on API calls); + let configs = config.extract(options); + configs = config.filterOnly(configs, options.only); + configs = config.filterExcept(configs, options.except); + config.validate(configs, options); for (let i = 0; i < configs.length; i++) { // skip over the functions emulator ports to avoid breaking changes diff --git a/src/test/deploy/hosting/convertConfig.spec.ts b/src/test/deploy/hosting/convertConfig.spec.ts index 21820132d3b6..1843bea793f4 100644 --- a/src/test/deploy/hosting/convertConfig.spec.ts +++ b/src/test/deploy/hosting/convertConfig.spec.ts @@ -432,8 +432,8 @@ describe("convertConfig", () => { // App Association. { name: "returns app association as it is set", - input: { appAssociation: "myApp" }, - want: { appAssociation: "myApp" }, + input: { appAssociation: "AUTO" }, + want: { appAssociation: "AUTO" }, }, // i18n. { diff --git a/src/test/deploy/hosting/validate.spec.ts b/src/test/deploy/hosting/validate.spec.ts deleted file mode 100644 index eb6bf4811d24..000000000000 --- a/src/test/deploy/hosting/validate.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from "chai"; -import { validateDeploy } from "../../../deploy/hosting/validate"; - -const PUBLIC_DIR_ERROR_PREFIX = 'Must supply a "public" directory'; - -describe("validateDeploy()", () => { - function testDeploy(merge: any) { - return () => { - validateDeploy( - { - site: "test-site", - version: "abc123", - config: { ...merge }, - }, - { - cwd: __dirname + "/../../fixtures/simplehosting", - configPath: __dirname + "/../../fixtures/simplehosting/firebase.json", - } - ); - }; - } - - it("should error out if there is no public directory but a 'destination' rewrite", () => { - expect( - testDeploy({ - rewrites: [ - { source: "/foo", destination: "/bar.html" }, - { source: "/baz", function: "app" }, - ], - }) - ).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is no public directory and no rewrites or redirects", () => { - expect(testDeploy({})).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is no public directory and an i18n with root", () => { - expect( - testDeploy({ - i18n: { root: "/foo" }, - rewrites: [{ source: "/foo", function: "pass" }], - }) - ).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is a public directory and an i18n with no root", () => { - expect( - testDeploy({ - public: "public", - i18n: {}, - rewrites: [{ source: "/foo", function: "pass" }], - }) - ).to.throw('Must supply a "root"'); - }); - - it("should pass with public and nothing else", () => { - expect(testDeploy({ public: "public" })).not.to.throw(); - }); - - it("should pass with no public but a function rewrite", () => { - expect(testDeploy({ rewrites: [{ source: "/", function: "app" }] })).not.to.throw(); - }); - - it("should pass with no public but a run rewrite", () => { - expect(testDeploy({ rewrites: [{ source: "/", run: { serviceId: "app" } }] })).not.to.throw(); - }); - - it("should pass with no public but a redirect", () => { - expect( - testDeploy({ redirects: [{ source: "/", destination: "https://google.com/", type: 302 }] }) - ).not.to.throw(); - }); -}); diff --git a/src/test/hosting/config.spec.ts b/src/test/hosting/config.spec.ts new file mode 100644 index 000000000000..79c417ddd7f2 --- /dev/null +++ b/src/test/hosting/config.spec.ts @@ -0,0 +1,338 @@ +import { expect } from "chai"; +import { FirebaseError } from "../../error"; +import { HostingConfig, HostingMultiple, HostingSingle } from "../../firebaseConfig"; + +import * as config from "../../hosting/config"; +import { HostingOptions } from "../../hosting/options"; +import { RequireAtLeastOne } from "../../metaprogramming"; + +function options( + hostingConfig: HostingConfig, + base?: Omit, + targetsToSites?: Record +): HostingOptions { + return { + project: "project", + config: { + src: { + hosting: hostingConfig, + }, + }, + rc: { + requireTarget: (project: string, type: string, name: string): string[] => { + return targetsToSites?.[name] || []; + }, + }, + cwd: __dirname + "/../fixtures/simplehosting", + configPath: __dirname + "/../fixtures/simplehosting/firebase.json", + ...base, + }; +} + +describe("config", () => { + describe("extract", () => { + it("should handle no hosting config", () => { + const opts = options({}); + delete opts.config.src.hosting; + expect(config.extract(opts)).to.deep.equal([]); + }); + + it("should fail if both site and target are specified", () => { + const singleSiteOpts = options({ site: "site", target: "target" }); + expect(() => config.extract(singleSiteOpts)).throws( + FirebaseError, + /configs should only include either/ + ); + + const manySiteOpts = options([{ site: "site", target: "target" }]); + expect(() => config.extract(manySiteOpts)).throws( + FirebaseError, + /configs should only include either/ + ); + }); + + it("should always return an array", () => { + const single: HostingMultiple[number] = { site: "site" }; + let extracted = config.extract(options(single)); + expect(extracted).to.deep.equal([single]); + + extracted = config.extract(options([single])); + expect(extracted).to.deep.equal([single]); + }); + + it("should support legacy method of specifying site", () => { + const opts = options({}, { site: "legacy-site" }); + const extracted = config.extract(opts); + expect(extracted).to.deep.equal([{ site: "legacy-site" }]); + }); + }); + + describe("resolveTargets", () => { + it("should not modify the config", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site"] }); + config.resolveTargets(cfg, opts); + expect(cfg).to.deep.equal([{ target: "target" }]); + }); + + it("should add sites when found", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site"] }); + const resolved = config.resolveTargets(cfg, opts); + expect(resolved).to.deep.equal([{ target: "target", site: "site" }]); + }); + + // Note: Not testing the case where the target cannot be found because this + // exception comes out of the RC class, which is being mocked in tests. + + it("should prohibit multiple sites", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site", "other-site"] }); + expect(() => config.resolveTargets(cfg, opts)).to.throw( + FirebaseError, + /is linked to multiple sites, but only one is permitted/ + ); + }); + }); + + describe("filterOnly", () => { + const tests: Array< + { + desc: string; + cfg: HostingMultiple; + only?: string; + } & RequireAtLeastOne<{ + want?: HostingMultiple; + wantErr?: RegExp; + }> + > = [ + { + desc: "a normal hosting config, specifying the default site", + cfg: [{ site: "site" }], + only: "hosting:site", + want: [{ site: "site" }], + }, + { + desc: "a hosting config with multiple sites, no targets, specifying the second site", + cfg: [{ site: "site" }, { site: "different-site" }], + only: `hosting:different-site`, + want: [{ site: "different-site" }], + }, + { + desc: "a normal hosting config with a target", + cfg: [{ target: "main" }, { site: "site" }], + only: "hosting:main", + want: [{ target: "main" }], + }, + { + desc: "a hosting config with multiple targets, specifying one", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting:t-two", + want: [{ target: "t-two" }], + }, + { + desc: "a hosting config with multiple targets, specifying all hosting", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting", + want: [{ target: "t-one" }, { target: "t-two" }], + }, + { + desc: "a hosting config with multiple targets, specifying an invalid target", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting:t-three", + wantErr: /Hosting site or target.+t-three.+not detected/, + }, + { + desc: "a hosting config with multiple sites but no targets, only an invalid target", + cfg: [{ site: "s-one" }], + only: "hosting:t-one", + wantErr: /Hosting site or target.+t-one.+not detected/, + }, + { + desc: "a hosting config without an only string", + cfg: [{ site: "site" }], + want: [{ site: "site" }], + }, + { + desc: "a hosting config with a non-hosting only flag", + cfg: [{ site: "site" }], + only: "functions", + want: [], + }, + ]; + + for (const t of tests) { + it(`should be able to parse ${t.desc}`, () => { + if (t.wantErr) { + expect(() => config.filterOnly(t.cfg, t.only)).to.throw(FirebaseError, t.wantErr); + } else { + const got = config.filterOnly(t.cfg, t.only); + expect(got).to.deep.equal(t.want); + } + }); + } + }); + + describe("with an except parameter, resolving targets", () => { + const tests: Array< + { + desc: string; + cfg: HostingMultiple; + except?: string; + } & RequireAtLeastOne<{ + want: HostingMultiple; + wantErr: RegExp; + }> + > = [ + { + desc: "a hosting config with multiple sites, no targets, omitting the second site", + cfg: [{ site: "default-site" }, { site: "different-site" }], + except: `hosting:different-site`, + want: [{ site: "default-site" }], + }, + { + desc: "a normal hosting config with a target, omitting the target", + cfg: [{ target: "main" }], + except: "hosting:main", + want: [], + }, + { + desc: "a hosting config with multiple targets, omitting one", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting:t-two", + want: [{ target: "t-one" }], + }, + { + desc: "a hosting config with multiple targets, omitting all hosting", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting", + want: [], + }, + { + desc: "a hosting config with multiple targets, omitting an invalid target", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting:t-three", + want: [{ target: "t-one" }, { target: "t-two" }], + }, + { + desc: "a hosting config with no excpet string", + cfg: [{ target: "target" }], + want: [{ target: "target" }], + }, + { + desc: "a hosting config with a non-hosting except string", + cfg: [{ target: "target" }], + except: "functions", + want: [{ target: "target" }], + }, + ]; + + for (const t of tests) { + it(`should be able to parse ${t.desc}`, () => { + if (t.wantErr) { + expect(() => config.filterExcept(t.cfg, t.except)).to.throw(FirebaseError, t.wantErr); + } else { + const got = config.filterExcept(t.cfg, t.except); + expect(got).to.deep.equal(t.want); + } + }); + } + }); + + const PUBLIC_DIR_ERROR_PREFIX = /Must supply a "public" directory/; + describe("validate", () => { + const tests: Array<{ + desc: string; + site: HostingSingle; + wantErr?: RegExp; + }> = [ + { + desc: "should error out if there is no puyblic directory but a 'destination' rewrite", + site: { + rewrites: [ + { source: "/foo", destination: "/bar.html" }, + { source: "/baz", function: "app" }, + ], + }, + wantErr: PUBLIC_DIR_ERROR_PREFIX, + }, + { + desc: "should error out if htere is no public directory and an i18n with root", + site: { + i18n: { root: "/foo" }, + rewrites: [{ source: "/foo", function: "pass" }], + }, + wantErr: PUBLIC_DIR_ERROR_PREFIX, + }, + { + desc: "should error out if there is a public direcotry and an i18n with no root", + site: { + public: "public", + i18n: {} as unknown as { root: string }, + rewrites: [{ source: "/foo", function: "pass" }], + }, + wantErr: /Must supply a "root"/, + }, + { + desc: "should error out if region is set and function is unset", + site: { + rewrites: [{ source: "/", region: "us-central1" } as any], + }, + wantErr: + /Rewrites only support 'region' as a top-level field when 'function' is set as a string/, + }, + { + desc: "should error out if region is set and functions is the new form", + site: { + rewrites: [ + { + source: "/", + region: "us-central1", + function: { + functionId: "id", + }, + }, + ], + }, + wantErr: + /Rewrites only support 'region' as a top-level field when 'function' is set as a string/, + }, + { + desc: "should pass with public and nothing else", + site: { public: "public" }, + }, + { + desc: "should pass with no public but a function rewrite", + site: { + rewrites: [{ source: "/", function: "app" }], + }, + }, + { + desc: "should pass with no public but a run rewrite", + site: { + rewrites: [{ source: "/", run: { serviceId: "app" } }], + }, + }, + { + desc: "should pass with no public but a redirect", + site: { + redirects: [{ source: "/", destination: "https://google.com", type: 302 }], + }, + }, + ]; + + for (const t of tests) { + it(t.desc, () => { + const configs: HostingMultiple = [{ site: "site", ...t.site }]; + if (t.wantErr) { + expect(() => config.validate(configs, options(t.site))).to.throw( + FirebaseError, + t.wantErr + ); + } else { + expect(() => config.validate(configs, options(t.site))).to.not.throw(); + } + }); + } + }); +}); diff --git a/src/test/hosting/expireUtils.spec.ts b/src/test/hosting/expireUtils.spec.ts index d58a4770662e..620f35046dde 100644 --- a/src/test/hosting/expireUtils.spec.ts +++ b/src/test/hosting/expireUtils.spec.ts @@ -10,7 +10,7 @@ describe("calculateChannelExpireTTL", () => { { input: "2d", want: 2 * 24 * 60 * 60 * 1000 }, { input: "2h", want: 2 * 60 * 60 * 1000 }, { input: "56m", want: 56 * 60 * 1000 }, - ]; + ] as const; for (const test of goodTests) { it(`should be able to parse time ${test.input}`, () => { @@ -29,7 +29,7 @@ describe("calculateChannelExpireTTL", () => { for (const test of badTests) { it(`should be able to parse time ${test.input || "undefined"}`, () => { - expect(() => calculateChannelExpireTTL(test.input)).to.throw( + expect(() => calculateChannelExpireTTL(test.input as any)).to.throw( FirebaseError, /flag must be a duration string/ ); diff --git a/src/test/hosting/normalizedHostingConfigs.spec.ts b/src/test/hosting/normalizedHostingConfigs.spec.ts deleted file mode 100644 index 7b2df96789bd..000000000000 --- a/src/test/hosting/normalizedHostingConfigs.spec.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { expect } from "chai"; -import { FirebaseError } from "../../error"; - -import { normalizedHostingConfigs } from "../../hosting/normalizedHostingConfigs"; - -describe("normalizedHostingConfigs", () => { - it("should fail if both site and target are specified", () => { - const singleHostingConfig = { site: "site", target: "target" }; - const cmdConfig = { - site: "default-site", - config: { get: () => singleHostingConfig }, - }; - expect(() => normalizedHostingConfigs(cmdConfig)).to.throw( - FirebaseError, - /configs should only include either/ - ); - - const hostingConfig = [{ site: "site", target: "target" }]; - const newCmdConfig = { - site: "default-site", - config: { get: () => hostingConfig }, - }; - expect(() => normalizedHostingConfigs(newCmdConfig)).to.throw( - FirebaseError, - /configs should only include either/ - ); - }); - - it("should not modify the config when resolving targets", () => { - const singleHostingConfig = { target: "target" }; - const cmdConfig = { - site: "default-site", - config: { get: () => singleHostingConfig }, - rc: { requireTarget: () => ["default-site"] }, - }; - normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(singleHostingConfig).to.deep.equal({ target: "target" }); - }); - - describe("without an only parameter", () => { - const DEFAULT_SITE = "default-hosting-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config", - cfg: Object.assign({}, baseConfig), - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "no hosting config", - want: [], - }, - { - desc: "a normal hosting config with a target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - want: [Object.assign({}, baseConfig, { target: "main" })], - }, - { - desc: "a hosting config with multiple targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - want: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - const cmdConfig = { - site: DEFAULT_SITE, - config: { get: () => t.cfg }, - }; - const got = normalizedHostingConfigs(cmdConfig); - expect(got).to.deep.equal(t.want); - }); - } - }); - - describe("with an only parameter, resolving targets", () => { - const DEFAULT_SITE = "default-hosting-site"; - const TARGETED_SITE = "targeted-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config, specifying the default site", - cfg: Object.assign({}, baseConfig), - only: `hosting:${DEFAULT_SITE}`, - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "a hosting config with multiple sites, no targets, specifying the second site", - cfg: [ - Object.assign({}, baseConfig, { site: DEFAULT_SITE }), - Object.assign({}, baseConfig, { site: "different-site" }), - ], - only: `hosting:different-site`, - want: [Object.assign({}, baseConfig, { site: "different-site" })], - }, - { - desc: "a normal hosting config with a target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - only: "hosting:main", - want: [Object.assign({}, baseConfig, { target: "main", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, specifying one", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting:t-two", - want: [Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, specifying all hosting", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting", - want: [ - Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE }), - Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE }), - ], - }, - { - desc: "a hosting config with multiple targets, specifying an invalid target", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting:t-three", - wantErr: /Hosting site or target.+t-three.+not detected/, - }, - { - desc: "a hosting config with multiple targets, with multiple matching targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-one" }), - ], - only: "hosting:t-one", - targetedSites: [TARGETED_SITE, TARGETED_SITE], - wantErr: /Hosting target.+t-one.+linked to multiple sites/, - }, - { - desc: "a hosting config with multiple sites but no targets, only all hosting", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - only: "hosting", - wantErr: /Must supply either "site" or "target"/, - }, - { - desc: "a hosting config with multiple sites but no targets, only an invalid target", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - only: "hosting:t-one", - wantErr: /Hosting site or target.+t-one.+not detected/, - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - if (!Array.isArray(t.targetedSites)) { - t.targetedSites = [TARGETED_SITE]; - } - const cmdConfig = { - site: DEFAULT_SITE, - only: t.only, - config: { get: () => t.cfg }, - rc: { requireTarget: () => t.targetedSites }, - }; - - if (t.wantErr) { - expect(() => normalizedHostingConfigs(cmdConfig, { resolveTargets: true })).to.throw( - FirebaseError, - t.wantErr - ); - } else { - const got = normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(got).to.deep.equal(t.want); - } - }); - } - }); - - describe("with an except parameter, resolving targets", () => { - const DEFAULT_SITE = "default-hosting-site"; - const TARGETED_SITE = "targeted-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config, omitting the default site", - cfg: Object.assign({}, baseConfig), - except: `hosting:${DEFAULT_SITE}`, - want: [], - }, - { - desc: "a hosting config with multiple sites, no targets, omitting the second site", - cfg: [ - Object.assign({}, baseConfig, { site: DEFAULT_SITE }), - Object.assign({}, baseConfig, { site: "different-site" }), - ], - except: `hosting:different-site`, - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "a normal hosting config with a target, omitting the target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - except: "hosting:main", - want: [], - }, - { - desc: "a hosting config with multiple targets, omitting one", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting:t-two", - want: [Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, omitting all hosting", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting", - want: [], - }, - { - desc: "a hosting config with multiple targets, omitting an invalid target", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting:t-three", - want: [ - Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE }), - Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE }), - ], - }, - { - desc: "a hosting config with multiple targets, with multiple matching targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-other" }), - ], - except: "hosting:t-other", - targetedSites: [TARGETED_SITE, TARGETED_SITE], - wantErr: /Hosting target.+t-one.+linked to multiple sites/, - }, - { - desc: "a hosting config with multiple sites but no targets, only all hosting", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - except: "hosting:site", - wantErr: /Must supply either "site" or "target"/, - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - if (!Array.isArray(t.targetedSites)) { - t.targetedSites = [TARGETED_SITE]; - } - const cmdConfig = { - site: DEFAULT_SITE, - except: t.except, - config: { get: () => t.cfg }, - rc: { requireTarget: () => t.targetedSites }, - }; - - if (t.wantErr) { - expect(() => normalizedHostingConfigs(cmdConfig, { resolveTargets: true })).to.throw( - FirebaseError, - t.wantErr - ); - } else { - const got = normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(got).to.deep.equal(t.want); - } - }); - } - }); -}); From 3a92daf7d663afb3d08790640f44bc77435c9d4e Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 3 Oct 2022 23:21:22 -0700 Subject: [PATCH 003/115] Depend on API library Replace raw API client REST calls with tested calls against an expanded Hosting API library. --- src/deploy/hosting/client.ts | 7 -- src/deploy/hosting/prepare.ts | 21 ++--- src/deploy/hosting/release.ts | 35 ++++----- src/gcp/proto.ts | 8 +- src/hosting/api.ts | 141 +++++++++++++++++++++++++++++----- src/test/hosting/api.spec.ts | 119 ++++++++++++++++++++++++++++ 6 files changed, 270 insertions(+), 61 deletions(-) delete mode 100644 src/deploy/hosting/client.ts diff --git a/src/deploy/hosting/client.ts b/src/deploy/hosting/client.ts deleted file mode 100644 index 4acd4920e2f8..000000000000 --- a/src/deploy/hosting/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { hostingApiOrigin } from "../../api"; -import { Client } from "../../apiv2"; - -export const client = new Client({ - urlPrefix: hostingApiOrigin, - apiVersion: "v1beta1", -}); diff --git a/src/deploy/hosting/prepare.ts b/src/deploy/hosting/prepare.ts index c628798cd4dc..9b6fe63388f5 100644 --- a/src/deploy/hosting/prepare.ts +++ b/src/deploy/hosting/prepare.ts @@ -1,6 +1,5 @@ import { FirebaseError } from "../../error"; -import { client } from "./client"; -import { needProjectNumber } from "../../projectUtils"; +import * as api from "../../hosting/api"; import * as config from "../../hosting/config"; import { convertConfig } from "./convertConfig"; import * as deploymentTool from "../../deploymentTool"; @@ -21,8 +20,6 @@ export async function prepare(context: Context, options: Options, payload: Paylo options.config.set("hosting.public", options.public); } - const projectNumber = await needProjectNumber(options); - const configs = config.hostingConfig(options); if (configs.length === 0) { return Promise.resolve(); @@ -39,20 +36,18 @@ export async function prepare(context: Context, options: Options, payload: Paylo for (const deploy of context.hosting.deploys) { const cfg = deploy.config; - const data = { + const data: Omit = { + status: "CREATED", config: await convertConfig(context, payload, cfg, false), labels: deploymentTool.labels(), }; versionCreates.push( - client - .post<{ config: unknown; labels: { [k: string]: string } }, { name: string }>( - `/projects/${projectNumber}/sites/${deploy.site}/versions`, - data - ) - .then((res) => { - deploy.version = res.body.name; - }) + (async () => { + // TODO: Fix this inconsistency. Site and project are ids but version + // is a name. + deploy.version = await api.createVersion(deploy.site, data); + })() ); } diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts index 83c555da550c..4796df9468a7 100644 --- a/src/deploy/hosting/release.ts +++ b/src/deploy/hosting/release.ts @@ -1,6 +1,5 @@ -import { client } from "./client"; +import * as api from "../../hosting/api"; import { logger } from "../../logger"; -import { needProjectNumber } from "../../projectUtils"; import * as utils from "../../utils"; import { convertConfig } from "./convertConfig"; import { Payload } from "./args"; @@ -16,8 +15,6 @@ export async function release(context: Context, options: Options, payload: Paylo return; } - const projectNumber = await needProjectNumber(options); - logger.debug(JSON.stringify(context.hosting.deploys, null, 2)); await Promise.all( context.hosting.deploys.map(async (deploy) => { @@ -29,31 +26,29 @@ export async function release(context: Context, options: Options, payload: Paylo } utils.logLabeledBullet(`hosting[${deploy.site}]`, "finalizing version..."); - const config = await convertConfig(context, payload, deploy.config, true); - const data = { status: "FINALIZED", config }; - const queryParams = { updateMask: "status,config" }; + const update: Partial = { + status: "FINALIZED", + config: await convertConfig(context, payload, deploy.config, /* finalize= */ true), + }; - const finalizeResult = await client.patch(`/${deploy.version}`, data, { queryParams }); + const parts = deploy.version.split("/"); + const versionId = parts[parts.length - 1]; + const finalizedVersion = await api.updateVersion(deploy.site, versionId, update); - logger.debug(`[hosting] finalized version for ${deploy.site}:${finalizeResult.body}`); + logger.debug(`[hosting] finalized version for ${deploy.site}:${finalizedVersion}`); utils.logLabeledSuccess(`hosting[${deploy.site}]`, "version finalized"); utils.logLabeledBullet(`hosting[${deploy.site}]`, "releasing new version..."); - // TODO: We should deploy to the resource we're given rather than have to check for a channel here. - const channelSegment = - context.hostingChannel && context.hostingChannel !== "live" - ? `/channels/${context.hostingChannel}` - : ""; - if (channelSegment) { + if (context.hostingChannel) { logger.debug("[hosting] releasing to channel:", context.hostingChannel); } - const releaseResult = await client.post( - `/projects/${projectNumber}/sites/${deploy.site}${channelSegment}/releases`, - { message: options.message || null }, - { queryParams: { versionName: deploy.version } } + const release = await api.createRelease( + deploy.site, + context.hostingChannel || "live", + deploy.version ); - logger.debug("[hosting] release:", releaseResult.body); + logger.debug("[hosting] release:", release); utils.logLabeledSuccess(`hosting[${deploy.site}]`, "release complete"); }) ); diff --git a/src/gcp/proto.ts b/src/gcp/proto.ts index 42b1bae02928..fff43dd1e512 100644 --- a/src/gcp/proto.ts +++ b/src/gcp/proto.ts @@ -165,7 +165,13 @@ function fieldMasksHelper( doNotRecurseIn: string[], masks: string[] ): void { - if (typeof cursor !== "object" || Array.isArray(cursor) || cursor === null) { + // Empty arrays should never be sent because they're dropped by the one platform + // gateway and then services get confused why there's an update mask for a missing field" + if (Array.isArray(cursor) && !cursor.length) { + return; + } + + if (typeof cursor !== "object" || (Array.isArray(cursor) && cursor.length) || cursor === null) { masks.push(prefixes.join(".")); return; } diff --git a/src/hosting/api.ts b/src/hosting/api.ts index 1d6e210ec0e9..8ffcbd5c12d4 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -4,6 +4,7 @@ import { Client } from "../apiv2"; import * as operationPoller from "../operation-poller"; import { DEFAULT_DURATION } from "../hosting/expireUtils"; import { getAuthDomains, updateAuthDomains } from "../gcp/auth"; +import * as proto from "../gcp/proto"; const ONE_WEEK_MS = 604800000; // 7 * 24 * 60 * 60 * 1000 @@ -13,7 +14,7 @@ interface ActingUser { // A profile image URL for the user. May not be present if the user has // changed their email address or deleted their account. - imageUrl: string; + imageUrl?: string; } enum ReleaseType { @@ -37,7 +38,7 @@ interface Release { // The configuration and content that was released. // TODO: create a Version type interface. - readonly version: any; // eslint-disable-line @typescript-eslint/no-explicit-any + readonly version: Version; // Explains the reason for the release. // Specify a value for this field only when creating a `SITE_DISABLE` @@ -87,30 +88,60 @@ export interface Channel { labels: { [key: string]: string }; } -enum VersionStatus { - // The default status; should not be intentionally used. - VERSION_STATUS_UNSPECIFIED = "VERSION_STATUS_UNSPECIFIED", +export type VersionStatus = // The version has been created, and content is currently being added to the // version. - CREATED = "CREATED", + | "CREATED" // All content has been added to the version, and the version can no longer be // changed. - FINALIZED = "FINALIZED", + | "FINALIZED" // The version has been deleted. - DELETED = "DELETED", + | "DELETED" // The version was not updated to `FINALIZED` within 12 hours and was // automatically deleted. - ABANDONED = "ABANDONED", + | "ABANDONED" // The version is outside the site-configured limit for the number of // retained versions, so the version's content is scheduled for deletion. - EXPIRED = "EXPIRED", + | "EXPIRED" // The version is being cloned from another version. All content is still // being copied over. - CLONING = "CLONING", + | "CLONING"; + +export type HasPattern = { glob: string } | { regex: string }; + +export type Header = HasPattern & { + regex?: string; + headers: Record; +}; + +export type Redirect = HasPattern & { + statusCode?: number; + location: string; +}; + +export interface RunRewrite { + serviceId: string; + region: string; + tag?: string; } -// TODO: define ServingConfig. -enum ServingConfig {} +export type RewriteBehavior = + | { path: string } + | { function: string; functionRegion?: string } + | { dynamicLinks: true } + | { run: RunRewrite }; + +export type Rewrite = HasPattern & RewriteBehavior; + +export interface ServingConfig { + headers?: Header[]; + redirects?: Redirect[]; + rewrites?: Rewrite[]; + cleanUrls?: boolean; + trailingSlashBehavior?: "ADD" | "REMOVE"; + appAssociation?: "AUTO" | "NONE"; + i18n?: { root: string }; +} export interface Version { // The unique identifier for a version, in the format: @@ -121,10 +152,10 @@ export interface Version { status: VersionStatus; // The configuration for the behavior of the site. - config: ServingConfig; + config?: ServingConfig; // The labels used for extra metadata and/or filtering. - labels: Map; + labels?: Record; // The time at which the version was created. readonly createTime: string; @@ -133,16 +164,16 @@ export interface Version { readonly createUser: ActingUser; // The time at which the version was `FINALIZED`. - readonly finalizeTime: string; + readonly finalizeTime?: string; // Identifies the user who `FINALIZED` the version. - readonly finalizeUser: ActingUser; + readonly finalizeUser?: ActingUser; // The time at which the version was `DELETED`. - readonly deleteTime: string; + readonly deleteTime?: string; // Identifies the user who `DELETED` the version. - readonly deleteUser: ActingUser; + readonly deleteUser?: ActingUser; // The total number of files associated with the version. readonly fileCount: number; @@ -151,6 +182,17 @@ export interface Version { readonly versionBytes: number; } +export type VERSION_OUTPUT_FIELDS = + | "name" + | "createTime" + | "createUser" + | "finalizeTime" + | "finalizeUser" + | "deleteTime" + | "deleteUser" + | "fileCount" + | "versionBytes"; + interface CloneVersionRequest { // The name of the version to be cloned, in the format: // `sites/{site}/versions/{version}` @@ -316,6 +358,65 @@ export async function deleteChannel( await apiClient.delete(`/projects/${project}/sites/${site}/channels/${channelId}`); } +/** + * Creates a version + */ +export async function createVersion( + siteId: string, + version: Omit +): Promise { + const res = await apiClient.post( + `projects/-/sites/${siteId}/versions`, + version + ); + return res.body.name; +} + +/** + * Updates a version. + */ +export async function updateVersion( + site: string, + versionId: string, + version: Partial +): Promise { + const res = await apiClient.patch, Version>( + `projects/-/sites/${site}/versions/${versionId}`, + version, + { + queryParams: { + updateMask: proto.fieldMasks(version).join(","), + }, + } + ); + return res.body; +} + +interface ListVersionsResponse { + versions: Version[]; + nextPageToken?: string; +} + +/** + * Get a list of all versions for a site, automatically handling pagination. + */ +export async function listVersions(site: string): Promise { + let pageToken: string | undefined = undefined; + const versions: Version[] = []; + do { + const queryParams: Record = {}; + if (pageToken) { + queryParams.pageToken = pageToken; + } + const res = await apiClient.get(`projects/-/sites/${site}/versions`, { + queryParams, + }); + versions.push(...res.body.versions); + pageToken = res.body.nextPageToken; + } while (pageToken); + return versions; +} + /** * Create a version a clone. * @param site the site for the version. @@ -355,7 +456,7 @@ export async function createRelease( channel: string, version: string ): Promise { - const res = await apiClient.request({ + const res = await apiClient.request({ method: "POST", path: `/projects/-/sites/${site}/channels/${channel}/releases`, queryParams: { versionName: version }, diff --git a/src/test/hosting/api.spec.ts b/src/test/hosting/api.spec.ts index de4019429024..24dc774240ad 100644 --- a/src/test/hosting/api.spec.ts +++ b/src/test/hosting/api.spec.ts @@ -266,6 +266,125 @@ describe("hosting", () => { }); }); + describe("createVersion", () => { + afterEach(nock.cleanAll); + + it("should make the API requests to create a version", async () => { + const VERSION = { status: "CREATED" } as const; + const FULL_NAME = `projects/-/sites/${SITE}/versions/my-new-version`; + nock(hostingApiOrigin) + .post(`/v1beta1/projects/-/sites/${SITE}/versions`, VERSION) + .reply(200, { name: FULL_NAME }); + + const res = await hostingApi.createVersion(SITE, VERSION); + + expect(res).to.deep.equal(FULL_NAME); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const VERSION = { status: "CREATED" } as const; + nock(hostingApiOrigin) + .post(`/v1beta1/projects/-/sites/${SITE}/versions`, VERSION) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.createVersion(SITE, VERSION)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/ + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateVersion", () => { + afterEach(nock.cleanAll); + + it("should make the API requests to update a version", async () => { + const VERSION = { status: "FINALIZED" } as const; + nock(hostingApiOrigin) + .patch(`/v1beta1/projects/-/sites/${SITE}/versions/my-version`, VERSION) + .query({ updateMask: "status" }) + .reply(200, VERSION); + + const res = await hostingApi.updateVersion(SITE, "my-version", VERSION); + + expect(res).to.deep.equal(VERSION); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const VERSION = { status: "FINALIZED" } as const; + nock(hostingApiOrigin) + .patch(`/v1beta1/projects/-/sites/${SITE}/versions/my-version`, VERSION) + .query({ updateMask: "status" }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.updateVersion(SITE, "my-version", VERSION) + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listVersions", () => { + afterEach(nock.cleanAll); + + const VERSION_1: hostingApi.Version = { + name: `projects/-/sites/${SITE}/versions/v1`, + status: "FINALIZED", + config: {}, + createTime: "now", + createUser: { + email: "inlined@google.com", + }, + fileCount: 0, + versionBytes: 0, + }; + const VERSION_2 = { + ...VERSION_1, + name: `projects/-/sites/${SITE}/versions/v2`, + }; + + it("returns a single page of versions", async () => { + nock(hostingApiOrigin) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(200, { versions: [VERSION_1] }); + nock(hostingApiOrigin); + + const versions = await hostingApi.listVersions(SITE); + expect(versions).deep.equals([VERSION_1]); + expect(nock.isDone()).to.be.true; + }); + + it("paginates through many versions", async () => { + nock(hostingApiOrigin) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(200, { versions: [VERSION_1], nextPageToken: "page2" }); + nock(hostingApiOrigin) + .get(`/v1beta1/projects/-/sites/${SITE}/versions?pageToken=page2`) + .reply(200, { versions: [VERSION_2] }); + + const versions = await hostingApi.listVersions(SITE); + expect(versions).deep.equals([VERSION_1, VERSION_2]); + expect(nock.isDone()).to.be.true; + }); + + it("handles errors", async () => { + nock(hostingApiOrigin) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.listVersions(SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/ + ); + + expect(nock.isDone()).to.be.true; + }); + }); + describe("cloneVersion", () => { afterEach(nock.cleanAll); From 443d13b6d8625144903e583a186591344d23fa3e Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 4 Oct 2022 11:00:59 -0700 Subject: [PATCH 004/115] Wire up pinTag (#5047) Add an experiment called "pintags". When this is set, you can set a "pinTag" filed to true on functions or run rewrite configs. This will cause the CLI to ensure that the latest version of that run service has a tag (garbage collecting if we exceed 500) and then send that tag along with the API request for the version. Once server-side changes are live, this will cause the hosting version to be forever locked to the Run Revision, which means rollbacks actually work. Was hoping this would fix preview channels as well, but we need to fix the fact that those functions are not deployed during preview channel deploys. --- schema/firebase-config.json | 306 +++++++++++++ src/deploy/functions/args.ts | 8 + src/deploy/functions/backend.ts | 43 +- src/deploy/hosting/context.ts | 5 +- src/deploy/hosting/convertConfig.ts | 406 ++++++++---------- src/deploy/hosting/deploy.ts | 8 +- src/deploy/hosting/prepare.ts | 40 +- src/deploy/hosting/release.ts | 23 +- src/experiments.ts | 11 + src/firebaseConfig.ts | 17 +- src/gcp/proto.ts | 27 ++ src/gcp/run.ts | 18 +- src/hosting/api.ts | 4 +- src/hosting/config.ts | 50 ++- src/hosting/functionsProxy.ts | 22 +- src/hosting/runTags.ts | 182 ++++++++ src/test/deploy/hosting/convertConfig.spec.ts | 375 +++++----------- src/test/gcp/proto.spec.ts | 38 ++ src/test/hosting/api.spec.ts | 6 +- src/test/hosting/config.spec.ts | 79 ++++ src/test/hosting/runTags.spec.ts | 307 +++++++++++++ 21 files changed, 1371 insertions(+), 604 deletions(-) create mode 100644 src/hosting/runTags.ts create mode 100644 src/test/hosting/runTags.spec.ts diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 96d65e516cbf..e78954181528 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -720,6 +720,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -729,6 +760,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -799,12 +833,46 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -878,6 +946,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -887,6 +986,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1209,6 +1311,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1218,6 +1351,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1288,12 +1424,46 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1367,6 +1537,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1376,6 +1577,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1698,6 +1902,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1707,6 +1942,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1777,12 +2015,46 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1856,6 +2128,37 @@ ], "type": "object" }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1865,6 +2168,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, diff --git a/src/deploy/functions/args.ts b/src/deploy/functions/args.ts index 846346baebf9..ee02675bfd7a 100644 --- a/src/deploy/functions/args.ts +++ b/src/deploy/functions/args.ts @@ -41,6 +41,14 @@ export interface Context { // Filled in the "prepare" and "deploy" phase. sources?: Record; // codebase -> source + + // Caching fields for backend.existingBackend() + existingBackend?: backend.Backend; + loadedExistingBackend?: boolean; + unreachableRegions?: { + gcfV1: string[]; + gcfV2: string[]; + }; } export interface FirebaseConfig { diff --git a/src/deploy/functions/backend.ts b/src/deploy/functions/backend.ts index 7ad49b55b905..cb7d0d65b10e 100644 --- a/src/deploy/functions/backend.ts +++ b/src/deploy/functions/backend.ts @@ -484,18 +484,6 @@ export function scheduleIdForFunction(cloudFunction: TargetIds): string { return `firebase-schedule-${cloudFunction.id}-${cloudFunction.region}`; } -interface PrivateContextFields { - existingBackend: Backend; - loadedExistingBackend?: boolean; - - // NOTE(inlined): Will this need to become a more nuanced data structure - // if we support GCFv1, v2, and Run? - unreachableRegions: { - gcfV1: string[]; - gcfV2: string[]; - }; -} - /** * A caching accessor of the existing backend. * The method explicitly loads Cloud Functions from their API but implicitly deduces @@ -509,14 +497,14 @@ interface PrivateContextFields { * @return The backend */ export async function existingBackend(context: Context, forceRefresh?: boolean): Promise { - const ctx = context as Context & PrivateContextFields; - if (!ctx.loadedExistingBackend || forceRefresh) { - await loadExistingBackend(ctx); + if (!context.loadedExistingBackend || forceRefresh) { + await loadExistingBackend(context); } - return ctx.existingBackend; + // loadExisting guarantees the validity of existingBackend and unreachableRegions + return context.existingBackend!; } -async function loadExistingBackend(ctx: Context & PrivateContextFields): Promise { +async function loadExistingBackend(ctx: Context): Promise { ctx.loadedExistingBackend = true; // Note: is it worth deducing the APIs that must have been enabled for this backend to work? // it could reduce redundant API calls for enabling the APIs. @@ -575,9 +563,8 @@ async function loadExistingBackend(ctx: Context & PrivateContextFields): Promise * @param want The desired backend. Can be backend.empty() to only warn about unavailability. */ export async function checkAvailability(context: Context, want: Backend): Promise { - const ctx = context as Context & PrivateContextFields; - if (!ctx.loadedExistingBackend) { - await loadExistingBackend(ctx); + if (!context.loadedExistingBackend) { + await loadExistingBackend(context); } const gcfV1Regions = new Set(); const gcfV2Regions = new Set(); @@ -589,13 +576,13 @@ export async function checkAvailability(context: Context, want: Backend): Promis } } - const neededUnreachableV1 = ctx.unreachableRegions.gcfV1.filter((region) => + const neededUnreachableV1 = context.unreachableRegions?.gcfV1.filter((region) => gcfV1Regions.has(region) ); - const neededUnreachableV2 = ctx.unreachableRegions.gcfV2.filter((region) => + const neededUnreachableV2 = context.unreachableRegions?.gcfV2.filter((region) => gcfV2Regions.has(region) ); - if (neededUnreachableV1.length) { + if (neededUnreachableV1?.length) { throw new FirebaseError( "The following Cloud Functions regions are currently unreachable:\n\t" + neededUnreachableV1.join("\n\t") + @@ -603,7 +590,7 @@ export async function checkAvailability(context: Context, want: Backend): Promis ); } - if (neededUnreachableV2.length) { + if (neededUnreachableV2?.length) { throw new FirebaseError( "The following Cloud Functions V2 regions are currently unreachable:\n\t" + neededUnreachableV2.join("\n\t") + @@ -611,20 +598,20 @@ export async function checkAvailability(context: Context, want: Backend): Promis ); } - if (ctx.unreachableRegions.gcfV1.length) { + if (context.unreachableRegions?.gcfV1.length) { utils.logLabeledWarning( "functions", "The following Cloud Functions regions are currently unreachable:\n" + - ctx.unreachableRegions.gcfV1.join("\n") + + context.unreachableRegions.gcfV1.join("\n") + "\nCloud Functions in these regions won't be deleted." ); } - if (ctx.unreachableRegions.gcfV2.length) { + if (context.unreachableRegions?.gcfV2.length) { utils.logLabeledWarning( "functions", "The following Cloud Functions V2 regions are currently unreachable:\n" + - ctx.unreachableRegions.gcfV2.join("\n") + + context.unreachableRegions.gcfV2.join("\n") + "\nCloud Functions in these regions won't be deleted." ); } diff --git a/src/deploy/hosting/context.ts b/src/deploy/hosting/context.ts index 05ee0769988f..05a69b076336 100644 --- a/src/deploy/hosting/context.ts +++ b/src/deploy/hosting/context.ts @@ -2,9 +2,10 @@ import { HostingResolved } from "../../firebaseConfig"; import { Context as FunctionsContext } from "../functions/args"; export interface HostingDeploy { + // Note: a HostingMultiple[number] is a stronger guarantee than a HostingSingle + // because at least one of site and target must exist. config: HostingResolved; - site: string; - version?: string; + version: string; } export interface Context extends FunctionsContext { diff --git a/src/deploy/hosting/convertConfig.ts b/src/deploy/hosting/convertConfig.ts index 60edd2c82727..30d6fa18a5e3 100644 --- a/src/deploy/hosting/convertConfig.ts +++ b/src/deploy/hosting/convertConfig.ts @@ -1,285 +1,223 @@ import { FirebaseError } from "../../error"; -import { HostingConfig, HostingRewrites, HostingHeaders } from "../../firebaseConfig"; -import { - existingBackend, - allEndpoints, - isHttpsTriggered, - isCallableTriggered, -} from "../functions/backend"; -import { Payload } from "./args"; +import { HostingSource } from "../../firebaseConfig"; +import { HostingDeploy } from "./context"; +import * as api from "../../hosting/api"; import * as backend from "../functions/backend"; import { Context } from "../functions/args"; import { logLabeledBullet, logLabeledWarning } from "../../utils"; - -function has(obj: { [k: string]: unknown }, k: string): boolean { - return obj[k] !== undefined; -} +import * as proto from "../../gcp/proto"; +import { bold } from "colorette"; +import * as runTags from "../../hosting/runTags"; +import { assertExhaustive } from "../../functional"; +import * as experiments from "../../experiments"; /** * extractPattern contains the logic for extracting exactly one glob/regexp * from a Hosting rewrite/redirect/header specification */ -function extractPattern(type: string, spec: HostingRewrites | HostingHeaders): any { - let glob = ""; - let regex = ""; - if ("source" in spec) { - glob = spec.source; +function extractPattern(type: string, source: HostingSource): api.HasPattern { + let glob: string | undefined; + let regex: string | undefined; + if ("source" in source) { + glob = source.source; } - if ("glob" in spec) { - glob = spec.glob; + if ("glob" in source) { + glob = source.glob; } - if ("regex" in spec) { - regex = spec.regex; + if ("regex" in source) { + regex = source.regex; } if (glob && regex) { throw new FirebaseError(`Cannot specify a ${type} pattern with both a glob and regex.`); } else if (glob) { - return { glob: glob }; + return { glob }; } else if (regex) { - return { regex: regex }; + return { regex }; } throw new FirebaseError( `Cannot specify a ${type} with no pattern (either a glob or regex required).` ); } -interface FunctionsEndpointInfo { - serviceId: string; - platform?: string; - region?: string; -} - /** - * convertConfig takes a hosting config object from firebase.json and transforms it into - * the valid format for sending to the Firebase Hosting REST API + * Finds an endpoint suitable for deploy at a site given an id and optional region */ -export async function convertConfig( - context: Context, - payload: Payload, - config: HostingConfig | undefined, - finalize: boolean -): Promise> { - if (Array.isArray(config)) { - throw new FirebaseError(`convertConfig should be given a single configuration, not an array.`, { - exit: 2, - }); +export function findEndpointForRewrite( + site: string, + targetBackend: backend.Backend, + id: string, + region: string | undefined +): backend.Endpoint | undefined { + const endpoints = backend.allEndpoints(targetBackend).filter((e) => e.id === id); + + if (endpoints.length === 0) { + return; } - const out: Record = {}; - - if (!config) { - return out; + if (endpoints.length === 1) { + if (region && region !== endpoints[0].region) { + return; + } + return endpoints[0]; } - - const endpointFromBackend = ( - targetBackend: backend.Backend, - functionsEndpointInfo: FunctionsEndpointInfo - ): backend.Endpoint | undefined => { - const backendsForId = backend.allEndpoints(targetBackend).filter((endpoint) => { - return endpoint.id === functionsEndpointInfo.serviceId; - }); - - const matchingBackends = backendsForId.filter((endpoint) => { - return ( - (!functionsEndpointInfo.region || endpoint.region === functionsEndpointInfo.region) && - (!functionsEndpointInfo.platform || endpoint.platform === functionsEndpointInfo.platform) - ); - }); - - if (matchingBackends.length > 1) { - // For now, if `us-central1` is specified, allow that to keep working. - for (const endpoint of matchingBackends) { - if (endpoint.region === "us-central1") { - logLabeledBullet( - `hosting[${config.site}]`, - `Function \`${functionsEndpointInfo.serviceId}\` found in multiple regions, defaulting to \`us-central1\`. ` + - `To rewrite to a different region, specify a \`region\` for the rewrite in \`firebase.json\`.` - ); - return endpoint; - } - } + if (!region) { + const us = endpoints.find((e) => e.region === "us-central1"); + if (!us) { throw new FirebaseError( - `More than one backend found for function name: ${functionsEndpointInfo.serviceId}. If the function is deployed in multiple regions, you must specify a region.` + `More than one backend found for function name: ${id}. If the function is deployed in multiple regions, you must specify a region.` ); } + logLabeledBullet( + `hosting[${site}]`, + `Function \`${id}\` found in multiple regions, defaulting to \`us-central1\`. ` + + `To rewrite to a different region, specify a \`region\` for the rewrite in \`firebase.json\`.` + ); + return us; + } + return endpoints.find((e) => e.region === region); +} - if (matchingBackends.length === 1) { - const endpoint = matchingBackends[0]; - if (endpoint && (isHttpsTriggered(endpoint) || isCallableTriggered(endpoint))) { - return endpoint; - } - } - return; - }; - - const endpointBeingDeployed = ( - functionsEndpointInfo: FunctionsEndpointInfo - ): backend.Endpoint | undefined => { - for (const { wantBackend } of Object.values(payload.functions || {})) { - if (!wantBackend) { - continue; - } - const endpoint = endpointFromBackend(wantBackend, functionsEndpointInfo); - if (endpoint) { - return endpoint; - } +/** + * convertConfig takes a hosting config object from firebase.json and transforms it into + * the valid format for sending to the Firebase Hosting REST API + */ +export async function convertConfig( + context: Context, + deploy: HostingDeploy +): Promise { + const config: api.ServingConfig = {}; + + // We need to be able to do a rewrite to an existing function that is may not + // even be part of Firebase's control or a function that we're currently + // deploying. + const haveBackend = await backend.existingBackend(context); + + config.rewrites = deploy.config.rewrites?.map((rewrite) => { + const target = extractPattern("rewrite", rewrite); + if ("destination" in rewrite) { + return { + ...target, + path: rewrite.destination, + }; } - return; - }; - - const matchingEndpoint = async ( - functionsEndpointInfo: FunctionsEndpointInfo - ): Promise => { - const pendingEndpoint = endpointBeingDeployed(functionsEndpointInfo); - if (pendingEndpoint) return pendingEndpoint; - const backend = await existingBackend(context); - return allEndpoints(backend).find( - (it) => - isHttpsTriggered(it) && - it.id === functionsEndpointInfo.serviceId && - (!functionsEndpointInfo.platform || it.platform === functionsEndpointInfo.platform) && - (!functionsEndpointInfo.region || it.region === functionsEndpointInfo.region) - ); - }; - const findEndpointWithValidRegion = async ( - rewrite: HostingRewrites, - context: Context - ): Promise => { if ("function" in rewrite) { - const foundEndpointToBeDeployed = endpointBeingDeployed({ - serviceId: rewrite.function, - region: rewrite.region, - }); - if (foundEndpointToBeDeployed) { - return foundEndpointToBeDeployed; + if (typeof rewrite.function === "string") { + throw new FirebaseError( + "Expected firebase config to be normalized, but got legacy functions format" + ); } - - const existingBackend = await backend.existingBackend(context); - - const endpointAlreadyDeployed = endpointFromBackend(existingBackend, { - serviceId: rewrite.function, - region: rewrite.region, - }); - if (endpointAlreadyDeployed) { - return endpointAlreadyDeployed; - } - } - return; - }; - - // rewrites - if (Array.isArray(config.rewrites)) { - out.rewrites = []; - for (const rewrite of config.rewrites) { - const vRewrite = extractPattern("rewrite", rewrite); - if ("destination" in rewrite) { - vRewrite.path = rewrite.destination; - } else if ("function" in rewrite) { - // Skip these rewrites during hosting prepare - if ( - !finalize && - endpointBeingDeployed({ - serviceId: rewrite.function, - platform: "gcfv2", - region: rewrite.region, - }) - ) { - continue; + const id = rewrite.function.functionId; + const region = rewrite.function.region; + const endpoint = findEndpointForRewrite(deploy.config.site, haveBackend, id, region); + if (!endpoint) { + // This could possibly succeed if there has been a function written + // outside firebase tooling. But it will break in v2. We might need to + // revisit this. + logLabeledWarning( + `hosting[${deploy.config.site}]`, + `Unable to find a valid endpoint for function \`${id}\`, but still including it in the config` + ); + const apiRewrite: api.Rewrite = { ...target, function: id }; + if (region) { + apiRewrite.functionRegion = region; } - // Convert function references to GCFv2 to their equivalent run config - // we can't use the already fetched endpoints, since those are scoped to the codebase - const endpoint = await matchingEndpoint({ - serviceId: rewrite.function, - platform: "gcfv2", - region: rewrite.region, - }); - if (endpoint) { - vRewrite.run = { serviceId: endpoint.id, region: endpoint.region }; - } else { - vRewrite.function = rewrite.function; - const foundEndpoint = await findEndpointWithValidRegion(rewrite, context); - if (foundEndpoint) { - vRewrite.functionRegion = foundEndpoint.region; - } else { - if (rewrite.region && rewrite.region !== "us-central1") { - throw new FirebaseError( - `Unable to find a valid endpoint for function \`${vRewrite.function}\`` - ); - } - logLabeledWarning( - `hosting[${config.site}]`, - `Unable to find a valid endpoint for function \`${vRewrite.function}\`, but still including it in the config` - ); - } + return apiRewrite; + } + if (endpoint.platform === "gcfv1") { + if (!backend.isHttpsTriggered(endpoint) && !backend.isCallableTriggered(endpoint)) { + throw new FirebaseError( + `Function ${endpoint.id} is a gen 1 function and therefore must be an https function type` + ); } - } else if ("dynamicLinks" in rewrite) { - vRewrite.dynamicLinks = rewrite.dynamicLinks; - } else if ("run" in rewrite) { - // Skip these rewrites during hosting prepare - if ( - !finalize && - endpointBeingDeployed({ - serviceId: rewrite.run.serviceId, - platform: "gcfv2", - region: rewrite.run.region, - }) - ) { - continue; + if (rewrite.function.pinTag) { + throw new FirebaseError( + `Function ${endpoint.id} is a gen 1 function and therefore does not support the ${bold( + "pinTag" + )} option` + ); } - vRewrite.run = Object.assign({ region: "us-central1" }, rewrite.run); + return { + ...target, + function: endpoint.id, + functionRegion: endpoint.region, + } as api.Rewrite; + } + + // V2 functions are actually deployed as run rewrites. This lets us target + // the service without a cloudfunctions.net URL and allows us to set a + // target tag. + const apiRewrite: api.Rewrite = { + ...target, + run: { + serviceId: endpoint.id, + region: endpoint.region, + }, + }; + if (rewrite.function.pinTag) { + experiments.assertEnabled("pintags", "pin a function version"); + apiRewrite.run.tag = runTags.TODO_TAG_NAME; } - out.rewrites.push(vRewrite); + return apiRewrite; } - } - // redirects - if (Array.isArray(config.redirects)) { - out.redirects = config.redirects.map((redirect) => { - const vRedirect = extractPattern("redirect", redirect); - vRedirect.location = redirect.destination; - if (redirect.type) { - vRedirect.statusCode = redirect.type; + if ("dynamicLinks" in rewrite) { + if (!rewrite.dynamicLinks) { + throw new FirebaseError("Can only set dynamicLinks to true in a rewrite"); } - return vRedirect; - }); - } + return { ...target, dynamicLinks: true }; + } - // headers - if (Array.isArray(config.headers)) { - out.headers = config.headers.map((header) => { - const vHeader = extractPattern("header", header); - vHeader.headers = {}; - if (Array.isArray(header.headers) && header.headers.length) { - header.headers.forEach((h) => { - vHeader.headers[h.key] = h.value; - }); + if ("run" in rewrite) { + const apiRewrite: api.Rewrite = { + ...target, + run: { + region: "us-central1", + ...rewrite.run, + }, + }; + if (apiRewrite.run.tag) { + experiments.assertEnabled("pintags", "pin to a run service revision"); } - return vHeader; - }); - } + return apiRewrite; + } - // cleanUrls - if (has(config, "cleanUrls")) { - out.cleanUrls = config.cleanUrls; - } + // This line makes sure this function breaks if there is ever added a new + // kind of rewrite and we haven't yet handled it. + assertExhaustive(rewrite); + }); - // trailingSlash - if (config.trailingSlash === true) { - out.trailingSlashBehavior = "ADD"; - } else if (config.trailingSlash === false) { - out.trailingSlashBehavior = "REMOVE"; + if (config.rewrites) { + await runTags.setRewriteTags(config.rewrites, context.projectId, deploy.version); } - // App association files - if (has(config, "appAssociation")) { - out.appAssociation = config.appAssociation; - } + config.redirects = deploy.config.redirects?.map((redirect) => { + const apiRedirect: api.Redirect = { + ...extractPattern("redirect", redirect), + location: redirect.destination, + }; + if (redirect.type) { + apiRedirect.statusCode = redirect.type; + } + return apiRedirect; + }); - // i18n config - if (has(config, "i18n")) { - out.i18n = config.i18n; - } + config.headers = deploy.config.headers?.map((header) => { + const headers: api.Header["headers"] = {}; + for (const { key, value } of header.headers || []) { + headers[key] = value; + } + return { + ...extractPattern("header", header), + headers, + }; + }); + + proto.copyIfPresent(config, deploy.config, "cleanUrls", "appAssociation", "i18n"); + proto.convertIfPresent(config, deploy.config, "trailingSlashBehavior", "trailingSlash", (b) => + b ? "ADD" : "REMOVE" + ); - return out; + proto.pruneUndefiends(config); + return config; } diff --git a/src/deploy/hosting/deploy.ts b/src/deploy/hosting/deploy.ts index 02fe79c5de00..31bc2da93da2 100644 --- a/src/deploy/hosting/deploy.ts +++ b/src/deploy/hosting/deploy.ts @@ -36,20 +36,20 @@ export async function deploy(context: Context, options: Options): Promise // No need to run Uploader for no-file deploys if (!deploy.config?.public) { logLabeledBullet( - `hosting[${deploy.site}]`, + `hosting[${deploy.config.site}]`, 'no "public" directory to upload, continuing with release' ); return runDeploys(deploys, debugging); } - logLabeledBullet(`hosting[${deploy.site}]`, "beginning deploy..."); + logLabeledBullet(`hosting[${deploy.config.site}]`, "beginning deploy..."); const t0 = Date.now(); const publicDir = options.config.path(deploy.config.public); const files = listFiles(publicDir, deploy.config.ignore); logLabeledBullet( - `hosting[${deploy.site}]`, + `hosting[${deploy.config.site}]`, `found ${files.length} files in ${bold(deploy.config.public)}` ); @@ -95,7 +95,7 @@ export async function deploy(context: Context, options: Options): Promise spinner.stop(); } - logLabeledSuccess(`hosting[${deploy.site}]`, "file upload complete"); + logLabeledSuccess(`hosting[${deploy.config.site}]`, "file upload complete"); const dt = Date.now() - t0; logger.debug(`[hosting] deploy completed after ${dt}ms`); diff --git a/src/deploy/hosting/prepare.ts b/src/deploy/hosting/prepare.ts index 9b6fe63388f5..effbf065b9df 100644 --- a/src/deploy/hosting/prepare.ts +++ b/src/deploy/hosting/prepare.ts @@ -1,16 +1,16 @@ import { FirebaseError } from "../../error"; import * as api from "../../hosting/api"; import * as config from "../../hosting/config"; -import { convertConfig } from "./convertConfig"; import * as deploymentTool from "../../deploymentTool"; -import { Payload } from "./args"; import { Context } from "./context"; import { Options } from "../../options"; +import { HostingOptions } from "../../hosting/options"; +import { zipIn } from "../../functional"; /** * Prepare creates versions for each Hosting site to be deployed. */ -export async function prepare(context: Context, options: Options, payload: Payload): Promise { +export async function prepare(context: Context, options: HostingOptions & Options): Promise { // Allow the public directory to be overridden by the --public flag if (options.public) { if (Array.isArray(options.config.get("hosting"))) { @@ -25,31 +25,17 @@ export async function prepare(context: Context, options: Options, payload: Paylo return Promise.resolve(); } + const version: Omit = { + status: "CREATED", + labels: deploymentTool.labels(), + }; + const versions = await Promise.all( + configs.map((config) => api.createVersion(config.site, version)) + ); context.hosting = { - deploys: configs.map((cfg) => { - return { config: cfg, site: cfg.site }; - }), + deploys: [], }; - - const versionCreates: unknown[] = []; - - for (const deploy of context.hosting.deploys) { - const cfg = deploy.config; - - const data: Omit = { - status: "CREATED", - config: await convertConfig(context, payload, cfg, false), - labels: deploymentTool.labels(), - }; - - versionCreates.push( - (async () => { - // TODO: Fix this inconsistency. Site and project are ids but version - // is a name. - deploy.version = await api.createVersion(deploy.site, data); - })() - ); + for (const [config, version] of configs.map(zipIn(versions))) { + context.hosting.deploys.push({ config, version }); } - - await Promise.all(versionCreates); } diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts index 4796df9468a7..7ea45f8df0bc 100644 --- a/src/deploy/hosting/release.ts +++ b/src/deploy/hosting/release.ts @@ -2,15 +2,13 @@ import * as api from "../../hosting/api"; import { logger } from "../../logger"; import * as utils from "../../utils"; import { convertConfig } from "./convertConfig"; -import { Payload } from "./args"; import { Context } from "./context"; -import { Options } from "../../options"; import { FirebaseError } from "../../error"; /** * Release finalized a Hosting release. */ -export async function release(context: Context, options: Options, payload: Payload): Promise { +export async function release(context: Context): Promise { if (!context.hosting || !context.hosting.deploys) { return; } @@ -24,32 +22,31 @@ export async function release(context: Context, options: Options, payload: Paylo { exit: 2 } ); } - utils.logLabeledBullet(`hosting[${deploy.site}]`, "finalizing version..."); + utils.logLabeledBullet(`hosting[${deploy.config.site}]`, "finalizing version..."); const update: Partial = { status: "FINALIZED", - config: await convertConfig(context, payload, deploy.config, /* finalize= */ true), + config: await convertConfig(context, deploy), }; - const parts = deploy.version.split("/"); - const versionId = parts[parts.length - 1]; - const finalizedVersion = await api.updateVersion(deploy.site, versionId, update); + const versionId = utils.last(deploy.version.split("/")); + const finalizedVersion = await api.updateVersion(deploy.config.site, versionId, update); - logger.debug(`[hosting] finalized version for ${deploy.site}:${finalizedVersion}`); - utils.logLabeledSuccess(`hosting[${deploy.site}]`, "version finalized"); - utils.logLabeledBullet(`hosting[${deploy.site}]`, "releasing new version..."); + logger.debug(`[hosting] finalized version for ${deploy.config.site}:${finalizedVersion}`); + utils.logLabeledSuccess(`hosting[${deploy.config.site}]`, "version finalized"); + utils.logLabeledBullet(`hosting[${deploy.config.site}]`, "releasing new version..."); if (context.hostingChannel) { logger.debug("[hosting] releasing to channel:", context.hostingChannel); } const release = await api.createRelease( - deploy.site, + deploy.config.site, context.hostingChannel || "live", deploy.version ); logger.debug("[hosting] release:", release); - utils.logLabeledSuccess(`hosting[${deploy.site}]`, "release complete"); + utils.logLabeledSuccess(`hosting[${deploy.config.site}]`, "release complete"); }) ); } diff --git a/src/experiments.ts b/src/experiments.ts index 0a01eda46edf..13ac85e3d865 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -86,6 +86,17 @@ export const ALL_EXPERIMENTS = experiments({ "may be required when the non-experimental support for these frameworks " + "is released", }, + pintags: { + shortDescription: "Adds the pinTag option to Run and Functions rewrites", + fullDescription: + "Adds support for the 'pinTag' boolean on Runction and Run rewrites for " + + "Firebase Hosting. With this option, newly released hosting sites will be " + + "bound to the current latest version of their referenced functions or services. " + + "This option depends on Run pinned traffic targets, of which only 2000 can " + + "exist per region. firebase-tools aggressively garbage collects tags it creates " + + "if any service exceeds 500 tags, but it is theoretically possible that a project " + + "exceeds the region-wide limit of tags and an old site version fails", + }, // Access experiments crossservicerules: { diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 2e38c8fa5de5..9d78537284a8 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -36,16 +36,29 @@ type HostingRedirects = HostingSource & { export type DestinationRewrite = { destination: string }; export type LegacyFunctionsRewrite = { function: string; region?: string }; -// TODO: add new format for FunctionsRewrite that looks like RunRewrite +export type FunctionsRewrite = { + function: { + functionId: string; + region?: string; + pinTag?: boolean; + }; +}; export type RunRewrite = { run: { serviceId: string; region?: string; + pinTag?: boolean; }; }; export type DynamicLinksRewrite = { dynamicLinks: boolean }; export type HostingRewrites = HostingSource & - (DestinationRewrite | LegacyFunctionsRewrite | RunRewrite | DynamicLinksRewrite); + ( + | DestinationRewrite + | LegacyFunctionsRewrite + | FunctionsRewrite + | RunRewrite + | DynamicLinksRewrite + ); export type HostingHeaders = HostingSource & { headers: { diff --git a/src/gcp/proto.ts b/src/gcp/proto.ts index fff43dd1e512..3f73d0f68c9b 100644 --- a/src/gcp/proto.ts +++ b/src/gcp/proto.ts @@ -235,3 +235,30 @@ export function formatServiceAccount(serviceAccount: string, projectId: string): } return `serviceAccount:${serviceAccount}`; } + +/** + * Remove keys whose values are undefined. + * When we write an interface { foo?: number } there are three possible + * forms: { foo: 1 }, {}, and { foo: undefined }. The latter surprises + * most people and make unit test comparison flaky. This cleans that up. + */ +export function pruneUndefiends(obj: unknown): void { + if (typeof obj !== "object" || obj === null) { + return; + } + const keyable = obj as Record; + for (const key of Object.keys(keyable)) { + if (keyable[key] === undefined) { + delete keyable[key]; + } else if (typeof keyable[key] === "object") { + if (Array.isArray(keyable[key])) { + for (const sub of keyable[key] as unknown[]) { + pruneUndefiends(sub); + } + keyable[key] = (keyable[key] as unknown[]).filter((e) => e !== undefined); + } else { + pruneUndefiends(keyable[key]); + } + } + } +} diff --git a/src/gcp/run.ts b/src/gcp/run.ts index 352f71582564..acd7c5e72605 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -109,7 +109,7 @@ export interface TrafficTarget { configurationName?: string; // RevisionName can be used to target a specific revision, // or customers can set latestRevision = true - revisionName: string; + revisionName?: string; latestRevision?: boolean; percent?: number; // optional when tagged tag?: string; @@ -127,6 +127,22 @@ export interface IamPolicy { etag?: string; } +export interface GCPIds { + serviceId: string; + region: string; + projectNumber: string; +} + +/** + * Gets the standard project/location/id tuple from the K8S style resource. + */ +export function gcpIds(service: Pick): GCPIds { + return { + serviceId: service.metadata.name, + projectNumber: service.metadata.namespace, + region: service.metadata.labels?.[LOCATION_LABEL] || "unknown-region", + }; +} /** * Gets a service with a given name. */ diff --git a/src/hosting/api.ts b/src/hosting/api.ts index 8ffcbd5c12d4..e3bb95b955c3 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -339,7 +339,7 @@ export async function updateChannelTtl( const res = await apiClient.patch<{ ttl: string }, Channel>( `/projects/${project}/sites/${site}/channels/${channelId}`, { ttl: `${ttlMillis / 1000}s` }, - { queryParams: { updateMask: ["ttl"].join(",") } } + { queryParams: { updateMask: "ttl" } } ); return res.body; } @@ -385,7 +385,7 @@ export async function updateVersion( version, { queryParams: { - updateMask: proto.fieldMasks(version).join(","), + updateMask: proto.fieldMasks(version, "labels").join(","), }, } ); diff --git a/src/hosting/config.ts b/src/hosting/config.ts index f0db92833a23..a67a08b371ce 100644 --- a/src/hosting/config.ts +++ b/src/hosting/config.ts @@ -2,7 +2,15 @@ import { bold } from "colorette"; import { cloneDeep, logLabeledWarning } from "../utils"; import { FirebaseError } from "../error"; -import { HostingMultiple, HostingSingle, HostingResolved } from "../firebaseConfig"; +import { + HostingMultiple, + HostingSingle, + HostingResolved, + HostingRewrites, + FunctionsRewrite, + LegacyFunctionsRewrite, + HostingSource, +} from "../firebaseConfig"; import { partition } from "../functional"; import { RequireAtLeastOne } from "../metaprogramming"; import { dirExistsSync } from "../fsutils"; @@ -224,6 +232,45 @@ export function resolveTargets( }); } +function isLegacyFunctionsRewrite( + rewrite: HostingRewrites +): rewrite is HostingSource & LegacyFunctionsRewrite { + return "function" in rewrite && typeof rewrite.function === "string"; +} + +/** + * Ensures that all configs are of a single modern format + */ +export function normalize(configs: HostingMultiple): void { + for (const config of configs) { + config.rewrites = config.rewrites?.map((rewrite) => { + if (!("function" in rewrite)) { + return rewrite; + } + if (isLegacyFunctionsRewrite(rewrite)) { + const modern: HostingRewrites & FunctionsRewrite = { + // Note: this copied in a bad "function" and "rewrite" in this splat + // we'll overwrite function and delete rewrite. + ...rewrite, + function: { + functionId: rewrite.function, + // Do not set pinTag so we can track how often it is used + }, + }; + delete (modern as unknown as LegacyFunctionsRewrite).region; + if ("region" in rewrite && typeof rewrite.region === "string") { + modern.function.region = rewrite.region; + } + if (rewrite.region) { + modern.function.region = rewrite.region; + } + return modern; + } + return rewrite; + }); + } +} + /** * Extract a validated normalized set of Hosting configs from the command options. * This also resolves targets, so it is not suitable for the emulator. @@ -233,6 +280,7 @@ export function hostingConfig(options: HostingOptions): HostingResolved[] { let configs: HostingMultiple = extract(options); configs = filterOnly(configs, options.only); configs = filterExcept(configs, options.except); + normalize(configs); // N.B. We're calling resolveTargets after filterOnly/except, which means // we won't recognize a --only when the config has a target. diff --git a/src/hosting/functionsProxy.ts b/src/hosting/functionsProxy.ts index 62d7861fe64a..7f314ac489ac 100644 --- a/src/hosting/functionsProxy.ts +++ b/src/hosting/functionsProxy.ts @@ -6,7 +6,7 @@ import { needProjectId } from "../projectUtils"; import { EmulatorRegistry } from "../emulator/registry"; import { Emulators } from "../emulator/types"; import { FunctionsEmulator } from "../emulator/functionsEmulator"; -import { HostingRewrites } from "../firebaseConfig"; +import { HostingRewrites, LegacyFunctionsRewrite } from "../firebaseConfig"; import { FirebaseError } from "../error"; export interface FunctionsProxyOptions { @@ -31,10 +31,16 @@ export function functionsProxy( exit: 2, }); } - if (!rewrite.region) { - rewrite.region = "us-central1"; + let functionId: string; + let region: string; + if (typeof rewrite.function === "string") { + functionId = rewrite.function; + region = (rewrite as LegacyFunctionsRewrite).region || "us-central1"; + } else { + functionId = rewrite.function.functionId; + region = rewrite.function.region || "us-central1"; } - let url = `https://${rewrite.region}-${projectId}.cloudfunctions.net/${rewrite.function}`; + let url = `https://${region}-${projectId}.cloudfunctions.net/${functionId}`; let destLabel = "live"; if (includes(options.targets, "functions")) { @@ -48,15 +54,13 @@ export function functionsProxy( functionsEmu.getInfo().host, functionsEmu.getInfo().port, projectId, - rewrite.function, - rewrite.region + functionId, + region ); } } - resolve( - proxyRequestHandler(url, `${destLabel} Function ${rewrite.region}/${rewrite.function}`) - ); + resolve(proxyRequestHandler(url, `${destLabel} Function ${region}/${functionId}`)); }); }; } diff --git a/src/hosting/runTags.ts b/src/hosting/runTags.ts new file mode 100644 index 000000000000..f2e0bf84f33e --- /dev/null +++ b/src/hosting/runTags.ts @@ -0,0 +1,182 @@ +import * as run from "../gcp/run"; +import * as api from "./api"; +import { FirebaseError } from "../error"; +import { flattenArray } from "../functional"; + +/** + * Sentinel to be used when creating an api.Rewrite with the tag option but + * you don't yet know the tag. Resolve this tag by passing the rewrite into + * setRewriteTags + */ +export const TODO_TAG_NAME = "this is an invalid tag name so it cannot be real"; + +/** + * Looks up all valid Hosting tags in this project and removes traffic targets + * from passed in services that don't match a valid tag. + * This makes no actual server-side changes to these services; you must then + * call run.updateService to save these changes. We divide this responsiblity + * because we want to possibly insert a new tagged target before saving. + */ +export async function gcTagsForServices(project: string, services: run.Service[]): Promise { + // region -> service -> tags + // We cannot simplify this into a single map because we might be mixing project + // id and number. + const validTagsByServiceByRegion: Record>> = {}; + const sites = await api.listSites(project); + const allVersionsNested = await Promise.all(sites.map((site) => api.listVersions(site.name))); + const activeVersions = [...flattenArray(allVersionsNested)].filter((version) => { + return version.status === "CREATED" || version.status === "FINALIZED"; + }); + for (const version of activeVersions) { + for (const rewrite of version?.config?.rewrites || []) { + if (!("run" in rewrite) || !rewrite.run.tag) { + continue; + } + validTagsByServiceByRegion[rewrite.run.region] = + validTagsByServiceByRegion[rewrite.run.region] || {}; + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId] = + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId] || new Set(); + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId].add(rewrite.run.tag); + } + } + + // Erase all traffic targets that have an expired tag and no serving percentage + for (const service of services) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const { region, serviceId } = run.gcpIds(service); + service.spec.traffic = (service.spec.traffic || []).filter((traffic) => { + // If we're serving traffic irrespective of the tag, leave this target + if (traffic.percent) { + return true; + } + // Only GC targets with tags + if (!traffic.tag) { + return true; + } + // Only GC targets with tags that look like we added them + if (!traffic.tag.startsWith("fh-")) { + return true; + } + if (validTagsByServiceByRegion[region]?.[serviceId]?.has(traffic.tag)) { + return true; + } + return false; + }); + } +} + +// The number of tags after which we start applying GC pressure. +let garbageCollectionThreshold = 500; + +/** + * Sets the garbage collection threshold for testing. + * @param threshold new GC threshold. + */ +export function setGarbageCollectionThreshold(threshold: number): void { + garbageCollectionThreshold = threshold; +} + +/** + * Ensures that all the listed run versions have pins. + */ +export async function setRewriteTags( + rewrites: api.Rewrite[], + project: string, + version: string +): Promise { + // Note: this is sub-optimal in the case where there are multiple rewrites + // to the same service. Should we deduplicate this? + const services: run.Service[] = await Promise.all( + rewrites + .map((rewrite) => { + if (!("run" in rewrite)) { + return null; + } + if (rewrite.run.tag !== TODO_TAG_NAME) { + return null; + } + + return run.getService( + `projects/${project}/locations/${rewrite.run.region}/services/${rewrite.run.serviceId}` + ); + }) + // filter does not drop the null annotation + .filter((s) => s !== null) as Array> + ); + // Unnecessary due to functional programming, but creates an observable side effect for tests + if (!services.length) { + return; + } + + const needsGC = services + .map((service) => { + return service.spec.traffic.filter((traffic) => traffic.tag).length; + }) + .some((length) => length >= garbageCollectionThreshold); + if (needsGC) { + await exports.gcTagsForServices(project, services); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const tags: Record> = await exports.ensureLatestRevisionTagged( + services, + `fh-${version}` + ); + for (const rewrite of rewrites) { + if (!("run" in rewrite) || rewrite.run.tag !== TODO_TAG_NAME) { + continue; + } + const tag = tags[rewrite.run.region][rewrite.run.serviceId]; + rewrite.run.tag = tag; + } +} + +/** + * Given an already fetched service, ensures that the latest revision + * has a tagged traffic target. + * If the service does not have a tagged target already, the service will be modified + * to include a new target and the change will be publisehd to prod. + * Returns a map of region to map of service to latest tag. + */ +export async function ensureLatestRevisionTagged( + services: run.Service[], + defaultTag: string +): Promise>> { + // Region -> Service -> Tag + const tags: Record> = {}; + const updateServices: Array> = []; + for (const service of services) { + const { projectNumber, region, serviceId } = run.gcpIds(service); + tags[region] = tags[region] || {}; + const latestRevisionTarget = service.status?.traffic.find((target) => target.latestRevision); + if (!latestRevisionTarget) { + throw new FirebaseError( + `Assertion failed: service ${service.metadata.name} has no latestRevision traffic target` + ); + } + const latestRevision = latestRevisionTarget.revisionName; + const alreadyTagged = service.spec.traffic.find( + (target) => target.revisionName === latestRevision && target.tag + ); + if (alreadyTagged) { + // Null assertion is safe because the predicate that found alreadyTagged + // checked for tag. + tags[region][serviceId] = alreadyTagged.tag!; + continue; + } + tags[region][serviceId] = defaultTag; + service.spec.traffic.push({ + revisionName: latestRevision, + tag: defaultTag, + }); + updateServices.push( + run.updateService( + `projects/${projectNumber}/locations/${region}/services/${serviceId}`, + service + ) + ); + } + + await Promise.all(updateServices); + return tags; +} diff --git a/src/test/deploy/hosting/convertConfig.spec.ts b/src/test/deploy/hosting/convertConfig.spec.ts index 1843bea793f4..0d0eff457cc5 100644 --- a/src/test/deploy/hosting/convertConfig.spec.ts +++ b/src/test/deploy/hosting/convertConfig.spec.ts @@ -1,32 +1,47 @@ import { expect } from "chai"; -import { HostingConfig } from "../../../firebaseConfig"; import { convertConfig } from "../../../deploy/hosting/convertConfig"; -import * as args from "../../../deploy/functions/args"; import * as backend from "../../../deploy/functions/backend"; +import { Context, HostingDeploy } from "../../../deploy/hosting/context"; +import { HostingSingle } from "../../../firebaseConfig"; +import * as api from "../../../hosting/api"; -const DEFAULT_CONTEXT = { - loadedExistingBackend: true, - existingBackend: { - endpoints: {}, - }, -}; +const FUNCTION_ID = "function"; +const PROJECT_ID = "project"; +const REGION = "region"; -const DEFAULT_PAYLOAD = {}; +function endpoint(opts?: Partial): backend.Endpoint { + // Createa type that allows us to not have a trigger + const ret: Omit & { httpsTrigger?: backend.HttpsTrigger } = { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + region: REGION, + runtime: "nodejs16", + platform: "gcfv1", + ...opts, + }; + if ( + !( + "httpsTrigger" in ret || + "eventTrigger" in ret || + "callableTrigger" in ret || + "scheduledTrigger" in ret || + "taskQueueTrigger" in ret || + "blockingTrigger" in ret + ) + ) { + ret.httpsTrigger = {}; + } + return ret as backend.Endpoint; +} describe("convertConfig", () => { const tests: Array<{ name: string; - input: HostingConfig | undefined; - want: any; - payload?: args.Payload; - finalize?: boolean; - context?: any; + input: HostingSingle; + want: api.ServingConfig; + existingBackend?: backend.Backend; }> = [ - { - name: "returns nothing if no config is provided", - input: undefined, - want: {}, - }, // Rewrites. { name: "returns rewrites for glob destination", @@ -40,238 +55,80 @@ describe("convertConfig", () => { }, { name: "checks for function region if unspecified", - input: { rewrites: [{ glob: "/foo", function: "foofn" }] }, - want: { rewrites: [{ glob: "/foo", function: "foofn", functionRegion: "us-central2" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central2", - platform: "gcfv1", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, - }, + input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, + existingBackend: backend.of(endpoint({ region: "us-central1" })), }, { name: "discovers the function region of a callable function", - input: { rewrites: [{ glob: "/foo", function: "foofn" }] }, - want: { rewrites: [{ glob: "/foo", function: "foofn", functionRegion: "us-central2" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central2", - platform: "gcfv1", - callableTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, - }, + input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, + existingBackend: backend.of(endpoint({ callableTrigger: {}, region: "us-central1" })), }, { name: "returns rewrites for glob CF3", - input: { rewrites: [{ glob: "/foo", function: "foofn", region: "europe-west2" }] }, - want: { rewrites: [{ glob: "/foo", function: "foofn", functionRegion: "europe-west2" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of( - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "europe-west2", - platform: "gcfv1", - httpsTrigger: {}, - }, - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - } - ), - haveBackend: backend.empty(), - }, - }, + input: { + rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID, region: "europe-west2" } }], }, + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "europe-west2" }] }, + existingBackend: backend.of(endpoint({ region: "europe-west2" }), endpoint()), }, { name: "defaults to a us-central1 rewrite if one is avaiable, v1 edition", - input: { rewrites: [{ glob: "/foo", function: "foofn" }] }, - want: { rewrites: [{ glob: "/foo", function: "foofn", functionRegion: "us-central1" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of( - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "europe-west2", - platform: "gcfv1", - httpsTrigger: {}, - }, - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv1", - httpsTrigger: {}, - } - ), - haveBackend: backend.empty(), - }, - }, - }, + input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, + existingBackend: backend.of(endpoint(), endpoint({ region: "us-central1" })), }, { name: "defaults to a us-central1 rewrite if one is avaiable, v2 edition", - input: { rewrites: [{ glob: "/foo", function: "foofn" }] }, - want: { rewrites: [{ glob: "/foo", run: { region: "us-central1", serviceId: "foofn" } }] }, - payload: { - functions: { - default: { - wantBackend: backend.of( - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "europe-west2", - platform: "gcfv1", - httpsTrigger: {}, - }, - { - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - } - ), - haveBackend: backend.empty(), - }, - }, + input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, + want: { + rewrites: [{ glob: "/foo", run: { region: "us-central1", serviceId: FUNCTION_ID } }], }, + existingBackend: backend.of( + endpoint({ platform: "gcfv2" }), + endpoint({ platform: "gcfv2", region: "us-central1" }) + ), }, { name: "returns rewrites for regex CF3", - input: { rewrites: [{ regex: "/foo$", function: "foofn", region: "us-central1" }] }, - want: { rewrites: [{ regex: "/foo$", function: "foofn", functionRegion: "us-central1" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv1", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, + input: { + rewrites: [{ regex: "/foo$", function: { functionId: FUNCTION_ID, region: REGION } }], }, - }, - { - name: "skips functions referencing CF3v2 functions being deployed (during prepare)", - input: { rewrites: [{ regex: "/foo$", function: "foofn", region: "us-central1" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, + want: { + rewrites: [{ regex: "/foo$", function: FUNCTION_ID, functionRegion: REGION }], }, - want: { rewrites: [] }, - finalize: false, + existingBackend: backend.of(endpoint()), }, { name: "rewrites referencing CF3v2 functions being deployed are changed to Cloud Run (during release)", - input: { rewrites: [{ regex: "/foo$", function: "foofn", region: "us-central1" }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "foofn", - project: "my-project", - entryPoint: "foofn", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, - }, - want: { rewrites: [{ regex: "/foo$", run: { serviceId: "foofn", region: "us-central1" } }] }, - finalize: true, + input: { rewrites: [{ regex: "/foo$", function: { functionId: FUNCTION_ID } }] }, + want: { rewrites: [{ regex: "/foo$", run: { serviceId: FUNCTION_ID, region: REGION } }] }, + existingBackend: backend.of(endpoint({ platform: "gcfv2" })), }, { name: "rewrites referencing existing CF3v2 functions are changed to Cloud Run (during prepare)", - input: { rewrites: [{ regex: "/foo$", function: "foofn", region: "us-central1" }] }, - context: { - loadedExistingBackend: true, - existingBackend: { - endpoints: { - "us-central1": { - foofn: { id: "foofn", region: "us-central1", platform: "gcfv2", httpsTrigger: true }, - }, - }, - }, + input: { + rewrites: [ + { regex: "/foo$", function: { functionId: FUNCTION_ID, region: "us-central1" } }, + ], + }, + want: { + rewrites: [{ regex: "/foo$", run: { serviceId: FUNCTION_ID, region: "us-central1" } }], }, - want: { rewrites: [{ regex: "/foo$", run: { serviceId: "foofn", region: "us-central1" } }] }, - finalize: true, + existingBackend: backend.of(endpoint({ platform: "gcfv2", region: "us-central1" })), }, { name: "rewrites referencing existing CF3v2 functions are changed to Cloud Run (during release)", - input: { rewrites: [{ regex: "/foo$", function: "foofn", region: "us-central1" }] }, - context: { - loadedExistingBackend: true, - existingBackend: { - endpoints: { - "us-central1": { - foofn: { id: "foofn", region: "us-central1", platform: "gcfv2", httpsTrigger: true }, - }, - }, - }, + input: { + rewrites: [ + { regex: "/foo$", function: { functionId: FUNCTION_ID, region: "us-central1" } }, + ], + }, + existingBackend: backend.of(endpoint({ platform: "gcfv2", region: "us-central1" })), + want: { + rewrites: [{ regex: "/foo$", run: { serviceId: FUNCTION_ID, region: "us-central1" } }], }, - want: { rewrites: [{ regex: "/foo$", run: { serviceId: "foofn", region: "us-central1" } }] }, - finalize: true, }, { name: "returns rewrites for glob Run", @@ -283,62 +140,16 @@ describe("convertConfig", () => { input: { rewrites: [{ regex: "/foo$", run: { serviceId: "hello" } }] }, want: { rewrites: [{ regex: "/foo$", run: { region: "us-central1", serviceId: "hello" } }] }, }, - { - name: "skips rewrites for Cloud Run instances being deployed (during prepare)", - input: { rewrites: [{ regex: "/foo$", run: { serviceId: "hello" } }] }, - want: { rewrites: [] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "hello", - project: "my-project", - entryPoint: "hello", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, - }, - finalize: false, - }, { name: "return rewrites for Cloud Run instances being deployed (during release)", input: { rewrites: [{ regex: "/foo$", run: { serviceId: "hello" } }] }, want: { rewrites: [{ regex: "/foo$", run: { region: "us-central1", serviceId: "hello" } }] }, - payload: { - functions: { - default: { - wantBackend: backend.of({ - id: "hello", - project: "my-project", - entryPoint: "hello", - runtime: "nodejs14", - region: "us-central1", - platform: "gcfv2", - httpsTrigger: {}, - }), - haveBackend: backend.empty(), - }, - }, - }, - finalize: true, }, { name: "returns the specified rewrite even if it's not found", - input: { rewrites: [{ glob: "/foo", function: "foofn" }] }, - payload: { - functions: { - default: { - wantBackend: backend.empty(), - haveBackend: backend.empty(), - }, - }, - }, - want: { rewrites: [{ glob: "/foo", function: "foofn" }] }, + input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID }] }, + existingBackend: backend.empty(), }, { name: "returns rewrites for Run with specified regions", @@ -443,16 +254,22 @@ describe("convertConfig", () => { }, ]; - for (const { - name, - context = DEFAULT_CONTEXT, - input, - payload = DEFAULT_PAYLOAD, - want, - finalize = true, - } of tests) { + for (const { name, input, existingBackend, want } of tests) { it(name, async () => { - const config = await convertConfig(context, payload, input, finalize); + const context: Context = { + projectId: PROJECT_ID, + loadedExistingBackend: true, + existingBackend: existingBackend || backend.empty(), + unreachableRegions: { + gcfV1: [], + gcfV2: [], + }, + }; + const deploy: HostingDeploy = { + config: { site: "site", ...input }, + version: "version", + }; + const config = await convertConfig(context, deploy); expect(config).to.deep.equal(want); }); } diff --git a/src/test/gcp/proto.spec.ts b/src/test/gcp/proto.spec.ts index b970bffe4a73..a8b52f214463 100644 --- a/src/test/gcp/proto.spec.ts +++ b/src/test/gcp/proto.spec.ts @@ -212,4 +212,42 @@ describe("proto", () => { expect(formatted).to.eq(`serviceAccount:${serviceAccount}`); }); }); + + it("pruneUndefindes", () => { + interface Interface { + foo?: string; + bar: string; + baz: { + alpha: Array; + bravo?: string; + charlie?: string; + }; + qux?: Record; + } + const src: Interface = { + foo: undefined, + bar: "bar", + baz: { + alpha: ["alpha", undefined], + bravo: undefined, + charlie: "charlie", + }, + qux: undefined, + }; + + const trimmed: Interface = { + bar: "bar", + baz: { + alpha: ["alpha"], + charlie: "charlie", + }, + }; + + // Show there is a problem + expect(src).to.not.deep.equal(trimmed); + + // Show we have the fix + proto.pruneUndefiends(src); + expect(src).to.deep.equal(trimmed); + }); }); diff --git a/src/test/hosting/api.spec.ts b/src/test/hosting/api.spec.ts index 24dc774240ad..5f00b7fb8f23 100644 --- a/src/test/hosting/api.spec.ts +++ b/src/test/hosting/api.spec.ts @@ -435,7 +435,8 @@ describe("hosting", () => { it("should make the API request to create a release", async () => { const CHANNEL_ID = "my-channel"; const RELEASE = { name: "my-new-release" }; - const VERSION_NAME = "versions/me"; + const VERSION = "version"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; nock(hostingApiOrigin) .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`) .query({ versionName: VERSION_NAME }) @@ -449,7 +450,8 @@ describe("hosting", () => { it("should throw an error if the server returns an error", async () => { const CHANNEL_ID = "my-channel"; - const VERSION_NAME = "versions/me"; + const VERSION = "VERSION"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; nock(hostingApiOrigin) .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`) .query({ versionName: VERSION_NAME }) diff --git a/src/test/hosting/config.spec.ts b/src/test/hosting/config.spec.ts index 79c417ddd7f2..745c0bf67b5c 100644 --- a/src/test/hosting/config.spec.ts +++ b/src/test/hosting/config.spec.ts @@ -5,6 +5,7 @@ import { HostingConfig, HostingMultiple, HostingSingle } from "../../firebaseCon import * as config from "../../hosting/config"; import { HostingOptions } from "../../hosting/options"; import { RequireAtLeastOne } from "../../metaprogramming"; +import { cloneDeep } from "../../utils"; function options( hostingConfig: HostingConfig, @@ -239,6 +240,84 @@ describe("config", () => { } }); + it("normalize", () => { + it("upgrades function configs", () => { + const configs: HostingMultiple = [ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + function: "functionId", + }, + { + glob: "**", + function: "function2", + region: "region", + }, + ], + }, + ]; + config.normalize(configs); + expect(configs).to.deep.equal([ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + function: { + functionid: "functionId", + }, + }, + { + glob: "**", + function: { + functionId: "function2", + region: "region", + }, + }, + ], + }, + ]); + }); + + it("leaves other rewrites alone", () => { + const configs: HostingMultiple = [ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + destination: "index.html", + }, + { + glob: "**", + function: { + functionId: "functionId", + }, + }, + { + glob: "**", + run: { + serviceId: "service", + }, + }, + { + glob: "**", + dynamicLinks: true, + }, + ], + }, + ]; + const expected = cloneDeep(configs); + config.normalize(configs); + expect(configs).to.deep.equal(expected); + }); + }); + const PUBLIC_DIR_ERROR_PREFIX = /Must supply a "public" directory/; describe("validate", () => { const tests: Array<{ diff --git a/src/test/hosting/runTags.spec.ts b/src/test/hosting/runTags.spec.ts new file mode 100644 index 000000000000..647dd73f907a --- /dev/null +++ b/src/test/hosting/runTags.spec.ts @@ -0,0 +1,307 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as runNS from "../../gcp/run"; +import * as hostingNS from "../../hosting/api"; +import * as runTagsNS from "../../hosting/runTags"; +import { cloneDeep } from "../../utils"; + +const REGION = "REGION"; +const SERVICE = "SERVICE"; +const PROJECT = "PROJECT"; + +describe("runTags", () => { + let run: sinon.SinonStubbedInstance; + let hosting: sinon.SinonStubbedInstance; + let runTags: sinon.SinonStubbedInstance; + const site: hostingNS.Site = { + name: "projects/project/sites/site", + defaultUrl: "https://google.com", + appId: "appId", + labels: {}, + }; + + function version( + version: string, + status: hostingNS.VersionStatus, + ...rewrites: hostingNS.RunRewrite[] + ): hostingNS.Version { + return { + name: `projects/project/sites/site/versions/${version}`, + status: status, + config: { + rewrites: rewrites.map((r) => { + return { regex: ".*", run: r }; + }), + }, + createTime: "now", + createUser: { + email: "inlined@gmail.com", + }, + fileCount: 0, + versionBytes: 0, + }; + } + + function service(id: string, ...tags: Array): runNS.Service { + return { + apiVersion: "serving.knative.dev/v1", + kind: "Service", + metadata: { + name: id, + namespace: PROJECT, + labels: { + [runNS.LOCATION_LABEL]: REGION, + }, + }, + spec: { + template: { + metadata: { + name: "revision", + namespace: "project", + }, + spec: { + containers: [], + }, + }, + traffic: [ + { + latestRevision: true, + percent: 100, + }, + ...tags.map((tag) => { + if (typeof tag === "string") { + return { + revisionName: `revision-${tag}`, + tag: tag, + percent: 0, + }; + } else { + return tag; + } + }), + ], + }, + status: { + observedGeneration: 50, + latestCreatedRevisionName: "latest", + latestRevisionName: "latest", + traffic: [ + { + revisionName: "latest", + latestRevision: true, + percent: 100, + }, + ...tags.map((tag) => { + if (typeof tag === "string") { + return { + revisionName: `revision-${tag}`, + tag: tag, + percent: 0, + }; + } else { + return { + percent: 0, + ...tag, + }; + } + }), + ], + conditions: [], + url: "https://google.com", + address: { + url: "https://google.com", + }, + }, + }; + } + + beforeEach(() => { + // We need the library to attempt to do something for us to observe side effects. + run = sinon.stub(runNS); + hosting = sinon.stub(hostingNS); + runTags = sinon.stub(runTagsNS); + + hosting.listSites.withArgs(PROJECT).resolves([site]); + hosting.listVersions.rejects(new Error("Unexpected hosting.listSites")); + + run.getService.rejects(new Error("Unexpected run.getService")); + run.updateService.rejects(new Error("Unexpected run.updateService")); + run.gcpIds.restore(); + + runTags.ensureLatestRevisionTagged.throws( + new Error("Unexpected runTags.ensureLatestRevisionTagged") + ); + runTags.gcTagsForServices.rejects(new Error("Unepxected runTags.gcTagsForServices")); + runTags.setRewriteTags.rejects(new Error("Unexpected runTags.setRewriteTags call")); + runTags.setGarbageCollectionThreshold.restore(); + }); + + afterEach(() => { + sinon.restore(); + }); + + function tagsIn(service: runNS.Service): string[] { + return service.spec.traffic.map((t) => t.tag).filter((t) => !!t) as string[]; + } + + describe("gcTagsForServices", () => { + beforeEach(() => { + runTags.gcTagsForServices.restore(); + }); + + it("leaves only active revisions", async () => { + hosting.listVersions.resolves([ + version("v1", "FINALIZED", { serviceId: "s1", region: REGION, tag: "fh-in-use1" }), + version("v2", "CREATED", { serviceId: "s1", region: REGION, tag: "fh-in-use2" }), + version("v3", "DELETED", { serviceId: "s1", region: REGION, tag: "fh-deleted-version" }), + ]); + + const s1 = service( + "s1", + "fh-in-use1", + "fh-in-use2", + "fh-deleted-version", + "fh-no-longer-referenced", + "not-by-us" + ); + const s2 = service("s2", "fh-no-reference"); + s2.spec.traffic.push({ + revisionName: "manual-split", + tag: "fh-manual-split", + percent: 1, + }); + await runTags.gcTagsForServices(PROJECT, [s1, s2]); + + expect(tagsIn(s1)).to.deep.equal(["fh-in-use1", "fh-in-use2", "not-by-us"]); + expect(tagsIn(s2)).to.deep.equal(["fh-manual-split"]); + }); + }); + + describe("setRewriteTags", () => { + const svc = service(SERVICE); + const svcName = `projects/${PROJECT}/locations/${REGION}/services/${SERVICE}`; + beforeEach(() => { + runTags.setRewriteTags.restore(); + }); + + it("preserves existing tags and other types of rewrites", async () => { + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + path: "/index.html", + }, + { + glob: "/dynamic", + run: { + serviceId: "service", + region: "us-central1", + tag: "someone-is-using-this-code-in-a-way-i-dont-expect", + }, + }, + { + glob: "/callable", + function: "function", + functionRegion: "us-central1", + }, + ]; + const original = cloneDeep(rewrites); + await runTags.setRewriteTags(rewrites, "project", "version"); + expect(rewrites).to.deep.equal(original); + }); + + it("replaces tags in rewrites with new/verified tags", async () => { + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: runTagsNS.TODO_TAG_NAME, + }, + }, + ]; + + run.getService.withArgs(svcName).resolves(svc); + // Calls fake apparently doesn't trum the default rejects command + runTags.ensureLatestRevisionTagged.resetBehavior(); + runTags.ensureLatestRevisionTagged.callsFake( + (svc: runNS.Service[], tag: string): Promise>> => { + expect(tag).to.equal("fh-version"); + svc[0].spec.traffic.push({ revisionName: "latest", tag }); + return Promise.resolve({ [REGION]: { [SERVICE]: tag } }); + } + ); + + await runTags.setRewriteTags(rewrites, PROJECT, "version"); + expect(rewrites).to.deep.equal([ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: "fh-version", + }, + }, + ]); + }); + + it("garbage collects if necessary", async () => { + runTagsNS.setGarbageCollectionThreshold(2); + const svc = service(SERVICE, "fh-1", "fh-2"); + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: runTagsNS.TODO_TAG_NAME, + }, + }, + ]; + run.getService.withArgs(svcName).resolves(svc); + runTags.gcTagsForServices.resolves(); + runTags.ensureLatestRevisionTagged.resolves({ [REGION]: { [SERVICE]: "fh-3" } }); + await runTags.setRewriteTags(rewrites, PROJECT, "3"); + expect(runTags.ensureLatestRevisionTagged); + expect(runTags.gcTagsForServices).to.have.been.called; + }); + }); + + describe("ensureLatestRevisionTagged", () => { + beforeEach(() => { + runTags.ensureLatestRevisionTagged.restore(); + }); + + it("Reuses existing tag names", async () => { + const svc = service(SERVICE, { revisionName: "latest", tag: "existing" }); + await runTags.ensureLatestRevisionTagged([svc], "new-tag"); + expect(svc.spec.traffic).to.deep.equal([ + { + latestRevision: true, + percent: 100, + }, + { + revisionName: "latest", + tag: "existing", + }, + ]); + expect(run.updateService).to.not.have.been.called; + }); + + it("Adds new tags as necessary", async () => { + const svc = service(SERVICE); + run.updateService.resolves(); + await runTags.ensureLatestRevisionTagged([svc], "new-tag"); + expect(svc.spec.traffic).to.deep.equal([ + { + latestRevision: true, + percent: 100, + }, + { + revisionName: "latest", + tag: "new-tag", + }, + ]); + }); + }); +}); From cd737c91b790b58555d4bf65320232b0b0a207fe Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Tue, 4 Oct 2022 16:09:51 -0400 Subject: [PATCH 005/115] Adds emulator support for v2 rtdb triggers (#5045) * changing how we detect non implemented events and splitting up the trigger register by platform * fixing pubsub string * adding int tests * formatter * increasing timeout of after func * fixing timeout * add comments & pr updates * add changelog entry --- CHANGELOG.md | 1 + scripts/integration-helpers/framework.ts | 6 ++ scripts/triggers-end-to-end-tests/tests.ts | 1 + .../triggers/package-lock.json | 14 +-- .../triggers/package.json | 2 +- .../v1/package-lock.json | 14 +-- .../triggers-end-to-end-tests/v1/package.json | 2 +- scripts/triggers-end-to-end-tests/v2/index.js | 7 ++ .../v2/package-lock.json | 14 +-- .../triggers-end-to-end-tests/v2/package.json | 2 +- src/emulator/functionsEmulator.ts | 87 ++++++++++++++----- src/emulator/functionsEmulatorRuntime.ts | 8 +- src/emulator/functionsEmulatorShared.ts | 32 +++++-- src/functions/events/v2.ts | 2 +- 14 files changed, 134 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..cf244b860b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add functions emulator support for RTDB v2 triggers (#5045). diff --git a/scripts/integration-helpers/framework.ts b/scripts/integration-helpers/framework.ts index 88596bcde166..a5aeb59df3fa 100644 --- a/scripts/integration-helpers/framework.ts +++ b/scripts/integration-helpers/framework.ts @@ -23,6 +23,7 @@ const STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG = "========== STORAGE BUCKET V2 FUNCTION FINALIZED =========="; const STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG = "========== STORAGE BUCKET V2 FUNCTION METADATA =========="; +const RTDB_V2_FUNCTION_LOG = "========== RTDB V2 FUNCTION =========="; /* Functions V1 */ const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION =========="; const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION =========="; @@ -141,6 +142,7 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest { storageBucketV2MetadataTriggerCount = 0; authBlockingCreateV2TriggerCount = 0; authBlockingSignInV2TriggerCount = 0; + rtdbV2TriggerCount = 0; rtdbFromFirestore = false; firestoreFromRtdb = false; @@ -176,6 +178,7 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest { this.storageBucketV2MetadataTriggerCount = 0; this.authBlockingCreateV2TriggerCount = 0; this.authBlockingSignInV2TriggerCount = 0; + this.rtdbV2TriggerCount = 0; } /* @@ -268,6 +271,9 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest { if (data.includes(AUTH_BLOCKING_SIGN_IN_V2_LOG)) { this.authBlockingSignInV2TriggerCount++; } + if (data.includes(RTDB_V2_FUNCTION_LOG)) { + this.rtdbV2TriggerCount++; + } }); return startEmulators; diff --git a/scripts/triggers-end-to-end-tests/tests.ts b/scripts/triggers-end-to-end-tests/tests.ts index 9baa9a819c36..17a3aadb85cb 100755 --- a/scripts/triggers-end-to-end-tests/tests.ts +++ b/scripts/triggers-end-to-end-tests/tests.ts @@ -148,6 +148,7 @@ describe("function triggers", () => { it("should have have triggered cloud functions", () => { expect(test.rtdbTriggerCount).to.equal(1); + expect(test.rtdbV2TriggerCount).to.eq(1); expect(test.firestoreTriggerCount).to.equal(1); /* * Check for the presence of all expected documents in the firestore diff --git a/scripts/triggers-end-to-end-tests/triggers/package-lock.json b/scripts/triggers-end-to-end-tests/triggers/package-lock.json index 1392dc6a4a4f..1d007bd3b1d3 100644 --- a/scripts/triggers-end-to-end-tests/triggers/package-lock.json +++ b/scripts/triggers-end-to-end-tests/triggers/package-lock.json @@ -10,7 +10,7 @@ "@google-cloud/pubsub": "^3.0.1", "firebase": "^9.9.0", "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" @@ -1652,9 +1652,9 @@ } }, "node_modules/firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", @@ -4589,9 +4589,9 @@ } }, "firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "requires": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", diff --git a/scripts/triggers-end-to-end-tests/triggers/package.json b/scripts/triggers-end-to-end-tests/triggers/package.json index cc2f50d7b05c..5e2e278cb5df 100644 --- a/scripts/triggers-end-to-end-tests/triggers/package.json +++ b/scripts/triggers-end-to-end-tests/triggers/package.json @@ -10,7 +10,7 @@ "@google-cloud/pubsub": "^3.0.1", "firebase": "^9.9.0", "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" diff --git a/scripts/triggers-end-to-end-tests/v1/package-lock.json b/scripts/triggers-end-to-end-tests/v1/package-lock.json index c4b4e5fc12de..4dc57f32c36b 100644 --- a/scripts/triggers-end-to-end-tests/v1/package-lock.json +++ b/scripts/triggers-end-to-end-tests/v1/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "@firebase/database-compat": "0.1.2", "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" @@ -993,9 +993,9 @@ } }, "node_modules/firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", @@ -3272,9 +3272,9 @@ } }, "firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "requires": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", diff --git a/scripts/triggers-end-to-end-tests/v1/package.json b/scripts/triggers-end-to-end-tests/v1/package.json index d502552027c9..4f5fa9cbe7b1 100644 --- a/scripts/triggers-end-to-end-tests/v1/package.json +++ b/scripts/triggers-end-to-end-tests/v1/package.json @@ -8,7 +8,7 @@ "dependencies": { "@firebase/database-compat": "0.1.2", "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" diff --git a/scripts/triggers-end-to-end-tests/v2/index.js b/scripts/triggers-end-to-end-tests/v2/index.js index 04c4e9013f9b..7d1aeb89bff3 100644 --- a/scripts/triggers-end-to-end-tests/v2/index.js +++ b/scripts/triggers-end-to-end-tests/v2/index.js @@ -22,8 +22,10 @@ const AUTH_BLOCKING_CREATE_V2_LOG = "========== AUTH BLOCKING CREATE V2 FUNCTION METADATA =========="; const AUTH_BLOCKING_SIGN_IN_V2_LOG = "========== AUTH BLOCKING SIGN IN V2 FUNCTION METADATA =========="; +const RTDB_LOG = "========== RTDB V2 FUNCTION =========="; const PUBSUB_TOPIC = "test-topic"; +const START_DOCUMENT_NAME = "test/start"; admin.initializeApp(); @@ -125,3 +127,8 @@ exports.onreqv2timeout = functionsV2.https.onRequest({ timeoutSeconds: 1 }, asyn }, 3_000); }); }); + +exports.rtdbv2reaction = functionsV2.database.onValueWritten(START_DOCUMENT_NAME, (event) => { + console.log(RTDB_LOG); + return; +}); diff --git a/scripts/triggers-end-to-end-tests/v2/package-lock.json b/scripts/triggers-end-to-end-tests/v2/package-lock.json index e16d96197cbd..0a40c182dec7 100644 --- a/scripts/triggers-end-to-end-tests/v2/package-lock.json +++ b/scripts/triggers-end-to-end-tests/v2/package-lock.json @@ -7,7 +7,7 @@ "name": "functions", "dependencies": { "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" @@ -870,9 +870,9 @@ } }, "node_modules/firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", @@ -3016,9 +3016,9 @@ } }, "firebase-functions": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.22.0.tgz", - "integrity": "sha512-d1BxBpT95MhvVqXkpLWDvWbyuX7e2l69cFAiqG3U1XQDaMV88bM9S+Zg7H8i9pitEGFr+76ErjKgrY0n+g3ZDA==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", "requires": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", diff --git a/scripts/triggers-end-to-end-tests/v2/package.json b/scripts/triggers-end-to-end-tests/v2/package.json index b09ddce4af3a..6ef584fe77af 100644 --- a/scripts/triggers-end-to-end-tests/v2/package.json +++ b/scripts/triggers-end-to-end-tests/v2/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "firebase-admin": "^11.0.0", - "firebase-functions": "^3.22.0" + "firebase-functions": "^3.24.1" }, "devDependencies": { "firebase-functions-test": "^0.2.0" diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 42ec5cc9f332..387cc334eea7 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -589,7 +589,9 @@ export class FunctionsEmulator implements EmulatorInstance { added = await this.addRealtimeDatabaseTrigger( this.args.projectId, key, - definition.eventTrigger + definition.eventTrigger, + signature, + definition.region ); break; case Constants.SERVICE_PUBSUB: @@ -721,21 +723,12 @@ export class FunctionsEmulator implements EmulatorInstance { } } - async addRealtimeDatabaseTrigger( - projectId: string, - key: string, - eventTrigger: EventTrigger - ): Promise { - const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE); - if (!databaseEmu) { - return false; - } - - const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource); + private getV1DatabaseApiAttributes(projectId: string, key: string, eventTrigger: EventTrigger) { + const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource!); if (result === null || result.length !== 3) { this.logger.log( "WARN", - `Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource}` + `Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource!}` ); throw new FirebaseError(`Event function ${key} has malformed resource member`); } @@ -748,11 +741,9 @@ export class FunctionsEmulator implements EmulatorInstance { topic: `projects/${projectId}/topics/${key}`, }); - logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); - - let setTriggersPath = "/.settings/functionTriggers.json"; + let apiPath = "/.settings/functionTriggers.json"; if (instance !== "") { - setTriggersPath += `?ns=${instance}`; + apiPath += `?ns=${instance}`; } else { this.logger.log( "WARN", @@ -760,12 +751,66 @@ export class FunctionsEmulator implements EmulatorInstance { ); } + return { bundle, apiPath, instance }; + } + + private getV2DatabaseApiAttributes( + projectId: string, + key: string, + eventTrigger: EventTrigger, + region: string + ) { + const instance = + eventTrigger.eventFilters?.instance || eventTrigger.eventFilterPathPatterns?.instance; + if (!instance) { + throw new FirebaseError("A database instance must be supplied."); + } + + const ref = eventTrigger.eventFilterPathPatterns?.ref; + if (!ref) { + throw new FirebaseError("A database reference must be supplied."); + } + + // The 'namespacePattern' determines that we are using the v2 interface + const bundle = JSON.stringify({ + name: `projects/${projectId}/locations/${region}/triggers/${key}`, + path: ref, + event: eventTrigger.eventType, + topic: `projects/${projectId}/topics/${key}`, + namespacePattern: instance, + }); + + // The query parameter '?ns=${instance}' is ignored in v2 + const apiPath = "/.settings/functionTriggers.json"; + + return { bundle, apiPath, instance }; + } + + async addRealtimeDatabaseTrigger( + projectId: string, + key: string, + eventTrigger: EventTrigger, + signature: SignatureType, + region: string + ): Promise { + const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE); + if (!databaseEmu) { + return false; + } + + const { bundle, apiPath, instance } = + signature === "cloudevent" + ? this.getV2DatabaseApiAttributes(projectId, key, eventTrigger, region) + : this.getV1DatabaseApiAttributes(projectId, key, eventTrigger); + + logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); + const client = new Client({ urlPrefix: `http://${EmulatorRegistry.getInfoHostString(databaseEmu.getInfo())}`, auth: false, }); try { - await client.post(setTriggersPath, bundle, { headers: { Authorization: "Bearer owner" } }); + await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } }); } catch (err: any) { this.logger.log("WARN", "Error adding Realtime Database function: " + err); throw err; @@ -819,7 +864,7 @@ export class FunctionsEmulator implements EmulatorInstance { logger.debug(`addPubsubTrigger`, JSON.stringify({ eventTrigger })); // "resource":\"projects/{PROJECT_ID}/topics/{TOPIC_ID}"; - const resource = eventTrigger.resource; + const resource = eventTrigger.resource!; let topic; if (schedule) { // In production this topic looks like @@ -852,8 +897,8 @@ export class FunctionsEmulator implements EmulatorInstance { addStorageTrigger(projectId: string, key: string, eventTrigger: EventTrigger): boolean { logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger })); - const bucket = eventTrigger.resource.startsWith("projects/_/buckets/") - ? eventTrigger.resource.split("/")[3] + const bucket = eventTrigger.resource!.startsWith("projects/_/buckets/") + ? eventTrigger.resource!.split("/")[3] : eventTrigger.resource; const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`; const triggers = this.multicastTriggers[eventTriggerId] || []; diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index 73bef36d545e..d514c43181ef 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -1048,11 +1048,9 @@ async function main(): Promise { case "cloudevent": const rawBody = (req as RequestWithRawBody).rawBody; let reqBody = JSON.parse(rawBody.toString()); - if (req.headers["content-type"]?.includes("cloudevent")) { - if (EventUtils.isBinaryCloudEvent(req)) { - reqBody = EventUtils.extractBinaryCloudEventContext(req); - reqBody.data = req.body; - } + if (EventUtils.isBinaryCloudEvent(req)) { + reqBody = EventUtils.extractBinaryCloudEventContext(req); + reqBody.data = req.body; } await processBackground(trigger, reqBody, FUNCTION_SIGNATURE); res.send({ status: "acknowledged" }); diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 784aea7dacf5..3c9d11099cd0 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -15,6 +15,14 @@ import { ExtensionSpec, ExtensionVersion } from "../extensions/types"; import { replaceConsoleLinks } from "./extensions/postinstall"; import { serviceForEndpoint } from "../deploy/functions/services"; import { inferBlockingDetails } from "../deploy/functions/prepare"; +import * as events from "../functions/events"; + +/** The current v2 events that are implemented in the emulator */ +const V2_EVENTS = [ + events.v2.PUBSUB_PUBLISH_EVENT, + ...events.v2.STORAGE_EVENTS, + ...events.v2.DATABASE_EVENTS, +]; export type SignatureType = "http" | "event" | "cloudevent"; @@ -50,10 +58,11 @@ export interface EventSchedule { } export interface EventTrigger { - resource: string; + resource?: string; eventType: string; channel?: string; eventFilters?: Record; + eventFilterPathPatterns?: Record; // Deprecated service?: string; } @@ -117,6 +126,13 @@ export class EmulatedTrigger { } } +/** + * Checks if the v2 event service has been implemented in the emulator + */ +export function eventServiceImplemented(eventType: string): boolean { + return V2_EVENTS.includes(eventType); +} + /** * Validates that triggers are correctly formed and fills in some defaults. */ @@ -172,19 +188,21 @@ export function emulatedFunctionsFromEndpoints( resource: eventTrigger.eventFilters!.resource, }; } else { - // Only pubsub and storage events are supported for gcfv2. - // Custom events require a channel. - const { resource, topic, bucket } = endpoint.eventTrigger.eventFilters as any; - const eventResource = resource || topic || bucket; - if (!eventResource && !eventTrigger.channel) { - // Unsupported event type for GCFv2 + // TODO(colerogers): v2 events implemented are pubsub, storage, rtdb, and custom events + if (!eventServiceImplemented(eventTrigger.eventType) && !eventTrigger.channel) { continue; } + + // We use resource for pubsub & storage + const { resource, topic, bucket } = endpoint.eventTrigger.eventFilters as any; + const eventResource = resource || topic || bucket; + def.eventTrigger = { eventType: eventTrigger.eventType, resource: eventResource, channel: eventTrigger.channel, eventFilters: eventTrigger.eventFilters, + eventFilterPathPatterns: eventTrigger.eventFilterPathPatterns, }; } } else if (backend.isScheduleTriggered(endpoint)) { diff --git a/src/functions/events/v2.ts b/src/functions/events/v2.ts index d4cf0ee4d246..01b9fc491c64 100644 --- a/src/functions/events/v2.ts +++ b/src/functions/events/v2.ts @@ -1,4 +1,4 @@ -export const PUBSUB_PUBLISH_EVENT = "google.cloud.pubsub.topic.v1.messagePublished" as const; +export const PUBSUB_PUBLISH_EVENT = "google.cloud.pubsub.topic.v1.messagePublished"; export const STORAGE_EVENTS = [ "google.cloud.storage.object.v1.finalized", From 7d9ece6c13b73574fdfd0a2a7c48caf408625bbc Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:47:36 -0700 Subject: [PATCH 006/115] Single project mode (#4890) * Single project mode config for the CLI and firestore. * JSON schema update * Prettier fix * Prettier fixes * Prettier Co-authored-by: Bryan Kendall --- CHANGELOG.md | 1 + schema/firebase-config.json | 3 +++ src/emulator/controller.ts | 19 ++++++++++++++++++- src/emulator/downloadableEmulators.ts | 4 ++++ src/emulator/firestoreEmulator.ts | 8 +++++--- src/firebaseConfig.ts | 1 + src/init/features/emulators.ts | 5 +++++ 7 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf244b860b9d..f7489e80d141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Add functions emulator support for RTDB v2 triggers (#5045). +- Enables single project mode for Firestore by default (#4890). diff --git a/schema/firebase-config.json b/schema/firebase-config.json index e78954181528..6a20dd6d5009 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -266,6 +266,9 @@ }, "type": "object" }, + "singleProjectMode": { + "type": "boolean" + }, "storage": { "additionalProperties": false, "properties": { diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 28f6069b50ad..d879e587f446 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -622,7 +622,7 @@ export async function startAll( host: firestoreAddr.host, port: firestoreAddr.port, websocket_port: websocketPort, - projectId, + project_id: projectId, auto_download: true, }; @@ -677,6 +677,23 @@ export async function startAll( ); } + // undefined in the config defaults to setting single_project_mode. + if ( + options.config.src.emulators?.singleProjectMode === undefined || + options.config.src.emulators?.singleProjectMode + ) { + if (projectId) { + args.single_project_mode = true; + args.single_project_mode_error = false; + } else { + firestoreLogger.logLabeled( + "DEBUG", + "firestore", + "Could not enable single_project_mode: missing projectId." + ); + } + } + const firestoreEmulator = new FirestoreEmulator(args); await startEmulator(firestoreEmulator); firestoreLogger.logLabeled( diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index c0487f2f2583..52ff258cfd9b 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -163,6 +163,10 @@ const Commands: { [s in DownloadableEmulators]: DownloadableEmulatorCommand } = "websocket_port", "functions_emulator", "seed_from_export", + "project_id", + "single_project_mode", + // TODO(christhompson) Re-enable after firestore accepts this flag. + // "single_project_mode_error", ], joinArgs: false, }, diff --git a/src/emulator/firestoreEmulator.ts b/src/emulator/firestoreEmulator.ts index 42b51d0fc820..87323172fe9c 100644 --- a/src/emulator/firestoreEmulator.ts +++ b/src/emulator/firestoreEmulator.ts @@ -15,11 +15,13 @@ export interface FirestoreEmulatorArgs { port?: number; host?: string; websocket_port?: number; - projectId?: string; + project_id?: string; rules?: string; functions_emulator?: string; auto_download?: boolean; seed_from_export?: string; + single_project_mode?: boolean; + single_project_mode_error?: boolean; } export class FirestoreEmulator implements EmulatorInstance { @@ -35,7 +37,7 @@ export class FirestoreEmulator implements EmulatorInstance { this.args.functions_emulator = EmulatorRegistry.getInfoHostString(functionsInfo); } - if (this.args.rules && this.args.projectId) { + if (this.args.rules && this.args.project_id) { const rulesPath = this.args.rules; this.rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true }); this.rulesWatcher.on("change", async () => { @@ -94,7 +96,7 @@ export class FirestoreEmulator implements EmulatorInstance { } private async updateRules(content: string): Promise { - const projectId = this.args.projectId; + const projectId = this.args.project_id; const info = this.getInfo(); const body = { diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 9d78537284a8..58c9879d396e 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -192,6 +192,7 @@ export type EmulatorsConfig = { host?: string; port?: number; }; + singleProjectMode?: boolean; }; export type ExtensionsConfig = Record; diff --git a/src/init/features/emulators.ts b/src/init/features/emulators.ts index 67b6353f5252..84e6ee97ad99 100644 --- a/src/init/features/emulators.ts +++ b/src/init/features/emulators.ts @@ -100,6 +100,11 @@ export async function doSetup(setup: any, config: any) { ]); } + // Set the default behavior to be single project mode. + if (setup.config.emulators.singleProjectMode === undefined) { + setup.config.emulators.singleProjectMode = true; + } + if (selections.download) { for (const selected of selections.emulators) { if (isDownloadableEmulator(selected)) { From 210a40e6b6f08c4bfea7b3d86a1215c57380daee Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Wed, 5 Oct 2022 12:41:32 -0700 Subject: [PATCH 007/115] Fix Hosting validation (#5060) Fix web frameworks --- src/deploy/index.ts | 5 +++-- src/frameworks/index.ts | 33 +++++++++++++++++++++++++++++---- src/hosting/api.ts | 8 +++++++- src/hosting/config.ts | 40 ++++++++++++++++++++++++++++------------ 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 1d03b59d4a46..173c3f5c641e 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -16,6 +16,7 @@ import * as StorageTarget from "./storage"; import * as RemoteConfigTarget from "./remoteconfig"; import * as ExtensionsTarget from "./extensions"; import { prepareFrameworks } from "../frameworks"; +import { HostingDeploy } from "./hosting/context"; const TARGETS = { hosting: HostingTarget, @@ -104,8 +105,8 @@ export const deploy = async function ( const deployedHosting = includes(targetNames, "hosting"); logger.info(bold("Project Console:"), consoleUrl(options.project, "/overview")); if (deployedHosting) { - each(context.hosting.deploys, (deploy) => { - logger.info(bold("Hosting URL:"), addSubdomain(hostingOrigin, deploy.site)); + each(context.hosting.deploys as HostingDeploy[], (deploy) => { + logger.info(bold("Hosting URL:"), addSubdomain(hostingOrigin, deploy.config.site)); }); const versionNames = context.hosting.deploys.map((deploy: any) => deploy.version); return { hosting: versionNames.length === 1 ? versionNames[0] : versionNames }; diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 7e9b900dd649..8cbc1a8cf753 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -21,6 +21,9 @@ import { getProjectDefaultAccount } from "../auth"; import { formatHost } from "../emulator/functionsEmulatorShared"; import { Constants } from "../emulator/constants"; import { FirebaseError } from "../error"; +import { requireHostingSite } from "../requireHostingSite"; +import { HostingRewrites } from "../firebaseConfig"; +import * as experiments from "../experiments"; // Use "true &&"" to keep typescript from compiling this file and rewriting // the import statement into a require @@ -257,7 +260,23 @@ export async function prepareFrameworks( // been booted up (at this point) and we may be offline, so just use projectId. Most of the time // the default site is named the same as the project & for frameworks this is only used for naming the // function... unless you're using authenticated server-context TODO explore the implication here. - const configs = hostingConfig({ site: project, ...options }); + + // N.B. Trying to work around this in a rush but it's not 100% clear what to do here. + // The code previously injected a cache for the hosting options after specifying site: project + // temporarily in options. But that means we're caching configs with the wrong + // site specified. As a compromise we'll do our best to set the correct site, + // which should succeed when this method is being called from "deploy". I don't + // think this breaks any other situation because we don't need a site during + // emulation unless we have multiple sites, in which case we're guaranteed to + // either have site or target set. + if (!options.site) { + try { + await requireHostingSite(options); + } catch { + options.site = project; + } + } + const configs = hostingConfig(options); let firebaseDefaults: FirebaseDefaults | undefined = undefined; if (configs.length === 0) return; for (const config of configs) { @@ -370,10 +389,16 @@ You can link a Web app to a Hosting site here https://console.firebase.google.co if (codegenFunctionsDirectory) { if (firebaseDefaults) firebaseDefaults._authTokenSyncURL = "/__session"; - config.rewrites.push({ + const rewrite: HostingRewrites = { source: "**", - function: functionName, - }); + function: { + functionId: functionName, + }, + }; + if (experiments.isEnabled("pintags")) { + rewrite.function.pinTag = true; + } + config.rewrites.push(rewrite); const existingFunctionsConfig = options.config.get("functions") ? [].concat(options.config.get("functions")) diff --git a/src/hosting/api.ts b/src/hosting/api.ts index e3bb95b955c3..bfa99fabe6c6 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -385,7 +385,13 @@ export async function updateVersion( version, { queryParams: { - updateMask: proto.fieldMasks(version, "labels").join(","), + // N.B. It's not clear why we need "config". If the Hosting server acted + // like a normal OP service, we could update config.foo and config.bar + // in a PATCH command even if config was the empty object already. But + // not setting config in createVersion and then setting config subfields + // in updateVersion is failing with + // "HTTP Error: 40 Unknown path in `updateMask`: `config.rewrites`" + updateMask: proto.fieldMasks(version, "labels", "config").join(","), }, } ); diff --git a/src/hosting/config.ts b/src/hosting/config.ts index a67a08b371ce..4f9103c8dabe 100644 --- a/src/hosting/config.ts +++ b/src/hosting/config.ts @@ -17,6 +17,7 @@ import { dirExistsSync } from "../fsutils"; import { resolveProjectPath } from "../projectPath"; import { HostingOptions } from "./options"; import path from "path"; +import * as experiments from "../experiments"; // assertMatches allows us to throw when an --only flag doesn't match a target // but an --except flag doesn't. Is this desirable behavior? @@ -145,21 +146,33 @@ function validateOne(config: HostingMultiple[number], options: HostingOptions): const hasAnyDynamicRewrites = !!config.rewrites?.find((rw) => !("destination" in rw)); const hasAnyRedirects = !!config.redirects?.length; - if (!config.public && hasAnyStaticRewrites) { - throw new FirebaseError('Must supply a "public" directory when using "destination" rewrites.'); + if (config.source && config.public) { + throw new FirebaseError('Can only specify "source" or "public" in a Hosting config, not both'); } + const root = experiments.isEnabled("webframeworks") + ? config.source || config.public + : config.public; + const orSource = experiments.isEnabled("webframeworks") ? ' or "source"' : ""; - if (!config.public && !hasAnyDynamicRewrites && !hasAnyRedirects) { + if (!root && hasAnyStaticRewrites) { throw new FirebaseError( - 'Must supply a "public" directory or at least one rewrite or redirect in each "hosting" config.' + `Must supply a "public"${orSource} directory when using "destination" rewrites.` ); } - if (config.public && !dirExistsSync(resolveProjectPath(options, config.public))) { + if (!root && !hasAnyDynamicRewrites && !hasAnyRedirects) { throw new FirebaseError( - `Specified "public" directory "${ - config.public - }" does not exist, can't deploy hosting to site "${config.site || config.target || ""}"` + `Must supply a "public"${orSource} directory or at least one rewrite or redirect in each "hosting" config.` + ); + } + + if (root && !dirExistsSync(resolveProjectPath(options, root))) { + throw new FirebaseError( + `Specified "${ + config.source ? "source" : "public" + }" directory "${root}" does not exist, can't deploy hosting to site "${ + config.site || config.target || "" + }"` ); } @@ -174,21 +187,23 @@ function validateOne(config: HostingMultiple[number], options: HostingOptions): } if (config.i18n) { - if (!config.public) { - throw new FirebaseError('Must supply a "public" directory when using "i18n" configuration.'); + if (!root) { + throw new FirebaseError( + `Must supply a "public"${orSource} directory when using "i18n" configuration.` + ); } if (!config.i18n.root) { throw new FirebaseError('Must supply a "root" in "i18n" config.'); } - const i18nPath = path.join(config.public, config.i18n.root); + const i18nPath = path.join(root, config.i18n.root); if (!dirExistsSync(resolveProjectPath(options, i18nPath))) { logLabeledWarning( "hosting", `Couldn't find specified i18n root directory ${bold( config.i18n.root - )} in public directory ${bold(config.public)}` + )} in public directory ${bold(root)}` ); } } @@ -281,6 +296,7 @@ export function hostingConfig(options: HostingOptions): HostingResolved[] { configs = filterOnly(configs, options.only); configs = filterExcept(configs, options.except); normalize(configs); + validate(configs, options); // N.B. We're calling resolveTargets after filterOnly/except, which means // we won't recognize a --only when the config has a target. From 5504f7caed7a715fbef30fb83be60e7a2f126703 Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Wed, 5 Oct 2022 14:17:34 -0700 Subject: [PATCH 008/115] Version bump emulator UI to 1.10.0. (#5065) * Version bump emulator UI to 1.10.0. * Added pull number. * Stylin --- CHANGELOG.md | 1 + src/emulator/downloadableEmulators.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7489e80d141..617006f8c9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Add functions emulator support for RTDB v2 triggers (#5045). - Enables single project mode for Firestore by default (#4890). +- Add Emulator UI support for HTTPS, launching UI v1.10.0 (#5065). diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index 52ff258cfd9b..c342e2a24318 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -80,15 +80,15 @@ export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDe }, } : { - version: "1.9.0", - downloadPath: path.join(CACHE_DIR, "ui-v1.9.0.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.9.0"), - binaryPath: path.join(CACHE_DIR, "ui-v1.9.0", "server", "server.js"), + version: "1.10.0", + downloadPath: path.join(CACHE_DIR, "ui-v1.10.0.zip"), + unzipDir: path.join(CACHE_DIR, "ui-v1.10.0"), + binaryPath: path.join(CACHE_DIR, "ui-v1.10.0", "server", "server.js"), opts: { cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.9.0.zip", - expectedSize: 3062710, - expectedChecksum: "984597f41d497bd318dac131615eb9d5", + remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.10.0.zip", + expectedSize: 3062540, + expectedChecksum: "7dec1e82acccc196efc4d364e2664288", namePrefix: "ui", }, }, From 44821566df3be4532ec4e3878650354a8fb3fcf5 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Wed, 5 Oct 2022 21:59:00 +0000 Subject: [PATCH 009/115] 11.14.0 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 45841fad3e88..b7c9431798be 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.13.0", + "version": "11.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.13.0", + "version": "11.14.0", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 8ca79c6f01f4..0356434d8346 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.13.0", + "version": "11.14.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 080d4bcdad3afef84e3688a79b5bfe3099f74d8e Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Wed, 5 Oct 2022 21:59:13 +0000 Subject: [PATCH 010/115] [firebase-release] Removed change log and reset repo after 11.14.0 release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 617006f8c9ff..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Add functions emulator support for RTDB v2 triggers (#5045). -- Enables single project mode for Firestore by default (#4890). -- Add Emulator UI support for HTTPS, launching UI v1.10.0 (#5065). From 302d07e01ed20522e1cdea13c73a60fe6a01de03 Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Thu, 6 Oct 2022 00:27:04 -0700 Subject: [PATCH 011/115] Single project mode for the auth emulator. (#4996) * Single project mode for the auth emulator. If set, the emulator will reject calls for project IDs that aren't the default project ID. --- CHANGELOG.md | 1 + src/emulator/auth/index.ts | 13 ++++++++++++- src/emulator/auth/server.ts | 19 +++++++++++++++++++ src/emulator/controller.ts | 13 ++++++++----- src/test/emulators/auth/misc.spec.ts | 26 ++++++++++++++++++++++++++ src/test/emulators/auth/setup.ts | 18 ++++++++++++------ 6 files changed, 78 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..1b608c77ef81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Enable single project mode for the auth emulator (#5068). diff --git a/src/emulator/auth/index.ts b/src/emulator/auth/index.ts index 3db9bde9c071..ddc2197efc80 100644 --- a/src/emulator/auth/index.ts +++ b/src/emulator/auth/index.ts @@ -13,6 +13,17 @@ export interface AuthEmulatorArgs { projectId: string; port?: number; host?: string; + singleProjectMode?: SingleProjectMode; +} + +/** + * An enum that dictates the behavior when the project ID in the request doesn't match the + * defaultProjectId. + */ +export enum SingleProjectMode { + NO_WARNING, + WARNING, + ERROR, } export class AuthEmulator implements EmulatorInstance { @@ -22,7 +33,7 @@ export class AuthEmulator implements EmulatorInstance { async start(): Promise { const { host, port } = this.getInfo(); - const app = await createApp(this.args.projectId); + const app = await createApp(this.args.projectId, this.args.singleProjectMode); const server = app.listen(port, host); this.destroyServer = utils.createDestroyer(server); } diff --git a/src/emulator/auth/server.ts b/src/emulator/auth/server.ts index 376d7d49f188..8ee6d0b2080d 100644 --- a/src/emulator/auth/server.ts +++ b/src/emulator/auth/server.ts @@ -3,6 +3,7 @@ import * as express from "express"; import * as exegesisExpress from "exegesis-express"; import { ValidationError } from "exegesis/lib/errors"; import * as _ from "lodash"; +import { SingleProjectMode } from "./index"; import { OpenAPIObject, PathsObject, ServerObject, OperationObject } from "openapi3-ts"; import { EmulatorLogger } from "../emulatorLogger"; import { Emulators } from "../types"; @@ -116,6 +117,7 @@ function specWithEmulatorServer(protocol: string, host: string | undefined): Ope */ export async function createApp( defaultProjectId: string, + singleProjectMode = SingleProjectMode.NO_WARNING, projectStateForId = new Map() ): Promise { const app = express(); @@ -362,6 +364,23 @@ export async function createApp( function getProjectStateById(projectId: string, tenantId?: string): ProjectState { let agentState = projectStateForId.get(projectId); + + if ( + singleProjectMode !== SingleProjectMode.NO_WARNING && + projectId && + defaultProjectId !== projectId + ) { + const errorString = + `Multiple projectIds are not recommended in single project mode. ` + + `Requested project ID ${projectId}, but the emulator is configured for ` + + `${defaultProjectId}. This warning will become an error in the future. To opt-out of ` + + `single project mode add/set the \'"single_project_mode"\' false' property in the` + + ` firebase.json emulators config.`; + EmulatorLogger.forEmulator(Emulators.AUTH).log("WARN", errorString); + if (singleProjectMode === SingleProjectMode.ERROR) { + throw new BadRequestError(errorString); + } + } if (!agentState) { agentState = new AgentProjectState(projectId); projectStateForId.set(projectId, agentState); diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index d879e587f446..022653ac751e 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -18,7 +18,7 @@ import { import { Constants, FIND_AVAILBLE_PORT_BY_DEFAULT } from "./constants"; import { EmulatableBackend, FunctionsEmulator } from "./functionsEmulator"; import { parseRuntimeVersion } from "./functionsEmulatorUtils"; -import { AuthEmulator } from "./auth"; +import { AuthEmulator, SingleProjectMode } from "./auth"; import { DatabaseEmulator, DatabaseEmulatorArgs } from "./databaseEmulator"; import { FirestoreEmulator, FirestoreEmulatorArgs } from "./firestoreEmulator"; import { HostingEmulator } from "./hostingEmulator"; @@ -390,6 +390,9 @@ export async function startAll( // 2) If the --only flag is passed, then this list is the intersection const targets = filterEmulatorTargets(options); options.targets = targets; + const singleProjectModeEnabled = + options.config.src.emulators?.singleProjectMode === undefined || + options.config.src.emulators?.singleProjectMode; if (targets.length === 0) { throw new FirebaseError( @@ -678,10 +681,7 @@ export async function startAll( } // undefined in the config defaults to setting single_project_mode. - if ( - options.config.src.emulators?.singleProjectMode === undefined || - options.config.src.emulators?.singleProjectMode - ) { + if (singleProjectModeEnabled) { if (projectId) { args.single_project_mode = true; args.single_project_mode_error = false; @@ -792,6 +792,9 @@ export async function startAll( host: authAddr.host, port: authAddr.port, projectId, + singleProjectMode: singleProjectModeEnabled + ? SingleProjectMode.WARNING + : SingleProjectMode.NO_WARNING, }); await startEmulator(authEmulator); diff --git a/src/test/emulators/auth/misc.spec.ts b/src/test/emulators/auth/misc.spec.ts index 125ea061913e..9ade23381f26 100644 --- a/src/test/emulators/auth/misc.spec.ts +++ b/src/test/emulators/auth/misc.spec.ts @@ -27,6 +27,7 @@ import { SESSION_COOKIE_MAX_VALID_DURATION, } from "../../../emulator/auth/operations"; import { toUnixTimestamp } from "../../../emulator/auth/utils"; +import { SingleProjectMode } from "../../../emulator/auth"; describeAuthEmulator("token refresh", ({ authApi, getClock }) => { it("should exchange refresh token for new tokens", async () => { @@ -554,6 +555,15 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => { }); }); + it("should not throw an exception on project ID mismatch if singleProjectMode is NO_WARNING", async () => { + await authApi() + .get(`/emulator/v1/projects/someproject/config`) // note the "wrong" project ID here + .send() + .then((res) => { + expectStatusCode(200, res); + }); + }); + it("should update allowDuplicateEmails on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { await authApi() .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) @@ -575,3 +585,19 @@ describeAuthEmulator("emulator utility APIs", ({ authApi }) => { }); }); }); + +describeAuthEmulator( + "emulator utility API; singleProjectMode=ERROR", + ({ authApi }) => { + it("should throw an exception on project ID mismatch if singleProjectMode is ERROR", async () => { + await authApi() + .get(`/emulator/v1/projects/someproject/config`) // note the "wrong" project ID here + .send() + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("single project mode"); + }); + }); + }, + SingleProjectMode.ERROR +); diff --git a/src/test/emulators/auth/setup.ts b/src/test/emulators/auth/setup.ts index 8b7cef46b533..12918a4d1d3f 100644 --- a/src/test/emulators/auth/setup.ts +++ b/src/test/emulators/auth/setup.ts @@ -3,6 +3,7 @@ import { useFakeTimers } from "sinon"; import supertest = require("supertest"); import { createApp } from "../../../emulator/auth/server"; import { AgentProjectState } from "../../../emulator/auth/state"; +import { SingleProjectMode } from "../../../emulator/auth"; export const PROJECT_ID = "example"; @@ -14,13 +15,14 @@ export const PROJECT_ID = "example"; */ export function describeAuthEmulator( title: string, - fn: (this: Suite, utils: AuthTestUtils) => void + fn: (this: Suite, utils: AuthTestUtils) => void, + singleProjectMode = SingleProjectMode.NO_WARNING ): Suite { return describe(`Auth Emulator: ${title}`, function (this) { let authApp: Express.Application; beforeEach("setup or reuse auth server", async function (this) { this.timeout(20000); - authApp = await createOrReuseApp(); + authApp = await createOrReuseApp(singleProjectMode); }); let clock: sinon.SinonFakeTimers; @@ -40,12 +42,16 @@ export type AuthTestUtils = { }; // Keep a global auth server since start-up takes too long: -let cachedAuthApp: Express.Application; +const cachedAuthAppMap = new Map(); const projectStateForId = new Map(); -async function createOrReuseApp(): Promise { - if (!cachedAuthApp) { - cachedAuthApp = await createApp(PROJECT_ID, projectStateForId); +async function createOrReuseApp( + singleProjectMode: SingleProjectMode +): Promise { + let cachedAuthApp: Express.Application | undefined = cachedAuthAppMap.get(singleProjectMode); + if (cachedAuthApp === undefined) { + cachedAuthApp = await createApp(PROJECT_ID, singleProjectMode, projectStateForId); + cachedAuthAppMap.set(singleProjectMode, cachedAuthApp); } // Clear the state every time to make it work like brand new. // NOTE: This probably won't work with parallel mode if we ever enable it. From 83167a437e44a59ffd777373fc006080c6814d6e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 6 Oct 2022 06:17:51 -0700 Subject: [PATCH 012/115] Fix function deploy test. (#5064) --- scripts/functions-deploy-tests/tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/functions-deploy-tests/tests.ts b/scripts/functions-deploy-tests/tests.ts index 51be2d73d4b0..7a4a75078ee3 100644 --- a/scripts/functions-deploy-tests/tests.ts +++ b/scripts/functions-deploy-tests/tests.ts @@ -276,7 +276,7 @@ describe("firebase deploy", function (this) { expect(result.stdout, "deploy result").to.match(/Deploy complete!/); const result2 = await setOptsAndDeploy(opts); - expect(result2.stdout, "deploy result").to.match(/Skipped (No changes detected)/); + expect(result2.stdout, "deploy result").to.match(/Skipped \(No changes detected\)/); }); it("leaves existing options when unspecified", async () => { From edf85b735b5d97f6256af2d93cd28ab7f1263898 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 6 Oct 2022 08:58:20 -0700 Subject: [PATCH 013/115] correct publish path to use tsconfig.publish (#5067) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0356434d8346..fe7c3f558f3b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "build": "tsc && npm run copyfiles", + "build:publish": "tsc --build tsconfig.publish.json", "build:watch": "npm run build && tsc --watch", "clean": "rimraf lib dev", "copyfiles": "node -e \"const fs = require('fs'); fs.mkdirSync('./lib', {recursive:true}); fs.copyFileSync('./src/dynamicImport.js', './lib/dynamicImport.js')\"", @@ -22,7 +23,7 @@ "lint:quiet": "npm run lint:ts -- --quiet && npm run lint:other", "lint:ts": "eslint --config .eslintrc.js --ext .ts,.js .", "mocha": "nyc mocha 'src/test/**/*.{ts,js}'", - "prepare": "npm run clean && npm run build -- --build tsconfig.publish.json", + "prepare": "npm run clean && npm run build:publish", "test": "npm run lint:quiet && npm run test:compile && npm run mocha", "test:client-integration": "bash ./scripts/client-integration-tests/run.sh", "test:compile": "tsc --project tsconfig.compile.json", From 97e0d04e874b822e046838932357918dfcf2b523 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Thu, 6 Oct 2022 12:28:13 -0500 Subject: [PATCH 014/115] fix(test) Fix broken test (#5070) This PR fixes config.spec.ts when the `webframeworks` experiment is enabled. The error messaging informs the user either their "public", or their "pulic or source" directory is compatible with other configs. The "fix" is simply turn off webframeworks in the test (doesn't actually save the user's state.) --- src/test/hosting/config.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/hosting/config.spec.ts b/src/test/hosting/config.spec.ts index 745c0bf67b5c..68527ae4686b 100644 --- a/src/test/hosting/config.spec.ts +++ b/src/test/hosting/config.spec.ts @@ -6,6 +6,7 @@ import * as config from "../../hosting/config"; import { HostingOptions } from "../../hosting/options"; import { RequireAtLeastOne } from "../../metaprogramming"; import { cloneDeep } from "../../utils"; +import { setEnabled } from "../../experiments"; function options( hostingConfig: HostingConfig, @@ -326,7 +327,7 @@ describe("config", () => { wantErr?: RegExp; }> = [ { - desc: "should error out if there is no puyblic directory but a 'destination' rewrite", + desc: "should error out if there is no public directory but a 'destination' rewrite", site: { rewrites: [ { source: "/foo", destination: "/bar.html" }, @@ -336,7 +337,7 @@ describe("config", () => { wantErr: PUBLIC_DIR_ERROR_PREFIX, }, { - desc: "should error out if htere is no public directory and an i18n with root", + desc: "should error out if there is no public directory and an i18n with root", site: { i18n: { root: "/foo" }, rewrites: [{ source: "/foo", function: "pass" }], @@ -402,6 +403,9 @@ describe("config", () => { for (const t of tests) { it(t.desc, () => { + // Setting experiment to "false" to handle mismatched error message. + setEnabled("webframeworks", false); + const configs: HostingMultiple = [{ site: "site", ...t.site }]; if (t.wantErr) { expect(() => config.validate(configs, options(t.site))).to.throw( From 1cc23a3c13a0a107b68edb5f87d455d370ca8053 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 6 Oct 2022 11:53:52 -0700 Subject: [PATCH 015/115] Fix Hosting deploys (#5077) * fix path import * adds check for permission issues when resolving Hosting configs --- CHANGELOG.md | 4 +++- src/deploy/hosting/convertConfig.ts | 31 +++++++++++++++++++++++++++-- src/hosting/config.ts | 2 +- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b608c77ef81..bd3a11ecccd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ -- Enable single project mode for the auth emulator (#5068). +- Enables single project mode for the auth emulator (#5068). +- Fixes issue deploying to Hosting with i18n enabled. +- Fixes issue where deploying to Hosting without Functions permissions would cause deployments to fail with 403 "Permission Denied" errors. (#5071) diff --git a/src/deploy/hosting/convertConfig.ts b/src/deploy/hosting/convertConfig.ts index 30d6fa18a5e3..9a35d6712d6d 100644 --- a/src/deploy/hosting/convertConfig.ts +++ b/src/deploy/hosting/convertConfig.ts @@ -10,6 +10,7 @@ import { bold } from "colorette"; import * as runTags from "../../hosting/runTags"; import { assertExhaustive } from "../../functional"; import * as experiments from "../../experiments"; +import { logger } from "../../logger"; /** * extractPattern contains the logic for extracting exactly one glob/regexp @@ -79,7 +80,11 @@ export function findEndpointForRewrite( /** * convertConfig takes a hosting config object from firebase.json and transforms it into - * the valid format for sending to the Firebase Hosting REST API + * the valid format for sending to the Firebase Hosting REST API. + * + * TODO: this currently lists remote backends (functions) and attemtps to validate them. + * We currently catch 403 issues and handle them, but it's probably not the best solution + * to have a required permission in functions when a deploy may "only" be to Hosting. */ export async function convertConfig( context: Context, @@ -87,10 +92,32 @@ export async function convertConfig( ): Promise { const config: api.ServingConfig = {}; + // Instead of *always* fetching backends, let's roughly sanity check our + // rewrites to see if it's necessary. + const hasBackends = !!deploy.config.rewrites?.some((r) => "function" in r || "run" in r); + // We need to be able to do a rewrite to an existing function that is may not // even be part of Firebase's control or a function that we're currently // deploying. - const haveBackend = await backend.existingBackend(context); + let haveBackend = backend.empty(); + if (hasBackends) { + try { + haveBackend = await backend.existingBackend(context); + } catch (err: unknown) { + if (err instanceof FirebaseError) { + if (err.status === 403) { + // If the callee doesn't have permission to list backends, we just won't + // be able to validate them. This is fine. + logger.debug( + `Deploying hosting site ${deploy.config.site}, did not have permissions to check for backends: `, + err + ); + } else { + throw err; + } + } + } + } config.rewrites = deploy.config.rewrites?.map((rewrite) => { const target = extractPattern("rewrite", rewrite); diff --git a/src/hosting/config.ts b/src/hosting/config.ts index 4f9103c8dabe..2d49e7557154 100644 --- a/src/hosting/config.ts +++ b/src/hosting/config.ts @@ -16,7 +16,7 @@ import { RequireAtLeastOne } from "../metaprogramming"; import { dirExistsSync } from "../fsutils"; import { resolveProjectPath } from "../projectPath"; import { HostingOptions } from "./options"; -import path from "path"; +import * as path from "node:path"; import * as experiments from "../experiments"; // assertMatches allows us to throw when an --only flag doesn't match a target From 655f8e8b1e9df0a0264724a73ec59d3d12546be9 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Thu, 6 Oct 2022 16:14:41 -0500 Subject: [PATCH 016/115] Add(webframeworks): Integration Test (#5063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description * Adding Integration Tests to test webframeworks Deploy ### Scenarios Tested * deploy with webframeworks 🚀 --- .eslintrc.js | 2 +- package.json | 3 +- .../webframeworks-deploy-tests/.firebaserc | 1 + scripts/webframeworks-deploy-tests/.gitignore | 66 + scripts/webframeworks-deploy-tests/README.md | 20 + scripts/webframeworks-deploy-tests/cli.ts | 61 + .../webframeworks-deploy-tests/firebase.json | 10 + .../hosting/.eslintrc.json | 2 + .../hosting/.gitignore | 36 + .../hosting/next.config.js | 7 + .../hosting/package-lock.json | 5244 +++++++++++++++++ .../hosting/package.json | 24 + .../hosting/pages/_app.tsx | 8 + .../hosting/pages/api/hello.ts | 13 + .../hosting/pages/index.tsx | 72 + .../hosting/styles/Home.module.css | 129 + .../hosting/styles/globals.css | 26 + .../hosting/tsconfig.json | 20 + scripts/webframeworks-deploy-tests/run.sh | 9 + scripts/webframeworks-deploy-tests/tests.ts | 51 + 20 files changed, 5802 insertions(+), 2 deletions(-) create mode 100644 scripts/webframeworks-deploy-tests/.firebaserc create mode 100644 scripts/webframeworks-deploy-tests/.gitignore create mode 100644 scripts/webframeworks-deploy-tests/README.md create mode 100644 scripts/webframeworks-deploy-tests/cli.ts create mode 100644 scripts/webframeworks-deploy-tests/firebase.json create mode 100644 scripts/webframeworks-deploy-tests/hosting/.eslintrc.json create mode 100644 scripts/webframeworks-deploy-tests/hosting/.gitignore create mode 100644 scripts/webframeworks-deploy-tests/hosting/next.config.js create mode 100644 scripts/webframeworks-deploy-tests/hosting/package-lock.json create mode 100644 scripts/webframeworks-deploy-tests/hosting/package.json create mode 100644 scripts/webframeworks-deploy-tests/hosting/pages/_app.tsx create mode 100644 scripts/webframeworks-deploy-tests/hosting/pages/api/hello.ts create mode 100644 scripts/webframeworks-deploy-tests/hosting/pages/index.tsx create mode 100644 scripts/webframeworks-deploy-tests/hosting/styles/Home.module.css create mode 100644 scripts/webframeworks-deploy-tests/hosting/styles/globals.css create mode 100644 scripts/webframeworks-deploy-tests/hosting/tsconfig.json create mode 100755 scripts/webframeworks-deploy-tests/run.sh create mode 100644 scripts/webframeworks-deploy-tests/tests.ts diff --git a/.eslintrc.js b/.eslintrc.js index 22753f7b16fa..a96550188fbe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -120,5 +120,5 @@ module.exports = { // don't want Typescript to turn the imports into requires. Ignoring as eslint // is complaining it doesn't belong to a project. // TODO(jamesdaniels): add this to overrides instead - ignorePatterns: ["src/dynamicImport.js"], + ignorePatterns: ["src/dynamicImport.js", "scripts/webframeworks-deploy-tests/hosting/*"], }; diff --git a/package.json b/package.json index fe7c3f558f3b..896660242838 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "test:triggers-end-to-end": "bash ./scripts/triggers-end-to-end-tests/run.sh", "test:triggers-end-to-end:inspect": "bash ./scripts/triggers-end-to-end-tests/run.sh inspect", "test:storage-deploy": "bash ./scripts/storage-deploy-tests/run.sh", - "test:storage-emulator-integration": "bash ./scripts/storage-emulator-integration/run.sh" + "test:storage-emulator-integration": "bash ./scripts/storage-emulator-integration/run.sh", + "test:webframeworks-deploy": "bash ./scripts/webframeworks-deploy-tests/run.sh" }, "files": [ "lib", diff --git a/scripts/webframeworks-deploy-tests/.firebaserc b/scripts/webframeworks-deploy-tests/.firebaserc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/scripts/webframeworks-deploy-tests/.firebaserc @@ -0,0 +1 @@ +{} diff --git a/scripts/webframeworks-deploy-tests/.gitignore b/scripts/webframeworks-deploy-tests/.gitignore new file mode 100644 index 000000000000..dbb58ffbfa3c --- /dev/null +++ b/scripts/webframeworks-deploy-tests/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/scripts/webframeworks-deploy-tests/README.md b/scripts/webframeworks-deploy-tests/README.md new file mode 100644 index 000000000000..6412edc6d535 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/README.md @@ -0,0 +1,20 @@ +# WebFrameworks Deploy Integration Test + +This integration test deploys a nextjs hosted project with webframeworks enabled. + +The test isn't "thread-safe" - there should be at most one test running on a project at any given time. +I suggest you to use your own project to run the test. + +You can set the test project and run the integration test as follows: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} npm run test:webframeworks-deploy +``` + +The integration test blows whats being hosted! Don't run it on a project where you have functions you'd like to keep. + +You can also run the test target with `FIREBASE_DEBUG=true` to pass `--debug` flag to CLI invocation: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} FIREBASE_DEBUG=true npm run test:webframeworks-deploy +``` diff --git a/scripts/webframeworks-deploy-tests/cli.ts b/scripts/webframeworks-deploy-tests/cli.ts new file mode 100644 index 000000000000..79f8d5560f84 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/cli.ts @@ -0,0 +1,61 @@ +import * as spawn from "cross-spawn"; +import { ChildProcess } from "child_process"; + +// NOTE: This code duplicates scripts/integration-helpers/cli.ts. +// There are minor differences in handling stdout/stderr that triggered forking of the code, +// but in an ideal world, we would have one, more feature-ful library for invoking CLI during tests. +// Blame taeold@ for taking this shortcut. + +export interface Result { + proc: ChildProcess; + stdout: string; + stderr: string; +} + +/** + * Execute a Firebase CLI command. + */ +export function exec( + cmd: string, + project: string, + additionalArgs: string[], + cwd: string, + quiet = true +): Promise { + const args = [cmd, "--project", project]; + + if (additionalArgs) { + args.push(...additionalArgs); + } + + const proc = spawn("firebase", args, { cwd }); + if (!proc) { + throw new Error("Failed to start firebase CLI"); + } + + const cli: Result = { + proc, + stdout: "", + stderr: "", + }; + + proc.stdout?.on("data", (data) => { + const s = data.toString(); + if (!quiet) { + console.log(s); + } + cli.stdout += s; + }); + + proc.stderr?.on("data", (data) => { + const s = data.toString(); + if (!quiet) { + console.log(s); + } + cli.stderr += s; + }); + + return new Promise((resolve) => { + proc.on("exit", () => resolve(cli)); + }); +} diff --git a/scripts/webframeworks-deploy-tests/firebase.json b/scripts/webframeworks-deploy-tests/firebase.json new file mode 100644 index 000000000000..2affae77b0d4 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "source": "hosting", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} diff --git a/scripts/webframeworks-deploy-tests/hosting/.eslintrc.json b/scripts/webframeworks-deploy-tests/hosting/.eslintrc.json new file mode 100644 index 000000000000..2c63c0851048 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/.eslintrc.json @@ -0,0 +1,2 @@ +{ +} diff --git a/scripts/webframeworks-deploy-tests/hosting/.gitignore b/scripts/webframeworks-deploy-tests/hosting/.gitignore new file mode 100644 index 000000000000..c87c9b392c02 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/scripts/webframeworks-deploy-tests/hosting/next.config.js b/scripts/webframeworks-deploy-tests/hosting/next.config.js new file mode 100644 index 000000000000..ae887958d3c9 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, +} + +module.exports = nextConfig diff --git a/scripts/webframeworks-deploy-tests/hosting/package-lock.json b/scripts/webframeworks-deploy-tests/hosting/package-lock.json new file mode 100644 index 000000000000..5b23704afabd --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/package-lock.json @@ -0,0 +1,5244 @@ +{ + "name": "hosting", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hosting", + "version": "0.1.0", + "dependencies": { + "next": "12.3.1", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/node": "18.8.2", + "@types/react": "18.0.21", + "@types/react-dom": "18.0.6", + "eslint": "8.24.0", + "eslint-config-next": "12.3.1", + "typescript": "4.8.4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", + "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.1.tgz", + "integrity": "sha512-j2vJGnkopRzH+ykJ8h68wrHnEUmtK//E723jjixiAl/PPf6FhqY/vYRcMVlNydRKQjQsTsYEjpx+DZMIvnGk/g==", + "dev": true, + "dependencies": { + "core-js-pure": "^3.25.1", + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", + "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@next/env": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.1.tgz", + "integrity": "sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.1.tgz", + "integrity": "sha512-sw+lTf6r6P0j+g/n9y4qdWWI2syPqZx+uc0+B/fRENqfR3KpSid6MIKqc9gNwGhJASazEQ5b3w8h4cAET213jw==", + "dev": true, + "dependencies": { + "glob": "7.1.7" + } + }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", + "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", + "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz", + "integrity": "sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", + "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", + "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", + "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", + "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", + "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", + "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", + "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", + "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", + "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", + "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", + "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.2.tgz", + "integrity": "sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", + "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", + "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz", + "integrity": "sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.39.0", + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/typescript-estree": "5.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz", + "integrity": "sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz", + "integrity": "sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz", + "integrity": "sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz", + "integrity": "sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.39.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "dev": true + }, + "node_modules/axe-core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", + "integrity": "sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001416", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", + "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/core-js-pure": { + "version": "3.25.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.5.tgz", + "integrity": "sha512-oml3M22pHM+igfWHDfdLVq2ShWmjM2V4L+dQEBs0DWVIqEm9WHCwGAlZ6BmyBQGy5sFrJmcx+856D9lVKyGWYg==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.6", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.2", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-12.3.1.tgz", + "integrity": "sha512-EN/xwKPU6jz1G0Qi6Bd/BqMnHLyRAL0VsaQaWA7F3KkjAgZHi4f1uL1JKGWNxdQpHTW/sdGONBd0bzxUka/DJg==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "12.3.1", + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/parser": "^5.21.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.31.7", + "eslint-plugin-react-hooks": "^4.5.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.31.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz", + "integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dev": true, + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", + "integrity": "sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==", + "dependencies": { + "@next/env": "12.3.1", + "@swc/helpers": "0.4.11", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.0.7", + "use-sync-external-store": "1.2.0" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=12.22.0" + }, + "optionalDependencies": { + "@next/swc-android-arm-eabi": "12.3.1", + "@next/swc-android-arm64": "12.3.1", + "@next/swc-darwin-arm64": "12.3.1", + "@next/swc-darwin-x64": "12.3.1", + "@next/swc-freebsd-x64": "12.3.1", + "@next/swc-linux-arm-gnueabihf": "12.3.1", + "@next/swc-linux-arm64-gnu": "12.3.1", + "@next/swc-linux-arm64-musl": "12.3.1", + "@next/swc-linux-x64-gnu": "12.3.1", + "@next/swc-linux-x64-musl": "12.3.1", + "@next/swc-win32-arm64-msvc": "12.3.1", + "@next/swc-win32-ia32-msvc": "12.3.1", + "@next/swc-win32-x64-msvc": "12.3.1" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^6.0.0 || ^7.0.0", + "react": "^17.0.2 || ^18.0.0-0", + "react-dom": "^17.0.2 || ^18.0.0-0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", + "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", + "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/runtime-corejs3": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.1.tgz", + "integrity": "sha512-j2vJGnkopRzH+ykJ8h68wrHnEUmtK//E723jjixiAl/PPf6FhqY/vYRcMVlNydRKQjQsTsYEjpx+DZMIvnGk/g==", + "dev": true, + "requires": { + "core-js-pure": "^3.25.1", + "regenerator-runtime": "^0.13.4" + } + }, + "@eslint/eslintrc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", + "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@next/env": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.1.tgz", + "integrity": "sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==" + }, + "@next/eslint-plugin-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.1.tgz", + "integrity": "sha512-sw+lTf6r6P0j+g/n9y4qdWWI2syPqZx+uc0+B/fRENqfR3KpSid6MIKqc9gNwGhJASazEQ5b3w8h4cAET213jw==", + "dev": true, + "requires": { + "glob": "7.1.7" + } + }, + "@next/swc-android-arm-eabi": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", + "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", + "optional": true + }, + "@next/swc-android-arm64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", + "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", + "optional": true + }, + "@next/swc-darwin-arm64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz", + "integrity": "sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", + "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", + "optional": true + }, + "@next/swc-freebsd-x64": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", + "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", + "optional": true + }, + "@next/swc-linux-arm-gnueabihf": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", + "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", + "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", + "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", + "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", + "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", + "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", + "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", + "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", + "dev": true + }, + "@swc/helpers": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", + "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/node": { + "version": "18.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.2.tgz", + "integrity": "sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "18.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", + "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", + "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "@typescript-eslint/parser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz", + "integrity": "sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.39.0", + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/typescript-estree": "5.39.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz", + "integrity": "sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0" + } + }, + "@typescript-eslint/types": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz", + "integrity": "sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz", + "integrity": "sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.39.0", + "@typescript-eslint/visitor-keys": "5.39.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz", + "integrity": "sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.39.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "dev": true + }, + "axe-core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz", + "integrity": "sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==", + "dev": true + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001416", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", + "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "core-js-pure": { + "version": "3.25.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.5.tgz", + "integrity": "sha512-oml3M22pHM+igfWHDfdLVq2ShWmjM2V4L+dQEBs0DWVIqEm9WHCwGAlZ6BmyBQGy5sFrJmcx+856D9lVKyGWYg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "es-abstract": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.6", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.2", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + } + }, + "eslint-config-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-12.3.1.tgz", + "integrity": "sha512-EN/xwKPU6jz1G0Qi6Bd/BqMnHLyRAL0VsaQaWA7F3KkjAgZHi4f1uL1JKGWNxdQpHTW/sdGONBd0bzxUka/DJg==", + "dev": true, + "requires": { + "@next/eslint-plugin-next": "12.3.1", + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/parser": "^5.21.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.31.7", + "eslint-plugin-react-hooks": "^4.5.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", + "integrity": "sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.9", + "aria-query": "^4.2.2", + "array-includes": "^3.1.5", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.4.3", + "axobject-query": "^2.2.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.2", + "language-tags": "^1.0.5", + "minimatch": "^3.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-react": { + "version": "7.31.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz", + "integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==", + "dev": true, + "requires": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dev": true, + "requires": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + } + }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", + "integrity": "sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==", + "requires": { + "@next/env": "12.3.1", + "@next/swc-android-arm-eabi": "12.3.1", + "@next/swc-android-arm64": "12.3.1", + "@next/swc-darwin-arm64": "12.3.1", + "@next/swc-darwin-x64": "12.3.1", + "@next/swc-freebsd-x64": "12.3.1", + "@next/swc-linux-arm-gnueabihf": "12.3.1", + "@next/swc-linux-arm64-gnu": "12.3.1", + "@next/swc-linux-arm64-musl": "12.3.1", + "@next/swc-linux-x64-gnu": "12.3.1", + "@next/swc-linux-x64-musl": "12.3.1", + "@next/swc-win32-arm64-msvc": "12.3.1", + "@next/swc-win32-ia32-msvc": "12.3.1", + "@next/swc-win32-x64-msvc": "12.3.1", + "@swc/helpers": "0.4.11", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.0.7", + "use-sync-external-store": "1.2.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "styled-jsx": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", + "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", + "requires": {} + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/scripts/webframeworks-deploy-tests/hosting/package.json b/scripts/webframeworks-deploy-tests/hosting/package.json new file mode 100644 index 000000000000..767c471e2fb8 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/package.json @@ -0,0 +1,24 @@ +{ + "name": "hosting", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "12.3.1", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/node": "18.8.2", + "@types/react": "18.0.21", + "@types/react-dom": "18.0.6", + "eslint": "8.24.0", + "eslint-config-next": "12.3.1", + "typescript": "4.8.4" + } +} diff --git a/scripts/webframeworks-deploy-tests/hosting/pages/_app.tsx b/scripts/webframeworks-deploy-tests/hosting/pages/_app.tsx new file mode 100644 index 000000000000..3f5c9d548586 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/pages/_app.tsx @@ -0,0 +1,8 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +function MyApp({ Component, pageProps }: AppProps) { + return +} + +export default MyApp diff --git a/scripts/webframeworks-deploy-tests/hosting/pages/api/hello.ts b/scripts/webframeworks-deploy-tests/hosting/pages/api/hello.ts new file mode 100644 index 000000000000..f8bcc7e5caed --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/pages/api/hello.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/scripts/webframeworks-deploy-tests/hosting/pages/index.tsx b/scripts/webframeworks-deploy-tests/hosting/pages/index.tsx new file mode 100644 index 000000000000..86b5b3b5bf3f --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/pages/index.tsx @@ -0,0 +1,72 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +const Home: NextPage = () => { + return ( + + ) +} + +export default Home diff --git a/scripts/webframeworks-deploy-tests/hosting/styles/Home.module.css b/scripts/webframeworks-deploy-tests/hosting/styles/Home.module.css new file mode 100644 index 000000000000..bd50f42ffe6a --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/styles/Home.module.css @@ -0,0 +1,129 @@ +.container { + padding: 0 2rem; +} + +.main { + min-height: 100vh; + padding: 4rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + display: flex; + flex: 1; + padding: 2rem 0; + border-top: 1px solid #eaeaea; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + margin: 4rem 0; + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + max-width: 300px; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} + +@media (prefers-color-scheme: dark) { + .card, + .footer { + border-color: #222; + } + .code { + background: #111; + } + .logo img { + filter: invert(1); + } +} diff --git a/scripts/webframeworks-deploy-tests/hosting/styles/globals.css b/scripts/webframeworks-deploy-tests/hosting/styles/globals.css new file mode 100644 index 000000000000..4f1842163d22 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/styles/globals.css @@ -0,0 +1,26 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } + body { + color: white; + background: black; + } +} diff --git a/scripts/webframeworks-deploy-tests/hosting/tsconfig.json b/scripts/webframeworks-deploy-tests/hosting/tsconfig.json new file mode 100644 index 000000000000..99710e857874 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/scripts/webframeworks-deploy-tests/run.sh b/scripts/webframeworks-deploy-tests/run.sh new file mode 100755 index 000000000000..6e759d28c773 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e # Immediately exit on failure + +# Globally link the CLI for the testing framework +./scripts/npm-link.sh + +(cd scripts/webframeworks-deploy-tests/hosting; npm i; npm run build) + +mocha scripts/webframeworks-deploy-tests/tests.ts diff --git a/scripts/webframeworks-deploy-tests/tests.ts b/scripts/webframeworks-deploy-tests/tests.ts new file mode 100644 index 000000000000..120bfad0074b --- /dev/null +++ b/scripts/webframeworks-deploy-tests/tests.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; + +import * as cli from "./cli"; +import { requireAuth } from "../../src/requireAuth"; + +const FIREBASE_PROJECT = process.env.GCLOUD_PROJECT || ""; +const FIREBASE_DEBUG = process.env.FIREBASE_DEBUG || ""; + +function genRandomId(n = 10): string { + const charset = "abcdefghijklmnopqrstuvwxyz"; + let id = ""; + for (let i = 0; i < n; i++) { + id += charset.charAt(Math.floor(Math.random() * charset.length)); + } + return id; +} + +describe("webframeworks deploy", function (this) { + this.timeout(1000_000); + + const RUN_ID = genRandomId(); + console.log(`TEST RUN: ${RUN_ID}`); + + async function setOptsAndDeploy(): Promise { + const args = []; + if (FIREBASE_DEBUG) { + args.push("--debug"); + } + return await cli.exec("deploy", FIREBASE_PROJECT, args, __dirname, false); + } + + before(async () => { + expect(FIREBASE_PROJECT).to.not.be.empty; + + await requireAuth({}); + }); + + after(() => { + // This is not an empty block. + }); + + it("deploys functions with runtime options", async () => { + process.env.FIREBASE_CLI_EXPERIMENTS = "webframeworks"; + + const result = await setOptsAndDeploy(); + + expect(result.stdout, "deploy result").to.match(/file upload complete/); + expect(result.stdout, "deploy result").to.match(/found 16 files/); + expect(result.stdout, "deploy result").to.match(/Deploy complete!/); + }); +}); From 1c0a2189f180548b51223e6b273c76ca5d112275 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Thu, 6 Oct 2022 15:18:16 -0700 Subject: [PATCH 017/115] Expose Firestore WebSocket port in hub. (#5081) --- CHANGELOG.md | 1 + src/emulator/firestoreEmulator.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3a11ecccd7..bbba27d9e4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Enables single project mode for the auth emulator (#5068). - Fixes issue deploying to Hosting with i18n enabled. - Fixes issue where deploying to Hosting without Functions permissions would cause deployments to fail with 403 "Permission Denied" errors. (#5071) +- Fixes issue where Firestore Emulator UI Requests tab wrongly show error requiring updates (#5051) diff --git a/src/emulator/firestoreEmulator.ts b/src/emulator/firestoreEmulator.ts index 87323172fe9c..b8c23eb7db5c 100644 --- a/src/emulator/firestoreEmulator.ts +++ b/src/emulator/firestoreEmulator.ts @@ -24,6 +24,14 @@ export interface FirestoreEmulatorArgs { single_project_mode_error?: boolean; } +export interface FirestoreEmulatorInfo extends EmulatorInfo { + // Used for the Emulator UI to connect to the WebSocket server. + // The casing of the fields below is sensitive and important. + // https://github.com/firebase/firebase-tools-ui/blob/2de1e80cce28454da3afeeb373fbbb45a67cb5ef/src/store/config/types.ts#L26-L27 + webSocketHost?: string; + webSocketPort?: number; +} + export class FirestoreEmulator implements EmulatorInstance { static FIRESTORE_EMULATOR_ENV_ALT = "FIREBASE_FIRESTORE_EMULATOR_ADDRESS"; @@ -77,7 +85,7 @@ export class FirestoreEmulator implements EmulatorInstance { return downloadableEmulators.stop(Emulators.FIRESTORE); } - getInfo(): EmulatorInfo { + getInfo(): FirestoreEmulatorInfo { const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.FIRESTORE); const reservedPorts = this.args.websocket_port ? [this.args.websocket_port] : []; @@ -88,6 +96,8 @@ export class FirestoreEmulator implements EmulatorInstance { port, pid: downloadableEmulators.getPID(Emulators.FIRESTORE), reservedPorts: reservedPorts, + webSocketHost: this.args.websocket_port ? host : undefined, + webSocketPort: this.args.websocket_port ? this.args.websocket_port : undefined, }; } From 2e507723f6bcf87be5a89dc1cd33747ee1648ddb Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 6 Oct 2022 16:38:43 -0700 Subject: [PATCH 018/115] allow missing public folders when validating Hosting config (#5082) --- CHANGELOG.md | 1 + src/deploy/hosting/deploy.ts | 5 +++++ src/hosting/config.ts | 7 ++++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbba27d9e4c3..b64c32c43436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ - Fixes issue deploying to Hosting with i18n enabled. - Fixes issue where deploying to Hosting without Functions permissions would cause deployments to fail with 403 "Permission Denied" errors. (#5071) - Fixes issue where Firestore Emulator UI Requests tab wrongly show error requiring updates (#5051) +- Fixes issue where Hosting configurations were being validated before predeploys could have been run (#5072). diff --git a/src/deploy/hosting/deploy.ts b/src/deploy/hosting/deploy.ts index 31bc2da93da2..2f9fc4e528a4 100644 --- a/src/deploy/hosting/deploy.ts +++ b/src/deploy/hosting/deploy.ts @@ -8,6 +8,8 @@ import { bold, cyan } from "colorette"; import * as ora from "ora"; import { Context, HostingDeploy } from "./context"; import { Options } from "../../options"; +import { dirExistsSync } from "../../fsutils"; +import { FirebaseError } from "../../error"; /** * Uploads static assets to the upcoming Hosting versions. @@ -46,6 +48,9 @@ export async function deploy(context: Context, options: Options): Promise const t0 = Date.now(); const publicDir = options.config.path(deploy.config.public); + if (!dirExistsSync(`${publicDir}`)) { + throw new FirebaseError(`Directory '${deploy.config.public}' for Hosting does not exist.`); + } const files = listFiles(publicDir, deploy.config.ignore); logLabeledBullet( diff --git a/src/hosting/config.ts b/src/hosting/config.ts index 2d49e7557154..6552cc04a082 100644 --- a/src/hosting/config.ts +++ b/src/hosting/config.ts @@ -18,6 +18,7 @@ import { resolveProjectPath } from "../projectPath"; import { HostingOptions } from "./options"; import * as path from "node:path"; import * as experiments from "../experiments"; +import { logger } from "../logger"; // assertMatches allows us to throw when an --only flag doesn't match a target // but an --except flag doesn't. Is this desirable behavior? @@ -167,12 +168,12 @@ function validateOne(config: HostingMultiple[number], options: HostingOptions): } if (root && !dirExistsSync(resolveProjectPath(options, root))) { - throw new FirebaseError( + logger.debug( `Specified "${ config.source ? "source" : "public" - }" directory "${root}" does not exist, can't deploy hosting to site "${ + }" directory "${root}" does not exist; Deploy to Hosting site "${ config.site || config.target || "" - }"` + }" may fail or be empty.` ); } From fb507c5d34477931f5fe3930d71dba235f89d1b7 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 6 Oct 2022 23:56:49 +0000 Subject: [PATCH 019/115] 11.14.1 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b7c9431798be..fe9b164aa70e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.14.0", + "version": "11.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.14.0", + "version": "11.14.1", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 896660242838..27533596041e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.14.0", + "version": "11.14.1", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 9f86b0f1bbad24742394891fcef788eeaa095839 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 6 Oct 2022 23:57:02 +0000 Subject: [PATCH 020/115] [firebase-release] Removed change log and reset repo after 11.14.1 release --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b64c32c43436..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +0,0 @@ -- Enables single project mode for the auth emulator (#5068). -- Fixes issue deploying to Hosting with i18n enabled. -- Fixes issue where deploying to Hosting without Functions permissions would cause deployments to fail with 403 "Permission Denied" errors. (#5071) -- Fixes issue where Firestore Emulator UI Requests tab wrongly show error requiring updates (#5051) -- Fixes issue where Hosting configurations were being validated before predeploys could have been run (#5072). From e08c6d436319f0ee02d2a088831082cbc56e1873 Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Thu, 6 Oct 2022 22:05:38 -0700 Subject: [PATCH 021/115] Pass singleProjectMode config down to the database emulator and bump the version to 4.10.0. (#5068) Pass singleProjectMode config down to the database emulator and bump the version to 4.10.0. (#5068) --- CHANGELOG.md | 1 + src/emulator/controller.ts | 2 ++ src/emulator/databaseEmulator.ts | 1 + src/emulator/downloadableEmulators.ts | 18 ++++++++++++------ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..c86f3ecd431f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Enable single project mode for the database emulator (#5068). diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 022653ac751e..8e2ba913a091 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -712,6 +712,8 @@ export async function startAll( port: databaseAddr.port, projectId, auto_download: true, + // Only set the flag (at all) if singleProjectMode is enabled. + single_project_mode: singleProjectModeEnabled ? "Warning" : undefined, }; // Try to fetch the default RTDB instance for a project, but don't hard-fail if we diff --git a/src/emulator/databaseEmulator.ts b/src/emulator/databaseEmulator.ts index 786f4d1995fc..6f83184de218 100644 --- a/src/emulator/databaseEmulator.ts +++ b/src/emulator/databaseEmulator.ts @@ -21,6 +21,7 @@ export interface DatabaseEmulatorArgs { functions_emulator_port?: number; functions_emulator_host?: string; auto_download?: boolean; + single_project_mode?: string; } export class DatabaseEmulator implements EmulatorInstance { diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index c342e2a24318..f8e385334e5f 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -27,14 +27,14 @@ const CACHE_DIR = export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { database: { - downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.9.0.jar"), - version: "4.9.0", + downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.10.0.jar"), + version: "4.10.0", opts: { cacheDir: CACHE_DIR, remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.9.0.jar", - expectedSize: 34204485, - expectedChecksum: "1c3f5974f0ee5559ebf27b56f2e62108", + "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.10.0.jar", + expectedSize: 34230230, + expectedChecksum: "e99b23f0e723813de4f4ea0e879b46b0", namePrefix: "firebase-database-emulator", }, }, @@ -144,7 +144,13 @@ const Commands: { [s in DownloadableEmulators]: DownloadableEmulatorCommand } = database: { binary: "java", args: ["-Duser.language=en", "-jar", getExecPath(Emulators.DATABASE)], - optionalArgs: ["port", "host", "functions_emulator_port", "functions_emulator_host"], + optionalArgs: [ + "port", + "host", + "functions_emulator_port", + "functions_emulator_host", + "single_project_mode", + ], joinArgs: false, }, firestore: { From 932fdf6fa35bed2808c1eb6b9f4e3bde5424233c Mon Sep 17 00:00:00 2001 From: Tony Huang Date: Fri, 7 Oct 2022 15:03:18 +0200 Subject: [PATCH 022/115] test cleanup (#5075) --- .../conformance/firebase-js-sdk.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts index 5ff116150925..df4accef337e 100644 --- a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts +++ b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts @@ -156,8 +156,8 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); describe(".ref()", () => { - describe("#put()", () => { - it("should upload a file", async () => { + describe("#putString()", () => { + it("should upload a string", async () => { await signInToFirebaseAuth(page); await page.evaluate(async (ref) => { await firebase.storage().ref(ref).putString("hello world"); @@ -180,7 +180,9 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); }); }); + }); + describe("#put()", () => { it("should upload a file with a really long path name to check for os filename character limit", async () => { await signInToFirebaseAuth(page); const uploadState = await uploadText( @@ -249,6 +251,10 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }, IMAGE_FILE_BASE64); expect(uploadState).to.equal("success"); + const [metadata] = await testBucket + .file("upload/allowIfContentTypeImage.png") + .getMetadata(); + expect(metadata.contentType).to.equal("image/blah"); }); it("should return a 403 on rules deny", async () => { @@ -287,6 +293,18 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { const uploadState = await shouldThrowOnUpload(); expect(uploadState!).to.include("User does not have permission"); }); + + it("should default to application/octet-stream", async () => { + await signInToFirebaseAuth(page); + const uploadState = await page.evaluate(async (TEST_FILE_NAME) => { + const task = await firebase.storage().ref(TEST_FILE_NAME).put(new ArrayBuffer(8)); + return task.state; + }, TEST_FILE_NAME); + + expect(uploadState).to.equal("success"); + const [metadata] = await testBucket.file(TEST_FILE_NAME).getMetadata(); + expect(metadata.contentType).to.equal("application/octet-stream"); + }); }); describe("#listAll()", () => { @@ -631,7 +649,7 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); }); - describe("deleteFile", () => { + describe("#delete()", () => { it("should delete file", async () => { await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); await signInToFirebaseAuth(page); From de60d621e406cacb2cf1d61a39c891722e60408a Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Fri, 7 Oct 2022 10:15:36 -0500 Subject: [PATCH 023/115] fix(webframeworks) Force webframeworks functions to lowercase (#5080) Fix an issue where uppercaes sites would be converted to v2 upper case functions, which would break due to limitations with v2 function naming. --- src/frameworks/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 8cbc1a8cf753..caba72703cdc 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -292,7 +292,7 @@ export async function prepareFrameworks( if (publicDir) throw new Error(`hosting.public and hosting.source cannot both be set in firebase.json`); const getProjectPath = (...args: string[]) => join(projectRoot, source, ...args); - const functionName = `ssr${site.replace(/-/g, "")}`; + const functionName = `ssr${site.toLowerCase().replace(/-/g, "")}`; const usesFirebaseAdminSdk = !!findDependency("firebase-admin", { cwd: getProjectPath() }); const usesFirebaseJsSdk = !!findDependency("@firebase/app", { cwd: getProjectPath() }); if (usesFirebaseAdminSdk) { From f71ac920fcb936a14f37176eea71e48653652c4b Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Fri, 7 Oct 2022 09:44:49 -0700 Subject: [PATCH 024/115] Remove getInfoHostString and use connectable hostnames. (#5079) * Remove getInfoHostString and use connectable hostnames. * Fix tests. * Restore Host:Port column. * Revert webframework changes. * Add connectableHostname to formatHost just in case. * Extract env helpers for dependency reasons. * Use fake instead of mock in emulator-tests. --- .../emulator-tests/functionsEmulator.spec.ts | 96 +++++------------- src/commands/emulators-start.ts | 40 ++++---- src/emulator/auth/cloudFunctions.ts | 24 ++--- src/emulator/auth/index.ts | 4 +- src/emulator/auth/utils.ts | 2 +- src/emulator/commandUtils.ts | 79 ++++++--------- src/emulator/constants.ts | 18 ++-- src/emulator/controller.ts | 2 + src/emulator/databaseEmulator.ts | 11 +-- src/emulator/env.ts | 47 +++++++++ src/emulator/eventarcEmulator.ts | 10 +- src/emulator/extensions/postinstall.ts | 16 +-- src/emulator/extensionsEmulator.ts | 16 ++- src/emulator/firestoreEmulator.ts | 15 +-- src/emulator/functionsEmulator.ts | 98 +++++-------------- src/emulator/functionsEmulatorShared.ts | 16 ++- src/emulator/functionsEmulatorShell.ts | 5 +- src/emulator/hub.ts | 5 +- src/emulator/hubExport.ts | 20 ++-- src/emulator/pubsubEmulator.ts | 25 ++--- src/emulator/registry.ts | 53 +++++----- src/emulator/storage/apis/gcloud.ts | 12 +-- src/emulator/storage/cloudFunctions.ts | 14 +-- src/emulator/storage/metadata.ts | 29 +++--- src/emulator/storage/rules/runtime.ts | 8 +- src/emulator/ui.ts | 3 +- src/hosting/functionsProxy.ts | 11 +-- src/hosting/implicitInit.ts | 18 +--- .../{ => auth}/cloudFunctions.spec.ts | 37 +++---- .../emulators/extensions/postinstall.spec.ts | 52 +++++----- src/test/utils.spec.ts | 14 +++ src/utils.ts | 21 ++++ templates/hosting/init.js | 5 +- 33 files changed, 360 insertions(+), 466 deletions(-) create mode 100644 src/emulator/env.ts rename src/test/emulators/{ => auth}/cloudFunctions.spec.ts (56%) diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index b326b0f247ed..a4fc1e68e655 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -11,10 +11,12 @@ import * as logform from "logform"; import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared"; import { FunctionsEmulator } from "../../src/emulator/functionsEmulator"; import { Emulators } from "../../src/emulator/types"; +import { FakeEmulator } from "../../src/test/emulators/fakeEmulator"; import { TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures"; import { logger } from "../../src/logger"; import * as registry from "../../src/emulator/registry"; import * as secretManager from "../../src/gcp/secretManager"; +import { findAvailablePort } from "../../src/emulator/portUtils"; if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { const dropLogLevels = (info: logform.TransformableInfo) => info.message; @@ -612,29 +614,31 @@ describe("FunctionsEmulator-Hub", function () { }); describe("environment variables", () => { - let emulatorRegistryStub: sinon.SinonStub; - - beforeEach(() => { - emulatorRegistryStub = sinon.stub(registry.EmulatorRegistry, "getInfo").returns(undefined); - }); + const host = "127.0.0.1"; + const startFakeEmulator = async (emulator: Emulators): Promise => { + const port = await findAvailablePort(host, 4000); + const fake = new FakeEmulator(emulator, host, port); + await registry.EmulatorRegistry.start(fake); + return port; + }; afterEach(() => { - emulatorRegistryStub.restore(); + return registry.EmulatorRegistry.stopAll(); }); - it("should set FIREBASE_DATABASE_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.DATABASE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, - }); + it("should set env vars when the emulator is running", async () => { + const databasePort = await startFakeEmulator(Emulators.DATABASE); + const firestorePort = await startFakeEmulator(Emulators.FIRESTORE); + const authPort = await startFakeEmulator(Emulators.AUTH); await useFunction(emu, "functionId", () => { return { functionId: require("firebase-functions").https.onRequest( (_req: express.Request, res: express.Response) => { res.json({ - var: process.env.FIREBASE_DATABASE_EMULATOR_HOST, + databaseHost: process.env.FIREBASE_DATABASE_EMULATOR_HOST, + firestoreHost: process.env.FIRESTORE_EMULATOR_HOST, + authHost: process.env.FIREBASE_AUTH_EMULATOR_HOST, }); } ), @@ -645,70 +649,14 @@ describe("FunctionsEmulator-Hub", function () { .get("/fake-project-id/us-central1/functionId") .expect(200) .then((res) => { - expect(res.body.var).to.eql("localhost:9090"); - }); - }).timeout(TIMEOUT_MED); - - it("should set FIRESTORE_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.FIRESTORE).returns({ - name: Emulators.FIRESTORE, - host: "localhost", - port: 9090, - }); - - await useFunction(emu, "functionId", () => { - return { - functionId: require("firebase-functions").https.onRequest( - (_req: express.Request, res: express.Response) => { - res.json({ - var: process.env.FIRESTORE_EMULATOR_HOST, - }); - } - ), - }; - }); - - await supertest(emu.createHubServer()) - .get("/fake-project-id/us-central1/functionId") - .expect(200) - .then((res) => { - expect(res.body.var).to.eql("localhost:9090"); - }); - }).timeout(TIMEOUT_MED); - - it("should set AUTH_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.AUTH).returns({ - name: Emulators.AUTH, - host: "localhost", - port: 9099, - }); - - await useFunction(emu, "functionId", () => { - return { - functionId: require("firebase-functions").https.onRequest( - (_req: express.Request, res: express.Response) => { - res.json({ - var: process.env.FIREBASE_AUTH_EMULATOR_HOST, - }); - } - ), - }; - }); - - await supertest(emu.createHubServer()) - .get("/fake-project-id/us-central1/functionId") - .expect(200) - .then((res) => { - expect(res.body.var).to.eql("localhost:9099"); + expect(res.body.databaseHost).to.eql(`${host}:${databasePort}`); + expect(res.body.firestoreHost).to.eql(`${host}:${firestorePort}`); + expect(res.body.authHost).to.eql(`${host}:${authPort}`); }); }).timeout(TIMEOUT_MED); it("should return an emulated databaseURL when RTDB emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.DATABASE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, - }); + const databasePort = await startFakeEmulator(Emulators.DATABASE); await useFunction(emu, "functionId", () => { return { @@ -725,7 +673,7 @@ describe("FunctionsEmulator-Hub", function () { .expect(200) .then((res) => { expect(res.body.databaseURL).to.eql( - "http://localhost:9090/?ns=fake-project-id-default-rtdb" + `http://${host}:${databasePort}/?ns=fake-project-id-default-rtdb` ); }); }).timeout(TIMEOUT_MED); diff --git a/src/commands/emulators-start.ts b/src/commands/emulators-start.ts index 7ea8103e2453..b2ac228325b1 100644 --- a/src/commands/emulators-start.ts +++ b/src/commands/emulators-start.ts @@ -64,12 +64,10 @@ function printEmulatorOverview(options: any): void { } const reservedPortsString = reservedPorts.length > 0 ? reservedPorts.join(", ") : "None"; - const uiInfo = EmulatorRegistry.getInfo(Emulators.UI); - const hubInfo = EmulatorRegistry.getInfo(Emulators.HUB); - const uiUrl = uiInfo ? `http://${EmulatorRegistry.getInfoHostString(uiInfo)}` : "unknown"; + const uiRunning = EmulatorRegistry.isRunning(Emulators.UI); const head = ["Emulator", "Host:Port"]; - if (uiInfo) { + if (uiRunning) { head.push(`View in ${Constants.description(Emulators.UI)}`); } @@ -77,8 +75,10 @@ function printEmulatorOverview(options: any): void { let successMsg = `${clc.green("✔")} ${clc.bold( "All emulators ready! It is now safe to connect your app." )}`; - if (uiInfo) { - successMsg += `\n${clc.cyan("i")} View Emulator UI at ${stylizeLink(uiUrl)}`; + if (uiRunning) { + successMsg += `\n${clc.cyan("i")} View Emulator UI at ${stylizeLink( + EmulatorRegistry.url(Emulators.UI).toString() + )}`; } successMessageTable.push([successMsg]); @@ -95,22 +95,26 @@ function printEmulatorOverview(options: any): void { .map((emulator) => { const emulatorName = Constants.description(emulator).replace(/ emulator/i, ""); const isSupportedByUi = EMULATORS_SUPPORTED_BY_UI.includes(emulator); - // The Extensions emulator runs as part of the Functions emulator, so display the Functions emulators info instead. - const info = EmulatorRegistry.getInfo(emulator); - if (!info) { - return [emulatorName, "Failed to initialize (see above)", "", ""]; + const listen = commandUtils.getListenOverview(emulator); + if (!listen) { + const row = [emulatorName, "Failed to initialize (see above)"]; + if (uiRunning) { + row.push(""); + } + } + let uiLink = "n/a"; + if (isSupportedByUi && uiRunning) { + const url = EmulatorRegistry.url(Emulators.UI); + url.pathname = `/${emulator}`; + uiLink = stylizeLink(url.toString()); } - return [ - emulatorName, - EmulatorRegistry.getInfoHostString(info), - isSupportedByUi && uiInfo ? stylizeLink(`${uiUrl}/${emulator}`) : clc.blackBright("n/a"), - ]; + return [emulatorName, listen, uiLink]; }) .map((col) => col.slice(0, head.length)) .filter((v) => v) ); - let extensionsTable: string = ""; + let extensionsTable = ""; if (EmulatorRegistry.isRunning(Emulators.EXTENSIONS)) { const extensionsEmulatorInstance = EmulatorRegistry.get( Emulators.EXTENSIONS @@ -121,8 +125,8 @@ function printEmulatorOverview(options: any): void { ${emulatorsTable} ${ - hubInfo - ? clc.blackBright(" Emulator Hub running at ") + EmulatorRegistry.getInfoHostString(hubInfo) + EmulatorRegistry.isRunning(Emulators.HUB) + ? clc.blackBright(" Emulator Hub running at ") + EmulatorRegistry.url(Emulators.HUB).host : clc.blackBright(" Emulator Hub not running.") } ${clc.blackBright(" Other reserved ports:")} ${reservedPortsString} diff --git a/src/emulator/auth/cloudFunctions.ts b/src/emulator/auth/cloudFunctions.ts index 73f930134473..ad0ccc78c433 100644 --- a/src/emulator/auth/cloudFunctions.ts +++ b/src/emulator/auth/cloudFunctions.ts @@ -1,9 +1,8 @@ import * as uuid from "uuid"; import { EventContext } from "firebase-functions"; -import { Client } from "../../apiv2"; -import { EmulatorInfo, Emulators } from "../types"; +import { Emulators } from "../types"; import { EmulatorLogger } from "../emulatorLogger"; import { EmulatorRegistry } from "../registry"; import { UserInfo } from "./state"; @@ -16,22 +15,10 @@ type CreateEvent = EventContext & { export class AuthCloudFunction { private logger = EmulatorLogger.forEmulator(Emulators.AUTH); - private functionsEmulatorInfo?: EmulatorInfo; - private multicastOrigin = ""; - private multicastPath = ""; private enabled = false; constructor(private projectId: string) { - const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - - if (functionsEmulator) { - this.enabled = true; - this.functionsEmulatorInfo = functionsEmulator.getInfo(); - this.multicastOrigin = `http://${EmulatorRegistry.getInfoHostString( - this.functionsEmulatorInfo - )}`; - this.multicastPath = `/functions/projects/${projectId}/trigger_multicast`; - } + this.enabled = EmulatorRegistry.isRunning(Emulators.FUNCTIONS); } public async dispatch(action: AuthCloudFunctionAction, user: UserInfo): Promise { @@ -40,11 +27,14 @@ export class AuthCloudFunction { const userInfoPayload = this.createUserInfoPayload(user); const multicastEventBody = this.createEventRequestBody(action, userInfoPayload); - const c = new Client({ urlPrefix: this.multicastOrigin, auth: false }); + const c = EmulatorRegistry.client(Emulators.FUNCTIONS); let res; let err: Error | undefined; try { - res = await c.post(this.multicastPath, multicastEventBody); + res = await c.post( + `/functions/projects/${this.projectId}/trigger_multicast`, + multicastEventBody + ); } catch (e: any) { err = e; } diff --git a/src/emulator/auth/index.ts b/src/emulator/auth/index.ts index ddc2197efc80..7b34615220c7 100644 --- a/src/emulator/auth/index.ts +++ b/src/emulator/auth/index.ts @@ -84,7 +84,7 @@ export class AuthEmulator implements EmulatorInstance { await importFromFile( { method: "PATCH", - host, + host: utils.connectableHostname(host), port, path: `/emulator/v1/projects/${projectId}/config`, headers: { @@ -110,7 +110,7 @@ export class AuthEmulator implements EmulatorInstance { await importFromFile( { method: "POST", - host, + host: utils.connectableHostname(host), port, path: `/identitytoolkit.googleapis.com/v1/projects/${projectId}/accounts:batchCreate`, headers: { diff --git a/src/emulator/auth/utils.ts b/src/emulator/auth/utils.ts index d77988726288..f654013dbe53 100644 --- a/src/emulator/auth/utils.ts +++ b/src/emulator/auth/utils.ts @@ -177,7 +177,7 @@ export function logError(err: Error): void { * terminal or Emulator UI). */ export function authEmulatorUrl(req: express.Request): URL { - if (EmulatorRegistry.getInfo(Emulators.AUTH)) { + if (EmulatorRegistry.isRunning(Emulators.AUTH)) { return EmulatorRegistry.url(Emulators.AUTH); } else { return EmulatorRegistry.url(Emulators.AUTH, req); diff --git a/src/emulator/commandUtils.ts b/src/emulator/commandUtils.ts index 02d1aca0996d..7f3aeadd5a26 100644 --- a/src/emulator/commandUtils.ts +++ b/src/emulator/commandUtils.ts @@ -12,15 +12,14 @@ import { requireConfig } from "../requireConfig"; import { Emulators, ALL_SERVICE_EMULATORS } from "./types"; import { FirebaseError } from "../error"; import { EmulatorRegistry } from "./registry"; -import { FirestoreEmulator } from "./firestoreEmulator"; import { getProjectId } from "../projectUtils"; import { promptOnce } from "../prompt"; -import { onExit } from "./controller"; import * as fsutils from "../fsutils"; import Signals = NodeJS.Signals; import SignalsListener = NodeJS.SignalsListener; import Table = require("cli-table"); import { emulatorSession } from "../track"; +import { setEnvVarsForEmulators } from "./env"; export const FLAG_ONLY = "--only "; export const DESC_ONLY = @@ -256,7 +255,7 @@ function processKillSignal( `Please wait for a clean shutdown or send the ${signalDisplay} signal again to stop right now.` ); // in case of a double 'Ctrl-C' we do not want to cleanly exit with onExit/cleanShutdown - await onExit(options); + await controller.onExit(options); await controller.cleanShutdown(); } else { logger.debug(`Skipping clean onExit() and cleanShutdown()`); @@ -286,7 +285,7 @@ function processKillSignal( pids.push(emulatorInfo.pid as number); emulatorsTable.push([ Constants.description(emulatorInfo.name), - EmulatorRegistry.getInfoHostString(emulatorInfo), + getListenOverview(emulatorInfo.name) ?? "unknown", emulatorInfo.pid, ]); } @@ -331,51 +330,7 @@ async function runScript(script: string, extraEnv: Record): Prom const env: NodeJS.ProcessEnv = { ...process.env, ...extraEnv }; - const databaseInstance = EmulatorRegistry.get(Emulators.DATABASE); - if (databaseInstance) { - const info = databaseInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = address; - } - - const firestoreInstance = EmulatorRegistry.get(Emulators.FIRESTORE); - if (firestoreInstance) { - const info = firestoreInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - - env[Constants.FIRESTORE_EMULATOR_HOST] = address; - env[FirestoreEmulator.FIRESTORE_EMULATOR_ENV_ALT] = address; - } - - const storageInstance = EmulatorRegistry.get(Emulators.STORAGE); - if (storageInstance) { - const info = storageInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - - env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = address; - env[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${address}`; - } - - const authInstance = EmulatorRegistry.get(Emulators.AUTH); - if (authInstance) { - const info = authInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = address; - } - - const hubInstance = EmulatorRegistry.get(Emulators.HUB); - if (hubInstance) { - const info = hubInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_EMULATOR_HUB] = address; - } - - const eventarcInstance = EmulatorRegistry.get(Emulators.EVENTARC); - if (eventarcInstance) { - const info = eventarcInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.CLOUD_EVENTARC_EMULATOR_HOST] = address; - } + setEnvVarsForEmulators(env); const proc = childProcess.spawn(script, { stdio: ["inherit", "inherit", "inherit"] as childProcess.StdioOptions, @@ -419,6 +374,30 @@ async function runScript(script: string, extraEnv: Record): Prom }); } +/** + * For overview tables ONLY. Use EmulatorRegistry methods instead for connecting. + * + * This method returns a string suitable for printing into CLI outputs, resembling + * a netloc part of URL. This makes it clickable in many terminal emulators, a + * specific customer request. + * + * Note that this method does not transform the hostname and may return 0.0.0.0 + * etc. that may not work in some browser / OS combinations. When trying to send + * a network request, use `EmulatorRegistry.client()` instead. When constructing + * URLs (especially links printed/shown), use `EmulatorRegistry.url()`. + */ +export function getListenOverview(emulator: Emulators): string | undefined { + const info = EmulatorRegistry.get(emulator)?.getInfo(); + if (!info) { + return undefined; + } + if (info.host.includes(":")) { + return `[${info.host}]:${info.port}`; + } else { + return `${info.host}:${info.port}`; + } +} + /** * The action function for emulators:exec. * Starts the appropriate emulators, executes the provided script, @@ -444,7 +423,7 @@ export async function emulatorExec(script: string, options: any): Promise const showUI = !!options.ui; ({ deprecationNotices } = await controller.startAll(options, showUI)); exitCode = await runScript(script, extraEnv); - await onExit(options); + await controller.onExit(options); } finally { await controller.cleanShutdown(); } diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 1e30d91c07f1..e56b295d0d93 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -1,5 +1,3 @@ -import * as url from "url"; - import { Emulators } from "./types"; export const DEFAULT_PORTS: { [s in Emulators]: number } = { @@ -60,6 +58,9 @@ export class Constants { // Environment variable to override SDK/CLI to point at the Firestore emulator. static FIRESTORE_EMULATOR_HOST = "FIRESTORE_EMULATOR_HOST"; + // Alternative (deprecated) env var for Firestore Emulator. + static FIRESTORE_EMULATOR_ENV_ALT = "FIREBASE_FIRESTORE_EMULATOR_ADDRESS"; + // Environment variable to override SDK/CLI to point at the Realtime Database emulator. static FIREBASE_DATABASE_EMULATOR_HOST = "FIREBASE_DATABASE_EMULATOR_HOST"; @@ -74,6 +75,9 @@ export class Constants { // this one must start with 'http://'. static CLOUD_STORAGE_EMULATOR_HOST = "STORAGE_EMULATOR_HOST"; + // Environment variable to discover the eventarc emulator. + static PUBSUB_EMULATOR_HOST = "PUBSUB_EMULATOR_HOST"; + // Environment variable to discover the eventarc emulator. static CLOUD_EVENTARC_EMULATOR_HOST = "CLOUD_EVENTARC_EMULATOR_HOST"; @@ -133,16 +137,6 @@ export class Constants { return EMULATOR_DESCRIPTION[name]; } - static normalizeHost(host: string): string { - let normalized = host; - if (!normalized.startsWith("http")) { - normalized = `http://${normalized}`; - } - - const u = url.parse(normalized); - return u.hostname || DEFAULT_HOST; - } - static isDemoProject(projectId?: string): boolean { return !!projectId && projectId.startsWith(this.FAKE_PROJECT_ID_PREFIX); } diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 8e2ba913a091..aa9d22798fbc 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -505,6 +505,8 @@ export async function startAll( const emulators: EmulatorInfo[] = []; if (experiments.isEnabled("webframeworks")) { for (const e of EMULATORS_SUPPORTED_BY_UI) { + // TODO: Double check if this actually works -- we're early in the startup + // process and emulators are probably not yet running / registered. const info = EmulatorRegistry.getInfo(e); if (info) emulators.push(info); } diff --git a/src/emulator/databaseEmulator.ts b/src/emulator/databaseEmulator.ts index 6f83184de218..9cfb94ebcacd 100644 --- a/src/emulator/databaseEmulator.ts +++ b/src/emulator/databaseEmulator.ts @@ -11,7 +11,7 @@ import { EmulatorRegistry } from "./registry"; import { EmulatorLogger } from "./emulatorLogger"; import { FirebaseError } from "../error"; import { parseBoltRules } from "../parseBoltRules"; -import { Client } from "../apiv2"; +import { connectableHostname } from "../utils"; export interface DatabaseEmulatorArgs { port?: number; @@ -132,7 +132,7 @@ export class DatabaseEmulator implements EmulatorInstance { const req = http.request( { method: "PUT", - host, + host: connectableHostname(host), port, path: `/.json?ns=${ns}&disableTriggers=true&writeSizeLimit=unlimited`, headers: { @@ -169,13 +169,8 @@ export class DatabaseEmulator implements EmulatorInstance { ? parseBoltRules(rulesPath).toString() : fs.readFileSync(rulesPath, "utf8"); - const info = this.getInfo(); try { - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(info)}`, - auth: false, - }); - await client.put(`/.settings/rules.json`, content, { + await EmulatorRegistry.client(Emulators.DATABASE).put(`/.settings/rules.json`, content, { headers: { Authorization: "Bearer owner" }, queryParams: { ns: instance }, }); diff --git a/src/emulator/env.ts b/src/emulator/env.ts new file mode 100644 index 000000000000..05524e25adf1 --- /dev/null +++ b/src/emulator/env.ts @@ -0,0 +1,47 @@ +import { Constants } from "./constants"; +import { Emulators } from "./types"; +import { EmulatorRegistry } from "./registry"; + +/** + * Adds or replaces emulator-related env vars (for Admin SDKs, etc.). + * @param env a `process.env`-like object or Record to be modified + */ +export function setEnvVarsForEmulators(env: Record): void { + if (EmulatorRegistry.isRunning(Emulators.DATABASE)) { + env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = EmulatorRegistry.url(Emulators.DATABASE).host; + } + + if (EmulatorRegistry.isRunning(Emulators.FIRESTORE)) { + const { host } = EmulatorRegistry.url(Emulators.FIRESTORE); + env[Constants.FIRESTORE_EMULATOR_HOST] = host; + env[Constants.FIRESTORE_EMULATOR_ENV_ALT] = host; + } + + if (EmulatorRegistry.isRunning(Emulators.STORAGE)) { + const { host } = EmulatorRegistry.url(Emulators.STORAGE); + env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = host; + // The protocol is required for the Google Cloud Storage Node.js Client SDK. + env[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${host}`; + } + + if (EmulatorRegistry.isRunning(Emulators.AUTH)) { + env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = EmulatorRegistry.url(Emulators.AUTH).host; + } + + if (EmulatorRegistry.isRunning(Emulators.HUB)) { + env[Constants.FIREBASE_EMULATOR_HUB] = EmulatorRegistry.url(Emulators.HUB).host; + } + + const pubsubEmulator = EmulatorRegistry.isRunning(Emulators.PUBSUB); + if (pubsubEmulator) { + env[Constants.PUBSUB_EMULATOR_HOST] = EmulatorRegistry.url(Emulators.PUBSUB).host; + } + + if (EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + // The protocol is required for the Firebase Admin Node.js SDK for Eventarc. + // https://github.com/firebase/firebase-admin-node/blob/ee60cd1acb8722ba4081b9837d2f90101e2b3227/src/eventarc/eventarc-client-internal.ts#L105 + env[Constants.CLOUD_EVENTARC_EMULATOR_HOST] = `http://${ + EmulatorRegistry.url(Emulators.EVENTARC).host + }`; + } +} diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index 02768dd6ec7e..e08f9c358c54 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -7,7 +7,6 @@ import { EmulatorLogger } from "./emulatorLogger"; import { EventTrigger } from "./functionsEmulatorShared"; import { CloudEvent } from "./events/types"; import { EmulatorRegistry } from "./registry"; -import { Client } from "../apiv2"; import { FirebaseError } from "../error"; interface CustomEventTrigger { @@ -106,17 +105,12 @@ export class EventarcEmulator implements EmulatorInstance { } async triggerCustomEventFunction(channel: string, event: CloudEvent) { - const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (!functionsEmulator) { + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { this.logger.log("INFO", "Functions emulator not found. This should not happen."); return Promise.reject(); } const key = `${event.type}-${channel}`; const triggers = this.customEvents[key] || []; - const apiClient = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(functionsEmulator.getInfo())}`, - auth: false, - }); return await Promise.all( triggers .filter( @@ -125,7 +119,7 @@ export class EventarcEmulator implements EmulatorInstance { this.matchesAll(event, trigger.eventTrigger.eventFilters) ) .map((trigger) => - apiClient + EmulatorRegistry.client(Emulators.FUNCTIONS) .request, NodeJS.ReadableStream>({ method: "POST", path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, diff --git a/src/emulator/extensions/postinstall.ts b/src/emulator/extensions/postinstall.ts index 666265b7ac22..a4f9fb2a20f5 100644 --- a/src/emulator/extensions/postinstall.ts +++ b/src/emulator/extensions/postinstall.ts @@ -6,33 +6,33 @@ import { Emulators } from "../types"; * @param postinstall The postinstall instructions to check for console links. */ export function replaceConsoleLinks(postinstall: string): string { - const uiInfo = EmulatorRegistry.getInfo(Emulators.UI); - const uiUrl = uiInfo ? `http://${EmulatorRegistry.getInfoHostString(uiInfo)}` : "unknown"; + const uiRunning = EmulatorRegistry.isRunning(Emulators.UI); + const uiUrl = uiRunning ? EmulatorRegistry.url(Emulators.UI).toString() : "unknown"; let subbedPostinstall = postinstall; const linkReplacements = new Map([ [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/storage[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/${Emulators.STORAGE}`, + `${uiUrl}${Emulators.STORAGE}`, ], // Storage console links [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/firestore[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/${Emulators.FIRESTORE}`, + `${uiUrl}${Emulators.FIRESTORE}`, ], // Firestore console links [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/database[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/${Emulators.DATABASE}`, + `${uiUrl}${Emulators.DATABASE}`, ], // RTDB console links [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/authentication[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/${Emulators.AUTH}`, + `${uiUrl}${Emulators.AUTH}`, ], // Auth console links [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/functions[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/logs`, // There is no functions page in the UI, so redirect to logs. + `${uiUrl}logs`, // There is no functions page in the UI, so redirect to logs. ], // Functions console links [ /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/extensions[A-Za-z0-9\/-]*(?=[\)\]\s])/, - `${uiUrl}/${Emulators.EXTENSIONS}`, + `${uiUrl}${Emulators.EXTENSIONS}`, ], // Extensions console links ]); for (const [consoleLinkRegex, replacement] of linkReplacements) { diff --git a/src/emulator/extensionsEmulator.ts b/src/emulator/extensionsEmulator.ts index 073ab05d118a..3d69232be73e 100644 --- a/src/emulator/extensionsEmulator.ts +++ b/src/emulator/extensionsEmulator.ts @@ -61,13 +61,13 @@ export class ExtensionsEmulator implements EmulatorInstance { } public getInfo(): EmulatorInfo { - const info = EmulatorRegistry.getInfo(Emulators.FUNCTIONS); - if (!info) { + const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); + if (!functionsEmulator) { throw new FirebaseError( "Extensions Emulator is running but Functions emulator is not. This should never happen." ); } - return info; + return functionsEmulator.getInfo(); } public getName(): Emulators { @@ -344,15 +344,13 @@ export class ExtensionsEmulator implements EmulatorInstance { } private extensionDetailsUILink(backend: EmulatableBackend): string { - const uiInfo = EmulatorRegistry.getInfo(Emulators.UI); - if (!uiInfo || !backend.extensionInstanceId) { + if (!EmulatorRegistry.isRunning(Emulators.UI) || !backend.extensionInstanceId) { // If the Emulator UI is not running, or if this is not an Extension backend, return an empty string return ""; } - const uiUrl = EmulatorRegistry.getInfoHostString(uiInfo); - return clc.underline( - clc.bold(`http://${uiUrl}/${Emulators.EXTENSIONS}/${backend.extensionInstanceId}`) - ); + const uiUrl = EmulatorRegistry.url(Emulators.UI); + uiUrl.pathname = `/${Emulators.EXTENSIONS}/${backend.extensionInstanceId}`; + return clc.underline(clc.bold(uiUrl.toString())); } public extensionsInfoTable(options: Options): string { diff --git a/src/emulator/firestoreEmulator.ts b/src/emulator/firestoreEmulator.ts index b8c23eb7db5c..190991b4834f 100644 --- a/src/emulator/firestoreEmulator.ts +++ b/src/emulator/firestoreEmulator.ts @@ -9,7 +9,6 @@ import { EmulatorInfo, EmulatorInstance, Emulators, Severity } from "../emulator import { EmulatorRegistry } from "./registry"; import { Constants } from "./constants"; import { Issue } from "./types"; -import { Client } from "../apiv2"; export interface FirestoreEmulatorArgs { port?: number; @@ -33,16 +32,13 @@ export interface FirestoreEmulatorInfo extends EmulatorInfo { } export class FirestoreEmulator implements EmulatorInstance { - static FIRESTORE_EMULATOR_ENV_ALT = "FIREBASE_FIRESTORE_EMULATOR_ADDRESS"; - rulesWatcher?: chokidar.FSWatcher; constructor(private args: FirestoreEmulatorArgs) {} async start(): Promise { - const functionsInfo = EmulatorRegistry.getInfo(Emulators.FUNCTIONS); - if (functionsInfo) { - this.args.functions_emulator = EmulatorRegistry.getInfoHostString(functionsInfo); + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + this.args.functions_emulator = EmulatorRegistry.url(Emulators.FUNCTIONS).host; } if (this.args.rules && this.args.project_id) { @@ -108,7 +104,6 @@ export class FirestoreEmulator implements EmulatorInstance { private async updateRules(content: string): Promise { const projectId = this.args.project_id; - const info = this.getInfo(); const body = { // Invalid rulesets will still result in a 200 response but with more information ignore_errors: true, @@ -122,11 +117,7 @@ export class FirestoreEmulator implements EmulatorInstance { }, }; - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(info)}`, - auth: false, - }); - const res = await client.put( + const res = await EmulatorRegistry.client(Emulators.FIRESTORE).put( `/emulator/v1/projects/${projectId}:securityRules`, body ); diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 387cc334eea7..1896ef14bdf6 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -42,7 +42,7 @@ import { RuntimeWorker, RuntimeWorkerPool } from "./functionsRuntimeWorker"; import { PubsubEmulator } from "./pubsubEmulator"; import { FirebaseError } from "../error"; import { WorkQueue } from "./workQueue"; -import { allSettled, createDestroyer, debounce } from "../utils"; +import { allSettled, connectableHostname, createDestroyer, debounce } from "../utils"; import { getCredentialPathAsync } from "../defaultCredentials"; import { AdminSdkConfig, @@ -57,8 +57,8 @@ import * as backend from "../deploy/functions/backend"; import * as functionsEnv from "../functions/env"; import { AUTH_BLOCKING_EVENTS, BEFORE_CREATE_EVENT } from "../functions/events/v1"; import { BlockingFunctionsConfig } from "../gcp/identityPlatform"; -import { Client } from "../apiv2"; import { resolveBackend } from "../deploy/functions/build"; +import { setEnvVarsForEmulators } from "./env"; const EVENT_INVOKE = "functions:invoke"; // event name for UA const EVENT_INVOKE_GA4 = "functions_invoke"; // event name GA4 (alphanumertic) @@ -153,13 +153,19 @@ interface EmulatedTriggerRecord { export class FunctionsEmulator implements EmulatorInstance { static getHttpFunctionUrl( - host: string, - port: number, projectId: string, name: string, - region: string + region: string, + info?: { host: string; port: number } ): string { - return `http://${host}:${port}/${projectId}/${region}/${name}`; + let url: URL; + if (info) { + url = new URL("http://" + formatHost(info)); + } else { + url = EmulatorRegistry.url(Emulators.FUNCTIONS); + } + url.pathname = `/${projectId}/${region}/${name}`; + return url.toString(); } private destroyServer?: () => Promise; @@ -289,7 +295,7 @@ export class FunctionsEmulator implements EmulatorInstance { return new Promise((resolve, reject) => { const trigReq = http.request( { - host, + host: connectableHostname(host), port, method: req.method, path: `/functions/projects/${projectId}/triggers/${triggerId}`, @@ -562,12 +568,9 @@ export class FunctionsEmulator implements EmulatorInstance { let added = false; let url: string | undefined = undefined; - const { host, port } = this.getInfo(); if (definition.httpsTrigger) { added = true; url = FunctionsEmulator.getHttpFunctionUrl( - host, - port, this.args.projectId, definition.name, definition.region @@ -621,10 +624,7 @@ export class FunctionsEmulator implements EmulatorInstance { break; } } else if (definition.blockingTrigger) { - const { host, port } = this.getInfo(); url = FunctionsEmulator.getHttpFunctionUrl( - host, - port, this.args.projectId, definition.name, definition.region @@ -663,8 +663,7 @@ export class FunctionsEmulator implements EmulatorInstance { } addEventarcTrigger(projectId: string, key: string, eventTrigger: EventTrigger): Promise { - const eventarcEmu = EmulatorRegistry.get(Emulators.EVENTARC); - if (!eventarcEmu) { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { return Promise.resolve(false); } const bundle = { @@ -674,11 +673,7 @@ export class FunctionsEmulator implements EmulatorInstance { }, }; logger.debug(`addEventarcTrigger`, JSON.stringify(bundle)); - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(eventarcEmu.getInfo())}`, - auth: false, - }); - return client + return EmulatorRegistry.client(Emulators.EVENTARC) .post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle) .then(() => true) .catch((err) => { @@ -695,18 +690,14 @@ export class FunctionsEmulator implements EmulatorInstance { return; } - const authEmu = EmulatorRegistry.get(Emulators.AUTH); - if (!authEmu) { + if (!EmulatorRegistry.isRunning(Emulators.AUTH)) { return; } const path = `/identitytoolkit.googleapis.com/v2/projects/${this.getProjectId()}/config?updateMask=blockingFunctions`; try { - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(authEmu.getInfo())}`, - auth: false, - }); + const client = EmulatorRegistry.client(Emulators.AUTH); await client.patch( path, { blockingFunctions: this.blockingFunctionsConfig }, @@ -793,8 +784,7 @@ export class FunctionsEmulator implements EmulatorInstance { signature: SignatureType, region: string ): Promise { - const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE); - if (!databaseEmu) { + if (!EmulatorRegistry.isRunning(Emulators.DATABASE)) { return false; } @@ -805,10 +795,7 @@ export class FunctionsEmulator implements EmulatorInstance { logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(databaseEmu.getInfo())}`, - auth: false, - }); + const client = EmulatorRegistry.client(Emulators.DATABASE); try { await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } }); } catch (err: any) { @@ -823,8 +810,7 @@ export class FunctionsEmulator implements EmulatorInstance { key: string, eventTrigger: EventTrigger ): Promise { - const firestoreEmu = EmulatorRegistry.get(Emulators.FIRESTORE); - if (!firestoreEmu) { + if (!EmulatorRegistry.isRunning(Emulators.FIRESTORE)) { return Promise.resolve(false); } @@ -836,10 +822,7 @@ export class FunctionsEmulator implements EmulatorInstance { }); logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle)); - const client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(firestoreEmu.getInfo())}`, - auth: false, - }); + const client = EmulatorRegistry.client(Emulators.FIRESTORE); try { await client.put(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle); } catch (err: any) { @@ -1149,43 +1132,8 @@ export class FunctionsEmulator implements EmulatorInstance { skipTokenVerification: true, enableCors: true, }); - // Make firebase-admin point at the Firestore emulator - const firestoreEmulator = this.getEmulatorInfo(Emulators.FIRESTORE); - if (firestoreEmulator != null) { - envs[Constants.FIRESTORE_EMULATOR_HOST] = formatHost(firestoreEmulator); - } - - // Make firebase-admin point at the Database emulator - const databaseEmulator = this.getEmulatorInfo(Emulators.DATABASE); - if (databaseEmulator) { - envs[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = formatHost(databaseEmulator); - } - - // Make firebase-admin point at the Auth emulator - const authEmulator = this.getEmulatorInfo(Emulators.AUTH); - if (authEmulator) { - envs[Constants.FIREBASE_AUTH_EMULATOR_HOST] = formatHost(authEmulator); - } - // Make firebase-admin point at the Storage emulator - const storageEmulator = this.getEmulatorInfo(Emulators.STORAGE); - if (storageEmulator) { - envs[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = formatHost(storageEmulator); - // TODO(taeold): We only need FIREBASE_STORAGE_EMULATOR_HOST, as long as the users are using new-ish SDKs. - // Clean up and update documentation in a subsequent patch. - envs[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${formatHost(storageEmulator)}`; - } - - const pubsubEmulator = this.getEmulatorInfo(Emulators.PUBSUB); - if (pubsubEmulator) { - const pubsubHost = formatHost(pubsubEmulator); - process.env.PUBSUB_EMULATOR_HOST = pubsubHost; - } - - const eventarcEmulator = this.getEmulatorInfo(Emulators.EVENTARC); - if (eventarcEmulator) { - envs[Constants.CLOUD_EVENTARC_EMULATOR_HOST] = `http://${formatHost(eventarcEmulator)}`; - } + setEnvVarsForEmulators(envs); if (this.args.debugPort) { // Start runtime in debug mode to allow triggers to share single runtime process. @@ -1304,7 +1252,7 @@ export class FunctionsEmulator implements EmulatorInstance { ); } else { const { host } = this.getInfo(); - args.unshift(`--inspect=${host}:${this.args.debugPort}`); + args.unshift(`--inspect=${connectableHostname(host)}:${this.args.debugPort}`); } } diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 3c9d11099cd0..77155871e4f7 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -16,6 +16,7 @@ import { replaceConsoleLinks } from "./extensions/postinstall"; import { serviceForEndpoint } from "../deploy/functions/services"; import { inferBlockingDetails } from "../deploy/functions/prepare"; import * as events from "../functions/events"; +import { connectableHostname } from "../utils"; /** The current v2 events that are implemented in the emulator */ const V2_EVENTS = [ @@ -407,13 +408,20 @@ export function findModuleRoot(moduleName: string, filepath: string): string { } /** - * Format a hostname for TCP dialing. + * Format a hostname for TCP dialing. Should only be used in Functions emulator. + * + * This is similar to EmulatorRegistry.url but with no explicit dependency on + * the registry and so on and thus can work in functions shell. + * + * For any other part of the CLI, please use EmulatorRegistry.url(...).host + * instead, which handles discovery, formatting, and fixing host in one go. */ export function formatHost(info: { host: string; port: number }): string { - if (info.host.includes(":")) { - return `[${info.host}]:${info.port}`; + const host = connectableHostname(info.host); + if (host.includes(":")) { + return `[${host}]:${info.port}`; } else { - return `${info.host}:${info.port}`; + return `${host}:${info.port}`; } } diff --git a/src/emulator/functionsEmulatorShell.ts b/src/emulator/functionsEmulatorShell.ts index 0cb07b332c0c..18c727325abd 100644 --- a/src/emulator/functionsEmulatorShell.ts +++ b/src/emulator/functionsEmulatorShell.ts @@ -25,11 +25,10 @@ export class FunctionsEmulatorShell implements FunctionsShellController { for (const trigger of this.triggers) { if (trigger.httpsTrigger) { this.urls[trigger.id] = FunctionsEmulator.getHttpFunctionUrl( - this.emu.getInfo().host, - this.emu.getInfo().port, this.emu.getProjectId(), trigger.name, - trigger.region + trigger.region, + this.emu.getInfo() // EmulatorRegistry is not available in shell ); } } diff --git a/src/emulator/hub.ts b/src/emulator/hub.ts index f78757abe5e4..c9b0bc22f046 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -81,9 +81,8 @@ export class EmulatorHub implements EmulatorInstance { this.hub.get(EmulatorHub.PATH_EMULATORS, (req, res) => { const body: GetEmulatorsResponse = {}; - for (const emulator of EmulatorRegistry.listRunning()) { - const info = EmulatorRegistry.getInfo(emulator); - body[emulator] = info!; + for (const info of EmulatorRegistry.listRunningWithInfo()) { + body[info.name] = info; } res.json(body); }); diff --git a/src/emulator/hubExport.ts b/src/emulator/hubExport.ts index bfe9c522025a..6065c63fbf97 100644 --- a/src/emulator/hubExport.ts +++ b/src/emulator/hubExport.ts @@ -10,9 +10,7 @@ import { FirebaseError } from "../error"; import { EmulatorHub } from "./hub"; import { getDownloadDetails } from "./downloadableEmulators"; import { DatabaseEmulator } from "./databaseEmulator"; -import { StorageEmulator } from "./storage"; import * as rimraf from "rimraf"; -import { Client } from "../apiv2"; import { trackEmulator } from "../track"; export interface FirestoreExportMetadata { @@ -142,23 +140,21 @@ export class HubExport { emulator_name: Emulators.FIRESTORE, }); - const firestoreInfo = EmulatorRegistry.get(Emulators.FIRESTORE)!.getInfo(); - const firestoreHost = `http://${EmulatorRegistry.getInfoHostString(firestoreInfo)}`; - const firestoreExportBody = { database: `projects/${this.projectId}/databases/(default)`, export_directory: this.tmpDir, export_name: metadata.firestore!!.path, }; - const client = new Client({ urlPrefix: firestoreHost, auth: false }); - await client.post(`/emulator/v1/projects/${this.projectId}:export`, firestoreExportBody); + await EmulatorRegistry.client(Emulators.FIRESTORE).post( + `/emulator/v1/projects/${this.projectId}:export`, + firestoreExportBody + ); } private async exportDatabase(metadata: ExportMetadata): Promise { const databaseEmulator = EmulatorRegistry.get(Emulators.DATABASE) as DatabaseEmulator; - const databaseAddr = `http://${EmulatorRegistry.getInfoHostString(databaseEmulator.getInfo())}`; - const client = new Client({ urlPrefix: databaseAddr, auth: true }); + const client = EmulatorRegistry.client(Emulators.DATABASE, { auth: true }); // Get the list of namespaces const inspectURL = `/.inspect/databases.json`; @@ -260,8 +256,6 @@ export class HubExport { } private async exportStorage(metadata: ExportMetadata): Promise { - const storageEmulator = EmulatorRegistry.get(Emulators.STORAGE) as StorageEmulator; - // Clear the export const storageExportPath = path.join(this.tmpDir, metadata.storage!.path); if (fs.existsSync(storageExportPath)) { @@ -269,14 +263,12 @@ export class HubExport { } fs.mkdirSync(storageExportPath, { recursive: true }); - const storageHost = `http://${EmulatorRegistry.getInfoHostString(storageEmulator.getInfo())}`; const storageExportBody = { path: storageExportPath, initiatedBy: this.options.initiatedBy, }; - const client = new Client({ urlPrefix: storageHost, auth: false }); - const res = await client.request({ + const res = await EmulatorRegistry.client(Emulators.STORAGE).request({ method: "POST", path: "/internal/export", headers: { "Content-Type": "application/json" }, diff --git a/src/emulator/pubsubEmulator.ts b/src/emulator/pubsubEmulator.ts index 37a6f795bf33..68500e5b9d7b 100644 --- a/src/emulator/pubsubEmulator.ts +++ b/src/emulator/pubsubEmulator.ts @@ -25,7 +25,7 @@ interface Trigger { } export class PubsubEmulator implements EmulatorInstance { - pubsub: PubSub; + private _pubsub: PubSub | undefined; // Map of topic name to a list of functions to trigger triggersForTopic: Map; @@ -38,12 +38,17 @@ export class PubsubEmulator implements EmulatorInstance { private logger = EmulatorLogger.forEmulator(Emulators.PUBSUB); + get pubsub(): PubSub { + if (!this._pubsub) { + this._pubsub = new PubSub({ + apiEndpoint: EmulatorRegistry.url(Emulators.PUBSUB).host, + projectId: this.args.projectId, + }); + } + return this._pubsub; + } + constructor(private args: PubsubEmulatorArgs) { - const { host, port } = this.getInfo(); - this.pubsub = new PubSub({ - apiEndpoint: `${host}:${port}`, - projectId: this.args.projectId, - }); this.triggersForTopic = new Map(); this.subscriptionForTopic = new Map(); } @@ -138,16 +143,12 @@ export class PubsubEmulator implements EmulatorInstance { private ensureFunctionsClient() { if (this.client !== undefined) return; - const funcEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (!funcEmulator) { + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { throw new FirebaseError( `Attempted to execute pubsub trigger but could not find the Functions emulator` ); } - this.client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(funcEmulator.getInfo())}`, - auth: false, - }); + this.client = EmulatorRegistry.client(Emulators.FUNCTIONS); } private createLegacyEventRequestBody(topic: string, message: Message) { diff --git a/src/emulator/registry.ts b/src/emulator/registry.ts index dbcc1440010f..19268ffb9173 100644 --- a/src/emulator/registry.ts +++ b/src/emulator/registry.ts @@ -4,6 +4,8 @@ import * as portUtils from "./portUtils"; import { Constants } from "./constants"; import { EmulatorLogger } from "./emulatorLogger"; import * as express from "express"; +import { connectableHostname } from "../utils"; +import { Client, ClientOptions } from "../apiv2"; /** * Static registry for running emulators to discover each other. @@ -26,7 +28,7 @@ export class EmulatorRegistry { // No need to wait for the Extensions emulator to close its port, since it runs on the Functions emulator. if (instance.getName() !== Emulators.EXTENSIONS) { const info = instance.getInfo(); - await portUtils.waitForPortClosed(info.port, info.host); + await portUtils.waitForPortClosed(info.port, connectableHostname(info.host)); } } @@ -122,37 +124,25 @@ export class EmulatorRegistry { * Get information about an emulator. Use `url` instead for creating URLs. */ static getInfo(emulator: Emulators): EmulatorInfo | undefined { - // For Extensions, return the info for the Functions Emulator. - const instance = this.INSTANCES.get( - emulator === Emulators.EXTENSIONS ? Emulators.FUNCTIONS : emulator - ); - if (!instance) { + const info = EmulatorRegistry.get(emulator)?.getInfo(); + if (!info) { return undefined; } - - return instance.getInfo(); - } - - /** - * Get the host:port string for emulator. Use `url` instead for creating URLs. - */ - static getInfoHostString(info: EmulatorInfo): string { - const { host, port } = info; - - // Quote IPv6 addresses - if (host.includes(":")) { - return `[${host}]:${port}`; - } else { - return `${host}:${port}`; - } + return { + ...info, + host: connectableHostname(info.host), + }; } /** * Return a URL object with the emulator protocol, host, and port populated. + * + * Need to make an API request? Use `.client` instead. + * * @param emulator for retrieving host and port from the registry * @param req if provided, will prefer reflecting back protocol+host+port from * the express request (if header available) instead of registry - * @returns a WHATWG URL object with .host set to the emulator host + port + * @return a WHATWG URL object with .host set to the emulator host + port */ static url(emulator: Emulators, req?: express.Request): URL { // WHATWG URL API has no way to create from parts, so let's use a minimal @@ -175,14 +165,7 @@ export class EmulatorRegistry { // another host, e.g. in Dockers or behind reverse proxies. const info = EmulatorRegistry.getInfo(emulator); if (info) { - // If listening to all IPv4/6 addresses, use loopback addresses instead. - // All-zero addresses are invalid and not tolerated by some browsers / OS. - // See: https://github.com/firebase/firebase-tools-ui/issues/286 - if (info.host === "0.0.0.0") { - url.hostname = "127.0.0.1"; - } else if (info.host === "::") { - url.hostname = "[::1]"; - } else if (info.host.includes(":")) { + if (info.host.includes(":")) { url.hostname = `[${info.host}]`; // IPv6 addresses need to be quoted. } else { url.hostname = info.host; @@ -196,6 +179,14 @@ export class EmulatorRegistry { return url; } + static client(emulator: Emulators, options: Omit = {}): Client { + return new Client({ + urlPrefix: EmulatorRegistry.url(emulator).toString(), + auth: false, + ...options, + }); + } + private static INSTANCES: Map = new Map(); private static set(emulator: Emulators, instance: EmulatorInstance): void { diff --git a/src/emulator/storage/apis/gcloud.ts b/src/emulator/storage/apis/gcloud.ts index 4eddd5c56732..ff4748921ae4 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -235,13 +235,15 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { const { metadata } = getObjectResponse; // We do an empty update to step metageneration forward; metadata.update({}); + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent( + metadata.name + )}/acl/allUsers`; return res.json({ kind: "storage#objectAccessControl", object: metadata.name, id: `${req.params.bucketId}/${metadata.name}/${metadata.generation}/allUsers`, - selfLink: `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent(metadata.name)}/acl/allUsers`, + selfLink: selfLink.toString(), bucket: metadata.bucket, entity: req.body.entity, role: req.body.role, @@ -255,10 +257,6 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { // Resumable upload protocol. if (uploadType === "resumable") { - const emulatorInfo = EmulatorRegistry.getInfo(Emulators.STORAGE); - if (emulatorInfo === undefined) { - return res.sendStatus(500); - } const name = getIncomingFileNameFromRequest(req.query, req.body); if (name === undefined) { res.sendStatus(400); diff --git a/src/emulator/storage/cloudFunctions.ts b/src/emulator/storage/cloudFunctions.ts index e79ee0d26419..7308c67018e1 100644 --- a/src/emulator/storage/cloudFunctions.ts +++ b/src/emulator/storage/cloudFunctions.ts @@ -1,7 +1,7 @@ import * as uuid from "uuid"; import { EmulatorRegistry } from "../registry"; -import { EmulatorInfo, Emulators } from "../types"; +import { Emulators } from "../types"; import { EmulatorLogger } from "../emulatorLogger"; import { CloudStorageObjectMetadata, toSerializedDate } from "./metadata"; import { Client } from "../../apiv2"; @@ -18,23 +18,15 @@ const STORAGE_V2_ACTION_MAP: Record = { export class StorageCloudFunctions { private logger = EmulatorLogger.forEmulator(Emulators.STORAGE); - private functionsEmulatorInfo?: EmulatorInfo; - private multicastOrigin = ""; private multicastPath = ""; private enabled = false; private client?: Client; constructor(private projectId: string) { - const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - - if (functionsEmulator) { + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { this.enabled = true; - this.functionsEmulatorInfo = functionsEmulator.getInfo(); - this.multicastOrigin = `http://${EmulatorRegistry.getInfoHostString( - this.functionsEmulatorInfo - )}`; this.multicastPath = `/functions/projects/${projectId}/trigger_multicast`; - this.client = new Client({ urlPrefix: this.multicastOrigin, auth: false }); + this.client = EmulatorRegistry.client(Emulators.FUNCTIONS); } } diff --git a/src/emulator/storage/metadata.ts b/src/emulator/storage/metadata.ts index f95734f5e316..2506c9c02123 100644 --- a/src/emulator/storage/metadata.ts +++ b/src/emulator/storage/metadata.ts @@ -369,9 +369,11 @@ export class CloudStorageBucketMetadata { constructor(id: string) { this.name = id; this.id = id; - this.selfLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/v1/b/${this.id}`; + + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/v1/b/${this.id}`; + this.selfLink = selfLink.toString(); + this.timeCreated = toSerializedDate(new Date()); this.updated = this.timeCreated; this.projectNumber = "000000000000"; @@ -480,14 +482,19 @@ export class CloudStorageObjectMetadata { this.timeStorageClassUpdated = toSerializedDate(metadata.timeCreated); this.id = `${metadata.bucket}/${metadata.name}/${metadata.generation}`; - this.selfLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent(metadata.name)}`; - this.mediaLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/download/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent(metadata.name)}?generation=${ - metadata.generation - }&alt=media`; + + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent(metadata.name)}`; + this.selfLink = selfLink.toString(); + + const mediaLink = EmulatorRegistry.url(Emulators.STORAGE); + mediaLink.pathname = `/download/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent( + metadata.name + )}`; + mediaLink.searchParams.set("generation", metadata.generation.toString()); + mediaLink.searchParams.set("alt", "media"); + + this.mediaLink = mediaLink.toString(); } } diff --git a/src/emulator/storage/rules/runtime.ts b/src/emulator/storage/rules/runtime.ts index 95b1f4f10bb2..1addf2f1b5f5 100644 --- a/src/emulator/storage/rules/runtime.ts +++ b/src/emulator/storage/rules/runtime.ts @@ -31,7 +31,6 @@ import { handleEmulatorProcessError, } from "../../downloadableEmulators"; import { EmulatorRegistry } from "../../registry"; -import { Client } from "../../../apiv2"; const lock = new AsyncLock(); const synchonizationKey = "key"; @@ -442,14 +441,9 @@ async function fetchFirestoreDocument( projectId: string, request: RuntimeActionFirestoreDataRequest ): Promise { - const url = EmulatorRegistry.url(Emulators.FIRESTORE); const pathname = `projects/${projectId}${request.context.path}`; - const client = new Client({ - urlPrefix: url.toString(), - apiVersion: "v1", - }); - + const client = EmulatorRegistry.client(Emulators.FIRESTORE, { apiVersion: "v1", auth: true }); try { const doc = await client.get(pathname); const { name, fields } = doc.body as { name: string; fields: string }; diff --git a/src/emulator/ui.ts b/src/emulator/ui.ts index 7828a01858e9..38f275594b12 100644 --- a/src/emulator/ui.ts +++ b/src/emulator/ui.ts @@ -23,13 +23,12 @@ export class EmulatorUI implements EmulatorInstance { )}!` ); } - const hubInfo = EmulatorRegistry.get(Emulators.HUB)!.getInfo(); const { auto_download: autoDownload, host, port, projectId } = this.args; const env: Partial = { HOST: host.toString(), PORT: port.toString(), GCLOUD_PROJECT: projectId, - [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.getInfoHostString(hubInfo), + [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.url(Emulators.HUB).host, }; const session = emulatorSession(); diff --git a/src/hosting/functionsProxy.ts b/src/hosting/functionsProxy.ts index 7f314ac489ac..3a3a12a1d768 100644 --- a/src/hosting/functionsProxy.ts +++ b/src/hosting/functionsProxy.ts @@ -48,15 +48,8 @@ export function functionsProxy( // If the functions emulator is running we know the port, otherwise // things still point to production. - const functionsEmu = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (functionsEmu) { - url = FunctionsEmulator.getHttpFunctionUrl( - functionsEmu.getInfo().host, - functionsEmu.getInfo().port, - projectId, - functionId, - region - ); + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + url = FunctionsEmulator.getHttpFunctionUrl(projectId, functionId, region); } } diff --git a/src/hosting/implicitInit.ts b/src/hosting/implicitInit.ts index 5a9231be1aa1..c87fc7a6818f 100644 --- a/src/hosting/implicitInit.ts +++ b/src/hosting/implicitInit.ts @@ -6,7 +6,7 @@ import { fetchWebSetup, getCachedWebSetup } from "../fetchWebSetup"; import * as utils from "../utils"; import { logger } from "../logger"; import { EmulatorRegistry } from "../emulator/registry"; -import { EMULATORS_SUPPORTED_BY_USE_EMULATOR, Address, Emulators } from "../emulator/types"; +import { EMULATORS_SUPPORTED_BY_USE_EMULATOR, Emulators } from "../emulator/types"; const INIT_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/hosting/init.js", "utf8"); @@ -63,25 +63,15 @@ export async function implicitInit(options: any): Promise { describe("dispatch", () => { - let sandbox: sinon.SinonSandbox; - const fakeEmulator = new FakeEmulator(Emulators.FUNCTIONS, "1.1.1.1", 4); - before(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(EmulatorRegistry, "get").returns(fakeEmulator); - }); + const host = "localhost"; + let port = 4000; + before(async () => { + port = await findAvailablePort(host, port); - after(() => { - sandbox.restore(); - nock.cleanAll(); - }); - - it("should make a request to the functions emulator", async () => { - nock("http://1.1.1.1:4") + const emu = new FakeEmulator(Emulators.FUNCTIONS, host, port); + await EmulatorRegistry.start(emu); + nock(EmulatorRegistry.url(Emulators.FUNCTIONS).toString()) .post("/functions/projects/project-foo/trigger_multicast", { eventId: /.*/, eventType: "providers/firebase.auth/eventTypes/user.create", @@ -35,7 +29,14 @@ describe("cloudFunctions", () => { data: { uid: "foobar", metadata: {}, customClaims: {} }, }) .reply(200, {}); + }); + after(async () => { + await EmulatorRegistry.stopAll(); + nock.cleanAll(); + }); + + it("should make a request to the functions emulator", async () => { const cf = new AuthCloudFunction("project-foo"); await cf.dispatch("create", { localId: "foobar" }); expect(nock.isDone()).to.be.true; diff --git a/src/test/emulators/extensions/postinstall.spec.ts b/src/test/emulators/extensions/postinstall.spec.ts index ba4cf79dea9e..900e29d164f1 100644 --- a/src/test/emulators/extensions/postinstall.spec.ts +++ b/src/test/emulators/extensions/postinstall.spec.ts @@ -1,85 +1,89 @@ import { expect } from "chai"; -import * as Sinon from "sinon"; import * as postinstall from "../../../emulator/extensions/postinstall"; +import { findAvailablePort } from "../../../emulator/portUtils"; import { EmulatorRegistry } from "../../../emulator/registry"; import { Emulators } from "../../../emulator/types"; +import { FakeEmulator } from "../fakeEmulator"; describe("replaceConsoleLinks", () => { - let sandbox: Sinon.SinonSandbox; - beforeEach(() => { - sandbox = Sinon.createSandbox(); - sandbox - .stub(EmulatorRegistry, "getInfo") - .returns({ name: Emulators.UI, host: "localhost", port: 4000 }); + const host = "localhost"; + let port = 4000; + before(async () => { + port = await findAvailablePort(host, port); + + const emu = new FakeEmulator(Emulators.UI, host, port); + return EmulatorRegistry.start(emu); }); - afterEach(() => { - sandbox.restore(); + after(async () => { + await EmulatorRegistry.stopAll(); }); const tests: { desc: string; input: string; - expected: string; + expected: () => string; }[] = [ { desc: "should replace Firestore links", input: " Go to your [Cloud Firestore dashboard](https://console.firebase.google.com/project/test-project/firestore/data) in the Firebase console.", - expected: - " Go to your [Cloud Firestore dashboard](http://localhost:4000/firestore) in the Firebase console.", + expected: () => + ` Go to your [Cloud Firestore dashboard](http://${host}:${port}/firestore) in the Firebase console.`, }, { desc: "should replace Functions links", input: " Go to your [Cloud Functions dashboard](https://console.firebase.google.com/project/test-project/functions/logs) in the Firebase console.", - expected: - " Go to your [Cloud Functions dashboard](http://localhost:4000/logs) in the Firebase console.", + expected: () => + ` Go to your [Cloud Functions dashboard](http://${host}:${port}/logs) in the Firebase console.`, }, { desc: "should replace Extensions links", input: " Go to your [Extensions dashboard](https://console.firebase.google.com/project/test-project/extensions) in the Firebase console.", - expected: - " Go to your [Extensions dashboard](http://localhost:4000/extensions) in the Firebase console.", + expected: () => + ` Go to your [Extensions dashboard](http://${host}:${port}/extensions) in the Firebase console.`, }, { desc: "should replace RTDB links", input: " Go to your [Realtime database dashboard](https://console.firebase.google.com/project/test-project/database/test-walkthrough/data) in the Firebase console.", - expected: - " Go to your [Realtime database dashboard](http://localhost:4000/database) in the Firebase console.", + expected: () => + ` Go to your [Realtime database dashboard](http://${host}:${port}/database) in the Firebase console.`, }, { desc: "should replace Auth links", input: " Go to your [Auth dashboard](https://console.firebase.google.com/project/test-project/authentication/users) in the Firebase console.", - expected: " Go to your [Auth dashboard](http://localhost:4000/auth) in the Firebase console.", + expected: () => + ` Go to your [Auth dashboard](http://${host}:${port}/auth) in the Firebase console.`, }, { desc: "should replace multiple GAIA user links ", input: " Go to your [Auth dashboard](https://console.firebase.google.com/u/0/project/test-project/authentication/users) in the Firebase console.", - expected: " Go to your [Auth dashboard](http://localhost:4000/auth) in the Firebase console.", + expected: () => + ` Go to your [Auth dashboard](http://${host}:${port}/auth) in the Firebase console.`, }, { desc: "should replace multiple links", input: " Go to your [Cloud Firestore dashboard](https://console.firebase.google.com/project/jh-walkthrough/firestore/data) or [Realtime database dashboard](https://console.firebase.google.com/project/test-project/database/test-walkthrough/data)in the Firebase console.", - expected: - " Go to your [Cloud Firestore dashboard](http://localhost:4000/firestore) or [Realtime database dashboard](http://localhost:4000/database)in the Firebase console.", + expected: () => + ` Go to your [Cloud Firestore dashboard](http://${host}:${port}/firestore) or [Realtime database dashboard](http://${host}:${port}/database)in the Firebase console.`, }, { desc: "should not replace other links", input: " Go to your [Stripe dashboard](https://stripe.com/payments) to see more information.", - expected: + expected: () => " Go to your [Stripe dashboard](https://stripe.com/payments) to see more information.", }, ]; for (const t of tests) { it(t.desc, () => { - expect(postinstall.replaceConsoleLinks(t.input)).to.equal(t.expected); + expect(postinstall.replaceConsoleLinks(t.input)).to.equal(t.expected()); }); } }); diff --git a/src/test/utils.spec.ts b/src/test/utils.spec.ts index e7e78f95a77f..a6dc65709068 100644 --- a/src/test/utils.spec.ts +++ b/src/test/utils.spec.ts @@ -450,4 +450,18 @@ describe("utils", () => { expect(fn).to.be.calledWith(99); }); }); + + describe("connnectableHostname", () => { + it("should change wildcard IP addresses to corresponding loopbacks", () => { + expect(utils.connectableHostname("0.0.0.0")).to.equal("127.0.0.1"); + expect(utils.connectableHostname("::")).to.equal("::1"); + expect(utils.connectableHostname("[::]")).to.equal("[::1]"); + }); + it("should not change non-wildcard IP addresses or hostnames", () => { + expect(utils.connectableHostname("169.254.20.1")).to.equal("169.254.20.1"); + expect(utils.connectableHostname("fe80::1")).to.equal("fe80::1"); + expect(utils.connectableHostname("[fe80::2]")).to.equal("[fe80::2]"); + expect(utils.connectableHostname("example.com")).to.equal("example.com"); + }); + }); }); diff --git a/src/utils.ts b/src/utils.ts index bc289c606865..e14d43ef73f0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -743,3 +743,24 @@ export function randomInt(min: number, max: number): number { max = Math.ceil(max) + 1; return Math.floor(Math.random() * (max - min) + min); } + +/** + * Return a connectable hostname, replacing wildcard 0.0.0.0 or :: with loopback + * addresses 127.0.0.1 / ::1 correspondingly. See below for why this is needed: + * https://github.com/firebase/firebase-tools-ui/issues/286 + * + * This assumes that the consumer (i.e. client SDK, etc.) is located on the same + * device as the Emulator hub (i.e. CLI), which may not be true on multi-device + * setups, etc. In that case, the customer can work around this by specifying a + * non-wildcard IP address (like the IP address on LAN, if accessing via LAN). + */ +export function connectableHostname(hostname: string): string { + if (hostname === "0.0.0.0") { + hostname = "127.0.0.1"; + } else if (hostname === "::" /* unquoted IPv6 wildcard */) { + hostname = "::1"; + } else if (hostname === "[::]" /* quoted IPv6 wildcard */) { + hostname = "[::1]"; + } + return hostname; +} diff --git a/templates/hosting/init.js b/templates/hosting/init.js index 5d895071e2c0..e2f04ce75bca 100644 --- a/templates/hosting/init.js +++ b/templates/hosting/init.js @@ -7,7 +7,7 @@ if (firebaseConfig) { if (firebaseEmulators) { console.log("Automatically connecting Firebase SDKs to running emulators:"); Object.keys(firebaseEmulators).forEach(function (key) { - console.log('\t' + key + ': http://' + firebaseEmulators[key].host + ':' + firebaseEmulators[key].port); + console.log('\t' + key + ': http://' + firebaseEmulators[key].hostAndPort); }); if (firebaseEmulators.database && typeof firebase.database === 'function') { @@ -23,7 +23,8 @@ if (firebaseConfig) { } if (firebaseEmulators.auth && typeof firebase.auth === 'function') { - firebase.auth().useEmulator('http://' + firebaseEmulators.auth.host + ':' + firebaseEmulators.auth.port); + // TODO: Consider using location.protocol + '//' instead (may help HTTPS). + firebase.auth().useEmulator('http://' + firebaseEmulators.auth.hostAndPort); } if (firebaseEmulators.storage && typeof firebase.storage === 'function') { From a7a3e62716ca175d081e8b2739ea8827855c3a9e Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 7 Oct 2022 11:35:32 -0700 Subject: [PATCH 025/115] Include roles and apis for Tasks and Secrets during DisplayExtensionInfo (#5040) * Include roles and apis for Tasks and Secrets during DisplayExtensionInfo * Remove accidentally included file * make sourceUrl optional --- src/extensions/displayExtensionInfo.ts | 53 ++++++++++++-- src/extensions/secretsUtils.ts | 1 + src/extensions/types.ts | 15 +++- .../extensions/displayExtensionInfo.spec.ts | 70 ++++++++++++++++++- 4 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 87a5fbe35872..f996762cca75 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -7,13 +7,17 @@ import * as utils from "../utils"; import { logPrefix } from "./extensionsHelper"; import { logger } from "../logger"; import { FirebaseError } from "../error"; -import { Api, ExtensionSpec, Role } from "./types"; +import { Api, ExtensionSpec, Role, Resource } from "./types"; import * as iam from "../gcp/iam"; +import { SECRET_ROLE, usesSecrets } from "./secretsUtils"; marked.setOptions({ renderer: new TerminalRenderer(), }); +const TASKS_ROLE = "cloudtasks.enqueuer"; +const TASKS_API = "cloudtasks.googleapis.com"; + /** * displayExtInfo prints the extension info displayed when running ext:install. * @@ -39,13 +43,17 @@ export async function displayExtInfo( if (spec.license) { lines.push(`**License**: ${spec.license}`); } - lines.push(`**Source code**: ${spec.sourceUrl}`); + if (spec.sourceUrl) { + lines.push(`**Source code**: ${spec.sourceUrl}`); + } } - if (spec.apis?.length) { - lines.push(displayApis(spec.apis)); + const apis = impliedApis(spec); + if (apis.length) { + lines.push(displayApis(apis)); } - if (spec.roles?.length) { - lines.push(await displayRoles(spec.roles)); + const roles = impliedRoles(spec); + if (roles.length) { + lines.push(await displayRoles(roles)); } if (lines.length > 0) { utils.logLabeledBullet(logPrefix, `information about '${clc.bold(extensionName)}':`); @@ -103,3 +111,36 @@ function displayApis(apis: Api[]): string { }); return "**APIs used by this Extension**:\n" + lines.join("\n"); } + +function usesTasks(spec: ExtensionSpec): boolean { + return spec.resources.some((r: Resource) => r.properties?.taskQueueTrigger !== undefined); +} + +function impliedRoles(spec: ExtensionSpec): Role[] { + const roles: Role[] = []; + if (usesSecrets(spec) && !spec.roles?.some((r: Role) => r.role === SECRET_ROLE)) { + roles.push({ + role: SECRET_ROLE, + reason: "Allows the extension to read secret values from Cloud Secret Manager", + }); + } + if (usesTasks(spec) && !spec.roles?.some((r: Role) => r.role === TASKS_ROLE)) { + roles.push({ + role: TASKS_ROLE, + reason: "Allows the extension to enqueue Cloud Tasks", + }); + } + return roles.concat(spec.roles ?? []); +} + +function impliedApis(spec: ExtensionSpec): Api[] { + const apis: Api[] = []; + if (usesTasks(spec) && !spec.apis?.some((a: Api) => a.apiName === TASKS_API)) { + apis.push({ + apiName: TASKS_API, + reason: "Allows the extension to enqueue Cloud Tasks", + }); + } + + return apis.concat(spec.apis ?? []); +} diff --git a/src/extensions/secretsUtils.ts b/src/extensions/secretsUtils.ts index e71523d578f6..e38070f316c3 100644 --- a/src/extensions/secretsUtils.ts +++ b/src/extensions/secretsUtils.ts @@ -7,6 +7,7 @@ import * as secretManagerApi from "../gcp/secretManager"; import { logger } from "../logger"; export const SECRET_LABEL = "firebase-extensions-managed"; +export const SECRET_ROLE = "secretmanager.secretAccessor"; export async function ensureSecretManagerApiEnabled(options: any): Promise { const projectId = needProjectId(options); diff --git a/src/extensions/types.ts b/src/extensions/types.ts index 301d9b791181..82abaeb524f2 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -98,7 +98,7 @@ export interface ExtensionSpec { contributors?: Author[]; license?: string; releaseNotesUrl?: string; - sourceUrl: string; + sourceUrl?: string; params: Param[]; preinstallContent?: string; postinstallContent?: string; @@ -139,6 +139,19 @@ export interface FunctionResourceProperties { availableMemoryMb?: MemoryOptions; runtime?: Runtime; httpsTrigger?: Record; + taskQueueTrigger?: { + rateLimits?: { + maxConcurrentDispatchs?: number; + maxDispatchesPerSecond?: number; + }; + retryConfig?: { + maxAttempts?: number; + maxRetrySeconds?: number; + maxBackoffSeconds?: number; + maxDoublings?: number; + minBackoffSeconds?: number; + }; + }; eventTrigger?: { eventType: string; resource: string; diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts index c2edc5b44f14..9de305461bc4 100644 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ b/src/test/extensions/displayExtensionInfo.spec.ts @@ -3,7 +3,8 @@ import { expect } from "chai"; import * as iam from "../../gcp/iam"; import * as displayExtensionInfo from "../../extensions/displayExtensionInfo"; -import { ExtensionSpec, Resource } from "../../extensions/types"; +import { ExtensionSpec, Param, Resource } from "../../extensions/types"; +import { ParamType } from "../../extensions/types"; const SPEC: ExtensionSpec = { name: "test", @@ -30,6 +31,20 @@ const SPEC: ExtensionSpec = { params: [], }; +const TASK_FUNCTION_RESOURCE: Resource = { + name: "taskResource", + type: "firebaseextensions.v1beta.function", + properties: { + taskQueueTrigger: {}, + }, +}; + +const SECRET_PARAM: Param = { + param: "secret", + label: "Secret", + type: ParamType.SECRET, +}; + describe("displayExtensionInfo", () => { describe("displayExtInfo", () => { let getRoleStub: sinon.SinonStub; @@ -43,11 +58,20 @@ describe("displayExtensionInfo", () => { title: "Role 2", description: "a role", }); + getRoleStub.withArgs("cloudtasks.enqueuer").resolves({ + title: "Cloud Task Enqueuer", + description: "Enqueue tasks", + }); + getRoleStub.withArgs("secretmanager.secretAccessor").resolves({ + title: "Secret Accessor", + description: "Access Secrets", + }); }); afterEach(() => { getRoleStub.restore(); }); + it("should display info during install", async () => { const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", SPEC); const expected: string[] = [ @@ -64,6 +88,7 @@ describe("displayExtensionInfo", () => { expect(loggedLines[3]).to.include("Role 1"); expect(loggedLines[3]).to.include("Role 2"); }); + it("should display additional information for a published extension", async () => { const loggedLines = await displayExtensionInfo.displayExtInfo( SPEC.name, @@ -91,5 +116,48 @@ describe("displayExtensionInfo", () => { expect(loggedLines[6]).to.include("Role 1"); expect(loggedLines[6]).to.include("Role 2"); }); + + it("should display role and api for Cloud Tasks during install", async () => { + const specWithTasks = JSON.parse(JSON.stringify(SPEC)) as ExtensionSpec; + specWithTasks.resources.push(TASK_FUNCTION_RESOURCE); + + const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", specWithTasks); + const expected: string[] = [ + "**Name**: Old", + "**Description**: descriptive", + "**APIs used by this Extension**:\n api1 ()\n api2 ()", + "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)\n Cloud Task Enqueuer (Enqueue tasks)", + ]; + expect(loggedLines.length).to.eql(expected.length); + expect(loggedLines[0]).to.include("Old"); + expect(loggedLines[1]).to.include("descriptive"); + expect(loggedLines[2]).to.include("api1"); + expect(loggedLines[2]).to.include("api2"); + expect(loggedLines[2]).to.include("Cloud Tasks"); + expect(loggedLines[3]).to.include("Role 1"); + expect(loggedLines[3]).to.include("Role 2"); + expect(loggedLines[3]).to.include("Cloud Task Enqueuer"); + }); + + it("should display role for Cloud Secret Manager during install", async () => { + const specWithSecret = JSON.parse(JSON.stringify(SPEC)) as ExtensionSpec; + specWithSecret.params.push(SECRET_PARAM); + + const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", specWithSecret); + const expected: string[] = [ + "**Name**: Old", + "**Description**: descriptive", + "**APIs used by this Extension**:\n api1 ()\n api2 ()", + "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)\n Secret Accessor (Access secrets)", + ]; + expect(loggedLines.length).to.eql(expected.length); + expect(loggedLines[0]).to.include("Old"); + expect(loggedLines[1]).to.include("descriptive"); + expect(loggedLines[2]).to.include("api1"); + expect(loggedLines[2]).to.include("api2"); + expect(loggedLines[3]).to.include("Role 1"); + expect(loggedLines[3]).to.include("Role 2"); + expect(loggedLines[3]).to.include("Secret Accessor"); + }); }); }); From 6d9a79ef1de6ecaf2a44a0ba0b5c54857f6bc141 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Fri, 7 Oct 2022 12:45:11 -0700 Subject: [PATCH 026/115] Assign emulator ports and resolve hostnames upfront. (#5083) * Assign emulator ports and resolve hostnames upfront. * Fix test ref. * Add changelog. * Restore IPv6 dual stack on wildcard. * Update hard-coded localhost in tests. --- CHANGELOG.md | 1 + .../emulator-tests/functionsEmulator.spec.ts | 27 +- scripts/extensions-emulator-tests/tests.ts | 2 +- scripts/storage-emulator-integration/utils.ts | 4 +- src/emulator/ExpressBasedEmulator.ts | 127 +++++++ src/emulator/controller.ts | 348 +++++++----------- src/emulator/dns.ts | 101 +++++ src/emulator/emulatorLogger.ts | 21 +- src/emulator/emulatorServer.ts | 37 -- src/emulator/hub.ts | 91 ++--- src/emulator/hubClient.ts | 42 ++- src/emulator/portUtils.ts | 346 +++++++++++++++-- src/emulator/types.ts | 14 +- src/frameworks/index.ts | 4 +- src/functionsShellCommandAction.ts | 24 +- src/serve/functions.ts | 24 +- src/serve/index.ts | 5 +- .../emulators/auth/cloudFunctions.spec.ts | 7 +- src/test/emulators/controller.spec.ts | 5 +- src/test/emulators/dns.spec.ts | 115 ++++++ src/test/emulators/emulatorServer.spec.ts | 24 -- .../emulators/extensions/postinstall.spec.ts | 11 +- src/test/emulators/fakeEmulator.ts | 44 +-- src/test/emulators/registry.spec.ts | 50 +-- src/test/hosting/functionsProxy.spec.ts | 20 +- 25 files changed, 996 insertions(+), 498 deletions(-) create mode 100644 src/emulator/ExpressBasedEmulator.ts create mode 100644 src/emulator/dns.ts delete mode 100644 src/emulator/emulatorServer.ts create mode 100644 src/test/emulators/dns.spec.ts delete mode 100644 src/test/emulators/emulatorServer.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c86f3ecd431f..25abc0a9c01b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Enable single project mode for the database emulator (#5068). +- Ravamp emulator networking to assign ports early and explictly listen on IP addresses (#5083). diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index a4fc1e68e655..08e5ea9bfb8b 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -10,13 +10,12 @@ import * as logform from "logform"; import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared"; import { FunctionsEmulator } from "../../src/emulator/functionsEmulator"; -import { Emulators } from "../../src/emulator/types"; +import { EmulatorInfo, Emulators } from "../../src/emulator/types"; import { FakeEmulator } from "../../src/test/emulators/fakeEmulator"; import { TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures"; import { logger } from "../../src/logger"; import * as registry from "../../src/emulator/registry"; import * as secretManager from "../../src/gcp/secretManager"; -import { findAvailablePort } from "../../src/emulator/portUtils"; if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { const dropLogLevels = (info: logform.TransformableInfo) => info.message; @@ -614,12 +613,10 @@ describe("FunctionsEmulator-Hub", function () { }); describe("environment variables", () => { - const host = "127.0.0.1"; - const startFakeEmulator = async (emulator: Emulators): Promise => { - const port = await findAvailablePort(host, 4000); - const fake = new FakeEmulator(emulator, host, port); + const startFakeEmulator = async (emulator: Emulators): Promise => { + const fake = await FakeEmulator.create(emulator); await registry.EmulatorRegistry.start(fake); - return port; + return fake.getInfo(); }; afterEach(() => { @@ -627,9 +624,9 @@ describe("FunctionsEmulator-Hub", function () { }); it("should set env vars when the emulator is running", async () => { - const databasePort = await startFakeEmulator(Emulators.DATABASE); - const firestorePort = await startFakeEmulator(Emulators.FIRESTORE); - const authPort = await startFakeEmulator(Emulators.AUTH); + const database = await startFakeEmulator(Emulators.DATABASE); + const firestore = await startFakeEmulator(Emulators.FIRESTORE); + const auth = await startFakeEmulator(Emulators.AUTH); await useFunction(emu, "functionId", () => { return { @@ -649,14 +646,14 @@ describe("FunctionsEmulator-Hub", function () { .get("/fake-project-id/us-central1/functionId") .expect(200) .then((res) => { - expect(res.body.databaseHost).to.eql(`${host}:${databasePort}`); - expect(res.body.firestoreHost).to.eql(`${host}:${firestorePort}`); - expect(res.body.authHost).to.eql(`${host}:${authPort}`); + expect(res.body.databaseHost).to.eql(`${database.host}:${database.port}`); + expect(res.body.firestoreHost).to.eql(`${firestore.host}:${firestore.port}`); + expect(res.body.authHost).to.eql(`${auth.host}:${auth.port}`); }); }).timeout(TIMEOUT_MED); it("should return an emulated databaseURL when RTDB emulator is running", async () => { - const databasePort = await startFakeEmulator(Emulators.DATABASE); + const database = await startFakeEmulator(Emulators.DATABASE); await useFunction(emu, "functionId", () => { return { @@ -673,7 +670,7 @@ describe("FunctionsEmulator-Hub", function () { .expect(200) .then((res) => { expect(res.body.databaseURL).to.eql( - `http://${host}:${databasePort}/?ns=fake-project-id-default-rtdb` + `http://${database.host}:${database.port}/?ns=fake-project-id-default-rtdb` ); }); }).timeout(TIMEOUT_MED); diff --git a/scripts/extensions-emulator-tests/tests.ts b/scripts/extensions-emulator-tests/tests.ts index 4aadfddee228..d738d6106ebd 100755 --- a/scripts/extensions-emulator-tests/tests.ts +++ b/scripts/extensions-emulator-tests/tests.ts @@ -51,7 +51,7 @@ describe("CF3 and Extensions emulator", () => { const config = readConfig(); const storagePort = config.emulators!.storage.port; - process.env.STORAGE_EMULATOR_HOST = `http://localhost:${storagePort}`; + process.env.STORAGE_EMULATOR_HOST = `http://127.0.0.1:${storagePort}`; const firestorePort = config.emulators!.firestore.port; process.env.FIRESTORE_EMULATOR_HOST = `localhost:${firestorePort}`; diff --git a/scripts/storage-emulator-integration/utils.ts b/scripts/storage-emulator-integration/utils.ts index 36a01fa4ad70..7681c7ecc2f6 100644 --- a/scripts/storage-emulator-integration/utils.ts +++ b/scripts/storage-emulator-integration/utils.ts @@ -26,7 +26,7 @@ export function readEmulatorConfig(config = FIREBASE_EMULATOR_CONFIG): Framework export function getStorageEmulatorHost(emulatorConfig: FrameworkOptions) { const port = emulatorConfig.emulators?.storage?.port; if (port) { - return `http://localhost:${port}`; + return `http://127.0.0.1:${port}`; } throw new Error("Storage emulator config not found or invalid"); } @@ -34,7 +34,7 @@ export function getStorageEmulatorHost(emulatorConfig: FrameworkOptions) { export function getAuthEmulatorHost(emulatorConfig: FrameworkOptions) { const port = emulatorConfig.emulators?.auth?.port; if (port) { - return `http://localhost:${port}`; + return `http://127.0.0.1:${port}`; } throw new Error("Auth emulator config not found or invalid"); } diff --git a/src/emulator/ExpressBasedEmulator.ts b/src/emulator/ExpressBasedEmulator.ts new file mode 100644 index 000000000000..1b09085f6450 --- /dev/null +++ b/src/emulator/ExpressBasedEmulator.ts @@ -0,0 +1,127 @@ +import * as cors from "cors"; +import * as express from "express"; +import * as bodyParser from "body-parser"; + +import * as utils from "../utils"; +import { Emulators, EmulatorInstance, EmulatorInfo, ListenSpec } from "./types"; +import { createServer } from "node:http"; +import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED } from "./dns"; +import { ListenOptions } from "node:net"; + +export interface ExpressBasedEmulatorOptions { + listen: ListenSpec[]; + noCors?: boolean; + noBodyParser?: boolean; +} + +/** + * An EmulatorInstance that starts express servers with multi-listen support. + * + * This class correctly destroys the server(s) when `stop()`-ed. When overriding + * life-cycle methods, make sure to call the super methods for those behaviors. + */ +export abstract class ExpressBasedEmulator implements EmulatorInstance { + static PATH_EXPORT = "/_admin/export"; + static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers"; + static PATH_ENABLE_FUNCTIONS = "/functions/enableBackgroundTriggers"; + static PATH_EMULATORS = "/emulators"; + + private destroyers = new Set<() => Promise>(); + + constructor(private options: ExpressBasedEmulatorOptions) {} + + protected createExpressApp(): Promise { + const app = express(); + if (!this.options.noCors) { + // Enable CORS for all APIs, all origins (reflected), and all headers (reflected). + // This is enabled by default since most emulators are cookieless. + app.use(cors({ origin: true })); + + // Return access-control-allow-private-network heder if requested + // Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227 + // Aligns with https://wicg.github.io/private-network-access/#headers + // Replace with cors option if adopted, see https://github.com/expressjs/cors/issues/236 + app.use((req, res, next) => { + if (req.headers["access-control-request-private-network"]) { + res.setHeader("access-control-allow-private-network", "true"); + } + next(); + }); + } + if (!this.options.noBodyParser) { + app.use(bodyParser.json()); // used in most emulators + } + app.set("json spaces", 2); + + return Promise.resolve(app); + } + + async start(): Promise { + const app = await this.createExpressApp(); + + const promises = []; + const specs = this.options.listen; + + const listenOptions: ListenOptions[] = []; + + const dualStackPorts = new Set(); + for (const spec of specs) { + if (spec.address === IPV6_UNSPECIFIED.address) { + if (specs.some((s) => s.port === spec.port && s.address === IPV4_UNSPECIFIED.address)) { + // We can use the default dual-stack behavior in Node.js to listen on + // the same port on both IPv4 and IPv6 unspecified addresses on most OSes. + // https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback + listenOptions.push({ + port: spec.port, + ipv6Only: false, + }); + dualStackPorts.add(spec.port); + } + } + } + // Then add options for non-dual-stack addresses and ports. + for (const spec of specs) { + if (!dualStackPorts.has(spec.port)) { + listenOptions.push({ + host: spec.address, + port: spec.port, + ipv6Only: spec.family === "IPv6", + }); + } + } + + for (const opt of listenOptions) { + promises.push( + new Promise((resolve, reject) => { + const server = createServer(app).listen(opt); + server.once("listening", resolve); + server.once("error", reject); + this.destroyers.add(utils.createDestroyer(server)); + }) + ); + } + } + + async connect(): Promise { + // no-op + } + + async stop(): Promise { + const promises = []; + for (const destroyer of this.destroyers) { + promises.push(destroyer().then(() => this.destroyers.delete(destroyer))); + } + await Promise.all(promises); + } + + getInfo(): EmulatorInfo { + return { + name: this.getName(), + listen: this.options.listen, + host: this.options.listen[0].address, + port: this.options.listen[0].port, + }; + } + + abstract getName(): Emulators; +} diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index aa9d22798fbc..95aa457c2c93 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -7,7 +7,7 @@ import { track, trackEmulator } from "../track"; import * as utils from "../utils"; import { EmulatorRegistry } from "./registry"; import { - Address, + ALL_EMULATORS, ALL_SERVICE_EMULATORS, EmulatorInfo, EmulatorInstance, @@ -33,7 +33,6 @@ import { EmulatorUI } from "./ui"; import { LoggingEmulator } from "./loggingEmulator"; import * as dbRulesConfig from "../database/rulesConfig"; import { EmulatorLogger } from "./emulatorLogger"; -import * as portUtils from "./portUtils"; import { EmulatorHubClient } from "./hubClient"; import { promptOnce } from "../prompt"; import { @@ -53,6 +52,7 @@ import { normalizeAndValidate } from "../functions/projectConfig"; import { requiresJava } from "./downloadableEmulators"; import { prepareFrameworks } from "../frameworks"; import * as experiments from "../experiments"; +import { EmulatorListenConfig, PortName, resolveHostAndAssignPorts } from "./portUtils"; const START_LOGGING_EMULATOR = utils.envOverride( "START_LOGGING_EMULATOR", @@ -60,130 +60,6 @@ const START_LOGGING_EMULATOR = utils.envOverride( (val) => val === "true" ); -async function getAndCheckAddress(emulator: Emulators, options: Options): Promise
{ - if (emulator === Emulators.EXTENSIONS) { - // The Extensions emulator always runs on the same port as the Functions emulator. - emulator = Emulators.FUNCTIONS; - } - let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(); - if (host === "localhost" && utils.isRunningInWSL()) { - // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 - // 127.0.0.1 instead of localhost. This, combined with the hack in - // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. - // The CLI (including the hub) will also consistently report 127.0.0.1, - // causing clients to connect via IPv4 only (which mitigates the problem of - // some clients resolving localhost to IPv6 and get connection refused). - host = "127.0.0.1"; - } - - const portVal = options.config.src.emulators?.[emulator]?.port; - let port; - let findAvailablePort = false; - if (portVal) { - port = parseInt(`${portVal}`, 10); - } else { - port = Constants.getDefaultPort(emulator); - findAvailablePort = FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; - } - - const loggerForEmulator = EmulatorLogger.forEmulator(emulator); - const portOpen = await portUtils.checkPortOpen(port, host); - if (!portOpen) { - if (findAvailablePort) { - const newPort = await portUtils.findAvailablePort(host, port); - if (newPort !== port) { - loggerForEmulator.logLabeled( - "WARN", - emulator, - `${Constants.description( - emulator - )} unable to start on port ${port}, starting on ${newPort} instead.` - ); - port = newPort; - } - } else { - await cleanShutdown(); - const description = Constants.description(emulator); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `Port ${port} is not open on ${host}, could not start ${description}.` - ); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `To select a different host/port, specify that host/port in a firebase.json config file: - { - // ... - "emulators": { - "${emulator}": { - "host": "${clc.yellow("HOST")}", - "port": "${clc.yellow("PORT")}" - } - } - }` - ); - return utils.reject(`Could not start ${description}, port taken.`, {}); - } - } - - if (portUtils.isRestricted(port)) { - const suggested = portUtils.suggestUnrestricted(port); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `Port ${port} is restricted by some web browsers, including Chrome. You may want to choose a different port such as ${suggested}.` - ); - } - - return { host, port }; -} - -async function getFirestoreWebSocketPort( - host: string, - port: number | undefined, - emulator: Emulators -): Promise { - let websocketPort; - if (port) { - // check if the port is available - const portOpen = await portUtils.checkPortOpen(port, host); - if (!portOpen) { - // shutdown if inputed port is not available - await cleanShutdown(); - - const logger = EmulatorLogger.forEmulator(emulator); - logger.logLabeled( - "WARN", - emulator, - `Port ${port} is not open on ${host}, could not start websocket server for Firestore emulator.` - ); - logger.logLabeled( - "WARN", - emulator, - `To select a different port, specify that port in a firebase.json config file: - { - // ... - "emulators": { - "${emulator}": { - "host": "${clc.yellow("HOST")}", - ... - "websocketPort": "${clc.yellow("WEBSOCKET_PORT")}" - } - } - }` - ); - return utils.reject(`Could not start websocket, port taken.`, {}); - } - - websocketPort = port; - } else { - // user did not specify a port, find any available port - websocketPort = await portUtils.findAvailablePort(host, 9150); - } - return websocketPort; -} - /** * Exports emulator data on clean exit (SIGINT or process end) * @param options @@ -447,6 +323,113 @@ export async function startAll( } } + const emulatableBackends: EmulatableBackend[] = []; + const projectDir = (options.extDevDir || options.config.projectDir) as string; + if (shouldStart(options, Emulators.FUNCTIONS)) { + const functionsCfg = normalizeAndValidate(options.config.src.functions); + // Note: ext:dev:emulators:* commands hit this path, not the Emulators.EXTENSIONS path + utils.assertIsStringOrUndefined(options.extDevDir); + + for (const cfg of functionsCfg) { + const functionsDir = path.join(projectDir, cfg.source); + emulatableBackends.push({ + functionsDir, + codebase: cfg.codebase, + env: { + ...options.extDevEnv, + }, + secretEnv: [], // CF3 secrets are bound to specific functions, so we'll get them during trigger discovery. + // TODO(b/213335255): predefinedTriggers and nodeMajorVersion are here to support ext:dev:emulators:* commands. + // Ideally, we should handle that case via ExtensionEmulator. + predefinedTriggers: options.extDevTriggers as ParsedTriggerDefinition[] | undefined, + nodeMajorVersion: parseRuntimeVersion((options.extDevNodeVersion as string) || cfg.runtime), + }); + } + } + + let extensionEmulator: ExtensionsEmulator | undefined = undefined; + if (shouldStart(options, Emulators.EXTENSIONS)) { + const projectNumber = isDemoProject + ? Constants.FAKE_PROJECT_NUMBER + : await needProjectNumber(options); + const aliases = getAliases(options, projectId); + extensionEmulator = new ExtensionsEmulator({ + projectId, + projectDir: options.config.projectDir, + projectNumber, + aliases, + extensions: options.config.get("extensions"), + }); + const extensionsBackends = await extensionEmulator.getExtensionBackends(); + const filteredExtensionsBackends = extensionEmulator.filterUnemulatedTriggers( + options, + extensionsBackends + ); + emulatableBackends.push(...filteredExtensionsBackends); + } + + const listenConfig = {} as Record; + for (const emulator of ALL_EMULATORS) { + if (emulator === Emulators.EXTENSIONS) { + // Same port as function, no need for separate assignment + continue; + } + if (emulator === Emulators.UI && !showUI) { + continue; + } + if ( + shouldStart(options, emulator) || + (emulator === Emulators.EVENTARC && emulatableBackends.length > 0) || + (emulator === Emulators.LOGGING && + ((showUI && shouldStart(options, Emulators.UI)) || START_LOGGING_EMULATOR)) + ) { + let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(); + if (host === "localhost" && utils.isRunningInWSL()) { + // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 + // 127.0.0.1 instead of localhost. This, combined with the hack in + // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. + // The CLI (including the hub) will also consistently report 127.0.0.1, + // causing clients to connect via IPv4 only (which mitigates the problem of + // some clients resolving localhost to IPv6 and get connection refused). + host = "127.0.0.1"; + } + + const portVal = options.config.src.emulators?.[emulator]?.port; + let port: number; + let portFixed: boolean; + if (portVal) { + port = parseInt(`${portVal}`, 10); + portFixed = true; + } else { + port = Constants.getDefaultPort(emulator); + portFixed = !FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; + } + listenConfig[emulator] = { + host, + port, + portFixed, + }; + if (emulator === Emulators.FIRESTORE) { + const wsPortConfig = options.config.src.emulators?.firestore?.websocketPort; + listenConfig["firestore.websocket"] = { + host, + port: wsPortConfig || 9150, + portFixed: !!wsPortConfig, + }; + } + } + } + const listenForEmulator = await resolveHostAndAssignPorts(listenConfig); + hubLogger.log("DEBUG", "assigned listening specs for emulators", { user: listenForEmulator }); + + function legacyGetFirstAddr(name: PortName): { host: string; port: number } { + const firstSpec = listenForEmulator[name][0]; + return { + host: firstSpec.address, + port: firstSpec.port, + }; + } + function startEmulator(instance: EmulatorInstance): Promise { const name = instance.getName(); @@ -460,9 +443,8 @@ export async function startAll( return EmulatorRegistry.start(instance); } - if (shouldStart(options, Emulators.HUB)) { - const hubAddr = await getAndCheckAddress(Emulators.HUB, options); - const hub = new EmulatorHub({ projectId, ...hubAddr }); + if (listenForEmulator.hub) { + const hub = new EmulatorHub({ projectId, listen: listenForEmulator[Emulators.HUB] }); // Log the command for analytics, we only report this for "hub" // since we originally mistakenly reported emulators:start events @@ -504,64 +486,26 @@ export async function startAll( experiments.assertEnabled("webframeworks", "emulate a web framework"); const emulators: EmulatorInfo[] = []; if (experiments.isEnabled("webframeworks")) { - for (const e of EMULATORS_SUPPORTED_BY_UI) { - // TODO: Double check if this actually works -- we're early in the startup - // process and emulators are probably not yet running / registered. - const info = EmulatorRegistry.getInfo(e); - if (info) emulators.push(info); + for (const e of ALL_SERVICE_EMULATORS) { + if (listenForEmulator[e]) { + emulators.push({ + name: e, + host: utils.connectableHostname(listenForEmulator[e][0].address), + port: listenForEmulator[e][0].port, + }); + } } } await prepareFrameworks(targets, options, options, emulators); } - const emulatableBackends: EmulatableBackend[] = []; - const projectDir = (options.extDevDir || options.config.projectDir) as string; - if (shouldStart(options, Emulators.FUNCTIONS)) { - const functionsCfg = normalizeAndValidate(options.config.src.functions); - // Note: ext:dev:emulators:* commands hit this path, not the Emulators.EXTENSIONS path - utils.assertIsStringOrUndefined(options.extDevDir); - - for (const cfg of functionsCfg) { - const functionsDir = path.join(projectDir, cfg.source); - emulatableBackends.push({ - functionsDir, - codebase: cfg.codebase, - env: { - ...options.extDevEnv, - }, - secretEnv: [], // CF3 secrets are bound to specific functions, so we'll get them during trigger discovery. - // TODO(b/213335255): predefinedTriggers and nodeMajorVersion are here to support ext:dev:emulators:* commands. - // Ideally, we should handle that case via ExtensionEmulator. - predefinedTriggers: options.extDevTriggers as ParsedTriggerDefinition[] | undefined, - nodeMajorVersion: parseRuntimeVersion((options.extDevNodeVersion as string) || cfg.runtime), - }); - } - } - - if (shouldStart(options, Emulators.EXTENSIONS)) { - const projectNumber = isDemoProject - ? Constants.FAKE_PROJECT_NUMBER - : await needProjectNumber(options); - const aliases = getAliases(options, projectId); - const extensionEmulator = new ExtensionsEmulator({ - projectId, - projectDir: options.config.projectDir, - projectNumber, - aliases, - extensions: options.config.get("extensions"), - }); - const extensionsBackends = await extensionEmulator.getExtensionBackends(); - const filteredExtensionsBackends = extensionEmulator.filterUnemulatedTriggers( - options, - extensionsBackends - ); - emulatableBackends.push(...filteredExtensionsBackends); + if (extensionEmulator) { await startEmulator(extensionEmulator); } if (emulatableBackends.length) { const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); - const functionsAddr = await getAndCheckAddress(Emulators.FUNCTIONS, options); + const functionsAddr = legacyGetFirstAddr(Emulators.FUNCTIONS); const projectId = needProjectId(options); let inspectFunctions: number | undefined; @@ -578,7 +522,7 @@ export async function startAll( // Warn the developer that the Functions/Extensions emulator can call out to production. const emulatorsNotRunning = ALL_SERVICE_EMULATORS.filter((e) => { - return e !== Emulators.FUNCTIONS && !shouldStart(options, e); + return e !== Emulators.FUNCTIONS && !listenForEmulator[e]; }); if (emulatorsNotRunning.length > 0 && !Constants.isDemoProject(projectId)) { functionsLogger.logLabeled( @@ -605,7 +549,7 @@ export async function startAll( }); await startEmulator(functionsEmulator); - const eventarcAddr = await getAndCheckAddress(Emulators.EVENTARC, options); + const eventarcAddr = legacyGetFirstAddr(Emulators.EVENTARC); const eventarcEmulator = new EventarcEmulator({ host: eventarcAddr.host, port: eventarcAddr.port, @@ -613,15 +557,10 @@ export async function startAll( await startEmulator(eventarcEmulator); } - if (shouldStart(options, Emulators.FIRESTORE)) { + if (listenForEmulator.firestore) { const firestoreLogger = EmulatorLogger.forEmulator(Emulators.FIRESTORE); - const firestoreAddr = await getAndCheckAddress(Emulators.FIRESTORE, options); - const portVal = options.config.src.emulators?.firestore?.websocketPort; - const websocketPort = await getFirestoreWebSocketPort( - firestoreAddr.host, - portVal, - Emulators.FIRESTORE - ); + const firestoreAddr = legacyGetFirstAddr(Emulators.FIRESTORE); + const websocketPort = legacyGetFirstAddr("firestore.websocket").port; const args: FirestoreEmulatorArgs = { host: firestoreAddr.host, @@ -705,9 +644,9 @@ export async function startAll( ); } - if (shouldStart(options, Emulators.DATABASE)) { + if (listenForEmulator.database) { const databaseLogger = EmulatorLogger.forEmulator(Emulators.DATABASE); - const databaseAddr = await getAndCheckAddress(Emulators.DATABASE, options); + const databaseAddr = legacyGetFirstAddr(Emulators.DATABASE); const args: DatabaseEmulatorArgs = { host: databaseAddr.host, @@ -782,7 +721,7 @@ export async function startAll( } } - if (shouldStart(options, Emulators.AUTH)) { + if (listenForEmulator.auth) { if (!projectId) { throw new FirebaseError( `Cannot start the ${Constants.description( @@ -791,7 +730,7 @@ export async function startAll( ); } - const authAddr = await getAndCheckAddress(Emulators.AUTH, options); + const authAddr = legacyGetFirstAddr(Emulators.AUTH); const authEmulator = new AuthEmulator({ host: authAddr.host, port: authAddr.port, @@ -811,14 +750,14 @@ export async function startAll( } } - if (shouldStart(options, Emulators.PUBSUB)) { + if (listenForEmulator.pubsub) { if (!projectId) { throw new FirebaseError( "Cannot start the Pub/Sub emulator without a project: run 'firebase init' or provide the --project flag" ); } - const pubsubAddr = await getAndCheckAddress(Emulators.PUBSUB, options); + const pubsubAddr = legacyGetFirstAddr(Emulators.PUBSUB); const pubsubEmulator = new PubsubEmulator({ host: pubsubAddr.host, port: pubsubAddr.port, @@ -828,8 +767,8 @@ export async function startAll( await startEmulator(pubsubEmulator); } - if (shouldStart(options, Emulators.STORAGE)) { - const storageAddr = await getAndCheckAddress(Emulators.STORAGE, options); + if (listenForEmulator.storage) { + const storageAddr = legacyGetFirstAddr(Emulators.STORAGE); const storageEmulator = new StorageEmulator({ host: storageAddr.host, @@ -849,8 +788,8 @@ export async function startAll( // Hosting emulator needs to start after all of the others so that we can detect // which are running and call useEmulator in __init.js - if (shouldStart(options, Emulators.HOSTING)) { - const hostingAddr = await getAndCheckAddress(Emulators.HOSTING, options); + if (listenForEmulator.hosting) { + const hostingAddr = legacyGetFirstAddr(Emulators.HOSTING); const hostingEmulator = new HostingEmulator({ host: hostingAddr.host, port: hostingAddr.port, @@ -870,8 +809,8 @@ export async function startAll( ); } - if (showUI && (shouldStart(options, Emulators.UI) || START_LOGGING_EMULATOR)) { - const loggingAddr = await getAndCheckAddress(Emulators.LOGGING, options); + if (listenForEmulator.logging) { + const loggingAddr = legacyGetFirstAddr(Emulators.LOGGING); const loggingEmulator = new LoggingEmulator({ host: loggingAddr.host, port: loggingAddr.port, @@ -880,8 +819,8 @@ export async function startAll( await startEmulator(loggingEmulator); } - if (showUI && shouldStart(options, Emulators.UI)) { - const uiAddr = await getAndCheckAddress(Emulators.UI, options); + if (listenForEmulator.ui) { + const uiAddr = legacyGetFirstAddr(Emulators.UI); const ui = new EmulatorUI({ projectId: projectId, auto_download: true, @@ -933,8 +872,9 @@ export async function exportEmulatorData(exportPath: string, options: any, initi ); } + let origin; try { - await hubClient.getStatus(); + origin = await hubClient.getStatus(); } catch (e: any) { const filePath = EmulatorHub.getLocatorFilePath(projectId); throw new FirebaseError( @@ -943,9 +883,7 @@ export async function exportEmulatorData(exportPath: string, options: any, initi ); } - utils.logBullet( - `Found running emulator hub for project ${clc.bold(projectId)} at ${hubClient.origin}` - ); + utils.logBullet(`Found running emulator hub for project ${clc.bold(projectId)} at ${origin}`); // If the export target directory does not exist, we should attempt to create it const exportAbsPath = path.resolve(exportPath); diff --git a/src/emulator/dns.ts b/src/emulator/dns.ts new file mode 100644 index 000000000000..62786282ed51 --- /dev/null +++ b/src/emulator/dns.ts @@ -0,0 +1,101 @@ +import { LookupAddress, LookupAllOptions, promises as dnsPromises } from "node:dns"; // Not using "dns/promises" for Node 14 compatibility. +import { isIP } from "node:net"; +import { logger } from "../logger"; + +export const IPV4_LOOPBACK = { address: "127.0.0.1", family: 4 } as const; +export const IPV6_LOOPBACK = { address: "::1", family: 6 } as const; +export const IPV4_UNSPECIFIED = { address: "0.0.0.0", family: 4 } as const; +export const IPV6_UNSPECIFIED = { address: "::", family: 6 } as const; + +/** + * Resolves hostnames to IP addresses consistently. + * + * The result(s) for a single hostname is cached in memory to ensure consistency + * throughout the lifetime or a single process (i.e. CLI command invocation). + */ +export class Resolver { + /** + * The default resolver. Preferred in all normal CLI operations. + */ + public static DEFAULT = new Resolver(); + + private cache = new Map([ + // Pre-populate cache with localhost (the most common hostname used in + // emulators) for quicker startup and better consistency across OSes. + ["localhost", [IPV4_LOOPBACK, IPV6_LOOPBACK]], + ]); + + /** + * Create a new Resolver instance with its own dedicated cache. + * + * @param lookup an underlying DNS lookup function (useful in tests) + */ + public constructor( + private lookup: ( + hostname: string, + options: LookupAllOptions + ) => Promise = dnsPromises.lookup + ) {} + + /** + * Returns the first IP address that a hostname map to, ignoring others. + * + * If possible, prefer `lookupAll` and handle all results instead, since the + * first one may not be what the user wants. Especially, when a domain name is + * specified as the listening hostname of a server, listening on both IPv4 and + * IPv6 addresses may be closer to user intention. + * + * A successful lookup will add the results to the cache, which will be used + * to serve subsequent requests to the same hostname on the same `Resolver`. + * + * @param hostname the hostname to resolve + * @return the first IP address (perferrably IPv4 for compatibility) + */ + async lookupFirst(hostname: string): Promise { + const addresses = await this.lookupAll(hostname); + if (addresses.length === 1) { + return addresses[0]; + } + + // Log a debug message when discarding additional results: + const result = addresses[0]; + const discarded: string[] = []; + for (let i = 1; i < addresses.length; i++) { + discarded.push(result.address); + } + logger.debug( + `Resolved hostname "${hostname}" to the first result "${ + result.address + }" (ignoring candidates: ${discarded.join(",")}).` + ); + return result; + } + + /** + * Returns all IP addresses that a hostname map to, IPv4 first (if present). + * + * A successful lookup will add the results to the cache, which will be used + * to serve subsequent requests to the same hostname on the same `Resolver`. + * + * @param hostname the hostname to resolve + * @return IP addresses (IPv4 addresses before IPv6 ones for compatibility) + */ + async lookupAll(hostname: string): Promise { + const family = isIP(hostname); + if (family > 0) { + return [{ family, address: hostname }]; + } + // We may want to make this case-insensitive if customers run into issues. + const cached = this.cache.get(hostname); + if (cached) { + return cached; + } + const addresses = await this.lookup(hostname, { + // Return IPv4 addresses first (for backwards compatibility). + verbatim: false, + all: true, + }); + this.cache.set(hostname, addresses); + return addresses; + } +} diff --git a/src/emulator/emulatorLogger.ts b/src/emulator/emulatorLogger.ts index 4714a74c91a2..3b029739b4e9 100644 --- a/src/emulator/emulatorLogger.ts +++ b/src/emulator/emulatorLogger.ts @@ -42,10 +42,10 @@ export class EmulatorLogger { static verbosity: Verbosity = Verbosity.DEBUG; static warnOnceCache = new Set(); - constructor(private data: LogData = {}) {} + constructor(public readonly name: string, private data: LogData = {}) {} static forEmulator(emulator: Emulators) { - return new EmulatorLogger({ + return new EmulatorLogger(emulator, { metadata: { emulator: { name: emulator, @@ -55,10 +55,10 @@ export class EmulatorLogger { } static forFunction(functionName: string, extensionLogInfo?: ExtensionLogInfo): EmulatorLogger { - return new EmulatorLogger({ + return new EmulatorLogger(Emulators.FUNCTIONS, { metadata: { emulator: { - name: "functions", + name: Emulators.FUNCTIONS, }, function: { name: functionName, @@ -69,10 +69,10 @@ export class EmulatorLogger { } static forExtension(extensionLogInfo: ExtensionLogInfo): EmulatorLogger { - return new EmulatorLogger({ + return new EmulatorLogger(Emulators.EXTENSIONS, { metadata: { emulator: { - name: "extensions", + name: Emulators.EXTENSIONS, }, extension: extensionLogInfo, }, @@ -274,7 +274,14 @@ You can probably fix this by running "npm install ${systemLog.data.name}@latest" * @param text * @param data */ - logLabeled(type: LogType, label: string, text: string): void { + logLabeled(type: LogType, text: string): void; + logLabeled(type: LogType, label: string, text: string): void; + logLabeled(type: LogType, labelOrText: string, text?: string): void { + let label = labelOrText; + if (text === undefined) { + text = label; + label = this.name; + } if (EmulatorLogger.shouldSupress(type)) { logger.debug(`[${label}] ${text}`); return; diff --git a/src/emulator/emulatorServer.ts b/src/emulator/emulatorServer.ts deleted file mode 100644 index 42dd929872fd..000000000000 --- a/src/emulator/emulatorServer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { EmulatorInstance } from "./types"; -import { EmulatorRegistry } from "./registry"; -import * as portUtils from "./portUtils"; -import { FirebaseError } from "../error"; - -/** - * Wrapper object to expose an EmulatorInstance for "firebase serve" that - * also registers the emulator with the registry. - */ -export class EmulatorServer { - constructor(public instance: EmulatorInstance) {} - - async start(): Promise { - const { port, host } = this.instance.getInfo(); - const portOpen = await portUtils.checkPortOpen(port, host); - - if (!portOpen) { - throw new FirebaseError( - `Port ${port} is not open on ${host}, could not start ${this.instance.getName()} emulator.` - ); - } - - await EmulatorRegistry.start(this.instance); - } - - async connect(): Promise { - await this.instance.connect(); - } - - async stop(): Promise { - await EmulatorRegistry.stop(this.instance.getName()); - } - - get(): EmulatorInstance { - return this.instance; - } -} diff --git a/src/emulator/hub.ts b/src/emulator/hub.ts index c9b0bc22f046..1c2a6d062dee 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -1,36 +1,33 @@ -import * as cors from "cors"; import * as express from "express"; import * as os from "os"; import * as fs from "fs"; import * as path from "path"; -import * as bodyParser from "body-parser"; import * as utils from "../utils"; import { logger } from "../logger"; -import { Constants } from "./constants"; -import { Emulators, EmulatorInstance, EmulatorInfo } from "./types"; +import { Emulators, EmulatorInfo, ListenSpec } from "./types"; import { HubExport } from "./hubExport"; import { EmulatorRegistry } from "./registry"; import { FunctionsEmulator } from "./functionsEmulator"; +import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; // We use the CLI version from package.json const pkg = require("../../package.json"); export interface Locator { version: string; - host: string; - port: number; + // Ways of reaching the hub as URL prefix, such as http://127.0.0.1:4000 + origins: string[]; } export interface EmulatorHubArgs { projectId: string; - port?: number; - host?: string; + listen: ListenSpec[]; } export type GetEmulatorsResponse = Record; -export class EmulatorHub implements EmulatorInstance { +export class EmulatorHub extends ExpressBasedEmulator { static CLI_VERSION = pkg.version; static PATH_EXPORT = "/_admin/export"; static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers"; @@ -65,21 +62,29 @@ export class EmulatorHub implements EmulatorInstance { return path.join(dir, filename); } - private hub: express.Express; - private destroyServer?: () => Promise; - constructor(private args: EmulatorHubArgs) { - this.hub = express(); - // Enable CORS for all APIs, all origins (reflected), and all headers (reflected). - // Safe since all Hub APIs are cookieless. - this.hub.use(cors({ origin: true })); - this.hub.use(bodyParser.json()); - - this.hub.get("/", (req, res) => { - res.json(this.getLocator()); + super({ + listen: args.listen, + }); + } + + override async start(): Promise { + await super.start(); + await this.writeLocatorFile(); + } + + protected override async createExpressApp(): Promise { + const app = await super.createExpressApp(); + app.get("/", (req, res) => { + res.json({ + ...this.getLocator(), + // For backward compatibility: + host: utils.connectableHostname(this.args.listen[0].address), + port: this.args.listen[0].port, + }); }); - this.hub.get(EmulatorHub.PATH_EMULATORS, (req, res) => { + app.get(EmulatorHub.PATH_EMULATORS, (req, res) => { const body: GetEmulatorsResponse = {}; for (const info of EmulatorRegistry.listRunningWithInfo()) { body[info.name] = info; @@ -87,7 +92,7 @@ export class EmulatorHub implements EmulatorInstance { res.json(body); }); - this.hub.post(EmulatorHub.PATH_EXPORT, async (req, res) => { + app.post(EmulatorHub.PATH_EXPORT, async (req, res) => { const path: string = req.body.path; const initiatedBy: string = req.body.initiatedBy || "unknown"; utils.logLabeledBullet("emulators", `Received export request. Exporting data to ${path}.`); @@ -109,7 +114,7 @@ export class EmulatorHub implements EmulatorInstance { } }); - this.hub.put(EmulatorHub.PATH_DISABLE_FUNCTIONS, async (req, res) => { + app.put(EmulatorHub.PATH_DISABLE_FUNCTIONS, async (req, res) => { utils.logLabeledBullet( "emulators", `Disabling Cloud Functions triggers, non-HTTP functions will not execute.` @@ -126,7 +131,7 @@ export class EmulatorHub implements EmulatorInstance { res.status(200).json({ enabled: false }); }); - this.hub.put(EmulatorHub.PATH_ENABLE_FUNCTIONS, async (req, res) => { + app.put(EmulatorHub.PATH_ENABLE_FUNCTIONS, async (req, res) => { utils.logLabeledBullet( "emulators", `Enabling Cloud Functions triggers, non-HTTP functions will execute.` @@ -142,48 +147,32 @@ export class EmulatorHub implements EmulatorInstance { await emu.reloadTriggers(); res.status(200).json({ enabled: true }); }); - } - - async start(): Promise { - const { host, port } = this.getInfo(); - const server = this.hub.listen(port, host); - this.destroyServer = utils.createDestroyer(server); - await this.writeLocatorFile(); - } - async connect(): Promise { - // No-op + return app; } async stop(): Promise { - if (this.destroyServer) { - await this.destroyServer(); - } + await super.stop(); await this.deleteLocatorFile(); } - getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(); - const port = this.args.port || Constants.getDefaultPort(Emulators.HUB); - - return { - name: this.getName(), - host, - port, - }; - } - getName(): Emulators { return Emulators.HUB; } private getLocator(): Locator { - const { host, port } = this.getInfo(); const version = pkg.version; + const origins: string[] = []; + for (const spec of this.args.listen) { + if (spec.family === "IPv6") { + origins.push(`http://[${utils.connectableHostname(spec.address)}]:${spec.port}`); + } else { + origins.push(`http://${utils.connectableHostname(spec.address)}:${spec.port}`); + } + } return { version, - host, - port, + origins, }; } diff --git a/src/emulator/hubClient.ts b/src/emulator/hubClient.ts index fb25b9b50585..a1d97b99bb95 100644 --- a/src/emulator/hubClient.ts +++ b/src/emulator/hubClient.ts @@ -14,27 +14,47 @@ export class EmulatorHubClient { return this.locator !== undefined; } - async getStatus(): Promise { - const apiClient = new Client({ urlPrefix: this.origin, auth: false }); - await apiClient.get("/"); + /** + * Ping possible hub origins for status and return the first successful. + */ + getStatus(): Promise { + return this.tryOrigins(async (client, origin) => { + await client.get("/"); + return origin; + }); + } + + private async tryOrigins(task: (client: Client, origin: string) => Promise): Promise { + const origins = this.assertLocator().origins; + let err: any = undefined; + for (const origin of origins) { + try { + const apiClient = new Client({ urlPrefix: origin, auth: false }); + return await task(apiClient, origin); + } catch (e) { + if (!err) { + err = e; // Only record the first error and only throw if all fails. + } + } + } + throw err ?? new Error("Cannot find working hub origin. Tried:" + origins.join(" ")); } async getEmulators(): Promise { - const apiClient = new Client({ urlPrefix: this.origin, auth: false }); - const res = await apiClient.get(EmulatorHub.PATH_EMULATORS); + const res = await this.tryOrigins((client) => + client.get(EmulatorHub.PATH_EMULATORS) + ); return res.body; } async postExport(options: ExportOptions): Promise { - const apiClient = new Client({ urlPrefix: this.origin, auth: false }); + // This is a POST operation that should not be retried / multicast, so we + // will try to find the right origin first via GET. + const origin = await this.getStatus(); + const apiClient = new Client({ urlPrefix: origin, auth: false }); await apiClient.post(EmulatorHub.PATH_EXPORT, options); } - get origin(): string { - const locator = this.assertLocator(); - return `http://${locator.host}:${locator.port}`; - } - private assertLocator(): Locator { if (this.locator === undefined) { throw new FirebaseError(`Cannot contact the Emulator Hub for project ${this.projectId}`); diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index 3fe48b11de6f..82dde2d7830c 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -1,16 +1,19 @@ -import * as pf from "portfinder"; +import * as clc from "colorette"; import * as tcpport from "tcp-port-used"; import * as dns from "dns"; +import { createServer } from "node:net"; import { FirebaseError } from "../error"; -import { logger } from "../logger"; - -dns.setDefaultResultOrder("ipv4first"); +import * as utils from "../utils"; +import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED, Resolver } from "./dns"; +import { Emulators, ListenSpec } from "./types"; +import { Constants } from "./constants"; +import { EmulatorLogger } from "./emulatorLogger"; // See: // - https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports // - https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/base/port_util.cc -const RESTRICTED_PORTS = [ +const RESTRICTED_PORTS = new Set([ 1, // tcpmux 7, // echo 9, // discard @@ -78,19 +81,19 @@ const RESTRICTED_PORTS = [ 6668, // Alternate IRC [Apple addition] 6669, // Alternate IRC [Apple addition] 6697, // IRC + TLS -]; +]); /** * Check if a given port is restricted by Chrome. */ -export function isRestricted(port: number): boolean { - return RESTRICTED_PORTS.includes(port); +function isRestricted(port: number): boolean { + return RESTRICTED_PORTS.has(port); } /** * Suggest a port equal to or higher than the given port which is not restricted by Chrome. */ -export function suggestUnrestricted(port: number): number { +function suggestUnrestricted(port: number): number { if (!isRestricted(port)) { return port; } @@ -104,37 +107,45 @@ export function suggestUnrestricted(port: number): number { } /** - * Find an available (unused) port on the given host. - * @param host the host. - * @param start the lowest port to search. - * @param avoidRestricted when true (default) ports which are restricted by Chrome are excluded. + * Check if a port is available for listening on the given address. */ -export async function findAvailablePort( - host: string, - start: number, - avoidRestricted = true -): Promise { - const openPort = await pf.getPortPromise({ host, port: start }); - - if (avoidRestricted && isRestricted(openPort)) { - logger.debug(`portUtils: skipping restricted port ${openPort}`); - return findAvailablePort(host, suggestUnrestricted(openPort), avoidRestricted); - } +export async function checkListenable(addr: dns.LookupAddress, port: number): Promise; +export async function checkListenable(listen: ListenSpec): Promise; +export async function checkListenable( + arg1: dns.LookupAddress | ListenSpec, + port?: number +): Promise { + const addr = + port === undefined ? (arg1 as ListenSpec) : listenSpec(arg1 as dns.LookupAddress, port); - return openPort; -} - -/** - * Check if a port is open on the given host. - */ -export async function checkPortOpen(port: number, host: string): Promise { - try { - const inUse = await tcpport.check(port, host); - return !inUse; - } catch (e: any) { - logger.debug(`port check error: ${e}`); - return false; - } + // Not using tcpport.check since it is based on trying to establish a Socket + // connection, not on *listening* on a host:port. + return new Promise((resolve, reject) => { + const dummyServer = createServer(() => { + // noop + }); + dummyServer.once("error", (err) => { + dummyServer.removeAllListeners(); + const e = err as Error & { code?: string }; + if (e.code === "EADDRINUSE" || e.code === "EACCES") { + resolve(false); + } else { + reject(e); + } + }); + dummyServer.once("listening", () => { + dummyServer.removeAllListeners(); + dummyServer.close((err) => { + dummyServer.removeAllListeners(); + if (err) { + reject(err); + } else { + resolve(true); + } + }); + }); + dummyServer.listen({ host: addr.address, port: addr.port, ipv6Only: addr.family === "IPv6" }); + }); } /** @@ -149,3 +160,262 @@ export async function waitForPortClosed(port: number, host: string): Promise = { + // External processes that accept only one hostname and one port, and will + // bind to only one of the addresses resolved from hostname. + database: true, + firestore: true, + "firestore.websocket": true, + pubsub: true, + + // Listening on multiple addresses to maximize the chance of discovery. + hub: false, + + // TODO: Modify the following emulators to listen on multiple addresses. + + // Separate Node.js process that requires a separate update. + // For consistency, we can resolve in the CLI and pass in the results. + ui: true, + + // Express-based servers, can be reused for multiple listen sockets. + auth: true, + eventarc: true, + extensions: true, + functions: true, + logging: true, + storage: true, + + // Only one hostname possible in .server mode, can switch to middleware later. + hosting: true, +}; + +export interface EmulatorListenConfig { + host: string; + port: number; + portFixed?: boolean; +} + +const MAX_PORT = 65535; // max TCP port + +/** + * Resolve the hostname and assign ports to a subset of emulators. + * + * @param listenConfig the config for each emulator + * @return a map from emulator to its resolved addresses with port. + */ +export async function resolveHostAndAssignPorts( + listenConfig: Partial> +): Promise> { + const entries = Object.entries(listenConfig) as [PortName, EmulatorListenConfig][]; + const lookupForHost = new Map>(); + const takenPorts = new Map(); + + const result = {} as Record; + const tasks = []; + for (const [name, { host, port, portFixed }] of entries) { + let lookup = lookupForHost.get(host); + if (!lookup) { + lookup = Resolver.DEFAULT.lookupAll(host); + lookupForHost.set(host, lookup); + } + const findAddrs = lookup.then(async (addrs) => { + const emuLogger = EmulatorLogger.forEmulator( + name === "firestore.websocket" ? Emulators.FIRESTORE : name + ); + if (addrs.some((addr) => addr.address === IPV6_UNSPECIFIED.address)) { + if (!addrs.some((addr) => addr.address === IPV4_UNSPECIFIED.address)) { + // In normal Node.js code (including CLI versions so far), listening + // on IPv6 :: will also listen on IPv4 0.0.0.0 (a.k.a. "dual stack"). + // Maintain that behavior if both are listenable. Warn otherwise. + emuLogger.logLabeled( + "DEBUG", + name, + `testing listening on IPv4 wildcard in addition to IPv6. To listen on IPv6 only, use "::0" instead.` + ); + addrs.push(IPV4_UNSPECIFIED); + } + } + for (let p = port; p <= MAX_PORT; p++) { + if (takenPorts.has(p)) { + continue; + } + if (!portFixed && RESTRICTED_PORTS.has(p)) { + emuLogger.logLabeled("DEBUG", name, `portUtils: skipping restricted port ${p}`); + continue; + } + const available: ListenSpec[] = []; + const unavailable: string[] = []; + let i; + for (i = 0; i < addrs.length; i++) { + const addr = addrs[i]; + const listen = listenSpec(addr, p); + // This must be done one by one since the addresses may overlap. + if (await checkListenable(listen)) { + available.push(listen); + } else { + if (!portFixed) { + // Try to find another port to avoid any potential conflict. + if (i > 0) { + emuLogger.logLabeled( + "DEBUG", + name, + `Port ${p} taken on secondary address ${addr.address}, will keep searching to find a better port.` + ); + } + break; + } + unavailable.push(addr.address); + } + } + if (i === addrs.length) { + if (unavailable.length > 0) { + if (unavailable[0] === addrs[0].address) { + // The port is not available on the primary address, we should err + // on the side of safety and let the customer choose a different port. + return fixedPortNotAvailable(name, host, port, emuLogger, unavailable); + } + // For backward compatibility, we'll start listening as long as + // the primary address is available. Skip listening on the + // unavailable ones with a warning. + warnPartiallyAvailablePort(emuLogger, port, available, unavailable); + } + + // If available, take it and prevent any other emulator from doing so. + if (takenPorts.has(p)) { + continue; + } + takenPorts.set(p, name); + + if (RESTRICTED_PORTS.has(p)) { + const suggested = suggestUnrestricted(port); + emuLogger.logLabeled( + "WARN", + name, + `Port ${port} is restricted by some web browsers, including Chrome. You may want to choose a different port such as ${suggested}.` + ); + } + if (p !== port && name !== "firestore.websocket") { + emuLogger.logLabeled( + "WARN", + `${portDescription(name)} unable to start on port ${port}, starting on ${p} instead.` + ); + } + if (available.length > 1 && EMULATOR_CAN_LISTEN_ON_PRIMARY_ONLY[name]) { + emuLogger.logLabeled( + "DEBUG", + name, + `${portDescription(name)} only supports listening on one address (${ + available[0].address + }). Not listening on ${addrs + .slice(1) + .map((s) => s.address) + .join(",")}` + ); + result[name] = [available[0]]; + } else { + result[name] = available; + } + return; + } + } + // This should be extremely rare. + return utils.reject( + `Could not find any open port in ${port}-${MAX_PORT} for ${portDescription(name)}`, + {} + ); + }); + tasks.push(findAddrs); + } + + await Promise.all(tasks); + return result; +} + +function portDescription(name: PortName): string { + return name === "firestore.websocket" + ? `websocket server for ${Emulators.FIRESTORE}` + : Constants.description(name); +} + +function warnPartiallyAvailablePort( + emuLogger: EmulatorLogger, + port: number, + available: ListenSpec[], + unavailable: string[] +): void { + emuLogger.logLabeled( + "WARN", + `Port ${port} is available on ` + + available.map((s) => s.address).join(",") + + ` but not ${unavailable.join(",")}. This may cause issues with some clients.` + ); + emuLogger.logLabeled( + "WARN", + `If you encounter connectivity issues, consider switching to a different port or explicitly specifying ${clc.yellow( + '"host": ""' + )} instead of hostname in firebase.json` + ); +} + +function fixedPortNotAvailable( + name: PortName, + host: string, + port: number, + emuLogger: EmulatorLogger, + unavailableAddrs: string[] +): Promise { + if (unavailableAddrs.length !== 1 || unavailableAddrs[0] !== host) { + // Show detailed resolved addresses + host = `${host} (${unavailableAddrs.join(",")})`; + } + const description = portDescription(name); + emuLogger.logLabeled( + "WARN", + `Port ${port} is not open on ${host}, could not start ${description}.` + ); + if (name === "firestore.websocket") { + emuLogger.logLabeled( + "WARN", + `To select a different port, specify that port in a firebase.json config file: + { + // ... + "emulators": { + "${Emulators.FIRESTORE}": { + "host": "${clc.yellow("HOST")}", + ... + "websocketPort": "${clc.yellow("WEBSOCKET_PORT")}" + } + } + }` + ); + } else { + emuLogger.logLabeled( + "WARN", + `To select a different host/port, specify that host/port in a firebase.json config file: + { + // ... + "emulators": { + "${emuLogger.name}": { + "host": "${clc.yellow("HOST")}", + "port": "${clc.yellow("PORT")}" + } + } + }` + ); + } + return utils.reject(`Could not start ${description}, port taken.`, {}); +} + +function listenSpec(lookup: dns.LookupAddress, port: number): ListenSpec { + if (lookup.family !== 4 && lookup.family !== 6) { + throw new Error(`Unsupported address family "${lookup.family}" for address ${lookup.address}.`); + } + return { + address: lookup.address, + family: lookup.family === 4 ? "IPv4" : "IPv6", + port: port, + }; +} diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 3fa29ece4e1b..78d5c7475aac 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -131,10 +131,15 @@ export interface EmulatorInstance { export interface EmulatorInfo { name: Emulators; - host: string; - port: number; pid?: number; reservedPorts?: number[]; + + /** All addresses that an emulator listens on. */ + listen?: ListenSpec[]; + + /** The primary IP address that the emulator listens on. */ + host: string; + port: number; } export interface DownloadableEmulatorCommand { @@ -178,9 +183,10 @@ export interface DownloadableEmulatorDetails { stdout: any | null; } -export interface Address { - host: string; +export interface ListenSpec { + address: string; port: number; + family: "IPv4" | "IPv6"; } export enum FunctionsExecutionMode { diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index caba72703cdc..77f8432d534c 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -15,7 +15,7 @@ import { hostingConfig } from "../hosting/config"; import { listSites } from "../hosting/api"; import { getAppConfig, AppPlatform } from "../management/apps"; import { promptOnce } from "../prompt"; -import { EmulatorInfo, Emulators } from "../emulator/types"; +import { EmulatorInfo, Emulators, EMULATORS_SUPPORTED_BY_USE_EMULATOR } from "../emulator/types"; import { getCredentialPathAsync } from "../defaultCredentials"; import { getProjectDefaultAccount } from "../auth"; import { formatHost } from "../emulator/functionsEmulatorShared"; @@ -313,7 +313,7 @@ export async function prepareFrameworks( if (info.name === Emulators.STORAGE) process.env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = formatHost(info); } - if (usesFirebaseJsSdk) { + if (usesFirebaseJsSdk && EMULATORS_SUPPORTED_BY_USE_EMULATOR.includes(info.name)) { firebaseDefaults ||= {}; firebaseDefaults.emulatorHosts ||= {}; firebaseDefaults.emulatorHosts[info.name] = formatHost(info); diff --git a/src/functionsShellCommandAction.ts b/src/functionsShellCommandAction.ts index d93386570474..e6ac89148eb7 100644 --- a/src/functionsShellCommandAction.ts +++ b/src/functionsShellCommandAction.ts @@ -13,8 +13,9 @@ import * as shell from "./emulator/functionsEmulatorShell"; import * as commandUtils from "./emulator/commandUtils"; import { EMULATORS_SUPPORTED_BY_FUNCTIONS, EmulatorInfo, Emulators } from "./emulator/types"; import { EmulatorHubClient } from "./emulator/hubClient"; -import { findAvailablePort } from "./emulator/portUtils"; +import { resolveHostAndAssignPorts } from "./emulator/portUtils"; import { Options } from "./options"; +import { Constants } from "./emulator/constants"; const serveFunctions = new FunctionsServer(); @@ -44,6 +45,10 @@ export const actionFunction = async (options: Options) => { (e) => remoteEmulators[e] === undefined ); + let host = Constants.getDefaultHost(); + // If the port was not set by the --port flag or determined from 'firebase.json', just scan + // up from 5000 + let port = 5000; const functionsInfo = remoteEmulators[Emulators.FUNCTIONS]; if (functionsInfo) { utils.logLabeledWarning( @@ -53,14 +58,19 @@ export const actionFunction = async (options: Options) => { } else if (!options.port) { // If the user did not pass in any port and the functions emulator is not already running, we can // use the port defined for the Functions emulator in their firebase.json - options.port = options.config.src.emulators?.functions?.port; + port = options.config.src.emulators?.functions?.port ?? port; + host = options.config.src.emulators?.functions?.host ?? host; + options.host = host; } - // If the port was not set by the --port flag or determined from 'firebase.json', just scan - // up from 5000 - if (!options.port) { - options.port = await findAvailablePort("localhost", 5000); - } + const listen = ( + await resolveHostAndAssignPorts({ + [Emulators.FUNCTIONS]: { host, port }, + }) + ).functions; + // TODO: Listen on secondary addresses. + options.host = listen[0].address; + options.port = listen[0].port; return serveFunctions .start(options, { diff --git a/src/serve/functions.ts b/src/serve/functions.ts index ba84234cfed4..6424eed384ce 100644 --- a/src/serve/functions.ts +++ b/src/serve/functions.ts @@ -4,24 +4,23 @@ import { FunctionsEmulator, FunctionsEmulatorArgs, } from "../emulator/functionsEmulator"; -import { EmulatorServer } from "../emulator/emulatorServer"; import { parseRuntimeVersion } from "../emulator/functionsEmulatorUtils"; import { needProjectId } from "../projectUtils"; import { getProjectDefaultAccount } from "../auth"; import { Options } from "../options"; import * as projectConfig from "../functions/projectConfig"; import * as utils from "../utils"; +import { EmulatorRegistry } from "../emulator/registry"; -// TODO(samstern): It would be better to convert this to an EmulatorServer -// but we don't have the "options" object until start() is called. export class FunctionsServer { - emulatorServer?: EmulatorServer; + emulator?: FunctionsEmulator; backends?: EmulatableBackend[]; - private assertServer() { - if (!this.emulatorServer || !this.backends) { + private assertServer(): FunctionsEmulator { + if (!this.emulator || !this.backends) { throw new Error("Must call start() before calling any other operation!"); } + return this.emulator; } async start(options: Options, partialArgs: Partial): Promise { @@ -75,22 +74,19 @@ export class FunctionsServer { } } - this.emulatorServer = new EmulatorServer(new FunctionsEmulator(args)); - await this.emulatorServer.start(); + this.emulator = new FunctionsEmulator(args); + return EmulatorRegistry.start(this.emulator); } async connect(): Promise { - this.assertServer(); - await this.emulatorServer!.connect(); + await this.assertServer().connect(); } async stop(): Promise { - this.assertServer(); - await this.emulatorServer!.stop(); + await this.assertServer().stop(); } get(): FunctionsEmulator { - this.assertServer(); - return this.emulatorServer!.get() as FunctionsEmulator; + return this.assertServer(); } } diff --git a/src/serve/index.ts b/src/serve/index.ts index 1e3226d2af3d..bfdb763332be 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -1,4 +1,3 @@ -import { EmulatorServer } from "../emulator/emulatorServer"; import { logger } from "../logger"; import { prepareFrameworks } from "../frameworks"; import * as experiments from "../experiments"; @@ -9,9 +8,7 @@ import { Constants } from "../emulator/constants"; const { FunctionsServer } = require("./functions"); const TARGETS: { - [key: string]: - | EmulatorServer - | { start: (o: any) => void; stop: (o: any) => void; connect: () => void }; + [key: string]: { start: (o: any) => void; stop: (o: any) => void; connect: () => void }; } = { hosting: require("./hosting"), functions: new FunctionsServer(), diff --git a/src/test/emulators/auth/cloudFunctions.spec.ts b/src/test/emulators/auth/cloudFunctions.spec.ts index 23c8fd523712..2ba644ac5f0e 100644 --- a/src/test/emulators/auth/cloudFunctions.spec.ts +++ b/src/test/emulators/auth/cloudFunctions.spec.ts @@ -2,19 +2,14 @@ import { expect } from "chai"; import * as nock from "nock"; import { AuthCloudFunction } from "../../../emulator/auth/cloudFunctions"; -import { findAvailablePort } from "../../../emulator/portUtils"; import { EmulatorRegistry } from "../../../emulator/registry"; import { Emulators } from "../../../emulator/types"; import { FakeEmulator } from "../fakeEmulator"; describe("cloudFunctions", () => { describe("dispatch", () => { - const host = "localhost"; - let port = 4000; before(async () => { - port = await findAvailablePort(host, port); - - const emu = new FakeEmulator(Emulators.FUNCTIONS, host, port); + const emu = await FakeEmulator.create(Emulators.FUNCTIONS); await EmulatorRegistry.start(emu); nock(EmulatorRegistry.url(Emulators.FUNCTIONS).toString()) .post("/functions/projects/project-foo/trigger_multicast", { diff --git a/src/test/emulators/controller.spec.ts b/src/test/emulators/controller.spec.ts index 835b4400a5ae..b7b173757750 100644 --- a/src/test/emulators/controller.spec.ts +++ b/src/test/emulators/controller.spec.ts @@ -13,9 +13,10 @@ describe("EmulatorController", () => { expect(EmulatorRegistry.isRunning(name)).to.be.false; - await EmulatorRegistry.start(new FakeEmulator(name, "localhost", 7777)); + const fake = await FakeEmulator.create(name); + await EmulatorRegistry.start(fake); expect(EmulatorRegistry.isRunning(name)).to.be.true; - expect(EmulatorRegistry.getInfo(name)!.port).to.eql(7777); + expect(EmulatorRegistry.getInfo(name)!.port).to.eql(fake.getInfo().port); }); }); diff --git a/src/test/emulators/dns.spec.ts b/src/test/emulators/dns.spec.ts new file mode 100644 index 000000000000..110ebad73d50 --- /dev/null +++ b/src/test/emulators/dns.spec.ts @@ -0,0 +1,115 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { IPV4_LOOPBACK, IPV6_LOOPBACK, Resolver } from "../../emulator/dns"; + +const IPV4_ADDR1 = { address: "169.254.20.1", family: 4 }; +const IPV4_ADDR2 = { address: "169.254.20.2", family: 4 }; +const IPV6_ADDR1 = { address: "fe80::1", family: 6 }; +const IPV6_ADDR2 = { address: "fe80::2", family: 6 }; + +describe("Resolver", () => { + describe("#lookupFirst", () => { + it("should return the first value of result", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example.test")).to.eventually.eql(IPV4_ADDR1); + }); + + it("should prefer IPv4 addresss using the underlying lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example.test")).to.eventually.eql(IPV4_ADDR1); + expect(lookup).to.be.calledOnceWithExactly("example.test", sinon.match({ verbatim: false })); + }); + + it("should return cached result if available", async () => { + const lookup = sinon.fake((hostname: string) => { + return hostname === "example1.test" ? [IPV4_ADDR1, IPV6_ADDR1] : [IPV4_ADDR2, IPV6_ADDR2]; + }); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example1.test")).to.eventually.eql(IPV4_ADDR1); + await expect(resolver.lookupFirst("example1.test")).to.eventually.eql(IPV4_ADDR1); + expect(lookup).to.be.calledOnce; // the second call should not trigger lookup + + lookup.resetHistory(); + // A call with a different name should cause a cache miss. + await expect(resolver.lookupFirst("example2.test")).to.eventually.eql(IPV4_ADDR2); + expect(lookup).to.be.calledOnce; + }); + + it("should pre-populate localhost in cache to resolve to IPv4 loopback address", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("localhost")).to.eventually.eql(IPV4_LOOPBACK); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv4 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("127.0.0.1")).to.eventually.eql(IPV4_LOOPBACK); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv6 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("::1")).to.eventually.eql(IPV6_LOOPBACK); + expect(lookup).not.to.be.called; + }); + }); + + describe("#lookupAll", () => { + it("should return all addresses returned", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example.test")).to.eventually.eql([IPV4_ADDR1, IPV4_ADDR2]); + }); + + it("should request IPv4 addresses to be listed first using the underlying lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example.test")).to.eventually.eql([IPV4_ADDR1, IPV4_ADDR2]); + expect(lookup).to.be.calledOnceWithExactly("example.test", sinon.match({ verbatim: false })); + }); + + it("should return cached results if available", async () => { + const lookup = sinon.fake((hostname: string) => { + return hostname === "example1.test" ? [IPV4_ADDR1, IPV6_ADDR1] : [IPV4_ADDR2, IPV6_ADDR2]; + }); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example1.test")).to.eventually.eql([IPV4_ADDR1, IPV6_ADDR1]); + await expect(resolver.lookupAll("example1.test")).to.eventually.eql([IPV4_ADDR1, IPV6_ADDR1]); + expect(lookup).to.be.calledOnce; // the second call should not trigger lookup + + lookup.resetHistory(); + // A call with a different name should cause a cache miss. + await expect(resolver.lookupAll("example2.test")).to.eventually.eql([IPV4_ADDR2, IPV6_ADDR2]); + expect(lookup).to.be.calledOnce; + }); + + it("should pre-populate localhost in cache to resolve to IPv4 + IPv6 loopback addresses (in that order)", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("localhost")).to.eventually.eql([ + IPV4_LOOPBACK, + IPV6_LOOPBACK, + ]); + expect(lookup).not.to.be.called; + }); + }); + + it("should parse and return IPv4 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("127.0.0.1")).to.eventually.eql([IPV4_LOOPBACK]); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv6 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("::1")).to.eventually.eql([IPV6_LOOPBACK]); + expect(lookup).not.to.be.called; + }); +}); diff --git a/src/test/emulators/emulatorServer.spec.ts b/src/test/emulators/emulatorServer.spec.ts deleted file mode 100644 index 8531821fd301..000000000000 --- a/src/test/emulators/emulatorServer.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Emulators } from "../../emulator/types"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { expect } from "chai"; -import { FakeEmulator } from "./fakeEmulator"; -import { EmulatorServer } from "../../emulator/emulatorServer"; -import { findAvailablePort } from "../../emulator/portUtils"; - -describe("EmulatorServer", () => { - it("should correctly start and stop an emulator", async () => { - const name = Emulators.FUNCTIONS; - const port = await findAvailablePort("localhost", 5000); - const emulator = new FakeEmulator(name, "localhost", port); - const server = new EmulatorServer(emulator); - - await server.start(); - - expect(EmulatorRegistry.isRunning(name)).to.be.true; - expect(EmulatorRegistry.get(name)).to.eql(emulator); - - await server.stop(); - - expect(EmulatorRegistry.isRunning(name)).to.be.false; - }); -}); diff --git a/src/test/emulators/extensions/postinstall.spec.ts b/src/test/emulators/extensions/postinstall.spec.ts index 900e29d164f1..ccaf31d502a4 100644 --- a/src/test/emulators/extensions/postinstall.spec.ts +++ b/src/test/emulators/extensions/postinstall.spec.ts @@ -1,17 +1,16 @@ import { expect } from "chai"; import * as postinstall from "../../../emulator/extensions/postinstall"; -import { findAvailablePort } from "../../../emulator/portUtils"; import { EmulatorRegistry } from "../../../emulator/registry"; import { Emulators } from "../../../emulator/types"; import { FakeEmulator } from "../fakeEmulator"; describe("replaceConsoleLinks", () => { - const host = "localhost"; - let port = 4000; + let host: string; + let port: number; before(async () => { - port = await findAvailablePort(host, port); - - const emu = new FakeEmulator(Emulators.UI, host, port); + const emu = await FakeEmulator.create(Emulators.UI); + host = emu.getInfo().host; + port = emu.getInfo().port; return EmulatorRegistry.start(emu); }); diff --git a/src/test/emulators/fakeEmulator.ts b/src/test/emulators/fakeEmulator.ts index 5321d375a571..c28cc5fb23aa 100644 --- a/src/test/emulators/fakeEmulator.ts +++ b/src/test/emulators/fakeEmulator.ts @@ -1,37 +1,25 @@ -import { EmulatorInfo, EmulatorInstance, Emulators } from "../../emulator/types"; -import * as express from "express"; -import { createDestroyer } from "../../utils"; +import { Emulators, ListenSpec } from "../../emulator/types"; +import { ExpressBasedEmulator } from "../../emulator/ExpressBasedEmulator"; +import { resolveHostAndAssignPorts } from "../../emulator/portUtils"; /** * A thing that acts like an emulator by just occupying a port. */ -export class FakeEmulator implements EmulatorInstance { - private exp: express.Express; - private destroyServer?: () => Promise; - - constructor(public name: Emulators, public host: string, public port: number) { - this.exp = express(); - } - - start(): Promise { - const server = this.exp.listen(this.port); - this.destroyServer = createDestroyer(server); - return Promise.resolve(); - } - connect(): Promise { - return Promise.resolve(); - } - stop(): Promise { - return this.destroyServer ? this.destroyServer() : Promise.resolve(); - } - getInfo(): EmulatorInfo { - return { - name: this.getName(), - host: this.host, - port: this.port, - }; +export class FakeEmulator extends ExpressBasedEmulator { + constructor(public name: Emulators, listen: ListenSpec[]) { + super({ listen, noBodyParser: true, noCors: true }); } getName(): Emulators { return this.name; } + + static async create(name: Emulators, host = "127.0.0.1"): Promise { + const listen = await resolveHostAndAssignPorts({ + [name]: { + host, + port: 4000, + }, + }); + return new FakeEmulator(name, listen[name]); + } } diff --git a/src/test/emulators/registry.spec.ts b/src/test/emulators/registry.spec.ts index 90aa4a7acefb..8ff98a888d71 100644 --- a/src/test/emulators/registry.spec.ts +++ b/src/test/emulators/registry.spec.ts @@ -2,7 +2,6 @@ import { ALL_EMULATORS, Emulators } from "../../emulator/types"; import { EmulatorRegistry } from "../../emulator/registry"; import { expect } from "chai"; import { FakeEmulator } from "./fakeEmulator"; -import { findAvailablePort } from "../../emulator/portUtils"; import * as express from "express"; import * as os from "os"; @@ -21,8 +20,7 @@ describe("EmulatorRegistry", () => { it("should correctly return information about a running emulator", async () => { const name = Emulators.FUNCTIONS; - const port = await findAvailablePort("localhost", 5000); - const emu = new FakeEmulator(name, "localhost", port); + const emu = await FakeEmulator.create(name); expect(EmulatorRegistry.isRunning(name)).to.be.false; @@ -31,13 +29,12 @@ describe("EmulatorRegistry", () => { expect(EmulatorRegistry.isRunning(name)).to.be.true; expect(EmulatorRegistry.listRunning()).to.eql([name]); expect(EmulatorRegistry.get(name)).to.eql(emu); - expect(EmulatorRegistry.getInfo(name)!.port).to.eql(port); + expect(EmulatorRegistry.getInfo(name)!.port).to.eql(emu.getInfo().port); }); it("once stopped, an emulator is no longer running", async () => { const name = Emulators.FUNCTIONS; - const port = await findAvailablePort("localhost", 5000); - const emu = new FakeEmulator(name, "localhost", port); + const emu = await FakeEmulator.create(name); expect(EmulatorRegistry.isRunning(name)).to.be.false; await EmulatorRegistry.start(emu); @@ -74,20 +71,20 @@ describe("EmulatorRegistry", () => { }); it("should craft URL from host and port in registry", async () => { - const port = await findAvailablePort("localhost", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "localhost", port)); + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); - expect(EmulatorRegistry.url(name).host).to.eql(`localhost:${port}`); + expect(EmulatorRegistry.url(name).host).to.eql(`${emu.getInfo().host}:${emu.getInfo().port}`); }); it("should quote IPv6 addresses", async function (this) { if (!ipv6Supported) { return this.skip(); } - const port = await findAvailablePort("::1", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "::1", port)); + const emu = await FakeEmulator.create(name, "::1"); + await EmulatorRegistry.start(emu); - expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${port}`); + expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${emu.getInfo().port}`); }); it("should use 127.0.0.1 instead of 0.0.0.0", async function (this) { @@ -95,10 +92,10 @@ describe("EmulatorRegistry", () => { return this.skip(); } - const port = await findAvailablePort("0.0.0.0", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "0.0.0.0", port)); + const emu = await FakeEmulator.create(name, "0.0.0.0"); + await EmulatorRegistry.start(emu); - expect(EmulatorRegistry.url(name).host).to.eql(`127.0.0.1:${port}`); + expect(EmulatorRegistry.url(name).host).to.eql(`127.0.0.1:${emu.getInfo().port}`); }); it("should use ::1 instead of ::", async function (this) { @@ -106,30 +103,33 @@ describe("EmulatorRegistry", () => { return this.skip(); } - const port = await findAvailablePort("::", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "::", port)); + const emu = await FakeEmulator.create(name, "::"); + await EmulatorRegistry.start(emu); - expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${port}`); + expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${emu.getInfo().port}`); }); it("should use protocol from request if available", async () => { - const port = await findAvailablePort("localhost", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "localhost", port)); + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); const req = { protocol: "https", headers: {} } as express.Request; expect(EmulatorRegistry.url(name, req).protocol).to.eql(`https:`); - expect(EmulatorRegistry.url(name, req).host).to.eql(`localhost:${port}`); + expect(EmulatorRegistry.url(name, req).host).to.eql( + `${emu.getInfo().host}:${emu.getInfo().port}` + ); }); it("should use host from request if available", async () => { - const port = await findAvailablePort("localhost", 5000); - await EmulatorRegistry.start(new FakeEmulator(name, "localhost", port)); + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); + const hostFromHeader = "mydomain.example.test:9999"; const req = { protocol: "http", - headers: { host: "mydomain.example.test:9999" }, + headers: { host: hostFromHeader }, } as express.Request; - expect(EmulatorRegistry.url(name, req).host).to.eql(`${req.headers.host}`); + expect(EmulatorRegistry.url(name, req).host).to.eql(hostFromHeader); }); }); }); diff --git a/src/test/hosting/functionsProxy.spec.ts b/src/test/hosting/functionsProxy.spec.ts index f86efb10e53d..3e82250d8537 100644 --- a/src/test/hosting/functionsProxy.spec.ts +++ b/src/test/hosting/functionsProxy.spec.ts @@ -25,7 +25,9 @@ describe("functionsProxy", () => { } as HostingRewrites; beforeEach(async () => { - const fakeFunctionsEmulator = new FakeEmulator(Emulators.FUNCTIONS, "localhost", 7778); + const fakeFunctionsEmulator = new FakeEmulator(Emulators.FUNCTIONS, [ + { address: "127.0.0.1", family: "IPv4", port: 7778 }, + ]); await EmulatorRegistry.start(fakeFunctionsEmulator); }); @@ -69,7 +71,7 @@ describe("functionsProxy", () => { }); it("should resolve a function that returns middleware that proxies to a local version", async () => { - nock("http://localhost:7778").get("/project-foo/us-central1/bar/").reply(200, "local version"); + nock("http://127.0.0.1:7778").get("/project-foo/us-central1/bar/").reply(200, "local version"); const options = cloneDeep(fakeOptions); options.targets = ["functions"]; @@ -87,7 +89,7 @@ describe("functionsProxy", () => { }); it("should resolve a function that returns middleware that proxies to a local version in another region", async () => { - nock("http://localhost:7778").get("/project-foo/europe-west3/bar/").reply(200, "local version"); + nock("http://127.0.0.1:7778").get("/project-foo/europe-west3/bar/").reply(200, "local version"); const options = cloneDeep(fakeOptions); options.targets = ["functions"]; @@ -105,7 +107,7 @@ describe("functionsProxy", () => { }); it("should maintain the location header as returned by the function", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "", { location: "/over-here" }); @@ -126,7 +128,7 @@ describe("functionsProxy", () => { }); it("should allow location headers that wouldn't redirect to itself", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "", { location: "https://example.com/foo" }); @@ -147,7 +149,7 @@ describe("functionsProxy", () => { }); it("should proxy a request body on a POST request", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .post("/project-foo/us-central1/bar/", "data") .reply(200, "you got post data"); @@ -168,7 +170,7 @@ describe("functionsProxy", () => { }); it("should proxy with a query string", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .query({ key: "value" }) .reply(200, "query!"); @@ -190,7 +192,7 @@ describe("functionsProxy", () => { }); it("should return 3xx responses directly", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "redirected", { Location: "https://example.com" }); @@ -210,7 +212,7 @@ describe("functionsProxy", () => { }); it("should pass through multiple set-cookie headers", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(200, "crisp", { "Set-Cookie": ["foo=bar", "bar=zap"], From 70180c9163a7d319a607eb41bad20b5fc53faaa1 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Fri, 7 Oct 2022 15:00:13 -0500 Subject: [PATCH 027/115] feat(experiments) Add support for FIREBASE_CLI_EXPERIMENTS (#5069) ### Description Enables the ability to set experiments via env variables. The intention behind this is to enable features during Github CI/CD hooks. ### Scenarios Tested * Enabling an experiment via env var * Attempting to enable a non-experiment via Env variable (it doesn't work) ### Sample Commands Locally ```bash FIREBASE_CLI_EXPERIMENTS=experiment1,experiment2 firebase deploy ``` Github Action ```yml - uses: FirebaseExtended/action-hosting-deploy@v0 with: # ... env: FIREBASE_CLI_EXPERIMENTS: webframeworks,pintag ``` --- src/bin/firebase.ts | 7 ++++--- src/experiments.ts | 18 ++++++++++++++++++ src/handlePreviewToggles.ts | 4 ++-- src/test/experiments.spec.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/test/experiments.spec.ts diff --git a/src/bin/firebase.ts b/src/bin/firebase.ts index 352051031673..f46fbe7c731a 100755 --- a/src/bin/firebase.ts +++ b/src/bin/firebase.ts @@ -96,7 +96,10 @@ if (utils.envOverrides.length) { logger.debug("-".repeat(70)); logger.debug(); +import { enableExperimentsFromCliEnvVariable } from "../experiments"; import { fetchMOTD } from "../fetchMOTD"; + +enableExperimentsFromCliEnvVariable(); fetchMOTD(); process.on("exit", (code) => { @@ -160,9 +163,7 @@ if (!handlePreviewToggles(args)) { cmd = client.cli.parse(process.argv); // determine if there are any non-option arguments. if not, display help - args = args.filter((arg) => { - return arg.indexOf("-") < 0; - }); + args = args.filter((arg) => !arg.includes("-")); if (!args.length) { client.cli.help(); } diff --git a/src/experiments.ts b/src/experiments.ts index 13ac85e3d865..f515afe761b1 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -164,6 +164,24 @@ export function setEnabled(name: ExperimentName, to: boolean | null): void { } } +/** + * Enables multiple experiments given a comma-delimited environment variable: + * `FIREBASE_CLI_EXPERIMENTS`. + * + * Example: + * FIREBASE_CLI_PREVIEWS=experiment1,experiment2,turtle + * + * Would silently enable `experiment1` and `experiment2`, but would not enable `turtle`. + */ +export function enableExperimentsFromCliEnvVariable(): void { + const experiments = process.env.FIREBASE_CLI_EXPERIMENTS || ""; + for (const experiment of experiments.split(",")) { + if (isValidExperiment(experiment)) { + setEnabled(experiment, true); + } + } +} + /** * Assert that an experiment is enabled before following a code path. * This code is unnecessary in code paths guarded by ifEnabled. When diff --git a/src/handlePreviewToggles.ts b/src/handlePreviewToggles.ts index 7ae200c7b6c2..7a73b2feb236 100644 --- a/src/handlePreviewToggles.ts +++ b/src/handlePreviewToggles.ts @@ -18,7 +18,7 @@ export function handlePreviewToggles(args: string[]): boolean { if (args[0] === "--open-sesame") { console.log( `${bold("firebase --open-sesame")} is deprecated and wil be removed in a future ` + - `version. Use the new "expirments" family of commands, including ${bold( + `version. Use the new "experiments" family of commands, including ${bold( "firebase experiments:enable" )}` ); @@ -34,7 +34,7 @@ export function handlePreviewToggles(args: string[]): boolean { } else if (args[0] === "--close-sesame") { console.log( `${bold("firebase --open-sesame")} is deprecated and wil be removed in a future ` + - `version. Use the new "expirments" family of commands, including ${bold( + `version. Use the new "experiments" family of commands, including ${bold( "firebase experiments:disable" )}` ); diff --git a/src/test/experiments.spec.ts b/src/test/experiments.spec.ts new file mode 100644 index 000000000000..aff1cef34d88 --- /dev/null +++ b/src/test/experiments.spec.ts @@ -0,0 +1,30 @@ +import { expect } from "chai"; +import { enableExperimentsFromCliEnvVariable, isEnabled, setEnabled } from "../experiments"; + +describe("experiments", () => { + let originalCLIState = process.env.FIREBASE_CLI_EXPERIMENTS; + + before(() => { + originalCLIState = process.env.FIREBASE_CLI_EXPERIMENTS; + }); + + beforeEach(() => { + process.env.FIREBASE_CLI_EXPERIMENTS = originalCLIState; + }); + + afterEach(() => { + process.env.FIREBASE_CLI_EXPERIMENTS = originalCLIState; + }); + + describe("enableExperimentsFromCliEnvVariable", () => { + it("should enable some experiments", () => { + expect(isEnabled("experiments")).to.be.false; + process.env.FIREBASE_CLI_EXPERIMENTS = "experiments,not_an_experiment"; + + enableExperimentsFromCliEnvVariable(); + + expect(isEnabled("experiments")).to.be.true; + setEnabled("experiments", false); + }); + }); +}); From 8f18fba3c806afbe9db686ad85c51395ba5f2c65 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Fri, 7 Oct 2022 15:29:46 -0700 Subject: [PATCH 028/115] Support UI listening on mutliple addresses. (#5088) * Support UI listening on mutliple addresses. * Release UI v1.11.0. --- CHANGELOG.md | 2 ++ src/commands/emulators-start.ts | 1 + src/emulator/ExpressBasedEmulator.ts | 28 ++++++++++++++++----------- src/emulator/controller.ts | 9 ++++++--- src/emulator/downloadableEmulators.ts | 14 +++++++------- src/emulator/hub.ts | 7 ++++++- src/emulator/portUtils.ts | 8 ++++---- src/emulator/ui.ts | 15 +++++++------- 8 files changed, 50 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25abc0a9c01b..3340085786d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,4 @@ - Enable single project mode for the database emulator (#5068). - Ravamp emulator networking to assign ports early and explictly listen on IP addresses (#5083). +- Emulator UI and hub now listen on both IPv4 and IPv6 address by default (if possible) (#5088). +- Fix Firestore emulator excessive logs about discovery endpoint not found (#5088). diff --git a/src/commands/emulators-start.ts b/src/commands/emulators-start.ts index b2ac228325b1..65a33784ca56 100644 --- a/src/commands/emulators-start.ts +++ b/src/commands/emulators-start.ts @@ -101,6 +101,7 @@ function printEmulatorOverview(options: any): void { if (uiRunning) { row.push(""); } + return row; } let uiLink = "n/a"; if (isSupportedByUi && uiRunning) { diff --git a/src/emulator/ExpressBasedEmulator.ts b/src/emulator/ExpressBasedEmulator.ts index 1b09085f6450..d380fe1640f8 100644 --- a/src/emulator/ExpressBasedEmulator.ts +++ b/src/emulator/ExpressBasedEmulator.ts @@ -62,6 +62,22 @@ export abstract class ExpressBasedEmulator implements EmulatorInstance { const promises = []; const specs = this.options.listen; + for (const opt of ExpressBasedEmulator.listenOptionsFromSpecs(specs)) { + promises.push( + new Promise((resolve, reject) => { + const server = createServer(app).listen(opt); + server.once("listening", resolve); + server.once("error", reject); + this.destroyers.add(utils.createDestroyer(server)); + }) + ); + } + } + + /** + * Translate addresses and ports to low-level net/http server options. + */ + static listenOptionsFromSpecs(specs: ListenSpec[]): ListenOptions[] { const listenOptions: ListenOptions[] = []; const dualStackPorts = new Set(); @@ -89,17 +105,7 @@ export abstract class ExpressBasedEmulator implements EmulatorInstance { }); } } - - for (const opt of listenOptions) { - promises.push( - new Promise((resolve, reject) => { - const server = createServer(app).listen(opt); - server.once("listening", resolve); - server.once("error", reject); - this.destroyers.add(utils.createDestroyer(server)); - }) - ); - } + return listenOptions; } async connect(): Promise { diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 95aa457c2c93..557c571ca0da 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -444,7 +444,11 @@ export async function startAll( } if (listenForEmulator.hub) { - const hub = new EmulatorHub({ projectId, listen: listenForEmulator[Emulators.HUB] }); + const hub = new EmulatorHub({ + projectId, + listen: listenForEmulator[Emulators.HUB], + listenForEmulator, + }); // Log the command for analytics, we only report this for "hub" // since we originally mistakenly reported emulators:start events @@ -820,11 +824,10 @@ export async function startAll( } if (listenForEmulator.ui) { - const uiAddr = legacyGetFirstAddr(Emulators.UI); const ui = new EmulatorUI({ projectId: projectId, auto_download: true, - ...uiAddr, + listen: listenForEmulator[Emulators.UI], }); await startEmulator(ui); } diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index f8e385334e5f..b480779f5cc5 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -80,15 +80,15 @@ export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDe }, } : { - version: "1.10.0", - downloadPath: path.join(CACHE_DIR, "ui-v1.10.0.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.10.0"), - binaryPath: path.join(CACHE_DIR, "ui-v1.10.0", "server", "server.js"), + version: "1.11.0", + downloadPath: path.join(CACHE_DIR, "ui-v1.11.0.zip"), + unzipDir: path.join(CACHE_DIR, "ui-v1.11.0"), + binaryPath: path.join(CACHE_DIR, "ui-v1.11.0", "server", "server.js"), opts: { cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.10.0.zip", - expectedSize: 3062540, - expectedChecksum: "7dec1e82acccc196efc4d364e2664288", + remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.11.0.zip", + expectedSize: 3061915, + expectedChecksum: "94679756dc270754e9a4dc9d1c6fc4e1", namePrefix: "ui", }, }, diff --git a/src/emulator/hub.ts b/src/emulator/hub.ts index 1c2a6d062dee..2f4e3e68db2c 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -10,6 +10,7 @@ import { HubExport } from "./hubExport"; import { EmulatorRegistry } from "./registry"; import { FunctionsEmulator } from "./functionsEmulator"; import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; +import { PortName } from "./portUtils"; // We use the CLI version from package.json const pkg = require("../../package.json"); @@ -23,6 +24,7 @@ export interface Locator { export interface EmulatorHubArgs { projectId: string; listen: ListenSpec[]; + listenForEmulator: Record; } export type GetEmulatorsResponse = Record; @@ -87,7 +89,10 @@ export class EmulatorHub extends ExpressBasedEmulator { app.get(EmulatorHub.PATH_EMULATORS, (req, res) => { const body: GetEmulatorsResponse = {}; for (const info of EmulatorRegistry.listRunningWithInfo()) { - body[info.name] = info; + body[info.name] = { + listen: this.args.listenForEmulator[info.name], + ...info, + }; } res.json(body); }); diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index 82dde2d7830c..69365697ea1d 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -174,11 +174,11 @@ const EMULATOR_CAN_LISTEN_ON_PRIMARY_ONLY: Record = { // Listening on multiple addresses to maximize the chance of discovery. hub: false, - // TODO: Modify the following emulators to listen on multiple addresses. + // Separate Node.js process that supports multi-listen. For consistency, we + // resolve the addresses in the CLI and pass the result to the UI. + ui: false, - // Separate Node.js process that requires a separate update. - // For consistency, we can resolve in the CLI and pass in the results. - ui: true, + // TODO: Modify the following emulators to listen on multiple addresses. // Express-based servers, can be reused for multiple listen sockets. auth: true, diff --git a/src/emulator/ui.ts b/src/emulator/ui.ts index 38f275594b12..2796f225a006 100644 --- a/src/emulator/ui.ts +++ b/src/emulator/ui.ts @@ -1,13 +1,13 @@ -import { EmulatorInstance, EmulatorInfo, Emulators } from "./types"; +import { EmulatorInstance, EmulatorInfo, Emulators, ListenSpec } from "./types"; import * as downloadableEmulators from "./downloadableEmulators"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; import { Constants } from "./constants"; import { emulatorSession } from "../track"; +import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; export interface EmulatorUIOptions { - port: number; - host: string; + listen: ListenSpec[]; projectId: string; auto_download?: boolean; } @@ -23,10 +23,9 @@ export class EmulatorUI implements EmulatorInstance { )}!` ); } - const { auto_download: autoDownload, host, port, projectId } = this.args; + const { auto_download: autoDownload, projectId } = this.args; const env: Partial = { - HOST: host.toString(), - PORT: port.toString(), + LISTEN: JSON.stringify(ExpressBasedEmulator.listenOptionsFromSpecs(this.args.listen)), GCLOUD_PROJECT: projectId, [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.url(Emulators.HUB).host, }; @@ -50,8 +49,8 @@ export class EmulatorUI implements EmulatorInstance { getInfo(): EmulatorInfo { return { name: this.getName(), - host: this.args.host, - port: this.args.port, + host: this.args.listen[0].address, + port: this.args.listen[0].port, pid: downloadableEmulators.getPID(Emulators.UI), }; } From 73c4c9649ea1e8a2c8bb9e5df56ee7209cd3d1c8 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Fri, 7 Oct 2022 17:42:52 -0700 Subject: [PATCH 029/115] Small fixes for run tag pinning (#5086) Small fixes for run tag pinning --- src/deploy/functions/release/fabricator.ts | 2 ++ src/deploy/hosting/convertConfig.ts | 5 +++-- src/gcp/run.ts | 5 ++--- src/hosting/runTags.ts | 7 +++---- src/test/gcp/run.spec.ts | 2 +- src/test/hosting/runTags.spec.ts | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index b4d7155cc2f7..4316cb0e37a4 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -528,6 +528,8 @@ export class Fabricator { return; } + // Without this there will be a conflict creating the new spec from the tempalte + delete service.spec.template.metadata.name; await run.updateService(serviceName, service); }) .catch(rethrowAs(endpoint, "set concurrency")); diff --git a/src/deploy/hosting/convertConfig.ts b/src/deploy/hosting/convertConfig.ts index 9a35d6712d6d..929a8b2f6e3b 100644 --- a/src/deploy/hosting/convertConfig.ts +++ b/src/deploy/hosting/convertConfig.ts @@ -4,7 +4,7 @@ import { HostingDeploy } from "./context"; import * as api from "../../hosting/api"; import * as backend from "../functions/backend"; import { Context } from "../functions/args"; -import { logLabeledBullet, logLabeledWarning } from "../../utils"; +import { last, logLabeledBullet, logLabeledWarning } from "../../utils"; import * as proto from "../../gcp/proto"; import { bold } from "colorette"; import * as runTags from "../../hosting/runTags"; @@ -215,7 +215,8 @@ export async function convertConfig( }); if (config.rewrites) { - await runTags.setRewriteTags(config.rewrites, context.projectId, deploy.version); + const versionId = last(deploy.version.split("/")); + await runTags.setRewriteTags(config.rewrites, context.projectId, versionId); } config.redirects = deploy.config.redirects?.map((redirect) => { diff --git a/src/gcp/run.ts b/src/gcp/run.ts index acd7c5e72605..c54692998c8a 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -68,7 +68,7 @@ export interface ServiceSpec { export interface ServiceStatus { observedGeneration: number; conditions: Condition[]; - latestRevisionName: string; + latestReadyRevisionName: string; latestCreatedRevisionName: string; traffic: TrafficTarget[]; url: string; @@ -101,7 +101,7 @@ export interface RevisionSpec { } export interface RevisionTemplate { - metadata: ObjectMetadata; + metadata: Partial; spec: RevisionSpec; } @@ -162,7 +162,6 @@ export async function getService(name: string): Promise { */ export async function updateService(name: string, service: Service): Promise { delete service.status; - delete (service.spec.template.metadata as any).name; service = await exports.replaceService(name, service); // Now we need to wait for reconciliation or we might delete the docker diff --git a/src/hosting/runTags.ts b/src/hosting/runTags.ts index f2e0bf84f33e..347022a734bc 100644 --- a/src/hosting/runTags.ts +++ b/src/hosting/runTags.ts @@ -148,13 +148,12 @@ export async function ensureLatestRevisionTagged( for (const service of services) { const { projectNumber, region, serviceId } = run.gcpIds(service); tags[region] = tags[region] || {}; - const latestRevisionTarget = service.status?.traffic.find((target) => target.latestRevision); - if (!latestRevisionTarget) { + const latestRevision = service.status?.latestReadyRevisionName; + if (!latestRevision) { throw new FirebaseError( - `Assertion failed: service ${service.metadata.name} has no latestRevision traffic target` + `Assertion failed: service ${service.metadata.name} has no ready revision` ); } - const latestRevision = latestRevisionTarget.revisionName; const alreadyTagged = service.spec.traffic.find( (target) => target.revisionName === latestRevision && target.tag ); diff --git a/src/test/gcp/run.spec.ts b/src/test/gcp/run.spec.ts index 08091e8af66f..9544e45a5711 100644 --- a/src/test/gcp/run.spec.ts +++ b/src/test/gcp/run.spec.ts @@ -409,7 +409,7 @@ describe("run", () => { }, ], latestCreatedRevisionName: "", - latestRevisionName: "", + latestReadyRevisionName: "", traffic: [], url: "", address: { diff --git a/src/test/hosting/runTags.spec.ts b/src/test/hosting/runTags.spec.ts index 647dd73f907a..0479e84e8a83 100644 --- a/src/test/hosting/runTags.spec.ts +++ b/src/test/hosting/runTags.spec.ts @@ -84,7 +84,7 @@ describe("runTags", () => { status: { observedGeneration: 50, latestCreatedRevisionName: "latest", - latestRevisionName: "latest", + latestReadyRevisionName: "latest", traffic: [ { revisionName: "latest", From a5a2837be11365c5c5fc88366ee2d0c3ab68cfc8 Mon Sep 17 00:00:00 2001 From: Victor Fan Date: Mon, 10 Oct 2022 00:39:27 -0700 Subject: [PATCH 030/115] Writing params to .env.local in emulator mode should not error if param exists in .env.projectId (#4983) --- src/functions/env.ts | 78 +++++++++++++++++++++++----------- src/test/functions/env.spec.ts | 72 +++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/src/functions/env.ts b/src/functions/env.ts index dffa52b52b8f..2bd72806e2c8 100644 --- a/src/functions/env.ts +++ b/src/functions/env.ts @@ -4,7 +4,7 @@ import * as path from "path"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import { logBullet } from "../utils"; +import { logBullet, logWarning } from "../utils"; const FUNCTIONS_EMULATOR_DOTENV = ".env.local"; @@ -264,44 +264,72 @@ export function writeUserEnvs(toWrite: Record, envOpts: UserEnvs if (Object.keys(toWrite).length === 0) { return; } - const { functionsSource, projectId, projectAlias, isEmulator } = envOpts; - const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator); - const projectScopedFileName = `.env.${projectId}`; - const projectScopedFileExists = envFiles.includes(projectScopedFileName); - if (!projectScopedFileExists) { - createEnvFile(envOpts); + // Determine which .env file to write to, and create it if it doesn't exist + const allEnvFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator); + const targetEnvFile = envOpts.isEmulator + ? FUNCTIONS_EMULATOR_DOTENV + : `.env.${envOpts.projectId}`; + const targetEnvFileExists = allEnvFiles.includes(targetEnvFile); + if (!targetEnvFileExists) { + fs.writeFileSync(path.join(envOpts.functionsSource, targetEnvFile), "", { flag: "wx" }); + logBullet( + clc.yellow(clc.bold("functions: ")) + + `Created new local file ${targetEnvFile} to store param values. We suggest explicitly adding or excluding this file from version control.` + ); } - const currentEnvs = loadUserEnvs(envOpts); + // Throw if any of the keys are duplicate (note special case if emulator) or malformed + const fullEnvs = loadUserEnvs(envOpts); + const prodEnvs = isEmulator + ? loadUserEnvs({ ...envOpts, isEmulator: false }) + : loadUserEnvs(envOpts); + checkForDuplicateKeys(isEmulator || false, Object.keys(toWrite), fullEnvs, prodEnvs); for (const k of Object.keys(toWrite)) { validateKey(k); - if (currentEnvs.hasOwnProperty(k)) { - throw new FirebaseError( - `Attempted to write param-defined key ${k} to .env files, but it was already defined.` - ); - } } + // Write all the keys in a single filesystem access logBullet( - clc.cyan(clc.bold("functions: ")) + - `Writing new parameter values to disk: ${projectScopedFileName}` + clc.cyan(clc.bold("functions: ")) + `Writing new parameter values to disk: ${targetEnvFile}` ); + let lines = ""; for (const k of Object.keys(toWrite)) { - fs.appendFileSync( - path.join(functionsSource, projectScopedFileName), - formatUserEnvForWrite(k, toWrite[k]) - ); + lines += formatUserEnvForWrite(k, toWrite[k]); } + fs.appendFileSync(path.join(functionsSource, targetEnvFile), lines); } -function createEnvFile(envOpts: UserEnvsOpts): string { - const fileToWrite = envOpts.isEmulator ? FUNCTIONS_EMULATOR_DOTENV : `.env.${envOpts.projectId}`; - logger.debug(`Creating ${fileToWrite}...`); - - fs.writeFileSync(path.join(envOpts.functionsSource, fileToWrite), "", { flag: "wx" }); - return fileToWrite; +/** + * Errors if any of the provided keys are aleady defined in the .env fields. + * This seems like a simple presence check, but... + * + * For emulator deploys, it's legal to write a key to .env.local even if it's + * already defined in .env.projectId. This is a special case designed to follow + * the principle of least surprise for emulator users. + */ +export function checkForDuplicateKeys( + isEmulator: boolean, + keys: string[], + fullEnv: Record, + envsWithoutLocal?: Record +): void { + for (const key of keys) { + const definedInEnv = fullEnv.hasOwnProperty(key); + if (definedInEnv) { + if (envsWithoutLocal && isEmulator && envsWithoutLocal.hasOwnProperty(key)) { + logWarning( + clc.cyan(clc.yellow("functions: ")) + + `Writing parameter ${key} to emulator-specific config .env.local. This will overwrite your existing definition only when emulating.` + ); + continue; + } + throw new FirebaseError( + `Attempted to write param-defined key ${key} to .env files, but it was already defined.` + ); + } + } } function formatUserEnvForWrite(key: string, value: string): string { diff --git a/src/test/functions/env.spec.ts b/src/test/functions/env.spec.ts index c210a27beea8..632fce4fe1dd 100644 --- a/src/test/functions/env.spec.ts +++ b/src/test/functions/env.spec.ts @@ -307,12 +307,30 @@ FOO=foo {}, { projectId: "project", projectAlias: "alias", functionsSource: tmpdir } ); - expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).throw; + env.writeUserEnvs( + {}, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true } + ); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw; }); it("touches .env.projectId if it doesn't already exist", () => { env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir }); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; expect(!!fs.statSync(path.join(tmpdir, ".env.project"))).to.be.true; + expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw; + }); + + it("touches .env.local if it doesn't already exist in emulator mode", () => { + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true } + ); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw; + expect(!!fs.statSync(path.join(tmpdir, ".env.local"))).to.be.true; }); it("throws if asked to write a key that already exists in .env.projectId", () => { @@ -324,18 +342,66 @@ FOO=foo ).to.throw(FirebaseError); }); + it("is fine writing a key that already exists in .env.projectId but not .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env.project"]: "FOO=foo", + }); + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true } + ); + expect( + env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + isEmulator: true, + })["FOO"] + ).to.equal("bar"); + }); + it("throws if asked to write a key that already exists in any .env", () => { createEnvFiles(tmpdir, { - [".env.alias"]: "BAR=foo", + [".env"]: "FOO=bar", }); expect(() => env.writeUserEnvs( - { FOO: "bar" }, + { FOO: "baz" }, { projectId: "project", projectAlias: "alias", functionsSource: tmpdir } ) ).to.throw(FirebaseError); }); + it("is fine writing a key that already exists in any .env but not .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env"]: "FOO=bar", + }); + env.writeUserEnvs( + { FOO: "baz" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true } + ); + expect( + env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + isEmulator: true, + })["FOO"] + ).to.equal("baz"); + }); + + it("throws if asked to write a key that already exists in .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env.local"]: "ASDF=foo", + }); + expect(() => + env.writeUserEnvs( + { ASDF: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true } + ) + ).to.throw(FirebaseError); + }); + it("throws if asked to write a key that fails key format validation", () => { expect(() => env.writeUserEnvs( From 0e80b17784358998f5fb4bd0d5c22ee1775fda22 Mon Sep 17 00:00:00 2001 From: Victor Fan Date: Mon, 10 Oct 2022 09:02:20 -0700 Subject: [PATCH 031/115] re-enable prompting for missing cloud secrets (#5066) --- src/deploy/functions/build.ts | 1 - src/deploy/functions/params.ts | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 24ce606fac6d..40d884b3d76c 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -405,7 +405,6 @@ class Resolver { } /** Converts a build specification into a Backend representation, with all Params resolved and interpolated */ -// TODO(vsfan): handle Expression types export function toBackend( build: Build, paramValues: Record diff --git a/src/deploy/functions/params.ts b/src/deploy/functions/params.ts index 1a1fbabca75f..5ea90edb081e 100644 --- a/src/deploy/functions/params.ts +++ b/src/deploy/functions/params.ts @@ -286,7 +286,7 @@ function canSatisfyParam(param: Param, value: RawParamValue): boolean { /** * A param defined by the SDK may resolve to: - * - a reference to a secret in Cloud Secret Manager, which we only validate existence for and leave the fetch up to GCF + * - a reference to a secret in Cloud Secret Manager, which we validate the existence of and prompt for if missing * - a literal value of the same type already defined in one of the .env files with key == param name * - the value returned by interactively prompting the user * - it is an error to have params that need to be prompted if the CLI is running in non-interactive mode @@ -382,25 +382,17 @@ function populateDefaultParams(config: FirebaseConfig): Record = { "firebase-hosting-managed": "yes" }; await secretManager.createSecret(projectId, secretParam.name, secretLabel); await secretManager.addVersion(projectId, secretParam.name, secretValue); return secretValue; - */ } else if (!metadata.secretVersion) { throw new FirebaseError( `Cloud Secret Manager has no latest version of the secret defined by param ${ From 49f87e4a0c21fd53e1327413e1ebe79337bc3843 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 10 Oct 2022 09:58:55 -0700 Subject: [PATCH 032/115] Fix Functions emulator not starting for frameworks. (#5090) * Fix Functions emulator not starting for frameworks. * Fix eventarc not getting assigned. --- src/emulator/controller.ts | 143 ++++++++++++++++++++++--------------- src/emulator/portUtils.ts | 18 +++-- 2 files changed, 101 insertions(+), 60 deletions(-) diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 557c571ca0da..2411374e52f8 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -324,29 +324,9 @@ export async function startAll( } const emulatableBackends: EmulatableBackend[] = []; - const projectDir = (options.extDevDir || options.config.projectDir) as string; - if (shouldStart(options, Emulators.FUNCTIONS)) { - const functionsCfg = normalizeAndValidate(options.config.src.functions); - // Note: ext:dev:emulators:* commands hit this path, not the Emulators.EXTENSIONS path - utils.assertIsStringOrUndefined(options.extDevDir); - - for (const cfg of functionsCfg) { - const functionsDir = path.join(projectDir, cfg.source); - emulatableBackends.push({ - functionsDir, - codebase: cfg.codebase, - env: { - ...options.extDevEnv, - }, - secretEnv: [], // CF3 secrets are bound to specific functions, so we'll get them during trigger discovery. - // TODO(b/213335255): predefinedTriggers and nodeMajorVersion are here to support ext:dev:emulators:* commands. - // Ideally, we should handle that case via ExtensionEmulator. - predefinedTriggers: options.extDevTriggers as ParsedTriggerDefinition[] | undefined, - nodeMajorVersion: parseRuntimeVersion((options.extDevNodeVersion as string) || cfg.runtime), - }); - } - } + // Process extensions config early so that we have a better guess at whether + // the Functions emulator needs to start. let extensionEmulator: ExtensionsEmulator | undefined = undefined; if (shouldStart(options, Emulators.EXTENSIONS)) { const projectNumber = isDemoProject @@ -369,57 +349,39 @@ export async function startAll( } const listenConfig = {} as Record; + if (emulatableBackends.length) { + // If we already know we need Functions (and Eventarc), assign them now. + listenConfig[Emulators.FUNCTIONS] = getListenConfig(options, Emulators.FUNCTIONS); + listenConfig[Emulators.EVENTARC] = getListenConfig(options, Emulators.EVENTARC); + } for (const emulator of ALL_EMULATORS) { - if (emulator === Emulators.EXTENSIONS) { - // Same port as function, no need for separate assignment - continue; - } - if (emulator === Emulators.UI && !showUI) { + if ( + emulator === Emulators.FUNCTIONS || + emulator === Emulators.EVENTARC || + // Same port as Functions, no need for separate assignment + emulator === Emulators.EXTENSIONS || + (emulator === Emulators.UI && !showUI) + ) { continue; } if ( shouldStart(options, emulator) || - (emulator === Emulators.EVENTARC && emulatableBackends.length > 0) || (emulator === Emulators.LOGGING && ((showUI && shouldStart(options, Emulators.UI)) || START_LOGGING_EMULATOR)) ) { - let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(); - if (host === "localhost" && utils.isRunningInWSL()) { - // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 - // 127.0.0.1 instead of localhost. This, combined with the hack in - // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. - // The CLI (including the hub) will also consistently report 127.0.0.1, - // causing clients to connect via IPv4 only (which mitigates the problem of - // some clients resolving localhost to IPv6 and get connection refused). - host = "127.0.0.1"; - } - - const portVal = options.config.src.emulators?.[emulator]?.port; - let port: number; - let portFixed: boolean; - if (portVal) { - port = parseInt(`${portVal}`, 10); - portFixed = true; - } else { - port = Constants.getDefaultPort(emulator); - portFixed = !FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; - } - listenConfig[emulator] = { - host, - port, - portFixed, - }; + const config = getListenConfig(options, emulator); + listenConfig[emulator] = config; if (emulator === Emulators.FIRESTORE) { const wsPortConfig = options.config.src.emulators?.firestore?.websocketPort; listenConfig["firestore.websocket"] = { - host, + host: config.host, port: wsPortConfig || 9150, portFixed: !!wsPortConfig, }; } } } - const listenForEmulator = await resolveHostAndAssignPorts(listenConfig); + let listenForEmulator = await resolveHostAndAssignPorts(listenConfig); hubLogger.log("DEBUG", "assigned listening specs for emulators", { user: listenForEmulator }); function legacyGetFirstAddr(name: PortName): { host: string; port: number } { @@ -491,6 +453,8 @@ export async function startAll( const emulators: EmulatorInfo[] = []; if (experiments.isEnabled("webframeworks")) { for (const e of ALL_SERVICE_EMULATORS) { + // TODO(yuchenshi): Functions and Eventarc may be missing if they are not + // yet known to be needed and then prepareFrameworks adds extra functions. if (listenForEmulator[e]) { emulators.push({ name: e, @@ -500,14 +464,49 @@ export async function startAll( } } } + // This may add additional sources for Functions emulator and must be done before it. await prepareFrameworks(targets, options, options, emulators); } + const projectDir = (options.extDevDir || options.config.projectDir) as string; + if (shouldStart(options, Emulators.FUNCTIONS)) { + const functionsCfg = normalizeAndValidate(options.config.src.functions); + // Note: ext:dev:emulators:* commands hit this path, not the Emulators.EXTENSIONS path + utils.assertIsStringOrUndefined(options.extDevDir); + + for (const cfg of functionsCfg) { + const functionsDir = path.join(projectDir, cfg.source); + emulatableBackends.push({ + functionsDir, + codebase: cfg.codebase, + env: { + ...options.extDevEnv, + }, + secretEnv: [], // CF3 secrets are bound to specific functions, so we'll get them during trigger discovery. + // TODO(b/213335255): predefinedTriggers and nodeMajorVersion are here to support ext:dev:emulators:* commands. + // Ideally, we should handle that case via ExtensionEmulator. + predefinedTriggers: options.extDevTriggers as ParsedTriggerDefinition[] | undefined, + nodeMajorVersion: parseRuntimeVersion((options.extDevNodeVersion as string) || cfg.runtime), + }); + } + } + if (extensionEmulator) { await startEmulator(extensionEmulator); } if (emulatableBackends.length) { + if (!listenForEmulator.functions || !listenForEmulator.eventarc) { + // We did not know that we need Functions and Eventarc earlier but now we do. + listenForEmulator = await resolveHostAndAssignPorts({ + ...listenForEmulator, + functions: listenForEmulator.functions ?? getListenConfig(options, Emulators.FUNCTIONS), + eventarc: listenForEmulator.eventarc ?? getListenConfig(options, Emulators.EVENTARC), + }); + hubLogger.log("DEBUG", "late-assigned ports for functions and eventarc emulators", { + user: listenForEmulator, + }); + } const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); const functionsAddr = legacyGetFirstAddr(Emulators.FUNCTIONS); const projectId = needProjectId(options); @@ -853,6 +852,38 @@ export async function startAll( return { deprecationNotices: [] }; } +function getListenConfig( + options: EmulatorOptions, + emulator: Exclude +): EmulatorListenConfig { + let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(); + if (host === "localhost" && utils.isRunningInWSL()) { + // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 + // 127.0.0.1 instead of localhost. This, combined with the hack in + // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. + // The CLI (including the hub) will also consistently report 127.0.0.1, + // causing clients to connect via IPv4 only (which mitigates the problem of + // some clients resolving localhost to IPv6 and get connection refused). + host = "127.0.0.1"; + } + + const portVal = options.config.src.emulators?.[emulator]?.port; + let port: number; + let portFixed: boolean; + if (portVal) { + port = parseInt(`${portVal}`, 10); + portFixed = true; + } else { + port = Constants.getDefaultPort(emulator); + portFixed = !FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; + } + return { + host, + port, + portFixed, + }; +} + /** * Exports data from emulators that support data export. Used with `emulators:export` and with the --export-on-exit flag. * @param exportPath diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index 69365697ea1d..67a2741755e5 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -203,19 +203,29 @@ const MAX_PORT = 65535; // max TCP port /** * Resolve the hostname and assign ports to a subset of emulators. * - * @param listenConfig the config for each emulator + * @param listenConfig the config for each emulator or previously resolved specs * @return a map from emulator to its resolved addresses with port. */ export async function resolveHostAndAssignPorts( - listenConfig: Partial> + listenConfig: Partial> ): Promise> { - const entries = Object.entries(listenConfig) as [PortName, EmulatorListenConfig][]; const lookupForHost = new Map>(); const takenPorts = new Map(); const result = {} as Record; const tasks = []; - for (const [name, { host, port, portFixed }] of entries) { + for (const name of Object.keys(listenConfig) as PortName[]) { + const config = listenConfig[name]; + if (!config) { + continue; + } else if (config instanceof Array) { + result[name] = config; + for (const { port } of config) { + takenPorts.set(port, name); + } + continue; + } + const { host, port, portFixed } = config; let lookup = lookupForHost.get(host); if (!lookup) { lookup = Resolver.DEFAULT.lookupAll(host); From 2a5ca0515d0936b78f653bf982447f90451b354a Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 10 Oct 2022 10:32:02 -0700 Subject: [PATCH 033/115] Check for functions permissions if web frameworks adds a functions dependency (#5091) --- src/commands/deploy.ts | 2 +- src/deploy/index.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 5383e83c6f5d..eb44be092cd3 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -18,7 +18,7 @@ export const VALID_DEPLOY_TARGETS = [ "remoteconfig", "extensions", ]; -const TARGET_PERMISSIONS: Record = { +export const TARGET_PERMISSIONS: Record = { database: ["firebasedatabase.instances.update"], hosting: ["firebasehosting.sites.update"], functions: [ diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 173c3f5c641e..e72e311c6b5d 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -17,6 +17,8 @@ import * as RemoteConfigTarget from "./remoteconfig"; import * as ExtensionsTarget from "./extensions"; import { prepareFrameworks } from "../frameworks"; import { HostingDeploy } from "./hosting/context"; +import { requirePermissions } from "../requirePermissions"; +import { TARGET_PERMISSIONS } from "../commands/deploy"; const TARGETS = { hosting: HostingTarget, @@ -61,7 +63,12 @@ export const deploy = async function ( const config = options.config.get("hosting"); if (Array.isArray(config) ? config.some((it) => it.source) : config.source) { experiments.assertEnabled("webframeworks", "deploy a web framework to hosting"); + const usedToTargetFunctions = targetNames.includes("functions"); await prepareFrameworks(targetNames, context, options); + const nowTargetsFunctions = targetNames.includes("functions"); + if (nowTargetsFunctions && !usedToTargetFunctions) { + await requirePermissions(TARGET_PERMISSIONS["functions"]); + } } } From 75d15d5c15a88d62e4dedec19809c3626b70419b Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 10 Oct 2022 10:56:03 -0700 Subject: [PATCH 034/115] Fix incorrect warning for Firestore websocket in UI. (#5100) --- src/emulator/downloadableEmulators.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index b480779f5cc5..a1b1aed3597c 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -80,15 +80,15 @@ export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDe }, } : { - version: "1.11.0", - downloadPath: path.join(CACHE_DIR, "ui-v1.11.0.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.11.0"), - binaryPath: path.join(CACHE_DIR, "ui-v1.11.0", "server", "server.js"), + version: "1.11.1", + downloadPath: path.join(CACHE_DIR, "ui-v1.11.1.zip"), + unzipDir: path.join(CACHE_DIR, "ui-v1.11.1"), + binaryPath: path.join(CACHE_DIR, "ui-v1.11.1", "server", "server.js"), opts: { cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.11.0.zip", - expectedSize: 3061915, - expectedChecksum: "94679756dc270754e9a4dc9d1c6fc4e1", + remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.11.1.zip", + expectedSize: 3061713, + expectedChecksum: "a4944414518be206280b495f526f18bf", namePrefix: "ui", }, }, From 174309e57b6e2fd581e11f490f567d2765008313 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 10 Oct 2022 13:49:44 -0700 Subject: [PATCH 035/115] Misc bugfixes (#5102) --- src/frameworks/index.ts | 35 ++++++++++++++++++++++++----------- src/serve/hosting.ts | 23 ++++++++++++++++------- src/serve/index.ts | 6 ++---- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 77f8432d534c..2e3cbd98b024 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -24,6 +24,7 @@ import { FirebaseError } from "../error"; import { requireHostingSite } from "../requireHostingSite"; import { HostingRewrites } from "../firebaseConfig"; import * as experiments from "../experiments"; +import { implicitInit } from "../hosting/implicitInit"; // Use "true &&"" to keep typescript from compiling this file and rewriting // the import statement into a require @@ -330,17 +331,29 @@ export async function prepareFrameworks( firebaseDefaults ||= {}; firebaseDefaults.config = firebaseConfig; } else { - console.warn( - `No Firebase app associated with site ${site}, unable to provide authenticated server context. -You can link a Web app to a Hosting site here https://console.firebase.google.com/project/_/settings/general/web` - ); - if (!options.nonInteractive) { - const continueDeploy = await promptOnce({ - type: "confirm", - default: true, - message: "Would you like to continue with the deploy?", - }); - if (!continueDeploy) exit(1); + const defaultConfig = await implicitInit(options); + if (defaultConfig.json) { + console.warn( + `Site ${site} is not associated with an app ID. Injecting default app config` + ); + firebaseDefaults ||= {}; + firebaseDefaults.config = JSON.parse(defaultConfig.json); + } else { + // N.B. None of us know when this can ever happen and the deploy would + // still succeed. Maaaaybe if someone tried calling firebase serve + // on a project that never initialized hosting? + console.warn( + `No Firebase app associated with site ${site}, unable to provide authenticated server context. + You can link a Web app to a Hosting site here https://console.firebase.google.com/project/_/settings/general/web` + ); + if (!options.nonInteractive) { + const continueDeploy = await promptOnce({ + type: "confirm", + default: true, + message: "Would you like to continue with the deploy?", + }); + if (!continueDeploy) exit(1); + } } } } diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index 3447a6b2753b..965fe7dda488 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -15,6 +15,7 @@ import { EmulatorLogger } from "../emulator/emulatorLogger"; import { Emulators } from "../emulator/types"; import { createDestroyer } from "../utils"; import { execSync } from "child_process"; +import { requireHostingSite } from "../requireHostingSite"; const MAX_PORT_ATTEMPTS = 10; let attempts = 0; @@ -139,13 +140,21 @@ export function stop(): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function start(options: any): Promise { const init = await implicitInit(options); - // Note: we cannot use the hostingConfig() method because it would resolve - // targets and we don't want to crash the emulator just because the target - // doesn't exist (nor do we want to depend on API calls); - let configs = config.extract(options); - configs = config.filterOnly(configs, options.only); - configs = config.filterExcept(configs, options.except); - config.validate(configs, options); + // N.B. Originally we didn't call this method because it could try to resolve + // targets and cause us to fail. But we might be calling prepareFrameworks, + // which modifies the cached result of config.hostingConfig. So if we don't + // call this, we won't get web frameworks. But we might need to change this + // as well to avoid validation errors. + // But hostingConfig tries to resolve targets and a customer might not have + // site/targets defined + if (!options.site) { + try { + await requireHostingSite(options); + } catch { + options.site = JSON.parse(init.json).projectId; + } + } + const configs = config.hostingConfig(options); for (let i = 0; i < configs.length; i++) { // skip over the functions emulator ports to avoid breaking changes diff --git a/src/serve/index.ts b/src/serve/index.ts index bfdb763332be..3b0fdc8c05c6 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -4,6 +4,7 @@ import * as experiments from "../experiments"; import { trackEmulator } from "../track"; import { getProjectId } from "../projectUtils"; import { Constants } from "../emulator/constants"; +import * as config from "../hosting/config"; const { FunctionsServer } = require("./functions"); @@ -21,10 +22,7 @@ const TARGETS: { export async function serve(options: any): Promise { const targetNames: string[] = options.targets || []; options.port = parseInt(options.port, 10); - if ( - targetNames.includes("hosting") && - [].concat(options.config.get("hosting")).some((it: any) => it.source) - ) { + if (targetNames.includes("hosting") && config.extract(options).some((it: any) => it.source)) { experiments.assertEnabled("webframeworks", "emulate a web framework"); await prepareFrameworks(targetNames, options, options); } From 325cd3245f4119e97cc01d7b31314d77cb8c3642 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 10 Oct 2022 14:34:49 -0700 Subject: [PATCH 036/115] Block deploying web frameworks' SSR to preview channels (#5104) --- src/deploy/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/deploy/index.ts b/src/deploy/index.ts index e72e311c6b5d..e1e885f8e9df 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -67,6 +67,11 @@ export const deploy = async function ( await prepareFrameworks(targetNames, context, options); const nowTargetsFunctions = targetNames.includes("functions"); if (nowTargetsFunctions && !usedToTargetFunctions) { + if (context.hostingChannel && !experiments.isEnabled("pintags")) { + throw new FirebaseError( + "Web frameworks with dynamic content do not yet support deploying to preview channels" + ); + } await requirePermissions(TARGET_PERMISSIONS["functions"]); } } From f14f737c823c84c97533ea7b7cd8ba50bbaa7849 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 10 Oct 2022 14:57:06 -0700 Subject: [PATCH 037/115] SSR sites only force deploy the SSR function (#5089) * SSR sites only force deploy the SSR function * Add utility for ensuring a function is part of an only string * Fix typo * Fix noop test * Get rid of extra space --- src/frameworks/index.ts | 11 ++++++-- src/functions/ensureTargeted.ts | 29 ++++++++++++++++++++ src/test/functions/ensureTargeted.spec.ts | 32 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/functions/ensureTargeted.ts create mode 100644 src/test/functions/ensureTargeted.spec.ts diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 2e3cbd98b024..d5bf4fe4069c 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -24,6 +24,7 @@ import { FirebaseError } from "../error"; import { requireHostingSite } from "../requireHostingSite"; import { HostingRewrites } from "../firebaseConfig"; import * as experiments from "../experiments"; +import { ensureTargeted } from "../functions/ensureTargeted"; import { implicitInit } from "../hosting/implicitInit"; // Use "true &&"" to keep typescript from compiling this file and rewriting @@ -413,6 +414,7 @@ export async function prepareFrameworks( } config.rewrites.push(rewrite); + const codebase = `firebase-frameworks-${site}`; const existingFunctionsConfig = options.config.get("functions") ? [].concat(options.config.get("functions")) : []; @@ -420,11 +422,16 @@ export async function prepareFrameworks( ...existingFunctionsConfig, { source: relative(projectRoot, functionsDist), - codebase: `firebase-frameworks-${site}`, + codebase, }, ]); - if (!targetNames.includes("functions")) targetNames.unshift("functions"); + if (!targetNames.includes("functions")) { + targetNames.unshift("functions"); + } + if (options.only) { + options.only = ensureTargeted(options.only, codebase); + } // if exists, delete everything but the node_modules directory and package-lock.json // this should speed up repeated NPM installs diff --git a/src/functions/ensureTargeted.ts b/src/functions/ensureTargeted.ts new file mode 100644 index 000000000000..dad2b0464a6e --- /dev/null +++ b/src/functions/ensureTargeted.ts @@ -0,0 +1,29 @@ +/** + * Ensures than an only string is modified so that it will enclude a function + * in its target. This is useful for making sure that an SSR function is included + * with a web framework, or that a traditional hosting site includes its pinned + * functions + * @param only original only string + * @param id function ID + * @param codebase function codebase + * @return new only string + */ +export function ensureTargeted(only: string, codebase: string, id?: string): string { + const parts = only.split(","); + if (parts.includes("functions")) { + return only; + } + + let newTarget = `functions:${codebase}`; + if (parts.includes(newTarget)) { + return only; + } + if (id) { + newTarget = `${newTarget}:${id}`; + if (parts.includes(newTarget)) { + return only; + } + } + + return `${only},${newTarget}`; +} diff --git a/src/test/functions/ensureTargeted.spec.ts b/src/test/functions/ensureTargeted.spec.ts new file mode 100644 index 000000000000..c39fc8a8997c --- /dev/null +++ b/src/test/functions/ensureTargeted.spec.ts @@ -0,0 +1,32 @@ +import { expect } from "chai"; +import { ensureTargeted } from "../../functions/ensureTargeted"; + +describe("ensureTargeted", () => { + it("does nothing if 'functions' is included", () => { + expect(ensureTargeted("hosting,functions", "codebase")).to.equal("hosting,functions"); + expect(ensureTargeted("hosting,functions", "codebase", "id")).to.equal("hosting,functions"); + }); + + it("does nothing if the codebase is targeted", () => { + expect(ensureTargeted("hosting,functions:codebase", "codebase")).to.equal( + "hosting,functions:codebase" + ); + expect(ensureTargeted("hosting,functions:codebase", "codebase", "id")).to.equal( + "hosting,functions:codebase" + ); + }); + + it("does nothing if the function is targeted", () => { + expect(ensureTargeted("hosting,functions:codebase:id", "codebase", "id")).to.equal( + "hosting,functions:codebase:id" + ); + }); + + it("adds the codebase if missing and no id is provided", () => { + expect(ensureTargeted("hosting", "codebase")).to.equal("hosting,functions:codebase"); + }); + + it("adds the function if missing", () => { + expect(ensureTargeted("hosting", "codebase", "id")).to.equal("hosting,functions:codebase:id"); + }); +}); From 178bbd58c12b7fe2a487d755b211efcb3d376310 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 10 Oct 2022 15:50:37 -0700 Subject: [PATCH 038/115] Add link to associate sites with app ids (#5105) --- src/frameworks/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index d5bf4fe4069c..453c350b0bfc 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -197,12 +197,12 @@ export async function discover(dir: string, warn = true) { } } if (frameworksDiscovered.length > 1) { - if (warn) console.error("Multiple conflicting frameworks discovered. TODO link"); + if (warn) console.error("Multiple conflicting frameworks discovered."); return; } if (frameworksDiscovered.length === 1) return frameworksDiscovered[0]; } - if (warn) console.warn("We can't detirmine the web framework in use. TODO link"); + if (warn) console.warn("Could not determine the web framework in use."); return; } @@ -335,7 +335,8 @@ export async function prepareFrameworks( const defaultConfig = await implicitInit(options); if (defaultConfig.json) { console.warn( - `Site ${site} is not associated with an app ID. Injecting default app config` + `No Firebase app associated with site ${site}, injecting project default config. + You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web` ); firebaseDefaults ||= {}; firebaseDefaults.config = JSON.parse(defaultConfig.json); @@ -345,7 +346,7 @@ export async function prepareFrameworks( // on a project that never initialized hosting? console.warn( `No Firebase app associated with site ${site}, unable to provide authenticated server context. - You can link a Web app to a Hosting site here https://console.firebase.google.com/project/_/settings/general/web` + You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web` ); if (!options.nonInteractive) { const continueDeploy = await promptOnce({ From 042836d66a94745361285c36d687d84113218adf Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Mon, 10 Oct 2022 18:26:21 -0500 Subject: [PATCH 039/115] Fix typo in webframeworks (#5103) * Remove unhelpful console output * expirimental -> experimental Co-authored-by: Bryan Kendall --- src/frameworks/angular/index.ts | 2 +- src/frameworks/express/index.ts | 2 +- src/frameworks/index.ts | 6 +++--- src/frameworks/next/index.ts | 2 +- src/frameworks/nuxt/index.ts | 2 +- src/frameworks/vite/index.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/frameworks/angular/index.ts b/src/frameworks/angular/index.ts index 2adf13834bc3..db5ccbf0fb8c 100644 --- a/src/frameworks/angular/index.ts +++ b/src/frameworks/angular/index.ts @@ -16,7 +16,7 @@ import { promptOnce } from "../../prompt"; import { proxyRequestHandler } from "../../hosting/proxy"; export const name = "Angular"; -export const support = SupportLevel.Expirimental; +export const support = SupportLevel.Experimental; export const type = FrameworkType.Framework; const CLI_COMMAND = join("node_modules", ".bin", process.platform === "win32" ? "ng.cmd" : "ng"); diff --git a/src/frameworks/express/index.ts b/src/frameworks/express/index.ts index 3bd096df1207..6f0a803ef5ee 100644 --- a/src/frameworks/express/index.ts +++ b/src/frameworks/express/index.ts @@ -9,7 +9,7 @@ import { BuildResult, FrameworkType, SupportLevel } from ".."; const { dynamicImport } = require(true && "../../dynamicImport"); export const name = "Express.js"; -export const support = SupportLevel.Expirimental; +export const support = SupportLevel.Experimental; export const type = FrameworkType.Custom; async function getConfig(root: string) { diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 453c350b0bfc..f232d6fb6624 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -89,13 +89,13 @@ export const enum FrameworkType { } export const enum SupportLevel { - Expirimental = "expirimental", + Experimental = "experimental", Community = "community-supported", } const SupportLevelWarnings = { - [SupportLevel.Expirimental]: clc.yellow( - `This is an expirimental integration, proceed with caution.` + [SupportLevel.Experimental]: clc.yellow( + `This is an experimental integration, proceed with caution.` ), [SupportLevel.Community]: clc.yellow( `This is a community-supported integration, support is best effort.` diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index 70597ad6c016..ccd2f3e7edcd 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -44,7 +44,7 @@ const CLI_COMMAND = join( ); export const name = "Next.js"; -export const support = SupportLevel.Expirimental; +export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; function getNextVersion(cwd: string) { diff --git a/src/frameworks/nuxt/index.ts b/src/frameworks/nuxt/index.ts index d06aad50a403..aba1129bcc75 100644 --- a/src/frameworks/nuxt/index.ts +++ b/src/frameworks/nuxt/index.ts @@ -5,7 +5,7 @@ import { gte } from "semver"; import { BuildResult, findDependency, FrameworkType, relativeRequire, SupportLevel } from ".."; export const name = "Nuxt"; -export const support = SupportLevel.Expirimental; +export const support = SupportLevel.Experimental; export const type = FrameworkType.Toolchain; export async function discover(dir: string) { diff --git a/src/frameworks/vite/index.ts b/src/frameworks/vite/index.ts index 37625378f662..d68c4177c4be 100644 --- a/src/frameworks/vite/index.ts +++ b/src/frameworks/vite/index.ts @@ -7,7 +7,7 @@ import { proxyRequestHandler } from "../../hosting/proxy"; import { promptOnce } from "../../prompt"; export const name = "Vite"; -export const support = SupportLevel.Expirimental; +export const support = SupportLevel.Experimental; export const type = FrameworkType.Toolchain; const CLI_COMMAND = join( From 975d28bf5ea31f911232227acd6805723aeff4be Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 11 Oct 2022 17:38:57 +0000 Subject: [PATCH 040/115] 11.14.2 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index fe9b164aa70e..94caf499364f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.14.1", + "version": "11.14.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.14.1", + "version": "11.14.2", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 27533596041e..aabeee8123b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.14.1", + "version": "11.14.2", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 3a9d1c37824c591b54a8399db5bfb5d446e08f9d Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 11 Oct 2022 17:39:08 +0000 Subject: [PATCH 041/115] [firebase-release] Removed change log and reset repo after 11.14.2 release --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3340085786d3..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +0,0 @@ -- Enable single project mode for the database emulator (#5068). -- Ravamp emulator networking to assign ports early and explictly listen on IP addresses (#5083). -- Emulator UI and hub now listen on both IPv4 and IPv6 address by default (if possible) (#5088). -- Fix Firestore emulator excessive logs about discovery endpoint not found (#5088). From b50317a6ef962e645c013afb6c8bc4a8cfc577d1 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 11 Oct 2022 16:38:40 -0700 Subject: [PATCH 042/115] better handle failed requests when listing functions for backends (#5111) * better handle failed requests when listing functions for backends * changelog --- CHANGELOG.md | 1 + src/deploy/hosting/convertConfig.ts | 4 +-- src/gcp/cloudfunctions.ts | 1 + src/test/deploy/hosting/convertConfig.spec.ts | 36 +++++++++++++++++++ src/test/gcp/cloudfunctions.spec.ts | 21 +++++++++++ src/test/gcp/cloudfunctionsv2.spec.ts | 24 +++++++++++++ 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..a7d253d7aa45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixes issue where errors were not properly propagating when listing backends. (#5071) diff --git a/src/deploy/hosting/convertConfig.ts b/src/deploy/hosting/convertConfig.ts index 929a8b2f6e3b..e052a04cd50d 100644 --- a/src/deploy/hosting/convertConfig.ts +++ b/src/deploy/hosting/convertConfig.ts @@ -112,9 +112,9 @@ export async function convertConfig( `Deploying hosting site ${deploy.config.site}, did not have permissions to check for backends: `, err ); - } else { - throw err; } + } else { + throw err; } } } diff --git a/src/gcp/cloudfunctions.ts b/src/gcp/cloudfunctions.ts index 7c0f62afd2f0..78f283173c72 100644 --- a/src/gcp/cloudfunctions.ts +++ b/src/gcp/cloudfunctions.ts @@ -423,6 +423,7 @@ async function list(projectId: string, region: string): Promise { expect(config).to.deep.equal(want); }); } + + describe("with permissions issues", () => { + let existingBackendStub: sinon.SinonStub; + + beforeEach(() => { + existingBackendStub = sinon + .stub(backend, "existingBackend") + .rejects("existingBackend unspecified behavior"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should not throw when resolving backends", async () => { + existingBackendStub.rejects( + new FirebaseError("Some permissions 403 error (that should be caught)", { status: 403 }) + ); + + await expect( + convertConfig( + { projectId: "1" }, + { + config: { + site: "foo", + rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }], + }, + version: "14", + } + ) + ).to.not.be.rejected; + }); + }); }); diff --git a/src/test/gcp/cloudfunctions.spec.ts b/src/test/gcp/cloudfunctions.spec.ts index 79484e407133..a1873e0c0c3a 100644 --- a/src/test/gcp/cloudfunctions.spec.ts +++ b/src/test/gcp/cloudfunctions.spec.ts @@ -8,6 +8,7 @@ import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../functions/event import * as cloudfunctions from "../../gcp/cloudfunctions"; import * as projectConfig from "../../functions/projectConfig"; import { BLOCKING_LABEL, CODEBASE_LABEL, HASH_LABEL } from "../../functions/constants"; +import { FirebaseError } from "../../error"; describe("cloudfunctions", () => { const FUNCTION_NAME: backend.TargetIds = { @@ -720,4 +721,24 @@ describe("cloudfunctions", () => { ).to.not.be.rejected; }); }); + + describe("listFunctions", () => { + it("should pass back an error with the correct status", async () => { + nock(functionsOrigin) + .get("/v1/projects/foo/locations/-/functions") + .reply(403, { error: "You don't have permissions." }); + + let errCaught = false; + try { + await cloudfunctions.listFunctions("foo", "-"); + } catch (err: unknown) { + errCaught = true; + expect(err).instanceOf(FirebaseError); + expect(err).has.property("status", 403); + } + + expect(errCaught, "should have caught an error").to.be.true; + expect(nock.isDone()).to.be.true; + }); + }); }); diff --git a/src/test/gcp/cloudfunctionsv2.spec.ts b/src/test/gcp/cloudfunctionsv2.spec.ts index 66a7646dcf37..54c4f623638a 100644 --- a/src/test/gcp/cloudfunctionsv2.spec.ts +++ b/src/test/gcp/cloudfunctionsv2.spec.ts @@ -1,10 +1,13 @@ import { expect } from "chai"; +import * as nock from "nock"; import * as cloudfunctionsv2 from "../../gcp/cloudfunctionsv2"; import * as backend from "../../deploy/functions/backend"; import * as events from "../../functions/events"; import * as projectConfig from "../../functions/projectConfig"; import { BLOCKING_LABEL, CODEBASE_LABEL, HASH_LABEL } from "../../functions/constants"; +import { functionsV2Origin } from "../../api"; +import { FirebaseError } from "../../error"; describe("cloudfunctionsv2", () => { const FUNCTION_NAME: backend.TargetIds = { @@ -661,4 +664,25 @@ describe("cloudfunctionsv2", () => { }); }); }); + + describe("listFunctions", () => { + it("should pass back an error with the correct status", async () => { + nock(functionsV2Origin) + .get("/v2/projects/foo/locations/-/functions") + .query({ filter: `environment="GEN_2"` }) + .reply(403, { error: "You don't have permissions." }); + + let errCaught = false; + try { + await cloudfunctionsv2.listFunctions("foo", "-"); + } catch (err: unknown) { + errCaught = true; + expect(err).instanceOf(FirebaseError); + expect(err).has.property("status", 403); + } + + expect(errCaught, "should have caught an error").to.be.true; + expect(nock.isDone()).to.be.true; + }); + }); }); From 6eed1c2d1d0434b792f146e364d4b4309845d1de Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 11 Oct 2022 16:57:43 -0700 Subject: [PATCH 043/115] fix missing message on deploy (#5109) * fix missing message on deploy * just a little more testing * inline complex type --- CHANGELOG.md | 1 + src/deploy/hosting/release.ts | 9 +- src/hosting/api.ts | 17 +-- src/test/deploy/hosting/release.spec.ts | 156 ++++++++++++++++++++++++ src/test/hosting/api.spec.ts | 21 ++++ 5 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 src/test/deploy/hosting/release.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d253d7aa45..5cb75b84cde0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Fixes issue where errors were not properly propagating when listing backends. (#5071) +- Fixes issue where message from `-m` on deploy was not being properly applied. (#5107) diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts index 7ea45f8df0bc..859ae2184166 100644 --- a/src/deploy/hosting/release.ts +++ b/src/deploy/hosting/release.ts @@ -8,7 +8,7 @@ import { FirebaseError } from "../../error"; /** * Release finalized a Hosting release. */ -export async function release(context: Context): Promise { +export async function release(context: Context, options: { message?: string }): Promise { if (!context.hosting || !context.hosting.deploys) { return; } @@ -40,10 +40,15 @@ export async function release(context: Context): Promise { logger.debug("[hosting] releasing to channel:", context.hostingChannel); } + const otherReleaseOpts: Partial> = {}; + if (options.message) { + otherReleaseOpts.message = options.message; + } const release = await api.createRelease( deploy.config.site, context.hostingChannel || "live", - deploy.version + deploy.version, + otherReleaseOpts ); logger.debug("[hosting] release:", release); utils.logLabeledSuccess(`hosting[${deploy.config.site}]`, "release complete"); diff --git a/src/hosting/api.ts b/src/hosting/api.ts index bfa99fabe6c6..101d83d3d7ce 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -31,7 +31,7 @@ enum ReleaseType { SITE_DISABLE = "SITE_DISABLE", } -interface Release { +export interface Release { // The unique identifier for the release, in the format: // sites/site-name/releases/releaseID readonly name: string; @@ -451,6 +451,8 @@ export async function cloneVersion( return pollRes; } +type PartialRelease = Partial>; + /** * Create a release on a channel. * @param site the site for the version. @@ -460,13 +462,14 @@ export async function cloneVersion( export async function createRelease( site: string, channel: string, - version: string + version: string, + partialRelease?: PartialRelease ): Promise { - const res = await apiClient.request({ - method: "POST", - path: `/projects/-/sites/${site}/channels/${channel}/releases`, - queryParams: { versionName: version }, - }); + const res = await apiClient.post( + `/projects/-/sites/${site}/channels/${channel}/releases`, + partialRelease, + { queryParams: { versionName: version } } + ); return res.body; } diff --git a/src/test/deploy/hosting/release.spec.ts b/src/test/deploy/hosting/release.spec.ts new file mode 100644 index 000000000000..d8ed13d6b509 --- /dev/null +++ b/src/test/deploy/hosting/release.spec.ts @@ -0,0 +1,156 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as api from "../../../hosting/api"; +import { Context } from "../../../deploy/hosting/context"; +import * as convertConfigPkg from "../../../deploy/hosting/convertConfig"; + +import { release } from "../../../deploy/hosting/release"; +import { last } from "../../../utils"; + +describe("release", () => { + const PROJECT = "fake-project"; + const SITE = "my-site"; + const VERSION = "it/ends/up/like/this/version-id"; + const FAKE_CONFIG = {}; + + let updateVersionStub: sinon.SinonStub; + let createReleaseStub: sinon.SinonStub; + + beforeEach(() => { + updateVersionStub = sinon.stub(api, "updateVersion").rejects("updateVersion unstubbed"); + createReleaseStub = sinon.stub(api, "createRelease").rejects("createRelease unstubbed"); + sinon.stub(convertConfigPkg, "convertConfig").resolves(FAKE_CONFIG); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("with no Hosting deploys", () => { + it("should bail", async () => { + await release({ projectId: "foo" }, {}); + + expect(updateVersionStub).to.have.been.not.called; + expect(createReleaseStub).to.have.been.not.called; + }); + }); + + describe("a single site", () => { + const CONTEXT: Context = { + projectId: PROJECT, + hosting: { + deploys: [{ config: { site: SITE }, version: VERSION }], + }, + }; + + const UPDATE: Partial = { + status: "FINALIZED", + config: FAKE_CONFIG, + }; + + it("should update a version and make a release", async () => { + updateVersionStub.resolves({}); + createReleaseStub.resolves({}); + + await release(CONTEXT, {}); + + expect(updateVersionStub).to.have.been.calledOnceWithExactly( + SITE, + last(VERSION.split("/")), + UPDATE + ); + expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, "live", VERSION, {}); + }); + + it("should update a version and make a release with a message", async () => { + updateVersionStub.resolves({}); + createReleaseStub.resolves({}); + + await release(CONTEXT, { message: "hello world" }); + + expect(updateVersionStub).to.have.been.calledOnceWithExactly( + SITE, + last(VERSION.split("/")), + UPDATE + ); + expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, "live", VERSION, { + message: "hello world", + }); + }); + }); + + describe("multiple sites", () => { + const CONTEXT: Context = { + projectId: PROJECT, + hosting: { + deploys: [ + { config: { site: SITE }, version: VERSION }, + { config: { site: `${SITE}-2` }, version: `${VERSION}-2` }, + ], + }, + }; + + const UPDATE: Partial = { + status: "FINALIZED", + config: FAKE_CONFIG, + }; + + it("should update a version and make a release", async () => { + updateVersionStub.resolves({}); + createReleaseStub.resolves({}); + + await release(CONTEXT, {}); + + expect(updateVersionStub).to.have.been.calledTwice; + expect(updateVersionStub).to.have.been.calledWithExactly( + SITE, + last(VERSION.split("/")), + UPDATE + ); + expect(updateVersionStub).to.have.been.calledWithExactly( + `${SITE}-2`, + `${last(VERSION.split("/"))}-2`, + UPDATE + ); + expect(createReleaseStub).to.have.been.calledTwice; + expect(createReleaseStub).to.have.been.calledWithExactly(SITE, "live", VERSION, {}); + expect(createReleaseStub).to.have.been.calledWithExactly( + `${SITE}-2`, + "live", + `${VERSION}-2`, + {} + ); + }); + }); + + describe("to a hosting channel", () => { + const CHANNEL = "my-channel"; + const CONTEXT: Context = { + projectId: PROJECT, + hostingChannel: CHANNEL, + hosting: { + deploys: [{ config: { site: SITE }, version: VERSION }], + }, + }; + + const UPDATE: Partial = { + status: "FINALIZED", + config: FAKE_CONFIG, + }; + + it("should update a version and make a release", async () => { + updateVersionStub.resolves({}); + createReleaseStub.resolves({}); + + await release(CONTEXT, {}); + + expect(updateVersionStub).to.have.been.calledOnceWithExactly( + SITE, + last(VERSION.split("/")), + UPDATE + ); + expect(createReleaseStub).to.have.been.calledOnceWithExactly(SITE, CHANNEL, VERSION, {}); + }); + }); +}); diff --git a/src/test/hosting/api.spec.ts b/src/test/hosting/api.spec.ts index 5f00b7fb8f23..af80ef7f7cfa 100644 --- a/src/test/hosting/api.spec.ts +++ b/src/test/hosting/api.spec.ts @@ -448,6 +448,27 @@ describe("hosting", () => { expect(nock.isDone()).to.be.true; }); + it("should include a message, if provided", async () => { + const CHANNEL_ID = "my-channel"; + const RELEASE = { name: "my-new-release" }; + const VERSION = "version"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; + const MESSAGE = "yo dawg"; + nock(hostingApiOrigin) + .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`, { + message: MESSAGE, + }) + .query({ versionName: VERSION_NAME }) + .reply(201, RELEASE); + + const res = await hostingApi.createRelease(SITE, CHANNEL_ID, VERSION_NAME, { + message: MESSAGE, + }); + + expect(res).to.deep.equal(RELEASE); + expect(nock.isDone()).to.be.true; + }); + it("should throw an error if the server returns an error", async () => { const CHANNEL_ID = "my-channel"; const VERSION = "VERSION"; From 42b94d69e62edb85d780e8997a724801d8a48041 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 12 Oct 2022 08:59:17 -0700 Subject: [PATCH 044/115] Fixes error `EADDRNOTAVAIL` in portUtils. (#5112) --- CHANGELOG.md | 1 + src/emulator/portUtils.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb75b84cde0..6294d9ace286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Fixes issue where errors were not properly propagating when listing backends. (#5071) - Fixes issue where message from `-m` on deploy was not being properly applied. (#5107) +- Fixes error `EADDRNOTAVAIL` when running emulators in Docker. diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index 67a2741755e5..77d34a0cc295 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -9,6 +9,7 @@ import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED, Resolver } from "./dns"; import { Emulators, ListenSpec } from "./types"; import { Constants } from "./constants"; import { EmulatorLogger } from "./emulatorLogger"; +import { logger } from "../logger"; // See: // - https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports @@ -127,10 +128,22 @@ export async function checkListenable( dummyServer.once("error", (err) => { dummyServer.removeAllListeners(); const e = err as Error & { code?: string }; - if (e.code === "EADDRINUSE" || e.code === "EACCES") { + if ( + e.code === "EADDRINUSE" || + e.code === "EACCES" || + // Where the address is not bindable (not just the port), e.g. in Docker: + // https://github.com/firebase/firebase-tools/issues/4741#issuecomment-1275318134 + e.code === "EADDRNOTAVAIL" || + e.code === "EINVAL" + ) { resolve(false); } else { - reject(e); + // Other unknown issues -- we'll log a warning and return unavailable. + logger.warn( + `portUtils: Error when trying to check port ${addr.port} (on ${addr.address}): ${e.code}` + ); + logger.warn(e); + resolve(false); } }); dummyServer.once("listening", () => { From 7b02b7420a21013191cab6b2b4a49e1ee7ad6793 Mon Sep 17 00:00:00 2001 From: Tyler Stark Date: Wed, 12 Oct 2022 11:00:47 -0500 Subject: [PATCH 045/115] fix(webframeworks) Update template to hello-world (#5113) Update the nextjs codegen to make use of the hello-world template. --- src/frameworks/next/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index ccd2f3e7edcd..a24d30db5b53 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -141,7 +141,7 @@ export async function init(setup: any) { choices: ["JavaScript", "TypeScript"], }); execSync( - `npx --yes create-next-app@latest ${setup.hosting.source} ${ + `npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} ${ language === "TypeScript" ? "--ts" : "" }`, { stdio: "inherit" } From e2b6a89e4e84952b4125b0329b563697f12aff3e Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Wed, 12 Oct 2022 14:15:54 -0700 Subject: [PATCH 046/115] Provide a backup when implicitInit fails (#5116) * Provide a backup when implicitInit fails * Try to use the project ID --- src/hosting/implicitInit.ts | 2 +- src/serve/hosting.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hosting/implicitInit.ts b/src/hosting/implicitInit.ts index c87fc7a6818f..4c356a6a9fae 100644 --- a/src/hosting/implicitInit.ts +++ b/src/hosting/implicitInit.ts @@ -18,7 +18,7 @@ export interface TemplateServerResponse { emulatorsJs: string; // firebaseConfig JSON - json: string; + json?: string; } /** diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index 965fe7dda488..b3a0519428b0 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -16,6 +16,7 @@ import { Emulators } from "../emulator/types"; import { createDestroyer } from "../utils"; import { execSync } from "child_process"; import { requireHostingSite } from "../requireHostingSite"; +import { getProjectId } from "../projectUtils"; const MAX_PORT_ATTEMPTS = 10; let attempts = 0; @@ -151,7 +152,11 @@ export async function start(options: any): Promise { try { await requireHostingSite(options); } catch { - options.site = JSON.parse(init.json).projectId; + if (init.json) { + options.site = JSON.parse(init.json).projectId; + } else { + options.site = getProjectId(options) || "site"; + } } } const configs = config.hostingConfig(options); From a5463fce76b004cb754c234a1fe634b1a9f73d5b Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 12 Oct 2022 15:11:57 -0700 Subject: [PATCH 047/115] Stop trying other ports given EADDRNOTAVAIL. (#5115) Co-authored-by: Bryan Kendall --- src/emulator/portUtils.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index 77d34a0cc295..be7d67698043 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -9,7 +9,6 @@ import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED, Resolver } from "./dns"; import { Emulators, ListenSpec } from "./types"; import { Constants } from "./constants"; import { EmulatorLogger } from "./emulatorLogger"; -import { logger } from "../logger"; // See: // - https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports @@ -128,22 +127,10 @@ export async function checkListenable( dummyServer.once("error", (err) => { dummyServer.removeAllListeners(); const e = err as Error & { code?: string }; - if ( - e.code === "EADDRINUSE" || - e.code === "EACCES" || - // Where the address is not bindable (not just the port), e.g. in Docker: - // https://github.com/firebase/firebase-tools/issues/4741#issuecomment-1275318134 - e.code === "EADDRNOTAVAIL" || - e.code === "EINVAL" - ) { + if (e.code === "EADDRINUSE" || e.code === "EACCES") { resolve(false); } else { - // Other unknown issues -- we'll log a warning and return unavailable. - logger.warn( - `portUtils: Error when trying to check port ${addr.port} (on ${addr.address}): ${e.code}` - ); - logger.warn(e); - resolve(false); + reject(e); } }); dummyServer.once("listening", () => { @@ -276,7 +263,22 @@ export async function resolveHostAndAssignPorts( const addr = addrs[i]; const listen = listenSpec(addr, p); // This must be done one by one since the addresses may overlap. - if (await checkListenable(listen)) { + let listenable: boolean; + try { + listenable = await checkListenable(listen); + } catch (err) { + emuLogger.logLabeled( + "WARN", + name, + `Error when trying to check port ${p} on ${addr.address}: ${err}` + ); + // Even if portFixed is false, don't try other ports since the + // address may be entirely unavailable on all ports (e.g. no IPv6). + // https://github.com/firebase/firebase-tools/issues/4741#issuecomment-1275318134 + unavailable.push(addr.address); + continue; + } + if (listenable) { available.push(listen); } else { if (!portFixed) { From 118c035408a47eb64cee5a2100fdaab99aa15d15 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Wed, 12 Oct 2022 18:14:43 -0700 Subject: [PATCH 048/115] assign Hosting port using portUtils (#5119) * allow Hosting to use Emulator's portUtils to pick a port * changelog * clean up a tiny bit of logic --- CHANGELOG.md | 1 + src/emulator/constants.ts | 2 +- src/emulator/hostingEmulator.ts | 11 ++++- src/emulator/portUtils.ts | 21 ++++++++-- src/serve/hosting.ts | 72 +++++++++++++-------------------- 5 files changed, 58 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6294d9ace286..16f7ce403527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Fixes issue where errors were not properly propagating when listing backends. (#5071) - Fixes issue where message from `-m` on deploy was not being properly applied. (#5107) - Fixes error `EADDRNOTAVAIL` when running emulators in Docker. +- Fixes further issues where ports were not correctly recognized as unavailable. diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index e56b295d0d93..b6fa3637d02b 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -19,7 +19,7 @@ export const FIND_AVAILBLE_PORT_BY_DEFAULT: Record = { ui: true, hub: true, logging: true, - hosting: false, + hosting: true, functions: false, firestore: false, database: false, diff --git a/src/emulator/hostingEmulator.ts b/src/emulator/hostingEmulator.ts index ce382f897f4b..7e9323c8feac 100644 --- a/src/emulator/hostingEmulator.ts +++ b/src/emulator/hostingEmulator.ts @@ -9,13 +9,19 @@ interface HostingEmulatorArgs { } export class HostingEmulator implements EmulatorInstance { + private reservedPorts?: number[]; + constructor(private args: HostingEmulatorArgs) {} - start(): Promise { + async start(): Promise { this.args.options.host = this.args.host; this.args.options.port = this.args.port; - return serveHosting.start(this.args.options); + const { ports } = await serveHosting.start(this.args.options); + this.args.port = ports[0]; + if (ports.length > 1) { + this.reservedPorts = ports.slice(1); + } } connect(): Promise { @@ -34,6 +40,7 @@ export class HostingEmulator implements EmulatorInstance { name: this.getName(), host, port, + reservedPorts: this.reservedPorts, }; } diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index be7d67698043..48627d378424 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -9,6 +9,7 @@ import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED, Resolver } from "./dns"; import { Emulators, ListenSpec } from "./types"; import { Constants } from "./constants"; import { EmulatorLogger } from "./emulatorLogger"; +import { execSync } from "node:child_process"; // See: // - https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports @@ -121,9 +122,19 @@ export async function checkListenable( // Not using tcpport.check since it is based on trying to establish a Socket // connection, not on *listening* on a host:port. return new Promise((resolve, reject) => { - const dummyServer = createServer(() => { - // noop - }); + // For SOME REASON, we can still create a server on port 5000 on macOS. Why + // we do not know, but we need to keep this stupid check here because we + // *do* want to still *try* to default to 5000. + if (process.platform === "darwin") { + try { + execSync(`lsof -i :${addr.port} -sTCP:LISTEN`); + // If this succeeds, it found something listening. Fail. + return resolve(false); + } catch (e) { + // If lsof errored the port is NOT in use, continue. + } + } + const dummyServer = createServer(); dummyServer.once("error", (err) => { dummyServer.removeAllListeners(); const e = err as Error & { code?: string }; @@ -256,6 +267,10 @@ export async function resolveHostAndAssignPorts( emuLogger.logLabeled("DEBUG", name, `portUtils: skipping restricted port ${p}`); continue; } + if (p === 5001 && /^hosting/i.exec(name)) { + // We don't want Hosting to ever try to take port 5001. + continue; + } const available: ListenSpec[] = []; const unavailable: string[] = []; let i; diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index b3a0519428b0..8b75cbcea6ab 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -2,6 +2,7 @@ const morgan = require("morgan"); import { IncomingMessage, ServerResponse } from "http"; import { server as superstatic } from "superstatic"; import * as clc from "colorette"; +import { isIPv4 } from "net"; import { detectProjectRoot } from "../detectProjectRoot"; import { FirebaseError } from "../error"; @@ -14,12 +15,10 @@ import { Writable } from "stream"; import { EmulatorLogger } from "../emulator/emulatorLogger"; import { Emulators } from "../emulator/types"; import { createDestroyer } from "../utils"; -import { execSync } from "child_process"; import { requireHostingSite } from "../requireHostingSite"; import { getProjectId } from "../projectUtils"; +import { checkListenable } from "../emulator/portUtils"; -const MAX_PORT_ATTEMPTS = 10; -let attempts = 0; let destroyServer: undefined | (() => Promise) = undefined; const logger = EmulatorLogger.forEmulator(Emulators.HOSTING); @@ -46,34 +45,6 @@ function startServer(options: any, config: any, port: number, init: TemplateServ stream: morganStream, }); - const portInUse = () => { - const message = "Port " + options.port + " is not available."; - logger.log("WARN", clc.yellow("hosting: ") + message + " Trying another port..."); - if (attempts < MAX_PORT_ATTEMPTS) { - // Another project that's running takes up to 4 ports: 1 hosting port and 3 functions ports - attempts++; - startServer(options, config, port + 5, init); - } else { - logger.log("WARN", message); - throw new FirebaseError("Could not find an open port for hosting development server.", { - exit: 1, - }); - } - }; - - // On OSX, some ports may be reserved by the OS in a way that node http doesn't detect. - // Starting in MacOS 12.3 it does this with port 5000 our default port. This is a bad - // enough devexp that we should special case and ensure it's available. - if (process.platform === "darwin") { - try { - execSync(`lsof -i :${port} -sTCP:LISTEN`); - portInUse(); - return; - } catch (e) { - // if lsof errored the port is NOT in use, continue - } - } - const after = options.frameworksDevModeHandle && { files: options.frameworksDevModeHandle, }; @@ -114,16 +85,11 @@ function startServer(options: any, config: any, port: number, init: TemplateServ destroyServer = createDestroyer(server); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server.on("error", (err: any) => { - if (err.code === "EADDRINUSE") { - portInUse(); - } else { - throw new FirebaseError( - "An error occurred while starting the hosting development server:\n\n" + err.toString(), - { exit: 1 } - ); - } + server.on("error", (err: Error) => { + logger.log("DEBUG", `Error from superstatic server: ${err.stack || ""}`); + throw new FirebaseError( + `An error occurred while starting the hosting development server:\n\n${err.message}` + ); }); } @@ -139,7 +105,7 @@ export function stop(): Promise { * @param options the Firebase CLI options. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function start(options: any): Promise { +export async function start(options: any): Promise<{ ports: number[] }> { const init = await implicitInit(options); // N.B. Originally we didn't call this method because it could try to resolve // targets and cause us to fail. But we might be calling prepareFrameworks, @@ -161,11 +127,23 @@ export async function start(options: any): Promise { } const configs = config.hostingConfig(options); + // We never want to try and take port 5001 because Functions likes that port + // quite a bit, and we don't want to make Functions mad. + const assignedPorts = new Set([5001]); for (let i = 0; i < configs.length; i++) { // skip over the functions emulator ports to avoid breaking changes - const port = i === 0 ? options.port : options.port + 4 + i; + let port = i === 0 ? options.port : options.port + 4 + i; + while (assignedPorts.has(port) || !(await availablePort(options.host, port))) { + port += 1; + } + assignedPorts.add(port); startServer(options, configs[i], port, init); } + + // We are not actually reserving 5001, so remove it from our set before + // returning. + assignedPorts.delete(5001); + return { ports: Array.from(assignedPorts) }; } /** @@ -174,3 +152,11 @@ export async function start(options: any): Promise { export async function connect(): Promise { await Promise.resolve(); } + +function availablePort(host: string, port: number): Promise { + return checkListenable({ + address: host, + port, + family: isIPv4(host) ? "IPv4" : "IPv6", + }); +} From bc294590cf3f9bf89376a31cff39c83cf2414586 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 13 Oct 2022 16:51:41 +0000 Subject: [PATCH 049/115] 11.14.3 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 94caf499364f..42b5a04c564b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.14.2", + "version": "11.14.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.14.2", + "version": "11.14.3", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index aabeee8123b4..95658a71db97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.14.2", + "version": "11.14.3", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 31fd81dec54c493e3a11b505780baa2e9a29f16a Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 13 Oct 2022 16:51:52 +0000 Subject: [PATCH 050/115] [firebase-release] Removed change log and reset repo after 11.14.3 release --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f7ce403527..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +0,0 @@ -- Fixes issue where errors were not properly propagating when listing backends. (#5071) -- Fixes issue where message from `-m` on deploy was not being properly applied. (#5107) -- Fixes error `EADDRNOTAVAIL` when running emulators in Docker. -- Fixes further issues where ports were not correctly recognized as unavailable. From 200ee7e8d7d62e6ff1c05e96cb120aabff02c237 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Thu, 13 Oct 2022 15:19:20 -0700 Subject: [PATCH 051/115] remove NODE_ENV in `npm` command (#5121) * remove NODE_ENV when looking for dependencies * add sample vite project * packagelock * add integration test for vite framework * ignore new folder of tests * add experiment --- .eslintrc.js | 6 +- .github/workflows/node-test.yml | 11 +- .prettierignore | 1 + package.json | 1 + scripts/frameworks-tests/run.sh | 32 + .../frameworks-tests/vite-project/.gitignore | 24 + .../frameworks-tests/vite-project/counter.js | 9 + .../vite-project/firebase.json | 15 + .../frameworks-tests/vite-project/index.html | 13 + .../vite-project/javascript.svg | 1 + scripts/frameworks-tests/vite-project/main.js | 23 + .../vite-project/package-lock.json | 881 ++++++++++++++++++ .../vite-project/package.json | 14 + .../vite-project/public/vite.svg | 1 + .../frameworks-tests/vite-project/style.css | 97 ++ src/frameworks/index.ts | 4 +- 16 files changed, 1126 insertions(+), 7 deletions(-) create mode 100755 scripts/frameworks-tests/run.sh create mode 100644 scripts/frameworks-tests/vite-project/.gitignore create mode 100644 scripts/frameworks-tests/vite-project/counter.js create mode 100644 scripts/frameworks-tests/vite-project/firebase.json create mode 100644 scripts/frameworks-tests/vite-project/index.html create mode 100644 scripts/frameworks-tests/vite-project/javascript.svg create mode 100644 scripts/frameworks-tests/vite-project/main.js create mode 100644 scripts/frameworks-tests/vite-project/package-lock.json create mode 100644 scripts/frameworks-tests/vite-project/package.json create mode 100644 scripts/frameworks-tests/vite-project/public/vite.svg create mode 100644 scripts/frameworks-tests/vite-project/style.css diff --git a/.eslintrc.js b/.eslintrc.js index a96550188fbe..3026ae4f7310 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -120,5 +120,9 @@ module.exports = { // don't want Typescript to turn the imports into requires. Ignoring as eslint // is complaining it doesn't belong to a project. // TODO(jamesdaniels): add this to overrides instead - ignorePatterns: ["src/dynamicImport.js", "scripts/webframeworks-deploy-tests/hosting/*"], + ignorePatterns: [ + "src/dynamicImport.js", + "scripts/webframeworks-deploy-tests/hosting/**", + "scripts/frameworks-tests/vite-project/**", + ], }; diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 6051e5590cac..5e72bc66f0fc 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -81,16 +81,17 @@ jobs: node-version: - "16" script: - - npm run test:hosting - # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. - npm run test:client-integration - npm run test:emulator - - npm run test:import-export - npm run test:extensions-emulator - - npm run test:triggers-end-to-end - - npm run test:triggers-end-to-end:inspect + - npm run test:frameworks + - npm run test:hosting + # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. + - npm run test:import-export - npm run test:storage-deploy - npm run test:storage-emulator-integration + - npm run test:triggers-end-to-end + - npm run test:triggers-end-to-end:inspect steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.prettierignore b/.prettierignore index 60f523e5f03d..d75f7c02f61f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ /node_modules /lib/**/* /CONTRIBUTING.md +/scripts/frameworks-tests/vite-project/** diff --git a/package.json b/package.json index 95658a71db97..d93e282a3611 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "test:emulator": "bash ./scripts/emulator-tests/run.sh", "test:extensions-deploy": "bash ./scripts/extensions-deploy-tests/run.sh", "test:extensions-emulator": "bash ./scripts/extensions-emulator-tests/run.sh", + "test:frameworks": "bash ./scripts/frameworks-tests/run.sh", "test:functions-deploy": "bash ./scripts/functions-deploy-tests/run.sh", "test:hosting": "bash ./scripts/hosting-tests/run.sh", "test:hosting-rewrites": "bash ./scripts/hosting-tests/rewrites-tests/run.sh", diff --git a/scripts/frameworks-tests/run.sh b/scripts/frameworks-tests/run.sh new file mode 100755 index 000000000000..f21bc96cc4a5 --- /dev/null +++ b/scripts/frameworks-tests/run.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e +CWD="$(pwd)" + +echo "Running in ${CWD}" +echo "Running with node: $(which node)" +echo "Running with npm: $(which npm)" +echo "Running with Application Creds: ${GOOGLE_APPLICATION_CREDENTIALS}" + +echo "Target project: ${FBTOOLS_TARGET_PROJECT}" + +echo "Installing firebase-tools..." +./scripts/npm-link.sh +echo "Installed firebase-tools: $(which firebase)" + +echo "Enabling experiment..." +firebase experiments:enable webframeworks +echo "Enabled experiment." + +echo "Vite..." +cd scripts/frameworks-tests/vite-project +npm ci + +echo "Testing local emulators:start..." +firebase emulators:start --project "${FBTOOLS_TARGET_PROJECT}" & +PID="$!" +sleep 15 +VALUE="$(curl localhost:8534)" +echo "${VALUE}" | grep "Vite App" || (echo "Expected response to include \"Vite App\"." && false) +kill "$PID" +wait +echo "Tested local serve." diff --git a/scripts/frameworks-tests/vite-project/.gitignore b/scripts/frameworks-tests/vite-project/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/scripts/frameworks-tests/vite-project/counter.js b/scripts/frameworks-tests/vite-project/counter.js new file mode 100644 index 000000000000..12ae65abfaea --- /dev/null +++ b/scripts/frameworks-tests/vite-project/counter.js @@ -0,0 +1,9 @@ +export function setupCounter(element) { + let counter = 0 + const setCounter = (count) => { + counter = count + element.innerHTML = `count is ${counter}` + } + element.addEventListener('click', () => setCounter(++counter)) + setCounter(0) +} diff --git a/scripts/frameworks-tests/vite-project/firebase.json b/scripts/frameworks-tests/vite-project/firebase.json new file mode 100644 index 000000000000..5b87e488e172 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/firebase.json @@ -0,0 +1,15 @@ +{ + "hosting": { + "source": ".", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + }, + "emulators": { + "hosting": { + "port": "8534" + } + } +} diff --git a/scripts/frameworks-tests/vite-project/index.html b/scripts/frameworks-tests/vite-project/index.html new file mode 100644 index 000000000000..2ad7b1ab9e69 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/scripts/frameworks-tests/vite-project/javascript.svg b/scripts/frameworks-tests/vite-project/javascript.svg new file mode 100644 index 000000000000..f9abb2b728d7 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/javascript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/frameworks-tests/vite-project/main.js b/scripts/frameworks-tests/vite-project/main.js new file mode 100644 index 000000000000..727b4ea209e9 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/main.js @@ -0,0 +1,23 @@ +import './style.css' +import javascriptLogo from './javascript.svg' +import { setupCounter } from './counter.js' + +document.querySelector('#app').innerHTML = ` +
+ + + + + + +

Hello Vite!

+
+ +
+

+ Click on the Vite logo to learn more +

+
+` + +setupCounter(document.querySelector('#counter')) diff --git a/scripts/frameworks-tests/vite-project/package-lock.json b/scripts/frameworks-tests/vite-project/package-lock.json new file mode 100644 index 000000000000..a33faf3c34cd --- /dev/null +++ b/scripts/frameworks-tests/vite-project/package-lock.json @@ -0,0 +1,881 @@ +{ + "name": "vite-project", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "vite-project", + "version": "0.0.0", + "devDependencies": { + "vite": "^3.1.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.10.tgz", + "integrity": "sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.10.tgz", + "integrity": "sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.10.tgz", + "integrity": "sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.10", + "@esbuild/linux-loong64": "0.15.10", + "esbuild-android-64": "0.15.10", + "esbuild-android-arm64": "0.15.10", + "esbuild-darwin-64": "0.15.10", + "esbuild-darwin-arm64": "0.15.10", + "esbuild-freebsd-64": "0.15.10", + "esbuild-freebsd-arm64": "0.15.10", + "esbuild-linux-32": "0.15.10", + "esbuild-linux-64": "0.15.10", + "esbuild-linux-arm": "0.15.10", + "esbuild-linux-arm64": "0.15.10", + "esbuild-linux-mips64le": "0.15.10", + "esbuild-linux-ppc64le": "0.15.10", + "esbuild-linux-riscv64": "0.15.10", + "esbuild-linux-s390x": "0.15.10", + "esbuild-netbsd-64": "0.15.10", + "esbuild-openbsd-64": "0.15.10", + "esbuild-sunos-64": "0.15.10", + "esbuild-windows-32": "0.15.10", + "esbuild-windows-64": "0.15.10", + "esbuild-windows-arm64": "0.15.10" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.10.tgz", + "integrity": "sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.10.tgz", + "integrity": "sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.10.tgz", + "integrity": "sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.10.tgz", + "integrity": "sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.10.tgz", + "integrity": "sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.10.tgz", + "integrity": "sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.10.tgz", + "integrity": "sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.10.tgz", + "integrity": "sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.10.tgz", + "integrity": "sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.10.tgz", + "integrity": "sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.10.tgz", + "integrity": "sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.10.tgz", + "integrity": "sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.10.tgz", + "integrity": "sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.10.tgz", + "integrity": "sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.10.tgz", + "integrity": "sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.10.tgz", + "integrity": "sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.10.tgz", + "integrity": "sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.10.tgz", + "integrity": "sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.10.tgz", + "integrity": "sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.10.tgz", + "integrity": "sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", + "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.78.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", + "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", + "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": "~2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.10.tgz", + "integrity": "sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.10.tgz", + "integrity": "sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.10.tgz", + "integrity": "sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.10", + "@esbuild/linux-loong64": "0.15.10", + "esbuild-android-64": "0.15.10", + "esbuild-android-arm64": "0.15.10", + "esbuild-darwin-64": "0.15.10", + "esbuild-darwin-arm64": "0.15.10", + "esbuild-freebsd-64": "0.15.10", + "esbuild-freebsd-arm64": "0.15.10", + "esbuild-linux-32": "0.15.10", + "esbuild-linux-64": "0.15.10", + "esbuild-linux-arm": "0.15.10", + "esbuild-linux-arm64": "0.15.10", + "esbuild-linux-mips64le": "0.15.10", + "esbuild-linux-ppc64le": "0.15.10", + "esbuild-linux-riscv64": "0.15.10", + "esbuild-linux-s390x": "0.15.10", + "esbuild-netbsd-64": "0.15.10", + "esbuild-openbsd-64": "0.15.10", + "esbuild-sunos-64": "0.15.10", + "esbuild-windows-32": "0.15.10", + "esbuild-windows-64": "0.15.10", + "esbuild-windows-arm64": "0.15.10" + } + }, + "esbuild-android-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.10.tgz", + "integrity": "sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.10.tgz", + "integrity": "sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.10.tgz", + "integrity": "sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.10.tgz", + "integrity": "sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.10.tgz", + "integrity": "sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.10.tgz", + "integrity": "sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.10.tgz", + "integrity": "sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.10.tgz", + "integrity": "sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.10.tgz", + "integrity": "sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.10.tgz", + "integrity": "sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.10.tgz", + "integrity": "sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.10.tgz", + "integrity": "sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.10.tgz", + "integrity": "sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.10.tgz", + "integrity": "sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.10.tgz", + "integrity": "sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.10.tgz", + "integrity": "sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.10.tgz", + "integrity": "sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.10.tgz", + "integrity": "sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.10.tgz", + "integrity": "sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.10.tgz", + "integrity": "sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==", + "dev": true, + "optional": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", + "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.78.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", + "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "vite": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", + "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "dev": true, + "requires": { + "esbuild": "^0.15.9", + "fsevents": "~2.3.2", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": "~2.78.0" + } + } + } +} diff --git a/scripts/frameworks-tests/vite-project/package.json b/scripts/frameworks-tests/vite-project/package.json new file mode 100644 index 000000000000..4e55fa3ed808 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/package.json @@ -0,0 +1,14 @@ +{ + "name": "vite-project", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.1.0" + } +} \ No newline at end of file diff --git a/scripts/frameworks-tests/vite-project/public/vite.svg b/scripts/frameworks-tests/vite-project/public/vite.svg new file mode 100644 index 000000000000..e7b8dfb1b2a6 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/frameworks-tests/vite-project/style.css b/scripts/frameworks-tests/vite-project/style.css new file mode 100644 index 000000000000..12320801d363 --- /dev/null +++ b/scripts/frameworks-tests/vite-project/style.css @@ -0,0 +1,97 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index f232d6fb6624..ad0df66f597f 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -222,6 +222,8 @@ function scanDependencyTree(searchingFor: string, dependencies = {}): any { */ export function findDependency(name: string, options: Partial = {}) { const { cwd, depth, omitDev } = { ...DEFAULT_FIND_DEP_OPTIONS, ...options }; + const env: any = Object.assign({}, process.env); + delete env.NODE_ENV; const result = spawnSync( NPM_COMMAND, [ @@ -231,7 +233,7 @@ export function findDependency(name: string, options: Partial = ...(omitDev ? ["--omit", "dev"] : []), ...(depth === undefined ? [] : ["--depth", depth.toString(10)]), ], - { cwd } + { cwd, env } ); if (!result.stdout) return; const json = JSON.parse(result.stdout.toString()); From 12a8887117b25b92db03e47fb444961507396a22 Mon Sep 17 00:00:00 2001 From: Victor Fan Date: Thu, 13 Oct 2022 15:20:00 -0700 Subject: [PATCH 052/115] Unbreak default ints in text input (#5118) * Unbreak default ints in text input * add a comment explaining this * the ternary is not needed * add release note * format:other --- CHANGELOG.md | 1 + src/deploy/functions/params.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..83a2716fb823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fixes a crash in integer params when a default value is selected in the prompt. (#5118) diff --git a/src/deploy/functions/params.ts b/src/deploy/functions/params.ts index 5ea90edb081e..9059607ba41e 100644 --- a/src/deploy/functions/params.ts +++ b/src/deploy/functions/params.ts @@ -605,7 +605,10 @@ async function promptText( return promptText(prompt, input, resolvedDefault, converter); } } - const converted = converter(res); + // TODO(vsfan): the toString() is because PromptOnce()'s return type of string + // is wrong--it will return the type of the default if selected. Remove this + // hack once we fix the prompt.ts metaprogramming. + const converted = converter(res.toString()); if (typeof converted === "object") { logger.error(converted.message); return promptText(prompt, input, resolvedDefault, converter); From b0637b98c534b9e4c8c552e1e4245a71ae25253c Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Fri, 14 Oct 2022 08:59:01 -0700 Subject: [PATCH 053/115] Update error handling for fetchBlockingFunction() (#5120) * Update error handling for fetchBlockingFunction() * Fix spacing and error message * Move .text() call to try block * Fix nits * Add changelog Co-authored-by: Bryan Kendall --- CHANGELOG.md | 1 + src/emulator/auth/operations.ts | 46 ++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a2716fb823..5348ea55f82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Fixes a crash in integer params when a default value is selected in the prompt. (#5118) +- Fixes error handling for auth blocking functions. diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index e590ceb67dd8..d72d6faca3d3 100644 --- a/src/emulator/auth/operations.ts +++ b/src/emulator/auth/operations.ts @@ -859,7 +859,7 @@ function sendOobCode( if (reqBody.continueUrl) { assert( parseAbsoluteUri(reqBody.continueUrl), - "INVALID_CONTINUE_URI: ((expected an absolute URI with valid scheme and host))" + "INVALID_CONTINUE_URI : ((expected an absolute URI with valid scheme and host))" ); } @@ -2193,11 +2193,11 @@ function updateConfig( if (Object.prototype.hasOwnProperty.call(reqBody.blockingFunctions!.triggers, event)) { assert( Object.values(BlockingFunctionEvents).includes(event as BlockingFunctionEvents), - "INVALID_BLOCKING_FUNCTION: ((Event type is invalid.))" + "INVALID_BLOCKING_FUNCTION : ((Event type is invalid.))" ); assert( parseAbsoluteUri(reqBody.blockingFunctions!.triggers[event].functionUri!), - "INVALID_BLOCKING_FUNCTION: ((Expected an absolute URI with valid scheme and host.))" + "INVALID_BLOCKING_FUNCTION : ((Expected an absolute URI with valid scheme and host.))" ); } } @@ -2907,7 +2907,7 @@ function createTenant( reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] ): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { if (!(state instanceof AgentProjectState)) { - throw new InternalError("INTERNAL_ERROR: Can only create tenant in agent project", "INTERNAL"); + throw new InternalError("INTERNAL_ERROR : Can only create tenant in agent project", "INTERNAL"); } const mfaConfig = reqBody.mfaConfig ?? {}; @@ -3024,7 +3024,10 @@ async function fetchBlockingFunction( controller.abort(); }, timeoutMs); - let response; + let response: BlockingFunctionResponsePayload; + let ok: boolean; + let status: number; + let text: string; try { const res = await fetch(url, { method: "POST", @@ -3032,29 +3035,42 @@ async function fetchBlockingFunction( body: JSON.stringify(reqBody), signal: controller.signal, }); - const text = await res.text(); - assert( - res.ok, - `BLOCKING_FUNCTION_ERROR_RESPONSE: ((HTTP request to ${url} returned HTTP error${res.status}: ${text}))` - ); - response = JSON.parse(text) as BlockingFunctionResponsePayload; + ok = res.ok; + status = res.status; + text = await res.text(); } catch (thrown: any) { const err = thrown instanceof Error ? thrown : new Error(thrown); const isAbortError = err.name.includes("AbortError"); if (isAbortError) { throw new InternalError( - `BLOCKING_FUNCTION_ERROR_RESPONSE: ((Deadline exceeded making request to ${url}.))`, + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Deadline exceeded making request to ${url}.))`, err.message ); } + // All other server errors throw new InternalError( - `BLOCKING_FUNCTION_ERROR_RESPONSE: ((Failed to make request to ${url}.))`, + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Failed to make request to ${url}.))`, err.message ); } finally { clearTimeout(timeout); } + assert( + ok, + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((HTTP request to ${url} returned HTTP error ${status}: ${text}))` + ); + + try { + response = JSON.parse(text) as BlockingFunctionResponsePayload; + } catch (thrown: any) { + const err = thrown instanceof Error ? thrown : new Error(thrown); + throw new InternalError( + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response body is not valid JSON.))`, + err.message + ); + } + return processBlockingFunctionResponse(event, response); } @@ -3072,7 +3088,7 @@ function processBlockingFunctionResponse( const userRecord = response.userRecord; assert( userRecord.updateMask, - "BLOCKING_FUNCTION_ERROR_RESPONSE: ((Response UserRecord is missing updateMask.))" + "BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response UserRecord is missing updateMask.))" ); const mask = userRecord.updateMask; const fields = mask.split(","); @@ -3103,7 +3119,7 @@ function processBlockingFunctionResponse( extraClaims = userRecord.sessionClaims; } catch { throw new BadRequestError( - "BLOCKING_FUNCTION_ERROR_RESPONSE: ((Response has malformed session claims.))" + "BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response has malformed session claims.))" ); } break; From 59f914367af799fbe0413707abf7646435ca0984 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Oct 2022 11:26:07 -0700 Subject: [PATCH 054/115] Fix Storage rules emulator runtime issue (#5126) * Fix Storage rules emulator runtime issue * changelog * More specific * Formatting --- CHANGELOG.md | 1 + .../conformance/firebase-js-sdk.test.ts | 4 ++-- src/emulator/storage/rules/runtime.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5348ea55f82b..03daa5adcd53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Fixes a crash in integer params when a default value is selected in the prompt. (#5118) - Fixes error handling for auth blocking functions. +- Fixes bug preventing Storage Rules from updating when ruleset compilation completed successfully but with warnings diff --git a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts index df4accef337e..791e5ac57a5a 100644 --- a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts +++ b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts @@ -544,7 +544,7 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); await signInToFirebaseAuth(page); - const metadata = await page.evaluate(async (filename) => { + const metadata = await page.evaluate((filename) => { return firebase .storage() .ref(filename) @@ -574,7 +574,7 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); await signInToFirebaseAuth(page); - const updatedMetadata = await page.evaluate(async (filename) => { + const updatedMetadata = await page.evaluate((filename) => { return firebase.storage().ref(filename).updateMetadata({ cacheControl: null, contentDisposition: null, diff --git a/src/emulator/storage/rules/runtime.ts b/src/emulator/storage/rules/runtime.ts index 1addf2f1b5f5..1f53eb05bfda 100644 --- a/src/emulator/storage/rules/runtime.ts +++ b/src/emulator/storage/rules/runtime.ts @@ -285,7 +285,7 @@ export class StorageRulesRuntime { runtimeActionRequest )) as RuntimeActionLoadRulesetResponse; - if (response.errors.length || response.warnings.length) { + if (response.errors.length) { return { issues: StorageRulesIssues.fromResponse(response), }; From 5708902683a477cd1e06232c5af8d415597fc53d Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 14 Oct 2022 20:48:11 +0000 Subject: [PATCH 055/115] 11.14.4 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 42b5a04c564b..209c58121fac 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.14.3", + "version": "11.14.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.14.3", + "version": "11.14.4", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index d93e282a3611..86aae2dc1d51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.14.3", + "version": "11.14.4", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 31f9b00eb09b09fa51e26faefd35234127029807 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 14 Oct 2022 20:48:24 +0000 Subject: [PATCH 056/115] [firebase-release] Removed change log and reset repo after 11.14.4 release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03daa5adcd53..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Fixes a crash in integer params when a default value is selected in the prompt. (#5118) -- Fixes error handling for auth blocking functions. -- Fixes bug preventing Storage Rules from updating when ruleset compilation completed successfully but with warnings From 8079f7c785fcfb701aa83927ef8c8fd0343c50c5 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Mon, 17 Oct 2022 21:32:56 -0700 Subject: [PATCH 057/115] Various fixes and improvements. (#5139) * Make web frameworks public (#5136) * We don't need no public dir (#5142) * We don't need no public dir * Add docs * Add changelog Co-authored-by: Thomas Bouldin --- CHANGELOG.md | 1 + src/experiments.ts | 4 +++- src/frameworks/next/index.ts | 29 ++++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..8574c68ed0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +Fix a bug where next.js applications would fail to deploy if they did not have a public dir (#5142) diff --git a/src/experiments.ts b/src/experiments.ts index f515afe761b1..f1898b1ed6bf 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -81,10 +81,12 @@ export const ALL_EXPERIMENTS = experiments({ shortDescription: "Native support for popular web frameworks", fullDescription: "Adds support for popular web frameworks such as Next.js " + - "Nuxt, Netlify, Angular, and Vite-compatible frameworks. Firebase is " + + "Angular, React, Svelte, and Vite-compatible frameworks. Firebase is " + "committed to support these platforms long-term, but a manual migration " + "may be required when the non-experimental support for these frameworks " + "is released", + docsUri: "https://firebase.google.com/docs/hosting/frameworks-overview", + public: true, }, pintags: { shortDescription: "Adds the pinTag option to Run and Functions rewrites", diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index a24d30db5b53..b79c5a0510f2 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -51,6 +51,9 @@ function getNextVersion(cwd: string) { return findDependency("next", { cwd, depth: 0, omitDev: false })?.version; } +/** + * Returns whether this codebase is a Next.js backend. + */ export async function discover(dir: string) { if (!(await pathExists(join(dir, "package.json")))) return; if (!(await pathExists("next.config.js")) && !getNextVersion(dir)) return; @@ -58,6 +61,9 @@ export async function discover(dir: string) { return { mayWantBackend: true, publicDirectory: join(dir, "public") }; } +/** + * Build a next.js application. + */ export async function build(dir: string): Promise { const { default: nextBuild } = relativeRequire(dir, "next/dist/build"); @@ -133,6 +139,9 @@ export async function build(dir: string): Promise { return { wantsBackend, headers, redirects, rewrites }; } +/** + * Utility method used during project initialization. + */ export async function init(setup: any) { const language = await promptOnce({ type: "list", @@ -148,6 +157,9 @@ export async function init(setup: any) { ); } +/** + * Create a directory for SSG content. + */ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: string) { const { distDir } = await getConfig(sourceDir); const exportDetailPath = join(sourceDir, distDir, "export-detail.json"); @@ -157,8 +169,11 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin if (exportDetailJson?.success) { copy(exportDetailJson.outDirectory, destDir); } else { + const publicPath = join(sourceDir, "public"); await mkdir(join(destDir, "_next", "static"), { recursive: true }); - await copy(join(sourceDir, "public"), destDir); + if (await pathExists(publicPath)) { + await copy(publicPath, destDir); + } await copy(join(sourceDir, distDir, "static"), join(destDir, "_next", "static")); const serverPagesDir = join(sourceDir, distDir, "server", "pages"); @@ -202,6 +217,9 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin } } +/** + * Create a directory for SSR content. + */ export async function ɵcodegenFunctionsDirectory(sourceDir: string, destDir: string) { const { distDir } = await getConfig(sourceDir); const packageJsonBuffer = await readFile(join(sourceDir, "package.json")); @@ -227,13 +245,18 @@ export async function ɵcodegenFunctionsDirectory(sourceDir: string, destDir: st platform: "node", }); } - await mkdir(join(destDir, "public")); + if (await pathExists(join(sourceDir, "public"))) { + await mkdir(join(destDir, "public")); + await copy(join(sourceDir, "public"), join(destDir, "public")); + } await mkdirp(join(destDir, distDir)); - await copy(join(sourceDir, "public"), join(destDir, "public")); await copy(join(sourceDir, distDir), join(destDir, distDir)); return { packageJson, frameworksEntry: "next.js" }; } +/** + * Create a dev server. + */ export async function getDevModeHandle(dir: string) { const { default: next } = relativeRequire(dir, "next"); const nextApp = next({ From dad66d436bbc2bb9db9bb3d94dcb54073c51a4e5 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 18 Oct 2022 13:10:24 +0000 Subject: [PATCH 058/115] 11.15.0 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 209c58121fac..e699a4cf8cb6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.14.4", + "version": "11.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.14.4", + "version": "11.15.0", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 86aae2dc1d51..24b9b06986fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.14.4", + "version": "11.15.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 99b9d36ff9946941a230f81cdec254de935bc318 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 18 Oct 2022 13:10:37 +0000 Subject: [PATCH 059/115] [firebase-release] Removed change log and reset repo after 11.15.0 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8574c68ed0cf..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -Fix a bug where next.js applications would fail to deploy if they did not have a public dir (#5142) From 60ddb12c8fd1c4107e94d5a4790685660aa36883 Mon Sep 17 00:00:00 2001 From: Hsin-pei Toh <37965489+tohhsinpei@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:13:56 -0400 Subject: [PATCH 060/115] Update RTDB emulator version to 4.11.0 (#5150) --- CHANGELOG.md | 1 + src/emulator/downloadableEmulators.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..bdac0018789d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +Release RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index a1b1aed3597c..bbedb48a868e 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -27,14 +27,14 @@ const CACHE_DIR = export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { database: { - downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.10.0.jar"), + downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.11.0.jar"), version: "4.10.0", opts: { cacheDir: CACHE_DIR, remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.10.0.jar", - expectedSize: 34230230, - expectedChecksum: "e99b23f0e723813de4f4ea0e879b46b0", + "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.11.0.jar", + expectedSize: 34318940, + expectedChecksum: "311609538bd65666eb724ef47c2e6466", namePrefix: "firebase-database-emulator", }, }, From f29a7202e1a17e272f9f1617357d11367f7ce805 Mon Sep 17 00:00:00 2001 From: Hsin-pei Toh <37965489+tohhsinpei@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:59:23 -0400 Subject: [PATCH 061/115] Update RTDB emulator version to 4.11.0 (2) (#5151) --- src/emulator/downloadableEmulators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index bbedb48a868e..8732fb716e51 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -28,7 +28,7 @@ const CACHE_DIR = export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { database: { downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.11.0.jar"), - version: "4.10.0", + version: "4.11.0", opts: { cacheDir: CACHE_DIR, remoteUrl: From 7775f4828c64c72bfb0760cac9c68db4e193ea0a Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:04:21 -0700 Subject: [PATCH 062/115] Update the flag name in the warning message for singleProjectMode to match the actual flag (#5168) * Update the flag name in the warning message for singleProjectMode to match the actual flag --- src/emulator/auth/server.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emulator/auth/server.ts b/src/emulator/auth/server.ts index 8ee6d0b2080d..e63eef89086a 100644 --- a/src/emulator/auth/server.ts +++ b/src/emulator/auth/server.ts @@ -373,9 +373,8 @@ export async function createApp( const errorString = `Multiple projectIds are not recommended in single project mode. ` + `Requested project ID ${projectId}, but the emulator is configured for ` + - `${defaultProjectId}. This warning will become an error in the future. To opt-out of ` + - `single project mode add/set the \'"single_project_mode"\' false' property in the` + - ` firebase.json emulators config.`; + `${defaultProjectId}. To opt-out of single project mode add/set the ` + + `\'"singleProjectMode"\' false' property in the firebase.json emulators config.`; EmulatorLogger.forEmulator(Emulators.AUTH).log("WARN", errorString); if (singleProjectMode === SingleProjectMode.ERROR) { throw new BadRequestError(errorString); From 7ea2400786e32a510485e6e3299356da978b9164 Mon Sep 17 00:00:00 2001 From: Hsin-pei Toh <37965489+tohhsinpei@users.noreply.github.com> Date: Tue, 25 Oct 2022 14:09:23 -0400 Subject: [PATCH 063/115] Use template for emulator update details (#5166) * Use template for emulator versions * Factor out all emulator update details --- src/emulator/downloadableEmulators.ts | 141 ++++++++++++++++---------- src/emulator/types.ts | 6 ++ 2 files changed, 91 insertions(+), 56 deletions(-) diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index 8732fb716e51..95821f8b304f 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -4,6 +4,7 @@ import { DownloadableEmulatorCommand, DownloadableEmulatorDetails, EmulatorDownloadDetails, + EmulatorUpdateDetails, } from "./types"; import { Constants } from "./constants"; @@ -25,88 +26,116 @@ const EMULATOR_INSTANCE_KILL_TIMEOUT = 4000; /* ms */ const CACHE_DIR = process.env.FIREBASE_EMULATORS_PATH || path.join(os.homedir(), ".cache", "firebase", "emulators"); -export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { +const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDetails } = { database: { - downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.11.0.jar"), version: "4.11.0", + expectedSize: 34318940, + expectedChecksum: "311609538bd65666eb724ef47c2e6466", + }, + firestore: { + version: "1.15.1", + expectedSize: 61475851, + expectedChecksum: "4f41d24a3c0f3b55ea22804a424cc0ee", + }, + storage: { + version: "1.1.1", + expectedSize: 46448285, + expectedChecksum: "691982db4019d49d345a97151bdea7e2", + }, + ui: experiments.isEnabled("emulatoruisnapshot") + ? { version: "SNAPSHOT", expectedSize: -1, expectedChecksum: "" } + : { + version: "1.11.1", + expectedSize: 3061713, + expectedChecksum: "a4944414518be206280b495f526f18bf", + }, + pubsub: { + version: "0.1.0", + expectedSize: 36623622, + expectedChecksum: "81704b24737d4968734d3e175f4cde71", + }, +}; + +export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { + database: { + downloadPath: path.join( + CACHE_DIR, + `firebase-database-emulator-v${EMULATOR_UPDATE_DETAILS.database.version}.jar` + ), + version: EMULATOR_UPDATE_DETAILS.database.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.11.0.jar", - expectedSize: 34318940, - expectedChecksum: "311609538bd65666eb724ef47c2e6466", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v${EMULATOR_UPDATE_DETAILS.database.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.database.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.database.expectedChecksum, namePrefix: "firebase-database-emulator", }, }, firestore: { - downloadPath: path.join(CACHE_DIR, "cloud-firestore-emulator-v1.15.1.jar"), - version: "1.15.1", + downloadPath: path.join( + CACHE_DIR, + `cloud-firestore-emulator-v${EMULATOR_UPDATE_DETAILS.firestore.version}.jar` + ), + version: EMULATOR_UPDATE_DETAILS.firestore.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.15.1.jar", - expectedSize: 61475851, - expectedChecksum: "4f41d24a3c0f3b55ea22804a424cc0ee", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v${EMULATOR_UPDATE_DETAILS.firestore.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.firestore.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.firestore.expectedChecksum, namePrefix: "cloud-firestore-emulator", }, }, storage: { - downloadPath: path.join(CACHE_DIR, "cloud-storage-rules-runtime-v1.1.1.jar"), - version: "1.1.1", + downloadPath: path.join( + CACHE_DIR, + `cloud-storage-rules-runtime-v${EMULATOR_UPDATE_DETAILS.storage.version}.jar` + ), + version: EMULATOR_UPDATE_DETAILS.storage.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-storage-rules-runtime-v1.1.1.jar", - expectedSize: 46448285, - expectedChecksum: "691982db4019d49d345a97151bdea7e2", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-storage-rules-runtime-v${EMULATOR_UPDATE_DETAILS.storage.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.storage.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.storage.expectedChecksum, namePrefix: "cloud-storage-rules-emulator", }, }, - ui: experiments.isEnabled("emulatoruisnapshot") - ? { - version: "SNAPSHOT", - downloadPath: path.join(CACHE_DIR, "ui-vSNAPSHOT.zip"), - unzipDir: path.join(CACHE_DIR, "ui-vSNAPSHOT"), - binaryPath: path.join(CACHE_DIR, "ui-vSNAPSHOT", "server", "server.js"), - opts: { - cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-vSNAPSHOT.zip", - expectedSize: -1, - expectedChecksum: "", - skipCache: true, - skipChecksumAndSize: true, - namePrefix: "ui", - }, - } - : { - version: "1.11.1", - downloadPath: path.join(CACHE_DIR, "ui-v1.11.1.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.11.1"), - binaryPath: path.join(CACHE_DIR, "ui-v1.11.1", "server", "server.js"), - opts: { - cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.11.1.zip", - expectedSize: 3061713, - expectedChecksum: "a4944414518be206280b495f526f18bf", - namePrefix: "ui", - }, - }, + ui: { + version: EMULATOR_UPDATE_DETAILS.ui.version, + downloadPath: path.join(CACHE_DIR, `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}.zip`), + unzipDir: path.join(CACHE_DIR, `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}`), + binaryPath: path.join( + CACHE_DIR, + `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}`, + "server", + "server.js" + ), + opts: { + cacheDir: CACHE_DIR, + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v${EMULATOR_UPDATE_DETAILS.ui.version}.zip`, + expectedSize: EMULATOR_UPDATE_DETAILS.ui.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.ui.expectedChecksum, + skipCache: experiments.isEnabled("emulatoruisnapshot"), + skipChecksumAndSize: experiments.isEnabled("emulatoruisnapshot"), + namePrefix: "ui", + }, + }, pubsub: { - downloadPath: path.join(CACHE_DIR, "pubsub-emulator-0.1.0.zip"), - version: "0.1.0", - unzipDir: path.join(CACHE_DIR, "pubsub-emulator-0.1.0"), + downloadPath: path.join( + CACHE_DIR, + `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}.zip` + ), + version: EMULATOR_UPDATE_DETAILS.pubsub.version, + unzipDir: path.join(CACHE_DIR, `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`), binaryPath: path.join( CACHE_DIR, - "pubsub-emulator-0.1.0", + `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`, `pubsub-emulator/bin/cloud-pubsub-emulator${process.platform === "win32" ? ".bat" : ""}` ), opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.1.0.zip", - expectedSize: 36623622, - expectedChecksum: "81704b24737d4968734d3e175f4cde71", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}.zip`, + expectedSize: EMULATOR_UPDATE_DETAILS.pubsub.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.pubsub.expectedChecksum, namePrefix: "pubsub-emulator", }, }, diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 78d5c7475aac..97d19fa97037 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -159,6 +159,12 @@ export interface EmulatorDownloadOptions { skipCache?: boolean; } +export interface EmulatorUpdateDetails { + version: string; + expectedSize: number; + expectedChecksum: string; +} + export interface EmulatorDownloadDetails { opts: EmulatorDownloadOptions; From a81ad120ba7f5a7152bcfc7696a229c6f35c4e46 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 25 Oct 2022 12:45:29 -0700 Subject: [PATCH 064/115] Track which web framework is deployed (#5140) --- src/deploy/index.ts | 7 ++++++- src/frameworks/index.ts | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/deploy/index.ts b/src/deploy/index.ts index e1e885f8e9df..fc07e21d43d9 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -61,10 +61,11 @@ export const deploy = async function ( if (targetNames.includes("hosting")) { const config = options.config.get("hosting"); + let deployedFrameworks: string[] = []; if (Array.isArray(config) ? config.some((it) => it.source) : config.source) { experiments.assertEnabled("webframeworks", "deploy a web framework to hosting"); const usedToTargetFunctions = targetNames.includes("functions"); - await prepareFrameworks(targetNames, context, options); + deployedFrameworks = await prepareFrameworks(targetNames, context, options); const nowTargetsFunctions = targetNames.includes("functions"); if (nowTargetsFunctions && !usedToTargetFunctions) { if (context.hostingChannel && !experiments.isEnabled("pintags")) { @@ -74,7 +75,11 @@ export const deploy = async function ( } await requirePermissions(TARGET_PERMISSIONS["functions"]); } + } else { + const count = Array.isArray(config) ? config.length : 1; + deployedFrameworks = Array(count).fill("classic"); } + await Promise.all(deployedFrameworks.map((framework) => track("hosting_deploy", framework))); } for (const targetName of targetNames) { diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index ad0df66f597f..3b4bcd2ccdd6 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -248,7 +248,7 @@ export async function prepareFrameworks( context: any, options: any, emulators: EmulatorInfo[] = [] -) { +): Promise { // `firebase-frameworks` requires Node >= 16. We must check for this to avoid horrible errors. const nodeVersion = process.version; if (!semver.satisfies(nodeVersion, ">=16.0.0")) { @@ -257,6 +257,7 @@ export async function prepareFrameworks( ); } + const deployedFrameworks: string[] = []; const project = needProjectId(context); const { projectRoot } = options; const account = getProjectDefaultAccount(projectRoot); @@ -282,10 +283,15 @@ export async function prepareFrameworks( } const configs = hostingConfig(options); let firebaseDefaults: FirebaseDefaults | undefined = undefined; - if (configs.length === 0) return; + if (configs.length === 0) { + return deployedFrameworks; + } for (const config of configs) { const { source, site, public: publicDir } = config; - if (!source) continue; + if (!source) { + deployedFrameworks.push("classic"); + continue; + } config.rewrites ||= []; config.redirects ||= []; config.headers ||= []; @@ -293,8 +299,9 @@ export async function prepareFrameworks( const dist = join(projectRoot, ".firebase", site); const hostingDist = join(dist, "hosting"); const functionsDist = join(dist, "functions"); - if (publicDir) + if (publicDir) { throw new Error(`hosting.public and hosting.source cannot both be set in firebase.json`); + } const getProjectPath = (...args: string[]) => join(projectRoot, source, ...args); const functionName = `ssr${site.toLowerCase().replace(/-/g, "")}`; const usesFirebaseAdminSdk = !!findDependency("firebase-admin", { cwd: getProjectPath() }); @@ -385,8 +392,9 @@ export async function prepareFrameworks( // Attach the handle to options, it will be used when spinning up superstatic options.frameworksDevModeHandle = devModeHandle; // null is the dev-mode entry for firebase-framework-tools - if (mayWantBackend && firebaseDefaults) + if (mayWantBackend && firebaseDefaults) { codegenFunctionsDirectory = codegenDevModeFunctionsDirectory; + } } else { const { wantsBackend = false, @@ -403,6 +411,7 @@ export async function prepareFrameworks( config.public = relative(projectRoot, hostingDist); if (wantsBackend) codegenFunctionsDirectory = codegenProdModeFunctionsDirectory; } + deployedFrameworks.push(`${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`); if (codegenFunctionsDirectory) { if (firebaseDefaults) firebaseDefaults._authTokenSyncURL = "/__session"; @@ -546,6 +555,7 @@ exports.ssr = onRequest((req, res) => server.then(it => it.handle(req, res))); }); } } + return deployedFrameworks; } function codegenDevModeFunctionsDirectory() { From fd7cc7653c5dd891aca13ca316ed226752a66dec Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 25 Oct 2022 14:39:37 -0700 Subject: [PATCH 065/115] downgrade superstatic to v8 (#5172) * downgrade superstatic to v8 * fix superstatic importing --- CHANGELOG.md | 3 +- npm-shrinkwrap.json | 488 ++++++++++++++++++++++++++++++++++++------- package.json | 2 +- src/serve/hosting.ts | 10 +- 4 files changed, 418 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdac0018789d..513ddd4e434b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -Release RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. +- Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. +- Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues in the Hosting emulator. diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e699a4cf8cb6..a8c9e68dfb4e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -57,7 +57,7 @@ "stream-chain": "^2.2.4", "stream-json": "^1.7.3", "strip-ansi": "^6.0.1", - "superstatic": "^9.0.0", + "superstatic": "^8.0.0", "tar": "^6.1.11", "tcp-port-used": "^1.0.2", "tmp": "^0.2.1", @@ -2531,8 +2531,7 @@ "node_modules/@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "node_modules/@types/configstore": { "version": "4.0.0", @@ -3612,6 +3611,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -4052,7 +4059,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", @@ -4074,7 +4080,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "dependencies": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -4090,7 +4095,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4103,7 +4107,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4114,14 +4117,12 @@ "node_modules/boxen/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/boxen/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4130,7 +4131,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4454,7 +4454,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -4846,6 +4845,14 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "node_modules/compare-semver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", + "integrity": "sha512-AENcdfhxsMCzzl+QRdOwMQeA8tZBEEacAmA4pGPoyco27G9sIaM98WNYkcToC9O0wIx1vE+1ErmaM4t0/fXhMw==", + "dependencies": { + "semver": "^5.0.1" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -7432,6 +7439,25 @@ "toxic": "^1.0.0" } }, + "node_modules/global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dependencies": { + "ini": "1.3.7" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7982,6 +8008,17 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -8518,6 +8555,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dependencies": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -8532,6 +8584,14 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "optional": true }, + "node_modules/is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -13029,6 +13089,28 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==", + "dependencies": { + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13193,47 +13275,81 @@ } }, "node_modules/superstatic": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.0.tgz", - "integrity": "sha512-4rvzTZdqBPtCjeo/V4YkbBeDnHxI2+3jP1FHGzvTeDswq+HQFB7l3JTjq31BfyJFTogn8JmbDW9sKOeBUGDAhg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-8.0.0.tgz", + "integrity": "sha512-PqlA2xuEwOlRZsknl58A/rZEmgCUcfWIFec0bn10wYE5/tbMhEbMXGHCYDppiXLXcuhGHyOp1IimM2hLqkLLuw==", "dependencies": { "basic-auth-connect": "^1.0.0", - "commander": "^9.4.0", + "chalk": "^1.1.3", + "commander": "^9.2.0", + "compare-semver": "^1.0.0", "compression": "^1.7.0", - "connect": "^3.7.0", + "connect": "^3.6.2", "destroy": "^1.0.4", "fast-url-parser": "^1.1.3", "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "lodash": "^4.17.19", - "mime-types": "^2.1.35", - "minimatch": "^5.1.0", + "mime-types": "^2.1.16", + "minimatch": "^3.0.4", "morgan": "^1.8.2", "on-finished": "^2.2.0", "on-headers": "^1.0.0", "path-to-regexp": "^1.8.0", "router": "^1.3.1", - "update-notifier": "^5.1.0" + "string-length": "^1.0.0", + "update-notifier": "^4.1.1" }, "bin": { - "superstatic": "lib/bin/server.js" + "superstatic": "bin/server" }, "engines": { - "node": "^14.18.0 || >=16.4.0" + "node": ">= 12.20" }, "optionalDependencies": { - "re2": "^1.17.7" + "re2": "^1.15.8" } }, - "node_modules/superstatic/node_modules/brace-expansion": { + "node_modules/superstatic/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superstatic/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superstatic/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "balanced-match": "^1.0.0" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, + "node_modules/superstatic/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/superstatic/node_modules/commander": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", @@ -13242,22 +13358,19 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/superstatic/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/superstatic/node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "node_modules/superstatic/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/superstatic/node_modules/path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -13266,6 +13379,88 @@ "isarray": "0.0.1" } }, + "node_modules/superstatic/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superstatic/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/superstatic/node_modules/update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dependencies": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/superstatic/node_modules/update-notifier/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/superstatic/node_modules/update-notifier/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/superstatic/node_modules/update-notifier/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supertest": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.3.tgz", @@ -13605,7 +13800,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", - "dev": true, "engines": { "node": ">=8" }, @@ -13882,7 +14076,6 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, "engines": { "node": ">=8" } @@ -16870,8 +17063,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/configstore": { "version": "4.0.0", @@ -17761,6 +17953,11 @@ } } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -18128,7 +18325,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, "requires": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", @@ -18144,7 +18340,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -18154,7 +18349,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -18164,7 +18358,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -18172,20 +18365,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -18418,8 +18608,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "camelcase-keys": { "version": "6.2.2", @@ -18706,6 +18895,14 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "compare-semver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", + "integrity": "sha512-AENcdfhxsMCzzl+QRdOwMQeA8tZBEEacAmA4pGPoyco27G9sIaM98WNYkcToC9O0wIx1vE+1ErmaM4t0/fXhMw==", + "requires": { + "semver": "^5.0.1" + } + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -20719,6 +20916,21 @@ "toxic": "^1.0.0" } }, + "global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "requires": { + "ini": "1.3.7" + }, + "dependencies": { + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + } + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -21165,6 +21377,14 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "requires": { + "ansi-regex": "^2.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -21566,6 +21786,15 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -21577,6 +21806,11 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "optional": true }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -25104,6 +25338,24 @@ "safe-buffer": "~5.1.0" } }, + "string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==", + "requires": { + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -25218,57 +25470,79 @@ } }, "superstatic": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.0.tgz", - "integrity": "sha512-4rvzTZdqBPtCjeo/V4YkbBeDnHxI2+3jP1FHGzvTeDswq+HQFB7l3JTjq31BfyJFTogn8JmbDW9sKOeBUGDAhg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-8.0.0.tgz", + "integrity": "sha512-PqlA2xuEwOlRZsknl58A/rZEmgCUcfWIFec0bn10wYE5/tbMhEbMXGHCYDppiXLXcuhGHyOp1IimM2hLqkLLuw==", "requires": { "basic-auth-connect": "^1.0.0", - "commander": "^9.4.0", + "chalk": "^1.1.3", + "commander": "^9.2.0", + "compare-semver": "^1.0.0", "compression": "^1.7.0", - "connect": "^3.7.0", + "connect": "^3.6.2", "destroy": "^1.0.4", "fast-url-parser": "^1.1.3", "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "lodash": "^4.17.19", - "mime-types": "^2.1.35", - "minimatch": "^5.1.0", + "mime-types": "^2.1.16", + "minimatch": "^3.0.4", "morgan": "^1.8.2", "on-finished": "^2.2.0", "on-headers": "^1.0.0", "path-to-regexp": "^1.8.0", - "re2": "^1.17.7", + "re2": "^1.15.8", "router": "^1.3.1", - "update-notifier": "^5.1.0" + "string-length": "^1.0.0", + "update-notifier": "^4.1.1" }, "dependencies": { - "brace-expansion": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "balanced-match": "^1.0.0" + "color-name": "~1.1.4" } }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "commander": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==" }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "requires": { - "brace-expansion": "^2.0.1" - } - }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -25276,6 +25550,66 @@ "requires": { "isarray": "0.0.1" } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } } } }, @@ -25543,8 +25877,7 @@ "term-size": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", - "dev": true + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" }, "terser": { "version": "5.15.0", @@ -25746,8 +26079,7 @@ "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" }, "type-is": { "version": "1.6.18", diff --git a/package.json b/package.json index 24b9b06986fb..35c4537e46b6 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "stream-chain": "^2.2.4", "stream-json": "^1.7.3", "strip-ansi": "^6.0.1", - "superstatic": "^9.0.0", + "superstatic": "^8.0.0", "tar": "^6.1.11", "tcp-port-used": "^1.0.2", "tmp": "^0.2.1", diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index 8b75cbcea6ab..e2bb51cbdd02 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -1,8 +1,8 @@ const morgan = require("morgan"); -import { IncomingMessage, ServerResponse } from "http"; -import { server as superstatic } from "superstatic"; +const { server: superstatic } = require("superstatic"); // eslint-disable-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment import * as clc from "colorette"; import { isIPv4 } from "net"; +import { NextFunction, Request, Response } from "express"; import { detectProjectRoot } from "../detectProjectRoot"; import { FirebaseError } from "../error"; @@ -52,13 +52,13 @@ function startServer(options: any, config: any, port: number, init: TemplateServ const server = superstatic({ debug: false, port: port, - hostname: options.host, + host: options.host, config: config, compression: true, - cwd: detectProjectRoot(options) || undefined, + cwd: detectProjectRoot(options), stack: "strict", before: { - files: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { + files: (req: Request, res: Response, next: NextFunction) => { // We do these in a single method to ensure order of operations morganMiddleware(req, res, () => null); firebaseMiddleware(req, res, next); From d67881de4848b52f821d993227ab62458791215f Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 25 Oct 2022 14:52:39 -0700 Subject: [PATCH 066/115] build:publish should also run copyfiles (#5173) * build:publish should also run copyfiles * changelog * formatting is hard * Update CHANGELOG.md --- CHANGELOG.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513ddd4e434b..df80ce2f267c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. -- Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues in the Hosting emulator. +- Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. +- Fixes internal library that was not being correctly published. diff --git a/package.json b/package.json index 35c4537e46b6..93cb8e3fa860 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "build": "tsc && npm run copyfiles", - "build:publish": "tsc --build tsconfig.publish.json", + "build:publish": "tsc --build tsconfig.publish.json && npm run copyfiles", "build:watch": "npm run build && tsc --watch", "clean": "rimraf lib dev", "copyfiles": "node -e \"const fs = require('fs'); fs.mkdirSync('./lib', {recursive:true}); fs.copyFileSync('./src/dynamicImport.js', './lib/dynamicImport.js')\"", From 2b4261ef801a5e95ef62d05297a2d2cfddee20f3 Mon Sep 17 00:00:00 2001 From: joehan Date: Wed, 26 Oct 2022 09:29:57 -0700 Subject: [PATCH 067/115] Provisioning checks should be best effort (#5163) --- CHANGELOG.md | 1 + src/extensions/provisioningHelper.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df80ce2f267c..7c58e867110a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixes an issue where an error during product provisioning check would block `firebase deploy --only extensions` (#5074). - Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. - Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. - Fixes internal library that was not being correctly published. diff --git a/src/extensions/provisioningHelper.ts b/src/extensions/provisioningHelper.ts index 3d71c9b4c951..ac4f91e3184f 100644 --- a/src/extensions/provisioningHelper.ts +++ b/src/extensions/provisioningHelper.ts @@ -7,6 +7,7 @@ import { Client } from "../apiv2"; import { flattenArray } from "../functional"; import { FirebaseError } from "../error"; import { getExtensionSpec, InstanceSpec } from "../deploy/extensions/planner"; +import { logger } from "../logger"; /** Product for which provisioning can be (or is) deferred */ export enum DeferredProduct { @@ -55,12 +56,16 @@ async function checkProducts(projectId: string, usedProducts: DeferredProduct[]) if (usedProducts.includes(DeferredProduct.AUTH)) { isAuthProvisionedPromise = isAuthProvisioned(projectId); } - - if (isStorageProvisionedPromise && !(await isStorageProvisionedPromise)) { - needProvisioning.push(DeferredProduct.STORAGE); - } - if (isAuthProvisionedPromise && !(await isAuthProvisionedPromise)) { - needProvisioning.push(DeferredProduct.AUTH); + try { + if (isStorageProvisionedPromise && !(await isStorageProvisionedPromise)) { + needProvisioning.push(DeferredProduct.STORAGE); + } + if (isAuthProvisionedPromise && !(await isAuthProvisionedPromise)) { + needProvisioning.push(DeferredProduct.AUTH); + } + } catch (err: any) { + // If a provisioning check throws, we should fail open since this is best effort. + logger.debug(`Error while checking product provisioning, failing open: ${err}`); } if (needProvisioning.length > 0) { From 793253ff4516e1d78ea14d8ef162a9540abc32cc Mon Sep 17 00:00:00 2001 From: Hsin-pei Toh <37965489+tohhsinpei@users.noreply.github.com> Date: Wed, 26 Oct 2022 15:50:16 -0400 Subject: [PATCH 068/115] Add `--disable-triggers` flag to database write commands (#5179) * Add --disable-triggers flag to database write commands * Update unit tests * Add changelog * Address PR feedback: add test, make constructor arg required --- CHANGELOG.md | 1 + src/commands/database-push.ts | 5 +++++ src/commands/database-remove.ts | 3 ++- src/commands/database-set.ts | 5 +++++ src/commands/database-update.ts | 5 +++++ src/database/remove.ts | 5 +++-- src/database/removeRemote.ts | 10 ++++++++-- src/test/database/remove.spec.ts | 18 ++++++++++++++---- src/test/database/removeRemote.spec.ts | 23 ++++++++++++++++++----- 9 files changed, 61 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c58e867110a..0a147d85e0e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ - Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. - Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. - Fixes internal library that was not being correctly published. +- Adds `--disable-triggers` flag to RTDB write commands. diff --git a/src/commands/database-push.ts b/src/commands/database-push.ts index 9f1e3ed234c4..7777f36dbfa3 100644 --- a/src/commands/database-push.ts +++ b/src/commands/database-push.ts @@ -21,6 +21,7 @@ export const command = new Command("database:push [infile]") "--instance ", "use the database .firebaseio.com (if omitted, use default database instance)" ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) @@ -33,6 +34,9 @@ export const command = new Command("database:push [infile]") utils.stringToStream(options.data) || (infile ? fs.createReadStream(infile) : process.stdin); const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); const u = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + u.searchParams.set("disableTriggers", "true"); + } if (!infile && !options.data) { utils.explainStdin(); @@ -46,6 +50,7 @@ export const command = new Command("database:push [infile]") method: "POST", path: u.pathname, body: inStream, + queryParams: u.searchParams, }); } catch (err: any) { logger.debug(err); diff --git a/src/commands/database-remove.ts b/src/commands/database-remove.ts index 6b0012090c97..04a38e461465 100644 --- a/src/commands/database-remove.ts +++ b/src/commands/database-remove.ts @@ -17,6 +17,7 @@ export const command = new Command("database:remove ") "--instance ", "use the database .firebaseio.com (if omitted, use default database instance)" ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) @@ -40,7 +41,7 @@ export const command = new Command("database:remove ") return utils.reject("Command aborted.", { exit: 1 }); } - const removeOps = new DatabaseRemove(options.instance, path, origin); + const removeOps = new DatabaseRemove(options.instance, path, origin, !!options.disableTriggers); await removeOps.execute(); utils.logSuccess("Data removed successfully"); }); diff --git a/src/commands/database-set.ts b/src/commands/database-set.ts index 6c9d788e1444..67304569c0f7 100644 --- a/src/commands/database-set.ts +++ b/src/commands/database-set.ts @@ -23,6 +23,7 @@ export const command = new Command("database:set [infile]") "--instance ", "use the database .firebaseio.com (if omitted, use default database instance)" ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) @@ -34,6 +35,9 @@ export const command = new Command("database:set [infile]") const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); const dbPath = utils.getDatabaseUrl(origin, options.instance, path); const dbJsonURL = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + dbJsonURL.searchParams.set("disableTriggers", "true"); + } const confirm = await promptOnce( { @@ -61,6 +65,7 @@ export const command = new Command("database:set [infile]") method: "PUT", path: dbJsonURL.pathname, body: inStream, + queryParams: dbJsonURL.searchParams, }); } catch (err: any) { logger.debug(err); diff --git a/src/commands/database-update.ts b/src/commands/database-update.ts index 01f8dc216233..f1b6d494c9c3 100644 --- a/src/commands/database-update.ts +++ b/src/commands/database-update.ts @@ -23,6 +23,7 @@ export const command = new Command("database:update [infile]") "--instance ", "use the database .firebaseio.com (if omitted, use default database instance)" ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) @@ -51,6 +52,9 @@ export const command = new Command("database:update [infile]") (infile && fs.createReadStream(infile)) || process.stdin; const jsonUrl = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + jsonUrl.searchParams.set("disableTriggers", "true"); + } if (!infile && !options.data) { utils.explainStdin(); @@ -62,6 +66,7 @@ export const command = new Command("database:update [infile]") method: "PATCH", path: jsonUrl.pathname, body: inStream, + queryParams: jsonUrl.searchParams, }); } catch (err: any) { throw new FirebaseError("Unexpected error while setting data"); diff --git a/src/database/remove.ts b/src/database/remove.ts index 9f5297d80191..2d0d863731cb 100644 --- a/src/database/remove.ts +++ b/src/database/remove.ts @@ -29,10 +29,11 @@ export default class DatabaseRemove { * @param instance RTBD instance ID. * @param path path to delete. * @param host db host. + * @param disableTriggers if true, suppresses any Cloud functions that would be triggered by this operation. */ - constructor(instance: string, path: string, host: string) { + constructor(instance: string, path: string, host: string, disableTriggers: boolean) { this.path = path; - this.remote = new RTDBRemoveRemote(instance, host); + this.remote = new RTDBRemoveRemote(instance, host, disableTriggers); this.deleteJobStack = new Stack({ name: "delete stack", concurrency: 1, diff --git a/src/database/removeRemote.ts b/src/database/removeRemote.ts index 2c655651e5c1..60885f411511 100644 --- a/src/database/removeRemote.ts +++ b/src/database/removeRemote.ts @@ -22,10 +22,12 @@ export class RTDBRemoveRemote implements RemoveRemote { private instance: string; private host: string; private apiClient: Client; + private disableTriggers: boolean; - constructor(instance: string, host: string) { + constructor(instance: string, host: string, disableTriggers: boolean) { this.instance = instance; this.host = host; + this.disableTriggers = disableTriggers; const url = new URL(utils.getDatabaseUrl(this.host, this.instance, "/")); this.apiClient = new Client({ urlPrefix: url.origin, auth: true }); @@ -46,7 +48,11 @@ export class RTDBRemoveRemote implements RemoveRemote { private async patch(path: string, body: any, note: string): Promise { const t0 = Date.now(); const url = new URL(utils.getDatabaseUrl(this.host, this.instance, path + ".json")); - const queryParams = { print: "silent", writeSizeLimit: "tiny" }; + const queryParams = { + print: "silent", + writeSizeLimit: "tiny", + disableTriggers: this.disableTriggers.toString(), + }; const res = await this.apiClient.request({ method: "PATCH", path: url.pathname, diff --git a/src/test/database/remove.spec.ts b/src/test/database/remove.spec.ts index 4a8c4998fc77..9938a38be038 100644 --- a/src/test/database/remove.spec.ts +++ b/src/test/database/remove.spec.ts @@ -8,7 +8,7 @@ const HOST = "https://firebaseio.com"; describe("DatabaseRemove", () => { it("should remove tiny tree", async () => { const fakeDb = new FakeRemoveRemote({ c: 1 }); - const removeOps = new DatabaseRemove("test-tiny-tree", "/", HOST); + const removeOps = new DatabaseRemove("test-tiny-tree", "/", HOST, /* disableTriggers= */ false); removeOps.remote = fakeDb; await removeOps.execute(); expect(fakeDb.data).to.eql(null); @@ -29,7 +29,7 @@ describe("DatabaseRemove", () => { const fakeList = new FakeListRemote(data); const fakeDb = new FakeRemoveRemote(data); - const removeOps = new DatabaseRemove("test-sub-path", "/a", HOST); + const removeOps = new DatabaseRemove("test-sub-path", "/a", HOST, /* disableTriggers= */ false); removeOps.remote = fakeDb; removeOps.listRemote = fakeList; await removeOps.execute(); @@ -57,7 +57,12 @@ describe("DatabaseRemove", () => { const data = buildData(3, 5); const fakeDb = new FakeRemoveRemote(data, threshold); const fakeLister = new FakeListRemote(data); - const removeOps = new DatabaseRemove("test-nested-tree", "/", HOST); + const removeOps = new DatabaseRemove( + "test-nested-tree", + "/", + HOST, + /* disableTriggers= */ false + ); removeOps.remote = fakeDb; removeOps.listRemote = fakeLister; await removeOps.execute(); @@ -68,7 +73,12 @@ describe("DatabaseRemove", () => { const data = buildData(1232, 1); const fakeDb = new FakeRemoveRemote(data, threshold); const fakeList = new FakeListRemote(data); - const removeOps = new DatabaseRemove("test-remover", "/", HOST); + const removeOps = new DatabaseRemove( + "test-remover", + "/", + HOST, + /* disableTriggers= */ false + ); removeOps.remote = fakeDb; removeOps.listRemote = fakeList; await removeOps.execute(); diff --git a/src/test/database/removeRemote.spec.ts b/src/test/database/removeRemote.spec.ts index 869c6a741ddb..3f8357df0599 100644 --- a/src/test/database/removeRemote.spec.ts +++ b/src/test/database/removeRemote.spec.ts @@ -7,7 +7,7 @@ import { RTDBRemoveRemote } from "../../database/removeRemote"; describe("RemoveRemote", () => { const instance = "fake-db"; const host = "https://firebaseio.com"; - const remote = new RTDBRemoveRemote(instance, host); + const remote = new RTDBRemoveRemote(instance, host, /* disableTriggers= */ false); const serverUrl = utils.getDatabaseUrl(host, instance, ""); afterEach(() => { @@ -17,7 +17,7 @@ describe("RemoveRemote", () => { it("should return true when patch is small", () => { nock(serverUrl) .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) + .query({ print: "silent", writeSizeLimit: "tiny", disableTriggers: "false" }) .reply(200, {}); return expect(remote.deletePath("/a/b")).to.eventually.eql(true); }); @@ -25,7 +25,7 @@ describe("RemoveRemote", () => { it("should return false whem patch is large", () => { nock(serverUrl) .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) + .query({ print: "silent", writeSizeLimit: "tiny", disableTriggers: "false" }) .reply(400, { error: "Data requested exceeds the maximum size that can be accessed with a single request.", @@ -36,7 +36,7 @@ describe("RemoveRemote", () => { it("should return true when multi-path patch is small", () => { nock(serverUrl) .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) + .query({ print: "silent", writeSizeLimit: "tiny", disableTriggers: "false" }) .reply(200, {}); return expect(remote.deleteSubPath("/a/b", ["1", "2", "3"])).to.eventually.eql(true); }); @@ -44,11 +44,24 @@ describe("RemoveRemote", () => { it("should return false when multi-path patch is large", () => { nock(serverUrl) .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) + .query({ print: "silent", writeSizeLimit: "tiny", disableTriggers: "false" }) .reply(400, { error: "Data requested exceeds the maximum size that can be accessed with a single request.", }); return expect(remote.deleteSubPath("/a/b", ["1", "2", "3"])).to.eventually.eql(false); }); + + it("should send disableTriggers param", () => { + const remoteWithDisableTriggers = new RTDBRemoveRemote( + instance, + host, + /* disableTriggers= */ true + ); + nock(serverUrl) + .patch("/a/b.json") + .query({ print: "silent", writeSizeLimit: "tiny", disableTriggers: "true" }) + .reply(200, {}); + return expect(remoteWithDisableTriggers.deletePath("/a/b")).to.eventually.eql(true); + }); }); From 74c1b19bfb8b6daedc4b9aacc6aeec1daa53134d Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Thu, 27 Oct 2022 14:29:46 -0700 Subject: [PATCH 069/115] Inlined.web frameworks label (#5176) * Move HostingResolved out of firebaseConfig * Move metric to deploy code and add label * Add some smoke tests to the hosting pepare library --- src/deploy/hosting/context.ts | 2 +- src/deploy/hosting/prepare.ts | 23 ++++- src/deploy/index.ts | 7 +- src/firebaseConfig.ts | 11 +-- src/frameworks/index.ts | 9 +- src/hosting/config.ts | 11 ++- src/hosting/options.ts | 3 +- src/test/deploy/hosting/prepare.spec.ts | 118 ++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 src/test/deploy/hosting/prepare.spec.ts diff --git a/src/deploy/hosting/context.ts b/src/deploy/hosting/context.ts index 05a69b076336..889df7b03b9e 100644 --- a/src/deploy/hosting/context.ts +++ b/src/deploy/hosting/context.ts @@ -1,4 +1,4 @@ -import { HostingResolved } from "../../firebaseConfig"; +import { HostingResolved } from "../../hosting/config"; import { Context as FunctionsContext } from "../functions/args"; export interface HostingDeploy { diff --git a/src/deploy/hosting/prepare.ts b/src/deploy/hosting/prepare.ts index effbf065b9df..639ae6c84aa2 100644 --- a/src/deploy/hosting/prepare.ts +++ b/src/deploy/hosting/prepare.ts @@ -6,6 +6,7 @@ import { Context } from "./context"; import { Options } from "../../options"; import { HostingOptions } from "../../hosting/options"; import { zipIn } from "../../functional"; +import { track } from "../../track"; /** * Prepare creates versions for each Hosting site to be deployed. @@ -25,12 +26,24 @@ export async function prepare(context: Context, options: HostingOptions & Option return Promise.resolve(); } - const version: Omit = { - status: "CREATED", - labels: deploymentTool.labels(), - }; const versions = await Promise.all( - configs.map((config) => api.createVersion(config.site, version)) + configs.map(async (config) => { + const labels: Record = { + ...deploymentTool.labels(), + }; + if (config.webFramework) { + labels["firebase-web-framework"] = config.webFramework; + } + const version: Omit = { + status: "CREATED", + labels, + }; + const [, versionName] = await Promise.all([ + track("hosting_deploy", config.webFramework || "classic"), + api.createVersion(config.site, version), + ]); + return versionName; + }) ); context.hosting = { deploys: [], diff --git a/src/deploy/index.ts b/src/deploy/index.ts index fc07e21d43d9..e1e885f8e9df 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -61,11 +61,10 @@ export const deploy = async function ( if (targetNames.includes("hosting")) { const config = options.config.get("hosting"); - let deployedFrameworks: string[] = []; if (Array.isArray(config) ? config.some((it) => it.source) : config.source) { experiments.assertEnabled("webframeworks", "deploy a web framework to hosting"); const usedToTargetFunctions = targetNames.includes("functions"); - deployedFrameworks = await prepareFrameworks(targetNames, context, options); + await prepareFrameworks(targetNames, context, options); const nowTargetsFunctions = targetNames.includes("functions"); if (nowTargetsFunctions && !usedToTargetFunctions) { if (context.hostingChannel && !experiments.isEnabled("pintags")) { @@ -75,11 +74,7 @@ export const deploy = async function ( } await requirePermissions(TARGET_PERMISSIONS["functions"]); } - } else { - const count = Array.isArray(config) ? config.length : 1; - deployedFrameworks = Array(count).fill("classic"); } - await Promise.all(deployedFrameworks.map((framework) => track("hosting_deploy", framework))); } for (const targetName of targetNames) { diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 58c9879d396e..9a56e09a0817 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -10,7 +10,7 @@ import { RequireAtLeastOne } from "./metaprogramming"; // should be sourced from - https://github.com/firebase/firebase-tools/blob/master/src/deploy/functions/runtimes/index.ts#L15 type CloudFunctionRuntimes = "nodejs10" | "nodejs12" | "nodejs14" | "nodejs16"; -type Deployable = { +export type Deployable = { predeploy?: string | string[]; postdeploy?: string | string[]; }; @@ -67,7 +67,7 @@ export type HostingHeaders = HostingSource & { }[]; }; -type HostingBase = { +export type HostingBase = { public?: string; source?: string; ignore?: string[]; @@ -101,13 +101,6 @@ export type HostingMultiple = (HostingBase & }> & Deployable)[]; -// After validating a HostingMultiple and resolving targets, we will instead -// have a HostingResolved. -export type HostingResolved = HostingBase & { - site: string; - target?: string; -} & Deployable; - type StorageSingle = { rules: string; target?: string; diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index 3b4bcd2ccdd6..a3158d8e0db4 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -248,7 +248,7 @@ export async function prepareFrameworks( context: any, options: any, emulators: EmulatorInfo[] = [] -): Promise { +): Promise { // `firebase-frameworks` requires Node >= 16. We must check for this to avoid horrible errors. const nodeVersion = process.version; if (!semver.satisfies(nodeVersion, ">=16.0.0")) { @@ -257,7 +257,6 @@ export async function prepareFrameworks( ); } - const deployedFrameworks: string[] = []; const project = needProjectId(context); const { projectRoot } = options; const account = getProjectDefaultAccount(projectRoot); @@ -284,12 +283,11 @@ export async function prepareFrameworks( const configs = hostingConfig(options); let firebaseDefaults: FirebaseDefaults | undefined = undefined; if (configs.length === 0) { - return deployedFrameworks; + return; } for (const config of configs) { const { source, site, public: publicDir } = config; if (!source) { - deployedFrameworks.push("classic"); continue; } config.rewrites ||= []; @@ -411,7 +409,7 @@ export async function prepareFrameworks( config.public = relative(projectRoot, hostingDist); if (wantsBackend) codegenFunctionsDirectory = codegenProdModeFunctionsDirectory; } - deployedFrameworks.push(`${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`); + config.webFramework = `${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`; if (codegenFunctionsDirectory) { if (firebaseDefaults) firebaseDefaults._authTokenSyncURL = "/__session"; @@ -555,7 +553,6 @@ exports.ssr = onRequest((req, res) => server.then(it => it.handle(req, res))); }); } } - return deployedFrameworks; } function codegenDevModeFunctionsDirectory() { diff --git a/src/hosting/config.ts b/src/hosting/config.ts index 6552cc04a082..c2707f4b0d88 100644 --- a/src/hosting/config.ts +++ b/src/hosting/config.ts @@ -5,7 +5,8 @@ import { FirebaseError } from "../error"; import { HostingMultiple, HostingSingle, - HostingResolved, + HostingBase, + Deployable, HostingRewrites, FunctionsRewrite, LegacyFunctionsRewrite, @@ -20,6 +21,14 @@ import * as path from "node:path"; import * as experiments from "../experiments"; import { logger } from "../logger"; +// After validating a HostingMultiple and resolving targets, we will instead +// have a HostingResolved. +export type HostingResolved = HostingBase & { + site: string; + target?: string; + webFramework?: string; +} & Deployable; + // assertMatches allows us to throw when an --only flag doesn't match a target // but an --except flag doesn't. Is this desirable behavior? function matchingConfigs( diff --git a/src/hosting/options.ts b/src/hosting/options.ts index 9cee6436858f..2b54bf82f805 100644 --- a/src/hosting/options.ts +++ b/src/hosting/options.ts @@ -1,6 +1,7 @@ -import { FirebaseConfig, HostingResolved } from "../firebaseConfig"; +import { FirebaseConfig } from "../firebaseConfig"; import { Implements } from "../metaprogramming"; import { Options } from "../options"; +import { HostingResolved } from "./config"; /** * The set of fields that the Hosting codebase needs from Options. diff --git a/src/test/deploy/hosting/prepare.spec.ts b/src/test/deploy/hosting/prepare.spec.ts new file mode 100644 index 000000000000..8a72f814c772 --- /dev/null +++ b/src/test/deploy/hosting/prepare.spec.ts @@ -0,0 +1,118 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FirebaseConfig } from "../../../firebaseConfig"; +import { HostingOptions } from "../../../hosting/options"; +import { Context } from "../../../deploy/hosting/context"; +import { Options } from "../../../options"; +import * as hostingApi from "../../../hosting/api"; +import * as tracking from "../../../track"; +import * as deploymentTool from "../../../deploymentTool"; +import * as config from "../../../hosting/config"; +import { prepare } from "../../../deploy/hosting"; + +describe("hosting prepare", () => { + let hostingStub: sinon.SinonStubbedInstance; + let trackingStub: sinon.SinonStubbedInstance; + let siteConfig: config.HostingResolved; + let firebaseJson: FirebaseConfig; + let options: HostingOptions & Options; + + beforeEach(() => { + hostingStub = sinon.stub(hostingApi); + trackingStub = sinon.stub(tracking); + + // We're intentionally using pointer references so that editing site + // edits the results of hostingConfig() and changes firebase.json + siteConfig = { + site: "site", + public: ".", + }; + firebaseJson = { + hosting: siteConfig, + }; + options = { + cwd: ".", + configPath: ".", + only: "", + except: "", + filteredTargets: ["HOSTING"], + force: false, + json: false, + nonInteractive: false, + interactive: true, + debug: false, + config: { + src: firebaseJson, + } as any, + rc: null as any, + + // Forces caching behavior of hostingConfig call + normalizedHostingConfig: [siteConfig], + }; + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("passes a smoke test with web framework", async () => { + siteConfig.webFramework = "fake-framework"; + + // Edit the in-memory config to add a web framework + hostingStub.createVersion.callsFake((siteId, version) => { + expect(siteId).to.equal(siteConfig.site); + expect(version.status).to.equal("CREATED"); + expect(version.labels).to.deep.equal({ + ...deploymentTool.labels(), + "firebase-web-framework": "fake-framework", + }); + return Promise.resolve("version"); + }); + + const context: Context = { + projectId: "project", + }; + await prepare(context, options); + + expect(trackingStub.track).to.have.been.calledOnceWith("hosting_deploy", "fake-framework"); + expect(hostingStub.createVersion).to.have.been.calledOnce; + expect(context.hosting).to.deep.equal({ + deploys: [ + { + config: siteConfig, + version: "version", + }, + ], + }); + }); + + it("passes a smoke test without web framework", async () => { + // Do not set a web framework on siteConfig + + // Edit the in-memory config to add a web framework + hostingStub.createVersion.callsFake((siteId, version) => { + expect(siteId).to.equal(siteConfig.site); + expect(version.status).to.equal("CREATED"); + // Note: we're missing the web framework label + expect(version.labels).to.deep.equal(deploymentTool.labels()); + return Promise.resolve("version"); + }); + + const context: Context = { + projectId: "project", + }; + await prepare(context, options); + + expect(trackingStub.track).to.have.been.calledOnceWith("hosting_deploy", "classic"); + expect(hostingStub.createVersion).to.have.been.calledOnce; + expect(context.hosting).to.deep.equal({ + deploys: [ + { + config: siteConfig, + version: "version", + }, + ], + }); + }); +}); From 818ea6c82cde6fccdb277eccaab8f7396811fac7 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Fri, 28 Oct 2022 13:10:27 -0700 Subject: [PATCH 070/115] Set environment variable necessary to be a custom events source (#5078) * Set environment variable necessary to be a custom events source * Set label in emulator --- src/deploy/functions/prepare.ts | 16 +++++++++++++++- src/emulator/functionsEmulatorShared.ts | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 60801d85158a..ad8d373e367a 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -31,7 +31,9 @@ import { AUTH_BLOCKING_EVENTS } from "../../functions/events/v1"; import { generateServiceIdentity } from "../../gcp/serviceusage"; import { applyBackendHashToBackends } from "./cache/applyHash"; import { allEndpoints, Backend } from "./backend"; +import { assertExhaustive } from "../../functional"; +export const EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE"; function hasUserConfig(config: Record): boolean { // "firebase" key is always going to exist in runtime config. // If any other key exists, we can assume that user is using runtime config. @@ -138,7 +140,19 @@ export async function prepare( } for (const endpoint of backend.allEndpoints(wantBackend)) { - endpoint.environmentVariables = wantBackend.environmentVariables; + endpoint.environmentVariables = wantBackend.environmentVariables || {}; + let resource: string; + if (endpoint.platform === "gcfv1") { + resource = `projects/${endpoint.project}/locations/${endpoint.region}/functions/${endpoint.id}`; + } else if (endpoint.platform === "gcfv2") { + // N.B. If GCF starts allowing v1's allowable characters in IDs they're + // going to need to have a transform to create a service ID (which has a + // more restrictive cahracter set). We'll need to reimplement that here. + resource = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.id}`; + } else { + assertExhaustive(endpoint.platform); + } + endpoint.environmentVariables[EVENTARC_SOURCE_ENV] = resource; endpoint.codebase = codebase; } wantBackends[codebase] = wantBackend; diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 77155871e4f7..d6414a09d451 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -25,6 +25,13 @@ const V2_EVENTS = [ ...events.v2.DATABASE_EVENTS, ]; +/** + * Label for eventarc event sources. + * TODO: Consider DRYing from functions/prepare.ts + * A nice place would be to put it in functionsv2.ts once we get rid of functions.ts + */ +export const EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE"; + export type SignatureType = "http" | "event" | "cloudevent"; export interface ParsedTriggerDefinition { @@ -171,6 +178,19 @@ export function emulatedFunctionsFromEndpoints( }; def.availableMemoryMb = endpoint.availableMemoryMb || 256; def.labels = endpoint.labels || {}; + if (endpoint.platform === "gcfv1") { + def.labels[EVENTARC_SOURCE_ENV] = + "cloudfunctions-emulated.googleapis.com" + + `/projects/${endpoint.project || "project"}/locations/${endpoint.region}/functions/${ + endpoint.id + }`; + } else if (endpoint.platform === "gcfv2") { + def.labels[EVENTARC_SOURCE_ENV] = + "run-emulated.googleapis.com" + + `/projects/${endpoint.project || "project"}/locations/${endpoint.region}/services/${ + endpoint.id + }`; + } def.timeoutSeconds = endpoint.timeoutSeconds || 60; def.secretEnvironmentVariables = endpoint.secretEnvironmentVariables || []; def.platform = endpoint.platform; From 87e8f0c4159179cda5540dd36c035c5d0c43b9d3 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 31 Oct 2022 11:14:01 -0700 Subject: [PATCH 071/115] Enable experiments that should have gone out with the 4.0 functions sdk (#5192) * Enable experiments that should have gone out with the 4.0 functions sdk * Add changelog --- CHANGELOG.md | 2 ++ src/experiments.ts | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a147d85e0e9..234f90edba06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,5 @@ - Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. - Fixes internal library that was not being correctly published. - Adds `--disable-triggers` flag to RTDB write commands. +- Default enables experiment to skip deploying unmodified functions (#5192) +- Default enables experiment to allow parameterized functions codebases (#5192) diff --git a/src/experiments.ts b/src/experiments.ts index f1898b1ed6bf..21014746d446 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -66,9 +66,7 @@ export const ALL_EXPERIMENTS = experiments({ }, functionsparams: { shortDescription: "Adds support for paramaterizing functions deployments", - }, - skipdeployingnoopfunctions: { - shortDescription: "Detect that there have been no changes to a function and skip deployment", + default: true, }, // Emulator experiments From 2a675d85dcadffa3e5289978cb4ddb45faf8a934 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 31 Oct 2022 16:13:26 -0400 Subject: [PATCH 072/115] Next 13 fixes (#5175) --- CHANGELOG.md | 2 + .../hosting/.gitignore | 2 + .../hosting/app/bar/page.tsx | 5 + .../hosting/app/foo/page.tsx | 3 + .../hosting/app/layout.tsx | 8 + .../hosting/next.config.js | 3 + .../hosting/package-lock.json | 277 +++++++++--------- .../hosting/package.json | 2 +- .../hosting/tsconfig.json | 24 +- scripts/webframeworks-deploy-tests/tests.ts | 2 +- src/frameworks/next/index.ts | 97 ++++-- 11 files changed, 261 insertions(+), 164 deletions(-) create mode 100644 scripts/webframeworks-deploy-tests/hosting/app/bar/page.tsx create mode 100644 scripts/webframeworks-deploy-tests/hosting/app/foo/page.tsx create mode 100644 scripts/webframeworks-deploy-tests/hosting/app/layout.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 234f90edba06..9b0d5784d68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. - Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. - Fixes internal library that was not being correctly published. +- Add support for Next.js 13 in firebase deploy. +- Next.js routes with revalidate are now handled by the a backing Cloud Function. - Adds `--disable-triggers` flag to RTDB write commands. - Default enables experiment to skip deploying unmodified functions (#5192) - Default enables experiment to allow parameterized functions codebases (#5192) diff --git a/scripts/webframeworks-deploy-tests/hosting/.gitignore b/scripts/webframeworks-deploy-tests/hosting/.gitignore index c87c9b392c02..4f360c89d2ac 100644 --- a/scripts/webframeworks-deploy-tests/hosting/.gitignore +++ b/scripts/webframeworks-deploy-tests/hosting/.gitignore @@ -34,3 +34,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.vscode diff --git a/scripts/webframeworks-deploy-tests/hosting/app/bar/page.tsx b/scripts/webframeworks-deploy-tests/hosting/app/bar/page.tsx new file mode 100644 index 000000000000..2db606988f13 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/app/bar/page.tsx @@ -0,0 +1,5 @@ +export const revalidate = 60; + +export default function Bar() { + return <>Bar; +} diff --git a/scripts/webframeworks-deploy-tests/hosting/app/foo/page.tsx b/scripts/webframeworks-deploy-tests/hosting/app/foo/page.tsx new file mode 100644 index 000000000000..3fb4cf4e4dc7 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/app/foo/page.tsx @@ -0,0 +1,3 @@ +export default function Foo() { + return <>Foo; +} diff --git a/scripts/webframeworks-deploy-tests/hosting/app/layout.tsx b/scripts/webframeworks-deploy-tests/hosting/app/layout.tsx new file mode 100644 index 000000000000..7b221173febd --- /dev/null +++ b/scripts/webframeworks-deploy-tests/hosting/app/layout.tsx @@ -0,0 +1,8 @@ +export default function RootLayout({ children }: any) { + return ( + + + {children} + + ) +} diff --git a/scripts/webframeworks-deploy-tests/hosting/next.config.js b/scripts/webframeworks-deploy-tests/hosting/next.config.js index ae887958d3c9..d3ef77accdd5 100644 --- a/scripts/webframeworks-deploy-tests/hosting/next.config.js +++ b/scripts/webframeworks-deploy-tests/hosting/next.config.js @@ -2,6 +2,9 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + experimental: { + appDir: true + }, } module.exports = nextConfig diff --git a/scripts/webframeworks-deploy-tests/hosting/package-lock.json b/scripts/webframeworks-deploy-tests/hosting/package-lock.json index 5b23704afabd..69330694bbc6 100644 --- a/scripts/webframeworks-deploy-tests/hosting/package-lock.json +++ b/scripts/webframeworks-deploy-tests/hosting/package-lock.json @@ -8,7 +8,7 @@ "name": "hosting", "version": "0.1.0", "dependencies": { - "next": "12.3.1", + "next": "13.0.0", "react": "18.2.0", "react-dom": "18.2.0" }, @@ -113,9 +113,9 @@ "dev": true }, "node_modules/@next/env": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.1.tgz", - "integrity": "sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==" + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.0.tgz", + "integrity": "sha512-65v9BVuah2Mplohm4+efsKEnoEuhmlGm8B2w6vD1geeEP2wXtlSJCvR/cCRJ3fD8wzCQBV41VcMBQeYET6MRkg==" }, "node_modules/@next/eslint-plugin-next": { "version": "12.3.1", @@ -127,9 +127,9 @@ } }, "node_modules/@next/swc-android-arm-eabi": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", - "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.0.tgz", + "integrity": "sha512-+DUQkYF93gxFjWY+CYWE1QDX6gTgnUiWf+W4UqZjM1Jcef8U97fS6xYh+i+8rH4MM0AXHm7OSakvfOMzmjU6VA==", "cpu": [ "arm" ], @@ -142,9 +142,9 @@ } }, "node_modules/@next/swc-android-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", - "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.0.tgz", + "integrity": "sha512-RW9Uy3bMSc0zVGCa11klFuwfP/jdcdkhdruqnrJ7v+7XHm6OFKkSRzX6ee7yGR1rdDZvTnP4GZSRSpzjLv/N0g==", "cpu": [ "arm64" ], @@ -157,9 +157,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz", - "integrity": "sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.0.tgz", + "integrity": "sha512-APA26nps1j4qyhOIzkclW/OmgotVHj1jBxebSpMCPw2rXfiNvKNY9FA0TcuwPmUCNqaTnm703h6oW4dvp73A4Q==", "cpu": [ "arm64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", - "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.0.tgz", + "integrity": "sha512-qsUhUdoFuRJiaJ7LnvTQ6GZv1QnMDcRXCIjxaN0FNVXwrjkq++U7KjBUaxXkRzLV4C7u0NHLNOp0iZwNNE7ypw==", "cpu": [ "x64" ], @@ -187,9 +187,9 @@ } }, "node_modules/@next/swc-freebsd-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", - "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.0.tgz", + "integrity": "sha512-sCdyCbboS7CwdnevKH9J6hkJI76LUw1jVWt4eV7kISuLiPba3JmehZSWm80oa4ADChRVAwzhLAo2zJaYRrInbg==", "cpu": [ "x64" ], @@ -202,9 +202,9 @@ } }, "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", - "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.0.tgz", + "integrity": "sha512-/X/VxfFA41C9jrEv+sUsPLQ5vbDPVIgG0CJrzKvrcc+b+4zIgPgtfsaWq9ockjHFQi3ycvlZK4TALOXO8ovQ6Q==", "cpu": [ "arm" ], @@ -217,9 +217,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", - "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.0.tgz", + "integrity": "sha512-x6Oxr1GIi0ZtNiT6jbw+JVcbEi3UQgF7mMmkrgfL4mfchOwXtWSHKTSSPnwoJWJfXYa0Vy1n8NElWNTGAqoWFw==", "cpu": [ "arm64" ], @@ -232,9 +232,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", - "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.0.tgz", + "integrity": "sha512-SnMH9ngI+ipGh3kqQ8+mDtWunirwmhQnQeZkEq9e/9Xsgjf04OetqrqRHKM1HmJtG2qMUJbyXFJ0F81TPuT+3g==", "cpu": [ "arm64" ], @@ -247,9 +247,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", - "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.0.tgz", + "integrity": "sha512-VSQwTX9EmdbotArtA1J67X8964oQfe0xHb32x4tu+JqTR+wOHyG6wGzPMdXH2oKAp6rdd7BzqxUXXf0J+ypHlw==", "cpu": [ "x64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", - "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.0.tgz", + "integrity": "sha512-xBCP0nnpO0q4tsytXkvIwWFINtbFRyVY5gxa1zB0vlFtqYR9lNhrOwH3CBrks3kkeaePOXd611+8sjdUtrLnXA==", "cpu": [ "x64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", - "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.0.tgz", + "integrity": "sha512-NutwDafqhGxqPj/eiUixJq9ImS/0sgx6gqlD7jRndCvQ2Q8AvDdu1+xKcGWGNnhcDsNM/n1avf1e62OG1GaqJg==", "cpu": [ "arm64" ], @@ -292,9 +292,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", - "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.0.tgz", + "integrity": "sha512-zNaxaO+Kl/xNz02E9QlcVz0pT4MjkXGDLb25qxtAzyJL15aU0+VjjbIZAYWctG59dvggNIUNDWgoBeVTKB9xLg==", "cpu": [ "ia32" ], @@ -307,9 +307,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", - "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.0.tgz", + "integrity": "sha512-FFOGGWwTCRMu9W7MF496Urefxtuo2lttxF1vwS+1rIRsKvuLrWhVaVTj3T8sf2EBL6gtJbmh4TYlizS+obnGKA==", "cpu": [ "x64" ], @@ -761,6 +761,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2195,43 +2200,43 @@ "dev": true }, "node_modules/next": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", - "integrity": "sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/next/-/next-13.0.0.tgz", + "integrity": "sha512-puH1WGM6rGeFOoFdXXYfUxN9Sgi4LMytCV5HkQJvVUOhHfC1DoVqOfvzaEteyp6P04IW+gbtK2Q9pInVSrltPA==", "dependencies": { - "@next/env": "12.3.1", + "@next/env": "13.0.0", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", - "styled-jsx": "5.0.7", + "styled-jsx": "5.1.0", "use-sync-external-store": "1.2.0" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=12.22.0" + "node": ">=14.6.0" }, "optionalDependencies": { - "@next/swc-android-arm-eabi": "12.3.1", - "@next/swc-android-arm64": "12.3.1", - "@next/swc-darwin-arm64": "12.3.1", - "@next/swc-darwin-x64": "12.3.1", - "@next/swc-freebsd-x64": "12.3.1", - "@next/swc-linux-arm-gnueabihf": "12.3.1", - "@next/swc-linux-arm64-gnu": "12.3.1", - "@next/swc-linux-arm64-musl": "12.3.1", - "@next/swc-linux-x64-gnu": "12.3.1", - "@next/swc-linux-x64-musl": "12.3.1", - "@next/swc-win32-arm64-msvc": "12.3.1", - "@next/swc-win32-ia32-msvc": "12.3.1", - "@next/swc-win32-x64-msvc": "12.3.1" + "@next/swc-android-arm-eabi": "13.0.0", + "@next/swc-android-arm64": "13.0.0", + "@next/swc-darwin-arm64": "13.0.0", + "@next/swc-darwin-x64": "13.0.0", + "@next/swc-freebsd-x64": "13.0.0", + "@next/swc-linux-arm-gnueabihf": "13.0.0", + "@next/swc-linux-arm64-gnu": "13.0.0", + "@next/swc-linux-arm64-musl": "13.0.0", + "@next/swc-linux-x64-gnu": "13.0.0", + "@next/swc-linux-x64-musl": "13.0.0", + "@next/swc-win32-arm64-msvc": "13.0.0", + "@next/swc-win32-ia32-msvc": "13.0.0", + "@next/swc-win32-x64-msvc": "13.0.0" }, "peerDependencies": { "fibers": ">= 3.1.0", "node-sass": "^6.0.0 || ^7.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -2859,9 +2864,12 @@ } }, "node_modules/styled-jsx": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", - "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.0.tgz", + "integrity": "sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==", + "dependencies": { + "client-only": "0.0.1" + }, "engines": { "node": ">= 12.0.0" }, @@ -3158,9 +3166,9 @@ "dev": true }, "@next/env": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.1.tgz", - "integrity": "sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==" + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.0.tgz", + "integrity": "sha512-65v9BVuah2Mplohm4+efsKEnoEuhmlGm8B2w6vD1geeEP2wXtlSJCvR/cCRJ3fD8wzCQBV41VcMBQeYET6MRkg==" }, "@next/eslint-plugin-next": { "version": "12.3.1", @@ -3172,81 +3180,81 @@ } }, "@next/swc-android-arm-eabi": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz", - "integrity": "sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.0.tgz", + "integrity": "sha512-+DUQkYF93gxFjWY+CYWE1QDX6gTgnUiWf+W4UqZjM1Jcef8U97fS6xYh+i+8rH4MM0AXHm7OSakvfOMzmjU6VA==", "optional": true }, "@next/swc-android-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz", - "integrity": "sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.0.tgz", + "integrity": "sha512-RW9Uy3bMSc0zVGCa11klFuwfP/jdcdkhdruqnrJ7v+7XHm6OFKkSRzX6ee7yGR1rdDZvTnP4GZSRSpzjLv/N0g==", "optional": true }, "@next/swc-darwin-arm64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz", - "integrity": "sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.0.tgz", + "integrity": "sha512-APA26nps1j4qyhOIzkclW/OmgotVHj1jBxebSpMCPw2rXfiNvKNY9FA0TcuwPmUCNqaTnm703h6oW4dvp73A4Q==", "optional": true }, "@next/swc-darwin-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz", - "integrity": "sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.0.tgz", + "integrity": "sha512-qsUhUdoFuRJiaJ7LnvTQ6GZv1QnMDcRXCIjxaN0FNVXwrjkq++U7KjBUaxXkRzLV4C7u0NHLNOp0iZwNNE7ypw==", "optional": true }, "@next/swc-freebsd-x64": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz", - "integrity": "sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.0.tgz", + "integrity": "sha512-sCdyCbboS7CwdnevKH9J6hkJI76LUw1jVWt4eV7kISuLiPba3JmehZSWm80oa4ADChRVAwzhLAo2zJaYRrInbg==", "optional": true }, "@next/swc-linux-arm-gnueabihf": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz", - "integrity": "sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.0.tgz", + "integrity": "sha512-/X/VxfFA41C9jrEv+sUsPLQ5vbDPVIgG0CJrzKvrcc+b+4zIgPgtfsaWq9ockjHFQi3ycvlZK4TALOXO8ovQ6Q==", "optional": true }, "@next/swc-linux-arm64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz", - "integrity": "sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.0.tgz", + "integrity": "sha512-x6Oxr1GIi0ZtNiT6jbw+JVcbEi3UQgF7mMmkrgfL4mfchOwXtWSHKTSSPnwoJWJfXYa0Vy1n8NElWNTGAqoWFw==", "optional": true }, "@next/swc-linux-arm64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz", - "integrity": "sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.0.tgz", + "integrity": "sha512-SnMH9ngI+ipGh3kqQ8+mDtWunirwmhQnQeZkEq9e/9Xsgjf04OetqrqRHKM1HmJtG2qMUJbyXFJ0F81TPuT+3g==", "optional": true }, "@next/swc-linux-x64-gnu": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz", - "integrity": "sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.0.tgz", + "integrity": "sha512-VSQwTX9EmdbotArtA1J67X8964oQfe0xHb32x4tu+JqTR+wOHyG6wGzPMdXH2oKAp6rdd7BzqxUXXf0J+ypHlw==", "optional": true }, "@next/swc-linux-x64-musl": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz", - "integrity": "sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.0.tgz", + "integrity": "sha512-xBCP0nnpO0q4tsytXkvIwWFINtbFRyVY5gxa1zB0vlFtqYR9lNhrOwH3CBrks3kkeaePOXd611+8sjdUtrLnXA==", "optional": true }, "@next/swc-win32-arm64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz", - "integrity": "sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.0.tgz", + "integrity": "sha512-NutwDafqhGxqPj/eiUixJq9ImS/0sgx6gqlD7jRndCvQ2Q8AvDdu1+xKcGWGNnhcDsNM/n1avf1e62OG1GaqJg==", "optional": true }, "@next/swc-win32-ia32-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz", - "integrity": "sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.0.tgz", + "integrity": "sha512-zNaxaO+Kl/xNz02E9QlcVz0pT4MjkXGDLb25qxtAzyJL15aU0+VjjbIZAYWctG59dvggNIUNDWgoBeVTKB9xLg==", "optional": true }, "@next/swc-win32-x64-msvc": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz", - "integrity": "sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.0.tgz", + "integrity": "sha512-FFOGGWwTCRMu9W7MF496Urefxtuo2lttxF1vwS+1rIRsKvuLrWhVaVTj3T8sf2EBL6gtJbmh4TYlizS+obnGKA==", "optional": true }, "@nodelib/fs.scandir": { @@ -3559,6 +3567,11 @@ "supports-color": "^7.1.0" } }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4637,28 +4650,28 @@ "dev": true }, "next": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-12.3.1.tgz", - "integrity": "sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/next/-/next-13.0.0.tgz", + "integrity": "sha512-puH1WGM6rGeFOoFdXXYfUxN9Sgi4LMytCV5HkQJvVUOhHfC1DoVqOfvzaEteyp6P04IW+gbtK2Q9pInVSrltPA==", "requires": { - "@next/env": "12.3.1", - "@next/swc-android-arm-eabi": "12.3.1", - "@next/swc-android-arm64": "12.3.1", - "@next/swc-darwin-arm64": "12.3.1", - "@next/swc-darwin-x64": "12.3.1", - "@next/swc-freebsd-x64": "12.3.1", - "@next/swc-linux-arm-gnueabihf": "12.3.1", - "@next/swc-linux-arm64-gnu": "12.3.1", - "@next/swc-linux-arm64-musl": "12.3.1", - "@next/swc-linux-x64-gnu": "12.3.1", - "@next/swc-linux-x64-musl": "12.3.1", - "@next/swc-win32-arm64-msvc": "12.3.1", - "@next/swc-win32-ia32-msvc": "12.3.1", - "@next/swc-win32-x64-msvc": "12.3.1", + "@next/env": "13.0.0", + "@next/swc-android-arm-eabi": "13.0.0", + "@next/swc-android-arm64": "13.0.0", + "@next/swc-darwin-arm64": "13.0.0", + "@next/swc-darwin-x64": "13.0.0", + "@next/swc-freebsd-x64": "13.0.0", + "@next/swc-linux-arm-gnueabihf": "13.0.0", + "@next/swc-linux-arm64-gnu": "13.0.0", + "@next/swc-linux-arm64-musl": "13.0.0", + "@next/swc-linux-x64-gnu": "13.0.0", + "@next/swc-linux-x64-musl": "13.0.0", + "@next/swc-win32-arm64-msvc": "13.0.0", + "@next/swc-win32-ia32-msvc": "13.0.0", + "@next/swc-win32-x64-msvc": "13.0.0", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", - "styled-jsx": "5.0.7", + "styled-jsx": "5.1.0", "use-sync-external-store": "1.2.0" } }, @@ -5077,10 +5090,12 @@ "dev": true }, "styled-jsx": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", - "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", - "requires": {} + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.0.tgz", + "integrity": "sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==", + "requires": { + "client-only": "0.0.1" + } }, "supports-color": { "version": "7.2.0", diff --git a/scripts/webframeworks-deploy-tests/hosting/package.json b/scripts/webframeworks-deploy-tests/hosting/package.json index 767c471e2fb8..337edffd67c6 100644 --- a/scripts/webframeworks-deploy-tests/hosting/package.json +++ b/scripts/webframeworks-deploy-tests/hosting/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "next": "12.3.1", + "next": "13.0.0", "react": "18.2.0", "react-dom": "18.2.0" }, diff --git a/scripts/webframeworks-deploy-tests/hosting/tsconfig.json b/scripts/webframeworks-deploy-tests/hosting/tsconfig.json index 99710e857874..b25c4f834cb7 100644 --- a/scripts/webframeworks-deploy-tests/hosting/tsconfig.json +++ b/scripts/webframeworks-deploy-tests/hosting/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -13,8 +17,20 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/scripts/webframeworks-deploy-tests/tests.ts b/scripts/webframeworks-deploy-tests/tests.ts index 120bfad0074b..843417961687 100644 --- a/scripts/webframeworks-deploy-tests/tests.ts +++ b/scripts/webframeworks-deploy-tests/tests.ts @@ -45,7 +45,7 @@ describe("webframeworks deploy", function (this) { const result = await setOptsAndDeploy(); expect(result.stdout, "deploy result").to.match(/file upload complete/); - expect(result.stdout, "deploy result").to.match(/found 16 files/); + expect(result.stdout, "deploy result").to.match(/found 20 files/); expect(result.stdout, "deploy result").to.match(/Deploy complete!/); }); }); diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index b79c5a0510f2..e13ba65b00e2 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -1,11 +1,12 @@ import { execSync } from "child_process"; -import { readFile, mkdir, copyFile, stat } from "fs/promises"; -import { dirname, extname, join } from "path"; +import { readFile, mkdir, copyFile } from "fs/promises"; +import { dirname, join } from "path"; import type { Header, Rewrite, Redirect } from "next/dist/lib/load-custom-routes"; import type { NextConfig } from "next"; import { copy, mkdirp, pathExists } from "fs-extra"; import { pathToFileURL, parse } from "url"; import { existsSync } from "fs"; + import { BuildResult, createServerResponseProxy, @@ -20,6 +21,7 @@ import { gte } from "semver"; import { IncomingMessage, ServerResponse } from "http"; import { logger } from "../../logger"; import { FirebaseError } from "../../error"; +import { fileExistsSync } from "../../fsutils"; // Next.js's exposed interface is incomplete here // TODO see if there's a better way to grab this @@ -47,10 +49,14 @@ export const name = "Next.js"; export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; -function getNextVersion(cwd: string) { +function getNextVersion(cwd: string): string | undefined { return findDependency("next", { cwd, depth: 0, omitDev: false })?.version; } +function getReactVersion(cwd: string): string | undefined { + return findDependency("react-dom", { cwd, omitDev: false })?.version; +} + /** * Returns whether this codebase is a Next.js backend. */ @@ -67,6 +73,12 @@ export async function discover(dir: string) { export async function build(dir: string): Promise { const { default: nextBuild } = relativeRequire(dir, "next/dist/build"); + const reactVersion = getReactVersion(dir); + if (reactVersion && gte(reactVersion, "18.0.0")) { + // This needs to be set for Next build to succeed with React 18 + process.env.__NEXT_REACT_ROOT = "true"; + } + await nextBuild(dir, null, false, false, true).catch((e) => { // Err on the side of displaying this error, since this is likely a bug in // the developer's code that we want to display immediately @@ -89,6 +101,10 @@ export async function build(dir: string): Promise { const exportDetailBuffer = exportDetailExists ? await readFile(exportDetailPath) : undefined; const exportDetailJson = exportDetailBuffer && JSON.parse(exportDetailBuffer.toString()); if (exportDetailJson?.success) { + const appPathRoutesManifestPath = join(dir, distDir, "app-path-routes-manifest.json"); + const appPathRoutesManifestJSON = fileExistsSync(appPathRoutesManifestPath) + ? await readFile(appPathRoutesManifestPath).then((it) => JSON.parse(it.toString())) + : {}; const prerenderManifestJSON = await readFile( join(dir, distDir, "prerender-manifest.json") ).then((it) => JSON.parse(it.toString())); @@ -100,10 +116,15 @@ export async function build(dir: string): Promise { ).then((it) => JSON.parse(it.toString())); const prerenderedRoutes = Object.keys(prerenderManifestJSON.routes); const dynamicRoutes = Object.keys(prerenderManifestJSON.dynamicRoutes); - const unrenderedPages = Object.keys(pagesManifestJSON).filter( + const unrenderedPages = [ + ...Object.keys(pagesManifestJSON), + // TODO flush out fully rendered detection with a app directory (Next 13) + // we shouldn't go too crazy here yet, as this is currently an expiriment + ...Object.values(appPathRoutesManifestJSON), + ].filter( (it) => !( - ["/_app", "/_error", "/_document", "/404"].includes(it) || + ["/_app", "/", "/_error", "/_document", "/404"].includes(it) || prerenderedRoutes.includes(it) || dynamicRoutes.includes(it) ) @@ -150,7 +171,7 @@ export async function init(setup: any) { choices: ["JavaScript", "TypeScript"], }); execSync( - `npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} ${ + `npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} --use-npm ${ language === "TypeScript" ? "--ts" : "" }`, { stdio: "inherit" } @@ -176,25 +197,37 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin } await copy(join(sourceDir, distDir, "static"), join(destDir, "_next", "static")); - const serverPagesDir = join(sourceDir, distDir, "server", "pages"); - await copy(serverPagesDir, destDir, { - filter: async (filename) => { - const status = await stat(filename); - if (status.isDirectory()) return true; - return extname(filename) === ".html"; - }, - }); + // Copy over the default html files + for (const file of ["index.html", "404.html", "500.html"]) { + const pagesPath = join(sourceDir, distDir, "server", "pages", file); + if (await pathExists(pagesPath)) { + await copyFile(pagesPath, join(destDir, file)); + continue; + } + const appPath = join(sourceDir, distDir, "server", "app", file); + if (await pathExists(appPath)) { + await copyFile(appPath, join(destDir, file)); + } + } const prerenderManifestBuffer = await readFile( join(sourceDir, distDir, "prerender-manifest.json") ); const prerenderManifest = JSON.parse(prerenderManifestBuffer.toString()); - // TODO drop from hosting if revalidate - for (const route in prerenderManifest.routes) { - if (prerenderManifest.routes[route]) { + for (const path in prerenderManifest.routes) { + if (prerenderManifest.routes[path]) { + // Skip ISR in the deploy to hosting + const { initialRevalidateSeconds } = prerenderManifest.routes[path]; + if (initialRevalidateSeconds) { + continue; + } + + // TODO(jamesdaniels) explore oppertunity to simplify this now that we + // are defaulting cleanURLs to true for frameworks + // / => index.json => index.html => index.html // /foo => foo.json => foo.html - const parts = route + const parts = path .split("/") .slice(1) .filter((it) => !!it); @@ -202,16 +235,26 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin const dataPath = `${join(...partsOrIndex)}.json`; const htmlPath = `${join(...partsOrIndex)}.html`; await mkdir(join(destDir, dirname(htmlPath)), { recursive: true }); - await copyFile( - join(sourceDir, distDir, "server", "pages", htmlPath), - join(destDir, htmlPath) - ); - const dataRoute = prerenderManifest.routes[route].dataRoute; + const pagesHtmlPath = join(sourceDir, distDir, "server", "pages", htmlPath); + if (await pathExists(pagesHtmlPath)) { + await copyFile(pagesHtmlPath, join(destDir, htmlPath)); + } else { + const appHtmlPath = join(sourceDir, distDir, "server", "app", htmlPath); + if (await pathExists(appHtmlPath)) { + await copyFile(appHtmlPath, join(destDir, htmlPath)); + } + } + const dataRoute = prerenderManifest.routes[path].dataRoute; await mkdir(join(destDir, dirname(dataRoute)), { recursive: true }); - await copyFile( - join(sourceDir, distDir, "server", "pages", dataPath), - join(destDir, dataRoute) - ); + const pagesDataPath = join(sourceDir, distDir, "server", "pages", dataPath); + if (await pathExists(pagesDataPath)) { + await copyFile(pagesDataPath, join(destDir, dataRoute)); + } else { + const appDataPath = join(sourceDir, distDir, "server", "app", dataPath); + if (await pathExists(appDataPath)) { + await copyFile(appDataPath, join(destDir, dataRoute)); + } + } } } } From 60f6d669472916c6c8e1d4feacdacf922bd6eb29 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Mon, 31 Oct 2022 16:29:16 -0700 Subject: [PATCH 073/115] Auto-downgrade implicit concurrency (#5196) * Auto-downgrade implicit concurrency * Alternate approach which also upgrades concurrency * Remove outdated comment --- src/deploy/functions/prepare.ts | 20 ++++---- src/test/deploy/functions/prepare.spec.ts | 53 ++++++++++++++++++++++ src/test/deploy/functions/validate.spec.ts | 8 ++-- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index ad8d373e367a..876a434ee16c 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -213,7 +213,7 @@ export async function prepare( for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) { inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase)); await ensureTriggerRegions(wantBackend); - resolveCpu(wantBackend); + resolveCpuAndConcurrency(wantBackend); validate.endpointsAreValid(wantBackend); inferBlockingDetails(wantBackend); } @@ -321,17 +321,15 @@ export function inferDetailsFromExisting( wantE.availableMemoryMb = haveE.availableMemoryMb; } - // N.B. This code doesn't handle automatic downgrading of concurrency if - // the customer sets CPU <1. We'll instead error that you can't have both. - // We may want to handle this case, though it might also be surprising to - // customers if they _don't_ get an error and we silently drop concurrency. - if (typeof wantE.concurrency === "undefined" && haveE.concurrency) { - wantE.concurrency = haveE.concurrency; - } if (typeof wantE.cpu === "undefined" && haveE.cpu) { wantE.cpu = haveE.cpu; } + // N.B. concurrency has different defaults based on CPU. If the customer + // only specifies CPU and they change that specification to < 1, we should + // turn off concurrency. + // We'll hanndle this in setCpuAndConcurrency + wantE.securityLevel = haveE.securityLevel ? haveE.securityLevel : "SECURE_ALWAYS"; maybeCopyTriggerRegion(wantE, haveE); @@ -408,7 +406,7 @@ export function inferBlockingDetails(want: backend.Backend): void { * provided and sets concurrency based on the CPU level if not provided. * After this function, CPU will be a real number and not "gcf_gen1". */ -export function resolveCpu(want: backend.Backend): void { +export function resolveCpuAndConcurrency(want: backend.Backend): void { for (const e of backend.allEndpoints(want)) { if (e.platform === "gcfv1") { continue; @@ -418,5 +416,9 @@ export function resolveCpu(want: backend.Backend): void { } else if (!e.cpu) { e.cpu = backend.memoryToGen2Cpu(e.availableMemoryMb || backend.DEFAULT_MEMORY); } + + if (!e.concurrency) { + e.concurrency = e.cpu >= 1 ? backend.DEFAULT_CONCURRENCY : 1; + } } } diff --git a/src/test/deploy/functions/prepare.spec.ts b/src/test/deploy/functions/prepare.spec.ts index 4f337a424ea8..ca96c93beef0 100644 --- a/src/test/deploy/functions/prepare.spec.ts +++ b/src/test/deploy/functions/prepare.spec.ts @@ -122,6 +122,59 @@ describe("prepare", () => { prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false); expect(want.availableMemoryMb).to.equal(512); }); + + it("downgrades concurrency if necessary (explicit)", () => { + const have: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + concurrency: 80, + cpu: 1, + }; + const want: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + cpu: 0.5, + }; + + prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* useDotEnv= */ false); + prepare.resolveCpuAndConcurrency(backend.of(want)); + expect(want.concurrency).to.equal(1); + }); + + it("downgrades concurrency if necessary (implicit)", () => { + const have: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + concurrency: 80, + cpu: 1, + }; + const want: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + cpu: "gcf_gen1", + }; + + prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* useDotEnv= */ false); + prepare.resolveCpuAndConcurrency(backend.of(want)); + expect(want.concurrency).to.equal(1); + }); + + it("upgrades default concurrency with CPU upgrades", () => { + const have: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + availableMemoryMb: 256, + cpu: "gcf_gen1", + }; + const want: backend.Endpoint = { + ...ENDPOINT_BASE, + httpsTrigger: {}, + }; + + prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* useDotEnv= */ false); + prepare.resolveCpuAndConcurrency(backend.of(want)); + expect(want.concurrency).to.equal(1); + }); }); describe("inferBlockingDetails", () => { diff --git a/src/test/deploy/functions/validate.spec.ts b/src/test/deploy/functions/validate.spec.ts index cbb834694a1f..b2f1bb63c4ef 100644 --- a/src/test/deploy/functions/validate.spec.ts +++ b/src/test/deploy/functions/validate.spec.ts @@ -8,7 +8,7 @@ import * as projectPath from "../../../projectPath"; import * as secretManager from "../../../gcp/secretManager"; import * as backend from "../../../deploy/functions/backend"; import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../../functions/events/v1"; -import { resolveCpu } from "../../../deploy/functions/prepare"; +import { resolveCpuAndConcurrency } from "../../../deploy/functions/prepare"; describe("validate", () => { describe("functionsDirectoryExists", () => { @@ -331,7 +331,7 @@ describe("validate", () => { availableMemoryMb: mem, cpu: "gcf_gen1", }; - resolveCpu(backend.of(ep)); + resolveCpuAndConcurrency(backend.of(ep)); expect(() => validate.endpointsAreValid(backend.of(ep))).to.not.throw; } }); @@ -344,7 +344,7 @@ describe("validate", () => { cpu: "gcf_gen1", concurrency: 42, }; - resolveCpu(backend.of(ep)); + resolveCpuAndConcurrency(backend.of(ep)); expect(() => validate.endpointsAreValid(backend.of(ep))).to.not.throw; } }); @@ -356,7 +356,7 @@ describe("validate", () => { concurrency: 2, cpu: "gcf_gen1", }; - resolveCpu(backend.of(ep)); + resolveCpuAndConcurrency(backend.of(ep)); expect(() => validate.endpointsAreValid(backend.of(ep))).to.throw( /concurrent execution and less than one full CPU/ ); From 88fe601054d8dfd077214cbf11f33c921e897d9c Mon Sep 17 00:00:00 2001 From: akongara-goog <106410469+akongara-goog@users.noreply.github.com> Date: Tue, 1 Nov 2022 00:28:40 -0700 Subject: [PATCH 074/115] Fixing handling of automatic rewrites detection to consider backends that don't exist but are being deployed. (#5164) Fix rewrites detection issue with detecting currently-deploying backends. Handle the case of a backend being deleted in the current deploy. --- scripts/hosting-tests/rewrites-tests/tests.ts | 47 ++++ src/deploy/hosting/convertConfig.ts | 64 ++++- src/deploy/hosting/release.ts | 9 +- src/test/deploy/hosting/convertConfig.spec.ts | 252 ++++++++++++++++-- src/test/deploy/hosting/release.spec.ts | 10 +- 5 files changed, 346 insertions(+), 36 deletions(-) diff --git a/scripts/hosting-tests/rewrites-tests/tests.ts b/scripts/hosting-tests/rewrites-tests/tests.ts index 9fbc63d30388..1c2cf4bf5d24 100644 --- a/scripts/hosting-tests/rewrites-tests/tests.ts +++ b/scripts/hosting-tests/rewrites-tests/tests.ts @@ -353,6 +353,53 @@ describe("deploy function-targeted rewrites And functions", () => { ).to.eventually.be.rejectedWith(FirebaseError, "Unable to find a valid endpoint for function"); }).timeout(1000 * 1e3); + it("should fail to deploy rewrites to a function being deleted in a region", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"] + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"] + ); + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions,hosting", + force: true, + }) + ).to.eventually.be.rejectedWith(FirebaseError, "Unable to find a valid endpoint for function"); + }).timeout(1000 * 1e3); + it("should deploy when a rewrite points to a non-existent function", async () => { const firebaseJson = { hosting: { diff --git a/src/deploy/hosting/convertConfig.ts b/src/deploy/hosting/convertConfig.ts index e052a04cd50d..f6669343bae6 100644 --- a/src/deploy/hosting/convertConfig.ts +++ b/src/deploy/hosting/convertConfig.ts @@ -3,7 +3,7 @@ import { HostingSource } from "../../firebaseConfig"; import { HostingDeploy } from "./context"; import * as api from "../../hosting/api"; import * as backend from "../functions/backend"; -import { Context } from "../functions/args"; +import { Context, Payload as FunctionsPayload } from "../functions/args"; import { last, logLabeledBullet, logLabeledWarning } from "../../utils"; import * as proto from "../../gcp/proto"; import { bold } from "colorette"; @@ -14,7 +14,7 @@ import { logger } from "../../logger"; /** * extractPattern contains the logic for extracting exactly one glob/regexp - * from a Hosting rewrite/redirect/header specification + * from a Hosting rewrite/redirect/header specification. */ function extractPattern(type: string, source: HostingSource): api.HasPattern { let glob: string | undefined; @@ -41,25 +41,31 @@ function extractPattern(type: string, source: HostingSource): api.HasPattern { ); } +interface EndpointSearchResult { + // An endpoint matching the functions ID (and optionally, the region) we're searching for. + matchingEndpoint: backend.Endpoint | undefined; + // Whether we found an endpoint with a matching function ID (but not necessarily function region) + foundMatchingId: boolean; +} + /** - * Finds an endpoint suitable for deploy at a site given an id and optional region + * Finds an endpoint suitable for deploy at a site given an id and optional region. */ export function findEndpointForRewrite( site: string, targetBackend: backend.Backend, id: string, region: string | undefined -): backend.Endpoint | undefined { +): EndpointSearchResult { const endpoints = backend.allEndpoints(targetBackend).filter((e) => e.id === id); - if (endpoints.length === 0) { - return; + return { matchingEndpoint: undefined, foundMatchingId: false }; } if (endpoints.length === 1) { if (region && region !== endpoints[0].region) { - return; + return { matchingEndpoint: undefined, foundMatchingId: true }; } - return endpoints[0]; + return { matchingEndpoint: endpoints[0], foundMatchingId: true }; } if (!region) { const us = endpoints.find((e) => e.region === "us-central1"); @@ -73,9 +79,12 @@ export function findEndpointForRewrite( `Function \`${id}\` found in multiple regions, defaulting to \`us-central1\`. ` + `To rewrite to a different region, specify a \`region\` for the rewrite in \`firebase.json\`.` ); - return us; + return { matchingEndpoint: us, foundMatchingId: true }; } - return endpoints.find((e) => e.region === region); + return { + matchingEndpoint: endpoints.find((e) => e.region === region), + foundMatchingId: true, + }; } /** @@ -88,6 +97,7 @@ export function findEndpointForRewrite( */ export async function convertConfig( context: Context, + functionsPayload: FunctionsPayload, deploy: HostingDeploy ): Promise { const config: api.ServingConfig = {}; @@ -96,9 +106,12 @@ export async function convertConfig( // rewrites to see if it's necessary. const hasBackends = !!deploy.config.rewrites?.some((r) => "function" in r || "run" in r); - // We need to be able to do a rewrite to an existing function that is may not - // even be part of Firebase's control or a function that we're currently - // deploying. + // We need to be able to do a rewrite to an existing function that may not be + // under Firebase's control or a function that we're currently deploying. + const wantBackend = backend.merge( + ...Object.values(functionsPayload.functions || {}).map((c) => c.wantBackend) + ); + let haveBackend = backend.empty(); if (hasBackends) { try { @@ -136,8 +149,31 @@ export async function convertConfig( } const id = rewrite.function.functionId; const region = rewrite.function.region; - const endpoint = findEndpointForRewrite(deploy.config.site, haveBackend, id, region); + + const deployingEndpointSearch = findEndpointForRewrite( + deploy.config.site, + wantBackend, + id, + region + ); + const existingEndpointSearch = + !deployingEndpointSearch.foundMatchingId && !deployingEndpointSearch.matchingEndpoint + ? findEndpointForRewrite(deploy.config.site, haveBackend, id, region) + : undefined; + const endpoint = deployingEndpointSearch.matchingEndpoint + ? deployingEndpointSearch.matchingEndpoint + : existingEndpointSearch?.matchingEndpoint; + if (!endpoint) { + // If we find a function matching the function ID we are looking for in either + // existing or currently-deploying backends, we consider it a firebase function. + // In this case, we throw an error if the rewrite doesn't point to a valid region. + if (deployingEndpointSearch.foundMatchingId || existingEndpointSearch?.foundMatchingId) { + throw new FirebaseError( + `Unable to find a valid endpoint for function. Functions matching the rewrite + are present but in the wrong region.` + ); + } // This could possibly succeed if there has been a function written // outside firebase tooling. But it will break in v2. We might need to // revisit this. diff --git a/src/deploy/hosting/release.ts b/src/deploy/hosting/release.ts index 859ae2184166..9ed607830794 100644 --- a/src/deploy/hosting/release.ts +++ b/src/deploy/hosting/release.ts @@ -4,11 +4,16 @@ import * as utils from "../../utils"; import { convertConfig } from "./convertConfig"; import { Context } from "./context"; import { FirebaseError } from "../../error"; +import { Payload as FunctionsPayload } from "../functions/args"; /** * Release finalized a Hosting release. */ -export async function release(context: Context, options: { message?: string }): Promise { +export async function release( + context: Context, + options: { message?: string }, + functionsPayload: FunctionsPayload +): Promise { if (!context.hosting || !context.hosting.deploys) { return; } @@ -26,7 +31,7 @@ export async function release(context: Context, options: { message?: string }): const update: Partial = { status: "FINALIZED", - config: await convertConfig(context, deploy), + config: await convertConfig(context, functionsPayload, deploy), }; const versionId = utils.last(deploy.version.split("/")); diff --git a/src/test/deploy/hosting/convertConfig.spec.ts b/src/test/deploy/hosting/convertConfig.spec.ts index a2bde18c624d..665656bf3710 100644 --- a/src/test/deploy/hosting/convertConfig.spec.ts +++ b/src/test/deploy/hosting/convertConfig.spec.ts @@ -7,13 +7,14 @@ import { Context, HostingDeploy } from "../../../deploy/hosting/context"; import { HostingSingle } from "../../../firebaseConfig"; import * as api from "../../../hosting/api"; import { FirebaseError } from "../../../error"; +import { Payload } from "../../../deploy/functions/args"; const FUNCTION_ID = "function"; const PROJECT_ID = "project"; const REGION = "region"; function endpoint(opts?: Partial): backend.Endpoint { - // Createa type that allows us to not have a trigger + // Create a type that allows us to not have a trigger const ret: Omit & { httpsTrigger?: backend.HttpsTrigger } = { id: FUNCTION_ID, project: PROJECT_ID, @@ -43,6 +44,7 @@ describe("convertConfig", () => { name: string; input: HostingSingle; want: api.ServingConfig; + functionsPayload?: Payload; existingBackend?: backend.Backend; }> = [ // Rewrites. @@ -59,14 +61,52 @@ describe("convertConfig", () => { { name: "checks for function region if unspecified", input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, - want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, - existingBackend: backend.of(endpoint({ region: "us-central1" })), + want: { + rewrites: [ + { + glob: "/foo", + function: FUNCTION_ID, + functionRegion: "us-central2", + }, + ], + }, + functionsPayload: { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "us-central2", + platform: "gcfv1", + httpsTrigger: {}, + }), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "discovers the function region of a callable function", input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, - want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, - existingBackend: backend.of(endpoint({ callableTrigger: {}, region: "us-central1" })), + want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central2" }] }, + functionsPayload: { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "us-central2", + platform: "gcfv1", + httpsTrigger: {}, + }), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "returns rewrites for glob CF3", @@ -74,13 +114,65 @@ describe("convertConfig", () => { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID, region: "europe-west2" } }], }, want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "europe-west2" }] }, - existingBackend: backend.of(endpoint({ region: "europe-west2" }), endpoint()), + functionsPayload: { + functions: { + default: { + wantBackend: backend.of( + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "europe-west2", + platform: "gcfv1", + httpsTrigger: {}, + }, + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "us-central1", + platform: "gcfv2", + httpsTrigger: {}, + } + ), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "defaults to a us-central1 rewrite if one is avaiable, v1 edition", input: { rewrites: [{ glob: "/foo", function: { functionId: FUNCTION_ID } }] }, want: { rewrites: [{ glob: "/foo", function: FUNCTION_ID, functionRegion: "us-central1" }] }, - existingBackend: backend.of(endpoint(), endpoint({ region: "us-central1" })), + functionsPayload: { + functions: { + default: { + wantBackend: backend.of( + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "europe-west2", + platform: "gcfv1", + httpsTrigger: {}, + }, + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "us-central1", + platform: "gcfv1", + httpsTrigger: {}, + } + ), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "defaults to a us-central1 rewrite if one is avaiable, v2 edition", @@ -88,10 +180,33 @@ describe("convertConfig", () => { want: { rewrites: [{ glob: "/foo", run: { region: "us-central1", serviceId: FUNCTION_ID } }], }, - existingBackend: backend.of( - endpoint({ platform: "gcfv2" }), - endpoint({ platform: "gcfv2", region: "us-central1" }) - ), + functionsPayload: { + functions: { + default: { + wantBackend: backend.of( + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "europe-west2", + platform: "gcfv2", + httpsTrigger: {}, + }, + { + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "us-central1", + platform: "gcfv2", + httpsTrigger: {}, + } + ), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "returns rewrites for regex CF3", @@ -101,13 +216,43 @@ describe("convertConfig", () => { want: { rewrites: [{ regex: "/foo$", function: FUNCTION_ID, functionRegion: REGION }], }, - existingBackend: backend.of(endpoint()), + functionsPayload: { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: REGION, + platform: "gcfv1", + httpsTrigger: {}, + }), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "rewrites referencing CF3v2 functions being deployed are changed to Cloud Run (during release)", input: { rewrites: [{ regex: "/foo$", function: { functionId: FUNCTION_ID } }] }, want: { rewrites: [{ regex: "/foo$", run: { serviceId: FUNCTION_ID, region: REGION } }] }, - existingBackend: backend.of(endpoint({ platform: "gcfv2" })), + functionsPayload: { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: REGION, + platform: "gcfv2", + httpsTrigger: {}, + }), + haveBackend: backend.empty(), + }, + }, + }, }, { name: "rewrites referencing existing CF3v2 functions are changed to Cloud Run (during prepare)", @@ -257,7 +402,7 @@ describe("convertConfig", () => { }, ]; - for (const { name, input, existingBackend, want } of tests) { + for (const { name, input, existingBackend, want, functionsPayload } of tests) { it(name, async () => { const context: Context = { projectId: PROJECT_ID, @@ -272,11 +417,87 @@ describe("convertConfig", () => { config: { site: "site", ...input }, version: "version", }; - const config = await convertConfig(context, deploy); + const config = await convertConfig(context, functionsPayload || {}, deploy); expect(config).to.deep.equal(want); }); } + describe("rewrites errors", () => { + it("should throw when rewrite points to function in the wrong region", async () => { + await expect( + convertConfig( + { projectId: "1" }, + { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "europe-west1", + platform: "gcfv1", + httpsTrigger: {}, + }), + haveBackend: backend.empty(), + }, + }, + }, + { + config: { + site: "foo", + rewrites: [ + { glob: "/foo", function: { functionId: FUNCTION_ID, region: "asia-northeast1" } }, + ], + }, + version: "14", + } + ) + ).to.eventually.be.rejectedWith(FirebaseError); + }); + + it("should throw when rewrite points to function being deleted", async () => { + await expect( + convertConfig( + { projectId: "1" }, + { + functions: { + default: { + wantBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "europe-west1", + platform: "gcfv1", + httpsTrigger: {}, + }), + haveBackend: backend.of({ + id: FUNCTION_ID, + project: PROJECT_ID, + entryPoint: FUNCTION_ID, + runtime: "nodejs16", + region: "asia-northeast1", + platform: "gcfv1", + httpsTrigger: {}, + }), + }, + }, + }, + { + config: { + site: "foo", + rewrites: [ + { glob: "/foo", function: { functionId: FUNCTION_ID, region: "asia-northeast1" } }, + ], + }, + version: "14", + } + ) + ).to.eventually.be.rejectedWith(FirebaseError); + }); + }); + describe("with permissions issues", () => { let existingBackendStub: sinon.SinonStub; @@ -298,6 +519,7 @@ describe("convertConfig", () => { await expect( convertConfig( { projectId: "1" }, + {}, { config: { site: "foo", diff --git a/src/test/deploy/hosting/release.spec.ts b/src/test/deploy/hosting/release.spec.ts index d8ed13d6b509..7ce5458c2db5 100644 --- a/src/test/deploy/hosting/release.spec.ts +++ b/src/test/deploy/hosting/release.spec.ts @@ -29,7 +29,7 @@ describe("release", () => { describe("with no Hosting deploys", () => { it("should bail", async () => { - await release({ projectId: "foo" }, {}); + await release({ projectId: "foo" }, {}, {}); expect(updateVersionStub).to.have.been.not.called; expect(createReleaseStub).to.have.been.not.called; @@ -53,7 +53,7 @@ describe("release", () => { updateVersionStub.resolves({}); createReleaseStub.resolves({}); - await release(CONTEXT, {}); + await release(CONTEXT, {}, {}); expect(updateVersionStub).to.have.been.calledOnceWithExactly( SITE, @@ -67,7 +67,7 @@ describe("release", () => { updateVersionStub.resolves({}); createReleaseStub.resolves({}); - await release(CONTEXT, { message: "hello world" }); + await release(CONTEXT, { message: "hello world" }, {}); expect(updateVersionStub).to.have.been.calledOnceWithExactly( SITE, @@ -100,7 +100,7 @@ describe("release", () => { updateVersionStub.resolves({}); createReleaseStub.resolves({}); - await release(CONTEXT, {}); + await release(CONTEXT, {}, {}); expect(updateVersionStub).to.have.been.calledTwice; expect(updateVersionStub).to.have.been.calledWithExactly( @@ -143,7 +143,7 @@ describe("release", () => { updateVersionStub.resolves({}); createReleaseStub.resolves({}); - await release(CONTEXT, {}); + await release(CONTEXT, {}, {}); expect(updateVersionStub).to.have.been.calledOnceWithExactly( SITE, From 4f639508e79b3dade896ac587923ed796bf13e9c Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Tue, 1 Nov 2022 11:07:31 -0400 Subject: [PATCH 075/115] Fix parallel requests in the functions emulator (#5149) * adding a promise queue to handle parallel requests * adding create state to workers for concurrent requests * update doc string * increase timeout for parallel firestore * adding e2e test for parallel requests * address pr comments --- CHANGELOG.md | 1 + scripts/triggers-end-to-end-tests/tests.ts | 18 ++++++++- scripts/triggers-end-to-end-tests/v2/index.js | 4 ++ src/emulator/functionsRuntimeWorker.ts | 20 +++++++++- .../emulators/functionsRuntimeWorker.spec.ts | 37 +++++++++++++++---- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b0d5784d68b..142ca90ffa8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ - Adds `--disable-triggers` flag to RTDB write commands. - Default enables experiment to skip deploying unmodified functions (#5192) - Default enables experiment to allow parameterized functions codebases (#5192) +- Fixes parallel requests in the functions emulator (#5149). diff --git a/scripts/triggers-end-to-end-tests/tests.ts b/scripts/triggers-end-to-end-tests/tests.ts index 17a3aadb85cb..a0dc6928a120 100755 --- a/scripts/triggers-end-to-end-tests/tests.ts +++ b/scripts/triggers-end-to-end-tests/tests.ts @@ -122,6 +122,20 @@ describe("function triggers", () => { await test.stopEmulators(); }); + describe("https triggers", () => { + it("should handle parallel requests", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const [resp1, resp2] = await Promise.all([ + test.invokeHttpFunction("httpsv2reaction"), + test.invokeHttpFunction("httpsv2reaction"), + ]); + + expect(resp1.status).to.eq(200); + expect(resp2.status).to.eq(200); + }); + }); + describe("database and firestore emulator triggers", () => { it("should write to the database emulator", async function (this) { this.timeout(EMULATOR_TEST_TIMEOUT); @@ -131,7 +145,7 @@ describe("function triggers", () => { }); it("should write to the firestore emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + this.timeout(EMULATOR_TEST_TIMEOUT * 2); const response = await test.writeToFirestore(); expect(response.status).to.equal(200); @@ -143,7 +157,7 @@ describe("function triggers", () => { * fixture state handlers to complete before we check * that state in the next test. */ - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 2)); }); it("should have have triggered cloud functions", () => { diff --git a/scripts/triggers-end-to-end-tests/v2/index.js b/scripts/triggers-end-to-end-tests/v2/index.js index 7d1aeb89bff3..3978bfad1126 100644 --- a/scripts/triggers-end-to-end-tests/v2/index.js +++ b/scripts/triggers-end-to-end-tests/v2/index.js @@ -29,6 +29,10 @@ const START_DOCUMENT_NAME = "test/start"; admin.initializeApp(); +exports.httpsv2reaction = functionsV2.https.onRequest((req, res) => { + res.send("httpsv2reaction"); +}); + exports.pubsubv2reaction = functionsV2.pubsub.onMessagePublished(PUBSUB_TOPIC, (cloudevent) => { console.log(PUBSUB_FUNCTION_LOG); console.log("Message", JSON.stringify(cloudevent.data.message.json)); diff --git a/src/emulator/functionsRuntimeWorker.ts b/src/emulator/functionsRuntimeWorker.ts index e40b51c98eb7..3358ac68311e 100644 --- a/src/emulator/functionsRuntimeWorker.ts +++ b/src/emulator/functionsRuntimeWorker.ts @@ -12,6 +12,9 @@ import { Serializable } from "child_process"; type LogListener = (el: EmulatorLog) => any; export enum RuntimeWorkerState { + // Worker has been created but is not ready to accept work + CREATED = "CREATED", + // Worker is ready to accept new work IDLE = "IDLE", @@ -34,7 +37,7 @@ export class RuntimeWorker { stateEvents: EventEmitter = new EventEmitter(); private logListeners: Array = []; - private _state: RuntimeWorkerState = RuntimeWorkerState.IDLE; + private _state: RuntimeWorkerState = RuntimeWorkerState.CREATED; constructor(key: string, runtime: FunctionsRuntimeInstance) { this.id = uuid.v4(); @@ -86,6 +89,10 @@ export class RuntimeWorker { return lines[lines.length - 1]; } + readyForWork(): void { + this.state = RuntimeWorkerState.IDLE; + } + sendDebugMsg(debug: FunctionsRuntimeBundle["debug"]): Promise { return new Promise((resolve, reject) => { this.runtime.process.send(JSON.stringify(debug), (err) => { @@ -178,7 +185,11 @@ export class RuntimeWorker { path: "/__/health", socketPath: this.runtime.socketPath, }, - () => resolve() + () => { + // Set the worker state to IDLE for new work + this.readyForWork(); + resolve(); + } ) .end(); req.on("error", (error) => { @@ -323,6 +334,11 @@ export class RuntimeWorkerPool { return; } + /** + * Adds a worker to the pool. + * Caller must set the worker status to ready by calling + * `worker.readyForWork()` or `worker.waitForSocketReady()`. + */ addWorker( triggerId: string | undefined, runtime: FunctionsRuntimeInstance, diff --git a/src/test/emulators/functionsRuntimeWorker.spec.ts b/src/test/emulators/functionsRuntimeWorker.spec.ts index cbb0015f7fe6..d0cb0ca557d4 100644 --- a/src/test/emulators/functionsRuntimeWorker.spec.ts +++ b/src/test/emulators/functionsRuntimeWorker.spec.ts @@ -41,6 +41,7 @@ class MockRuntimeInstance implements FunctionsRuntimeInstance { */ class WorkerStateCounter { counts: { [state in RuntimeWorkerState]: number } = { + CREATED: 0, IDLE: 0, BUSY: 0, FINISHING: 0, @@ -49,6 +50,9 @@ class WorkerStateCounter { constructor(worker: RuntimeWorker) { this.increment(worker.state); + worker.stateEvents.on(RuntimeWorkerState.CREATED, () => { + this.increment(RuntimeWorkerState.CREATED); + }); worker.stateEvents.on(RuntimeWorkerState.IDLE, () => { this.increment(RuntimeWorkerState.IDLE); }); @@ -68,7 +72,13 @@ class WorkerStateCounter { } get total() { - return this.counts.IDLE + this.counts.BUSY + this.counts.FINISHING + this.counts.FINISHED; + return ( + this.counts.CREATED + + this.counts.IDLE + + this.counts.BUSY + + this.counts.FINISHING + + this.counts.FINISHED + ); } } @@ -76,47 +86,52 @@ describe("FunctionsRuntimeWorker", () => { const workerPool = new RuntimeWorkerPool(); describe("RuntimeWorker", () => { - it("goes from idle --> busy --> idle in normal operation", async () => { + it("goes from created --> idle --> busy --> idle in normal operation", async () => { const scope = nock("http://localhost").get("/").reply(200); const worker = new RuntimeWorker(workerPool.getKey("trigger"), new MockRuntimeInstance()); const counter = new WorkerStateCounter(worker); + worker.readyForWork(); await worker.request( { method: "GET", path: "/" }, httpMocks.createResponse({ eventEmitter: EventEmitter }) ); scope.done(); + expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.BUSY).to.eql(1); expect(counter.counts.IDLE).to.eql(2); - expect(counter.total).to.eql(3); + expect(counter.total).to.eql(4); }); - it("goes from idle --> busy --> finished when there's an error", async () => { + it("goes from created --> idle --> busy --> finished when there's an error", async () => { const scope = nock("http://localhost").get("/").replyWithError("boom"); const worker = new RuntimeWorker(workerPool.getKey("trigger"), new MockRuntimeInstance()); const counter = new WorkerStateCounter(worker); + worker.readyForWork(); await worker.request( { method: "GET", path: "/" }, httpMocks.createResponse({ eventEmitter: EventEmitter }) ); scope.done(); + expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.IDLE).to.eql(1); expect(counter.counts.BUSY).to.eql(1); expect(counter.counts.FINISHED).to.eql(1); - expect(counter.total).to.eql(3); + expect(counter.total).to.eql(4); }); - it("goes from busy --> finishing --> finished when marked", async () => { + it("goes from created --> busy --> finishing --> finished when marked", async () => { const scope = nock("http://localhost").get("/").replyWithError("boom"); const worker = new RuntimeWorker(workerPool.getKey("trigger"), new MockRuntimeInstance()); const counter = new WorkerStateCounter(worker); + worker.readyForWork(); const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); resp.on("end", () => { worker.state = RuntimeWorkerState.FINISHING; @@ -124,11 +139,12 @@ describe("FunctionsRuntimeWorker", () => { await worker.request({ method: "GET", path: "/" }, resp); scope.done(); + expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.IDLE).to.eql(1); expect(counter.counts.BUSY).to.eql(1); expect(counter.counts.FINISHING).to.eql(1); expect(counter.counts.FINISHED).to.eql(1); - expect(counter.total).to.eql(4); + expect(counter.total).to.eql(5); }); }); @@ -144,6 +160,7 @@ describe("FunctionsRuntimeWorker", () => { // Add a worker and make sure it's there const worker = pool.addWorker(trigger, new MockRuntimeInstance()); + worker.readyForWork(); const triggerWorkers = pool.getTriggerWorkers(trigger); expect(triggerWorkers.length).length.to.eq(1); expect(pool.getIdleWorker(trigger)).to.eql(worker); @@ -170,6 +187,7 @@ describe("FunctionsRuntimeWorker", () => { // Add a worker to the pool that's destined to fail. const scope = nock("http://localhost").get("/").replyWithError("boom"); const worker = pool.addWorker(trigger, new MockRuntimeInstance()); + worker.readyForWork(); expect(pool.getIdleWorker(trigger)).to.eql(worker); // Send request to the worker. Request should fail, killing the worker. @@ -188,9 +206,11 @@ describe("FunctionsRuntimeWorker", () => { const trigger = "trigger1"; const busyWorker = pool.addWorker(trigger, new MockRuntimeInstance()); + busyWorker.readyForWork(); const busyWorkerCounter = new WorkerStateCounter(busyWorker); const idleWorker = pool.addWorker(trigger, new MockRuntimeInstance()); + idleWorker.readyForWork(); const idleWorkerCounter = new WorkerStateCounter(idleWorker); // Add a worker to the pool that's destined to fail. @@ -217,9 +237,11 @@ describe("FunctionsRuntimeWorker", () => { const trigger = "trigger1"; const busyWorker = pool.addWorker(trigger, new MockRuntimeInstance()); + busyWorker.readyForWork(); const busyWorkerCounter = new WorkerStateCounter(busyWorker); const idleWorker = pool.addWorker(trigger, new MockRuntimeInstance()); + idleWorker.readyForWork(); const idleWorkerCounter = new WorkerStateCounter(idleWorker); // Add a worker to the pool that's destined to fail. @@ -248,6 +270,7 @@ describe("FunctionsRuntimeWorker", () => { const pool = new RuntimeWorkerPool(FunctionsExecutionMode.SEQUENTIAL); const worker = pool.addWorker(trigger1, new MockRuntimeInstance()); + worker.readyForWork(); const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); resp.on("end", () => { From 5a6b3760880d47c1a6402c3192a8bdc9a3a64af0 Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 1 Nov 2022 15:28:44 -0700 Subject: [PATCH 076/115] Add changelog (#5202) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 142ca90ffa8a..31f7fbf03491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,3 +8,4 @@ - Default enables experiment to skip deploying unmodified functions (#5192) - Default enables experiment to allow parameterized functions codebases (#5192) - Fixes parallel requests in the functions emulator (#5149). +- Unspecified functions concurrency will shift between the defaults of 1 or 80 when CPU is changed to support/not support concurrency (#5196) From 6742d76cea8988ecb48587977471d325d887b288 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 1 Nov 2022 22:38:26 +0000 Subject: [PATCH 077/115] 11.16.0 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a8c9e68dfb4e..db6a738cd421 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.15.0", + "version": "11.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.15.0", + "version": "11.16.0", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 93cb8e3fa860..3ccb1fc8118d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.15.0", + "version": "11.16.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 7abccc63e6851a33aae8ff3461d15677516c3fb1 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 1 Nov 2022 22:38:39 +0000 Subject: [PATCH 078/115] [firebase-release] Removed change log and reset repo after 11.16.0 release --- CHANGELOG.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f7fbf03491..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +0,0 @@ -- Fixes an issue where an error during product provisioning check would block `firebase deploy --only extensions` (#5074). -- Releases RTDB Emulator v4.11.0: Wire protocol update for `startAfter`, `endBefore`. -- Changes `superstatic` dependency to `v8`, addressing Hosting emulator issues on Windows. -- Fixes internal library that was not being correctly published. -- Add support for Next.js 13 in firebase deploy. -- Next.js routes with revalidate are now handled by the a backing Cloud Function. -- Adds `--disable-triggers` flag to RTDB write commands. -- Default enables experiment to skip deploying unmodified functions (#5192) -- Default enables experiment to allow parameterized functions codebases (#5192) -- Fixes parallel requests in the functions emulator (#5149). -- Unspecified functions concurrency will shift between the defaults of 1 or 80 when CPU is changed to support/not support concurrency (#5196) From ab27d16717781c296627efc0547a0e8d11e5c1d0 Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Wed, 2 Nov 2022 15:20:01 -0700 Subject: [PATCH 079/115] Storage default rules (#5167) * Default storage config using demo prefix. --- src/emulator/storage/rules/config.ts | 13 ++++++++++ .../emulators/storage/rules/config.spec.ts | 26 +++++++++++++++++++ templates/emulators/default_storage.rules | 8 ++++++ 3 files changed, 47 insertions(+) create mode 100644 templates/emulators/default_storage.rules diff --git a/src/emulator/storage/rules/config.ts b/src/emulator/storage/rules/config.ts index 3786ecae7186..911e1effd016 100644 --- a/src/emulator/storage/rules/config.ts +++ b/src/emulator/storage/rules/config.ts @@ -3,6 +3,9 @@ import { FirebaseError } from "../../../error"; import { readFile } from "../../../fsutils"; import { Options } from "../../../options"; import { SourceFile } from "./types"; +import { Constants } from "../../constants"; +import { Emulators } from "../../types"; +import { EmulatorLogger } from "../../emulatorLogger"; function getSourceFile(rules: string, options: Options): SourceFile { const path = options.config.path(rules); @@ -21,6 +24,16 @@ export function getStorageRulesConfig( ): SourceFile | RulesConfig[] { const storageConfig = options.config.data.storage; if (!storageConfig) { + if (Constants.isDemoProject(projectId)) { + const storageLogger = EmulatorLogger.forEmulator(Emulators.STORAGE); + storageLogger.logLabeled( + "BULLET", + "storage", + `Detected demo project ID "${projectId}", using a default (open) rules configuration.` + ); + const path = __dirname + "/../../../../templates/emulators/default_storage.rules"; + return { name: path, content: readFile(path) }; + } throw new FirebaseError( "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration" ); diff --git a/src/test/emulators/storage/rules/config.spec.ts b/src/test/emulators/storage/rules/config.spec.ts index a797c83e3e90..ddd121dabec5 100644 --- a/src/test/emulators/storage/rules/config.spec.ts +++ b/src/test/emulators/storage/rules/config.spec.ts @@ -31,6 +31,32 @@ describe("Storage Rules Config", () => { expect(result.content).to.contain("allow read, write: if true"); }); + it("should use default config for project IDs using demo- prefix if no rules file exists", () => { + const config = getOptions({ + data: {}, + path: resolvePath, + }); + const result = getStorageRulesConfig("demo-projectid", config) as SourceFile; + + expect(result.name).to.contain("templates/emulators/default_storage.rules"); + expect(result.content).to.contain("allow read, write;"); + }); + + it("should use provided config for project IDs using demo- prefix if the provided config exists", () => { + const rulesFile = "storage.rules"; + const rulesContent = Buffer.from(StorageRulesFiles.readWriteIfTrue.content); + const path = persistence.appendBytes(rulesFile, rulesContent); + + const config = getOptions({ + data: { storage: { rules: path } }, + path: resolvePath, + }); + const result = getStorageRulesConfig("demo-projectid", config) as SourceFile; + + expect(result.name).to.equal(path); + expect(result.content).to.contain("allow read, write: if true"); + }); + it("should parse rules file for multiple targets", () => { const mainRulesContent = Buffer.from(StorageRulesFiles.readWriteIfTrue.content); const otherRulesContent = Buffer.from(StorageRulesFiles.readWriteIfAuth.content); diff --git a/templates/emulators/default_storage.rules b/templates/emulators/default_storage.rules new file mode 100644 index 000000000000..80c50ce59be9 --- /dev/null +++ b/templates/emulators/default_storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write; + } + } +} From b02065f779a30470ce7d32aa3e0683a1def29c2d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 3 Nov 2022 08:45:02 -0700 Subject: [PATCH 080/115] Fix integration test. (#5204) Concurrency will aggressively default to 80 per https://github.com/firebase/firebase-tools/pull/5196. --- scripts/functions-deploy-tests/tests.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/functions-deploy-tests/tests.ts b/scripts/functions-deploy-tests/tests.ts index 7a4a75078ee3..2f18b924aa99 100644 --- a/scripts/functions-deploy-tests/tests.ts +++ b/scripts/functions-deploy-tests/tests.ts @@ -306,7 +306,10 @@ describe("firebase deploy", function (this) { if (e.platform === "gcfv2") { expect(e).to.include({ cpu: 2, - concurrency: 42, + // EXCEPTION: concurrency + // Firebase will aggressively set concurrency to 80 when the CPU setting allows for it + // AND when the concurrency is NOT set on the source code. + concurrency: 80, }); } // BUGBUG: As implemented, Cloud Tasks update doesn't preserve existing setting. Instead, it overwrites the From ea2a9ecab402febcd28ccaa402fd56e7202b7a94 Mon Sep 17 00:00:00 2001 From: abhis3 Date: Thu, 3 Nov 2022 16:55:56 -0400 Subject: [PATCH 081/115] Add support for object list using certain Admin SDKs (#5209) * Add support for object list using certain Admin SDKs --- CHANGELOG.md | 1 + .../conformance/gcs.endpoints.test.ts | 26 +++++++++++++++++++ src/emulator/storage/apis/gcloud.ts | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..74ad7091bcf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add support for object list using certain Admin SDKs (#5208) diff --git a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts index 6185ac6d2edb..792ec8f44efd 100644 --- a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts +++ b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts @@ -293,4 +293,30 @@ describe("GCS endpoint conformance tests", () => { }); }); }); + + describe("List protocols", () => { + describe("list objects", () => { + // This test is for the '/storage/v1/b/:bucketId/o' url pattern, which is used specifically by the GO Admin SDK + it("should list objects in the provided bucket", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}2`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const data = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + expect(data.items.length).to.equal(2); + }); + }); + }); }); diff --git a/src/emulator/storage/apis/gcloud.ts b/src/emulator/storage/apis/gcloud.ts index ff4748921ae4..017ab40f6715 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -135,7 +135,7 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { return res.json(new CloudStorageObjectMetadata(updatedMetadata)); }); - gcloudStorageAPI.get("/b/:bucketId/o", async (req, res) => { + gcloudStorageAPI.get(["/b/:bucketId/o", "/storage/v1/b/:bucketId/o"], async (req, res) => { let listResponse: ListObjectsResponse; // TODO validate that all query params are single strings and are not repeated. try { From 98e23eded74e49f58d48be0417dc66bd85ea6e11 Mon Sep 17 00:00:00 2001 From: blidd-google <112491344+blidd-google@users.noreply.github.com> Date: Fri, 4 Nov 2022 19:06:20 -0400 Subject: [PATCH 082/115] Fixes source token expiration by token refresh (#5198) * refresh source token * check if token is expired right before deploy call * use Date lib, rename states & remove enums * clean up, revert timer changes * add type guard to token states --- CHANGELOG.md | 1 + src/deploy/functions/release/fabricator.ts | 6 +- .../functions/release/sourceTokenScraper.ts | 51 +++++++++++---- .../release/sourceTokenScraper.spec.ts | 63 ++++++++++++++++--- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ad7091bcf3..a0516d8cf0ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Add support for object list using certain Admin SDKs (#5208) +- Fixes source token expiration issue by acquiring new source token upon expiration. diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index 4316cb0e37a4..310cf82dcb66 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -210,9 +210,10 @@ export class Fabricator { if (apiFunction.httpsTrigger) { apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS"; } - apiFunction.sourceToken = await scraper.tokenPromise(); const resultFunction = await this.functionExecutor .run(async () => { + // try to get the source token right before deploying + apiFunction.sourceToken = await scraper.getToken(); const op: { name: string } = await gcf.createFunction(apiFunction); return poller.pollOperation({ ...gcfV1PollerOptions, @@ -374,9 +375,10 @@ export class Fabricator { throw new Error("Precondition failed"); } const apiFunction = gcf.functionFromEndpoint(endpoint, sourceUrl); - apiFunction.sourceToken = await scraper.tokenPromise(); + const resultFunction = await this.functionExecutor .run(async () => { + apiFunction.sourceToken = await scraper.getToken(); const op: { name: string } = await gcf.updateFunction(apiFunction); return await poller.pollOperation({ ...gcfV1PollerOptions, diff --git a/src/deploy/functions/release/sourceTokenScraper.ts b/src/deploy/functions/release/sourceTokenScraper.ts index 482e3d77406a..96b5a04d7428 100644 --- a/src/deploy/functions/release/sourceTokenScraper.ts +++ b/src/deploy/functions/release/sourceTokenScraper.ts @@ -1,28 +1,55 @@ +import { FirebaseError } from "../../../error"; +import { assertExhaustive } from "../../../functional"; import { logger } from "../../../logger"; +type TokenFetchState = "NONE" | "FETCHING" | "VALID"; + /** * GCF v1 deploys support reusing a build between function deploys. * This class will return a resolved promise for its first call to tokenPromise() * and then will always return a promise that is resolved by the poller function. */ export class SourceTokenScraper { - private firstCall = true; - private resolve!: (token: string) => void; + private tokenValidDurationMs; + private resolve!: (token?: string) => void; private promise: Promise; + private expiry: number | undefined; + private fetchState: TokenFetchState; - constructor() { + constructor(validDurationMs = 1500000) { + this.tokenValidDurationMs = validDurationMs; this.promise = new Promise((resolve) => (this.resolve = resolve)); + this.fetchState = "NONE"; + } + + async getToken(): Promise { + if (this.fetchState === "NONE") { + this.fetchState = "FETCHING"; + return undefined; + } else if (this.fetchState === "FETCHING") { + return this.promise; // wait until we get a source token + } else if (this.fetchState === "VALID") { + if (this.isTokenExpired()) { + this.fetchState = "FETCHING"; + this.promise = new Promise((resolve) => (this.resolve = resolve)); + return undefined; + } + return this.promise; + } else { + assertExhaustive(this.fetchState); + } } - // Token Promise will return undefined for the first caller - // (because we presume it's this function's source token we'll scrape) - // and then returns the promise generated from the first function's onCall - tokenPromise(): Promise { - if (this.firstCall) { - this.firstCall = false; - return Promise.resolve(undefined); + isTokenExpired(): boolean { + if (this.expiry === undefined) { + throw new FirebaseError( + "Your deployment is checking the expiration of a source token that has not yet been polled. " + + "Hitting this case should never happen and should be considered a bug. " + + "Please file an issue at https://github.com/firebase/firebase-tools/issues " + + "and try deploying your functions again." + ); } - return this.promise; + return Date.now() >= this.expiry; } get poller() { @@ -32,6 +59,8 @@ export class SourceTokenScraper { op.metadata?.target?.split("/") || []; logger.debug(`Got source token ${op.metadata?.sourceToken} for region ${region as string}`); this.resolve(op.metadata?.sourceToken); + this.fetchState = "VALID"; + this.expiry = Date.now() + this.tokenValidDurationMs; } }; } diff --git a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts b/src/test/deploy/functions/release/sourceTokenScraper.spec.ts index 40927a0b25c8..28a0490e99ea 100644 --- a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts +++ b/src/test/deploy/functions/release/sourceTokenScraper.spec.ts @@ -2,23 +2,23 @@ import { expect } from "chai"; import { SourceTokenScraper } from "../../../../deploy/functions/release/sourceTokenScraper"; -describe("SourcTokenScraper", () => { +describe("SourceTokenScraper", () => { it("immediately provides the first result", async () => { const scraper = new SourceTokenScraper(); - await expect(scraper.tokenPromise()).to.eventually.be.undefined; + await expect(scraper.getToken()).to.eventually.be.undefined; }); - it("provides results after the firt operation completes", async () => { + it("provides results after the first operation completes", async () => { const scraper = new SourceTokenScraper(); // First result comes right away; - await expect(scraper.tokenPromise()).to.eventually.be.undefined; + await expect(scraper.getToken()).to.eventually.be.undefined; let gotResult = false; const timeout = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Timeout")), 10); }); const getResult = (async () => { - await scraper.tokenPromise(); + await scraper.getToken(); gotResult = true; })(); await expect(Promise.race([getResult, timeout])).to.be.rejectedWith("Timeout"); @@ -31,7 +31,7 @@ describe("SourcTokenScraper", () => { it("provides tokens from an operation", async () => { const scraper = new SourceTokenScraper(); // First result comes right away - await expect(scraper.tokenPromise()).to.eventually.be.undefined; + await expect(scraper.getToken()).to.eventually.be.undefined; scraper.poller({ metadata: { @@ -39,6 +39,55 @@ describe("SourcTokenScraper", () => { target: "projects/p/locations/l/functions/f", }, }); - await expect(scraper.tokenPromise()).to.eventually.equal("magic token"); + await expect(scraper.getToken()).to.eventually.equal("magic token"); + }); + + it("refreshes token after timer expires", async () => { + const scraper = new SourceTokenScraper(10); + await expect(scraper.getToken()).to.eventually.be.undefined; + scraper.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + await expect(scraper.getToken()).to.eventually.equal("magic token"); + const timeout = (duration: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, duration)); + }; + await timeout(50); + await expect(scraper.getToken()).to.eventually.be.undefined; + scraper.poller({ + metadata: { + sourceToken: "magic token #2", + target: "projects/p/locations/l/functions/f", + }, + }); + await expect(scraper.getToken()).to.eventually.equal("magic token #2"); + }); + + it("concurrent requests for source token", async () => { + const scraper = new SourceTokenScraper(); + + const promises = []; + for (let i = 0; i < 3; i++) { + promises.push(scraper.getToken()); + } + scraper.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + + let successes = 0; + const tokens = await Promise.all(promises); + for (const tok of tokens) { + if (tok === "magic token") { + successes++; + } + } + expect(tokens.includes(undefined)).to.be.true; + expect(successes).to.equal(2); }); }); From 72d23ec4e77f0d5b52f62e7be1bf083fa94c95af Mon Sep 17 00:00:00 2001 From: Tony Huang Date: Mon, 7 Nov 2022 15:58:34 +0900 Subject: [PATCH 083/115] Handle gzip compression in Storage Emulator (#5185) * fix gzip for gcloud * fix firebase * revert * lint * update changelog --- CHANGELOG.md | 1 + .../conformance/env.ts | 9 ++- .../conformance/firebase-js-sdk.test.ts | 23 +++--- .../conformance/firebase.endpoints.test.ts | 73 ++++++++++++++++++- .../conformance/gcs-js-sdk.test.ts | 72 +++++++++++------- .../conformance/gcs.endpoints.test.ts | 72 ++++++++++++++++++ scripts/storage-emulator-integration/run.sh | 4 + src/emulator/storage/apis/firebase.ts | 43 ++--------- src/emulator/storage/apis/gcloud.ts | 37 +--------- src/emulator/storage/apis/shared.ts | 57 +++++++++++++++ 10 files changed, 277 insertions(+), 114 deletions(-) create mode 100644 src/emulator/storage/apis/shared.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a0516d8cf0ff..cb3dd4e68f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ +- Fixes gzipped file handling in Storage Emulator. - Add support for object list using certain Admin SDKs (#5208) - Fixes source token expiration issue by acquiring new source token upon expiration. diff --git a/scripts/storage-emulator-integration/conformance/env.ts b/scripts/storage-emulator-integration/conformance/env.ts index ab5d29736b18..c131178e4eec 100644 --- a/scripts/storage-emulator-integration/conformance/env.ts +++ b/scripts/storage-emulator-integration/conformance/env.ts @@ -74,7 +74,7 @@ function readEmulatorConfig(): FrameworkOptions { class ConformanceTestEnvironment { private _prodAppConfig: any; private _emulatorConfig: any; - private _prodServiceAccountKeyJson?: any; + private _prodServiceAccountKeyJson?: any | null; private _adminAccessToken?: string; get useProductionServers() { @@ -125,9 +125,10 @@ class ConformanceTestEnvironment { get prodServiceAccountKeyJson() { if (this._prodServiceAccountKeyJson === undefined) { const filePath = path.join(__dirname, TEST_CONFIG.prodServiceAccountKeyFilePath); - return TEST_CONFIG.prodServiceAccountKeyFilePath && fs.existsSync(filePath) - ? readAbsoluteJson(filePath) - : null; + this._prodServiceAccountKeyJson = + TEST_CONFIG.prodServiceAccountKeyFilePath && fs.existsSync(filePath) + ? readAbsoluteJson(filePath) + : null; } return this._prodServiceAccountKeyJson; } diff --git a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts index 791e5ac57a5a..3f3e51f56ecb 100644 --- a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts +++ b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts @@ -14,11 +14,14 @@ import { SMALL_FILE_SIZE, TEST_SETUP_TIMEOUT, getTmpDir, - writeToFile, } from "../utils"; const TEST_FILE_NAME = "testing/storage_ref/testFile"; +// Test case that should only run when targeting the emulator. +// Example use: emulatorOnly.it("Local only test case", () => {...}); +const emulatorOnly = { it: TEST_ENV.useProductionServers ? it.skip : it }; + describe("Firebase Storage JavaScript SDK conformance tests", () => { const storageBucket = TEST_ENV.appConfig.storageBucket; const expectedFirebaseHost = TEST_ENV.firebaseHost; @@ -27,11 +30,6 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { const tmpDir = getTmpDir(); const smallFilePath: string = createRandomFile("small_file", SMALL_FILE_SIZE, tmpDir); const emptyFilePath: string = createRandomFile("empty_file", 0, tmpDir); - const imageFilePath = writeToFile( - "image_base64", - Buffer.from(IMAGE_FILE_BASE64, "base64"), - tmpDir - ); let test: EmulatorEndToEndTest; let testBucket: Bucket; @@ -451,7 +449,8 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); it("serves the right content", async () => { - await testBucket.upload(imageFilePath, { destination: TEST_FILE_NAME }); + const contents = Buffer.from("hello world"); + await testBucket.file(TEST_FILE_NAME).save(contents); await signInToFirebaseAuth(page); const downloadUrl = await page.evaluate((filename) => { @@ -460,11 +459,13 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { await new Promise((resolve, reject) => { TEST_ENV.requestClient.get(downloadUrl, (response) => { - const data: any = []; + let data = Buffer.alloc(0); response - .on("data", (chunk) => data.push(chunk)) + .on("data", (chunk) => { + data = Buffer.concat([data, chunk]); + }) .on("end", () => { - expect(Buffer.concat(data)).to.deep.equal(Buffer.from(IMAGE_FILE_BASE64, "base64")); + expect(data).to.deep.equal(contents); }) .on("close", resolve) .on("error", reject); @@ -472,7 +473,7 @@ describe("Firebase Storage JavaScript SDK conformance tests", () => { }); }); - it("serves content successfully when spammed with calls", async () => { + emulatorOnly.it("serves content successfully when spammed with calls", async () => { const NUMBER_OF_FILES = 10; const allFileNames: string[] = []; for (let i = 0; i < NUMBER_OF_FILES; i++) { diff --git a/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts index a087279e3bd0..dcd7e2464117 100644 --- a/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts +++ b/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts @@ -3,6 +3,7 @@ import { expect } from "chai"; import * as admin from "firebase-admin"; import * as fs from "fs"; import * as supertest from "supertest"; +import { gunzipSync } from "zlib"; import { TEST_ENV } from "./env"; import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; import { @@ -343,7 +344,6 @@ describe("Firebase Storage endpoint conformance tests", () => { }) .expect(200) .then((res) => { - console.log(res); return new URL(res.header["x-goog-upload-url"]); }); @@ -475,6 +475,77 @@ describe("Firebase Storage endpoint conformance tests", () => { }); }); + describe("gzip", () => { + it("should serve gunzipped file by default", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${firebaseHost}/v0/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.undefined; + expect(res.headers["content-length"]).to.be.undefined; + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + } + ); + }); + }); + + it("should serve gzipped file if Accept-Encoding header allows", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${firebaseHost}/v0/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader, "Accept-Encoding": "gzip" } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.eql("gzip"); + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.not.be.eql(contents); + const decompressed = gunzipSync(responseBody); + expect(decompressed).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + } + ); + }); + }); + }); + describe("tokens", () => { beforeEach(async () => { await testBucket.upload(smallFilePath, { destination: TEST_FILE_NAME }); diff --git a/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts index 17c2ca0bda4f..af10db916932 100644 --- a/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts +++ b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts @@ -3,7 +3,6 @@ import { expect } from "chai"; import * as admin from "firebase-admin"; import * as fs from "fs"; import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; -import * as supertest from "supertest"; import { TEST_ENV } from "./env"; import { createRandomFile, @@ -13,6 +12,7 @@ import { TEST_SETUP_TIMEOUT, getTmpDir, } from "../utils"; +import { gunzipSync } from "zlib"; // Test case that should only run when targeting the emulator. // Example use: emulatorOnly.it("Local only test case", () => {...}); @@ -27,7 +27,6 @@ describe("GCS Javascript SDK conformance tests", () => { const storageBucket = TEST_ENV.appConfig.storageBucket; const otherStorageBucket = TEST_ENV.secondTestBucket; const storageHost = TEST_ENV.storageHost; - const firebaseHost = TEST_ENV.firebaseHost; const googleapisHost = TEST_ENV.googleapisHost; let test: EmulatorEndToEndTest; @@ -109,14 +108,6 @@ describe("GCS Javascript SDK conformance tests", () => { fs.unlinkSync(content2); }); - it("should handle gzip'd uploads", async () => { - // This appears to pass, but the file gets corrupted cause it's gzipped? - // expect(true).to.be.false; - await testBucket.upload(smallFilePath, { - gzip: true, - }); - }); - it("should upload with provided metadata", async () => { const metadata = { contentDisposition: "attachment", @@ -139,27 +130,15 @@ describe("GCS Javascript SDK conformance tests", () => { metadata: {}, }); - const cloudFile = testBucket.file(testFileName); + const file = testBucket.file(testFileName); const incomingMetadata = { metadata: { firebaseStorageDownloadTokens: "myFirstToken,mySecondToken", }, }; - await cloudFile.setMetadata(incomingMetadata); - - // Check that the tokens are saved in Firebase metadata - await supertest(firebaseHost) - .get(`/v0/b/${testBucket.name}/o/${encodeURIComponent(testFileName)}`) - .expect(200) - .then((res) => { - const firebaseMd = res.body; - expect(firebaseMd.downloadTokens).to.equal( - incomingMetadata.metadata.firebaseStorageDownloadTokens - ); - }); + await file.setMetadata(incomingMetadata); - // Check that the tokens are saved in Cloud metadata - const [storedMetadata] = await cloudFile.getMetadata(); + const [storedMetadata] = await file.getMetadata(); expect(storedMetadata.metadata.firebaseStorageDownloadTokens).to.deep.equal( incomingMetadata.metadata.firebaseStorageDownloadTokens ); @@ -392,6 +371,26 @@ describe("GCS Javascript SDK conformance tests", () => { }); describe(".file()", () => { + describe("#save()", () => { + it("should save", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { contentType: "text/plain" }); + + expect(file.metadata.contentType).to.be.eql("text/plain"); + const [downloadedContents] = await file.download(); + expect(downloadedContents).to.be.eql(contents); + }); + + it("should handle gzipped uploads", async () => { + const file = testBucket.file("gzippedFile"); + await file.save("hello world", { gzip: true }); + + expect(file.metadata.contentEncoding).to.be.eql("gzip"); + }); + }); + describe("#exists()", () => { it("should return false for a file that does not exist", async () => { // Ensure that the file exists on the bucket before deleting it @@ -488,6 +487,29 @@ describe("GCS Javascript SDK conformance tests", () => { expect(err).to.have.property("code", 404); expect(err).not.have.nested.property("errors[0]"); }); + + it("should decompress gzipped file", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { gzip: true }); + + const [downloadedContents] = await file.download(); + expect(downloadedContents).to.be.eql(contents); + }); + + it("should serve gzipped file if decompress option specified", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { gzip: true }); + + const [downloadedContents] = await file.download({ decompress: false }); + expect(downloadedContents).to.not.be.eql(contents); + + const ungzippedContents = gunzipSync(downloadedContents); + expect(ungzippedContents).to.be.eql(contents); + }); }); describe("#copy()", () => { diff --git a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts index 792ec8f44efd..1f772f5248a3 100644 --- a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts +++ b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts @@ -4,6 +4,7 @@ import * as admin from "firebase-admin"; import * as fs from "fs"; import * as supertest from "supertest"; import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import { gunzipSync } from "zlib"; import { TEST_ENV } from "./env"; import { EMULATORS_SHUTDOWN_DELAY_MS, @@ -294,6 +295,77 @@ describe("GCS endpoint conformance tests", () => { }); }); + describe("Gzip", () => { + it("should serve gunzipped file by default", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.undefined; + expect(res.headers["content-length"]).to.be.undefined; + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + } + ); + }); + }); + + it("should serve gzipped file if Accept-Encoding header allows", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader, "Accept-Encoding": "gzip" } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.eql("gzip"); + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.not.be.eql(contents); + const decompressed = gunzipSync(responseBody); + expect(decompressed).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + } + ); + }); + }); + }); + describe("List protocols", () => { describe("list objects", () => { // This test is for the '/storage/v1/b/:bucketId/o' url pattern, which is used specifically by the GO Admin SDK diff --git a/scripts/storage-emulator-integration/run.sh b/scripts/storage-emulator-integration/run.sh index 413fd28b9b3f..f2fe4cefceb9 100755 --- a/scripts/storage-emulator-integration/run.sh +++ b/scripts/storage-emulator-integration/run.sh @@ -4,6 +4,9 @@ set -e # Immediately exit on failure # Globally link the CLI for the testing framework ./scripts/npm-link.sh +# Set application default credentials. +source scripts/set-default-credentials.sh + # Prepare the storage emulator rules runtime firebase setup:emulators:storage @@ -16,3 +19,4 @@ mocha scripts/storage-emulator-integration/internal/tests.ts mocha scripts/storage-emulator-integration/multiple-targets/tests.ts mocha scripts/storage-emulator-integration/conformance/*.test.ts + diff --git a/src/emulator/storage/apis/firebase.ts b/src/emulator/storage/apis/firebase.ts index 60544de9c319..5a0c88369de2 100644 --- a/src/emulator/storage/apis/firebase.ts +++ b/src/emulator/storage/apis/firebase.ts @@ -1,10 +1,10 @@ import { EmulatorLogger } from "../../emulatorLogger"; import { Emulators } from "../../types"; import * as uuid from "uuid"; -import { gunzipSync } from "zlib"; import { IncomingMetadata, OutgoingFirebaseMetadata, StoredFileMetadata } from "../metadata"; import { Request, Response, Router } from "express"; import { StorageEmulator } from "../index"; +import { sendFileBytes } from "./shared"; import { EmulatorRegistry } from "../../registry"; import { parseObjectUploadMultipartRequest } from "../multipart"; import { NotFoundError, ForbiddenError } from "../errors"; @@ -27,7 +27,7 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { if (process.env.STORAGE_EMULATOR_DEBUG) { firebaseStorageAPI.use((req, res, next) => { - console.log("--------------INCOMING REQUEST--------------"); + console.log("--------------INCOMING FIREBASE REQUEST--------------"); console.log(`${req.method.toUpperCase()} ${req.path}`); console.log("-- query:"); console.log(JSON.stringify(req.query, undefined, 2)); @@ -121,29 +121,7 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { // Object data request if (req.query.alt === "media") { - const isGZipped = metadata.contentEncoding === "gzip"; - if (isGZipped) { - data = gunzipSync(data); - } - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Type", metadata.contentType || "application/octet-stream"); - res.setHeader("Content-Disposition", metadata.contentDisposition || "inline"); - setObjectHeaders(res, metadata, { "Content-Encoding": isGZipped ? "identity" : undefined }); - - const byteRange = req.range(data.byteLength, { combine: true }); - - if (Array.isArray(byteRange) && byteRange.type === "bytes" && byteRange.length > 0) { - const range = byteRange[0]; - res.setHeader( - "Content-Range", - `${byteRange.type} ${range.start}-${range.end}/${data.byteLength}` - ); - // Byte range requests are inclusive for start and end - res.status(206).end(data.slice(range.start, range.end + 1)); - } else { - res.end(data); - } - return; + return sendFileBytes(metadata, data, req, res); } // Object metadata request @@ -531,27 +509,16 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { return firebaseStorageAPI; } -function setObjectHeaders( - res: Response, - metadata: StoredFileMetadata, - headerOverride: { - "Content-Encoding": string | undefined; - } = { "Content-Encoding": undefined } -): void { +function setObjectHeaders(res: Response, metadata: StoredFileMetadata): void { if (metadata.contentDisposition) { res.setHeader("Content-Disposition", metadata.contentDisposition); } - - if (headerOverride["Content-Encoding"]) { - res.setHeader("Content-Encoding", headerOverride["Content-Encoding"]); - } else if (metadata.contentEncoding) { + if (metadata.contentEncoding) { res.setHeader("Content-Encoding", metadata.contentEncoding); } - if (metadata.cacheControl) { res.setHeader("Cache-Control", metadata.cacheControl); } - if (metadata.contentLanguage) { res.setHeader("Content-Language", metadata.contentLanguage); } diff --git a/src/emulator/storage/apis/gcloud.ts b/src/emulator/storage/apis/gcloud.ts index 017ab40f6715..1efc031052c2 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -1,5 +1,4 @@ import { Router } from "express"; -import { gunzipSync } from "zlib"; import { Emulators } from "../../types"; import { CloudStorageObjectAccessControlMetadata, @@ -7,11 +6,11 @@ import { IncomingMetadata, StoredFileMetadata, } from "../metadata"; +import { sendFileBytes } from "./shared"; import { EmulatorRegistry } from "../../registry"; import { StorageEmulator } from "../index"; import { EmulatorLogger } from "../../emulatorLogger"; import { GetObjectResponse, ListObjectsResponse } from "../files"; -import { crc32cToString } from "../crc"; import type { Request, Response } from "express"; import { parseObjectUploadMultipartRequest } from "../multipart"; import { Upload, UploadNotActiveError } from "../upload"; @@ -28,7 +27,7 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { // Debug statements if (process.env.STORAGE_EMULATOR_DEBUG) { gcloudStorageAPI.use((req, res, next) => { - console.log("--------------INCOMING REQUEST--------------"); + console.log("--------------INCOMING GCS REQUEST--------------"); console.log(`${req.method.toUpperCase()} ${req.path}`); console.log("-- query:"); console.log(JSON.stringify(req.query, undefined, 2)); @@ -429,38 +428,6 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { return gcloudStorageAPI; } -function sendFileBytes(md: StoredFileMetadata, data: Buffer, req: Request, res: Response): void { - const isGZipped = md.contentEncoding === "gzip"; - if (isGZipped) { - data = gunzipSync(data); - } - - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Type", md.contentType || "application/octet-stream"); - res.setHeader("Content-Disposition", md.contentDisposition || "attachment"); - res.setHeader("Content-Encoding", isGZipped ? "identity" : md.contentEncoding || ""); - res.setHeader("ETag", md.etag); - res.setHeader("Cache-Control", md.cacheControl || ""); - res.setHeader("x-goog-generation", `${md.generation}`); - res.setHeader("x-goog-metadatageneration", `${md.metageneration}`); - res.setHeader("x-goog-storage-class", md.storageClass); - res.setHeader("x-goog-hash", `crc32c=${crc32cToString(md.crc32c)},md5=${md.md5Hash}`); - - const byteRange = req.range(data.byteLength, { combine: true }); - - if (Array.isArray(byteRange) && byteRange.type === "bytes" && byteRange.length > 0) { - const range = byteRange[0]; - res.setHeader( - "Content-Range", - `${byteRange.type} ${range.start}-${range.end}/${data.byteLength}` - ); - // Byte range requests are inclusive for start and end - res.status(206).end(data.slice(range.start, range.end + 1)); - } else { - res.end(data); - } -} - /** Sends 404 matching API */ function sendObjectNotFound(req: Request, res: Response): void { res.status(404); diff --git a/src/emulator/storage/apis/shared.ts b/src/emulator/storage/apis/shared.ts new file mode 100644 index 000000000000..8cdb0fd09d91 --- /dev/null +++ b/src/emulator/storage/apis/shared.ts @@ -0,0 +1,57 @@ +import { gunzipSync } from "zlib"; +import { StoredFileMetadata } from "../metadata"; +import { Request, Response } from "express"; +import { crc32cToString } from "../crc"; + +/** Populates an object media GET Express response. */ +export function sendFileBytes( + md: StoredFileMetadata, + data: Buffer, + req: Request, + res: Response +): void { + let didGunzip = false; + if (md.contentEncoding === "gzip") { + const acceptEncoding = req.header("accept-encoding") || ""; + const shouldGunzip = !acceptEncoding.includes("gzip"); + if (shouldGunzip) { + data = gunzipSync(data); + didGunzip = true; + } + } + res.setHeader("Accept-Ranges", "bytes"); + res.setHeader("Content-Type", md.contentType || "application/octet-stream"); + res.setHeader("Content-Disposition", md.contentDisposition || "attachment"); + if (didGunzip) { + // Set to mirror server behavior and supress express's "content-length" header. + res.setHeader("Transfer-Encoding", "chunked"); + } else { + // Don't populate Content-Encoding if decompressed, see + // https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding. + res.setHeader("Content-Encoding", md.contentEncoding || ""); + } + res.setHeader("ETag", md.etag); + res.setHeader("Cache-Control", md.cacheControl || ""); + res.setHeader("x-goog-generation", `${md.generation}`); + res.setHeader("x-goog-metadatageneration", `${md.metageneration}`); + res.setHeader("x-goog-storage-class", md.storageClass); + res.setHeader("x-goog-hash", `crc32c=${crc32cToString(md.crc32c)},md5=${md.md5Hash}`); + + // Content Range headers should be respected only if data was not decompressed, see + // https://cloud.google.com/storage/docs/transcoding#range. + const shouldRespectContentRange = !didGunzip; + if (shouldRespectContentRange) { + const byteRange = req.range(data.byteLength, { combine: true }); + if (Array.isArray(byteRange) && byteRange.type === "bytes" && byteRange.length > 0) { + const range = byteRange[0]; + res.setHeader( + "Content-Range", + `${byteRange.type} ${range.start}-${range.end}/${data.byteLength}` + ); + // Byte range requests are inclusive for start and end + res.status(206).end(data.slice(range.start, range.end + 1)); + return; + } + } + res.end(data); +} From db6223ecdafddf36266275800f02e71036c89c5f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 7 Nov 2022 09:57:13 -0800 Subject: [PATCH 084/115] Fix bug where event triggered functions failed in debug mode (#5211) A classic case of deadlock! Internally, the Functions Emulator maintains a work queue which processes unit of work called "tasks" with configured parallelism. When Functions Emulator runs in debug mode, the work queue runs in `SEQUENTIAL` mode where tasks are processed one by one in FIFO order. When event functions are triggered via the multicast route (e.g. storage and auth triggers), Functions Emulator submits a task per function associated with the event to the work queue where each task makes call to the event function via its HTTP endpoint. Let's call this task an "invocation task". When the Function Emulator receives an HTTP request for function invocation, it submits a new task to the work queue to handle the http request. Let's call this task a "http task". So, a call to the multicast route begets zero or more "invocation task" (one per trigger). Each "invocation task" begets exactly one "http task". The code today is written such that an "invocation task" will wait for the corresponding "http task" to complete. In debug mode this causes the invocation task to wait forever - the "http task" is stuck behind its "invocation task" in the queue. It never has chance to be processed. This PR proposes that "invocation task" doesn't wait for the corresponding "http task" to complete, effectively breaking the deadlock. The result is that "invocation task" now fire-and-forget an "http task". AFAIK, this doesn't have any negative effect because "invocation task" didn't do anything with the result of the "http task" anyway. Fixes https://github.com/firebase/firebase-tools/issues/5008, https://github.com/firebase/firebase-tools/issues/5050 --- CHANGELOG.md | 1 + .../tests.inspect.ts | 84 ++++++++++++------- src/emulator/functionsEmulator.ts | 20 ++--- 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3dd4e68f49..6ec1619b56d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Fixes gzipped file handling in Storage Emulator. - Add support for object list using certain Admin SDKs (#5208) - Fixes source token expiration issue by acquiring new source token upon expiration. +- Fix bug where emulated event triggered function broke in debug mode (#5211) diff --git a/scripts/triggers-end-to-end-tests/tests.inspect.ts b/scripts/triggers-end-to-end-tests/tests.inspect.ts index 9ea3d84cc1f9..a25fe795d9b8 100755 --- a/scripts/triggers-end-to-end-tests/tests.inspect.ts +++ b/scripts/triggers-end-to-end-tests/tests.inspect.ts @@ -10,6 +10,7 @@ const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; * parallel emulator subprocesses. */ const TEST_SETUP_TIMEOUT = 80000; +const EMULATORS_WRITE_DELAY_MS = 5000; const EMULATORS_SHUTDOWN_DELAY_MS = 5000; function readConfig(): FrameworkOptions { @@ -28,7 +29,7 @@ describe("function triggers with inspect flag", () => { const config = readConfig(); test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions", "--inspect-functions"]); + await test.startEmulators(["--only", "functions,auth,storage", "--inspect-functions"]); }); after(async function (this) { @@ -36,37 +37,60 @@ describe("function triggers with inspect flag", () => { await test.stopEmulators(); }); - it("should invoke correct function in the same codebase", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - const v1response = await test.invokeHttpFunction("onreqv2b"); - expect(v1response.status).to.equal(200); - const v1body = await v1response.text(); - expect(v1body).to.deep.equal("onreqv2b"); - - const v2response = await test.invokeHttpFunction("onreqv2a"); - expect(v2response.status).to.equal(200); - const v2body = await v2response.text(); - expect(v2body).to.deep.equal("onreqv2a"); - }); + describe("http functions", () => { + it("should invoke correct function in the same codebase", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v1response = await test.invokeHttpFunction("onreqv2b"); + expect(v1response.status).to.equal(200); + const v1body = await v1response.text(); + expect(v1body).to.deep.equal("onreqv2b"); - it("should invoke correct function across codebases", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - const v1response = await test.invokeHttpFunction("onReq"); - expect(v1response.status).to.equal(200); - const v1body = await v1response.text(); - expect(v1body).to.deep.equal("onReq"); - - const v2response = await test.invokeHttpFunction("onreqv2a"); - expect(v2response.status).to.equal(200); - const v2body = await v2response.text(); - expect(v2body).to.deep.equal("onreqv2a"); + const v2response = await test.invokeHttpFunction("onreqv2a"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2a"); + }); + + it("should invoke correct function across codebases", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v1response = await test.invokeHttpFunction("onReq"); + expect(v1response.status).to.equal(200); + const v1body = await v1response.text(); + expect(v1body).to.deep.equal("onReq"); + + const v2response = await test.invokeHttpFunction("onreqv2a"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2a"); + }); + + it("should disable timeout", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v2response = await test.invokeHttpFunction("onreqv2timeout"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2timeout"); + }); }); - it("should disable timeout", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - const v2response = await test.invokeHttpFunction("onreqv2timeout"); - expect(v2response.status).to.equal(200); - const v2body = await v2response.text(); - expect(v2body).to.deep.equal("onreqv2timeout"); + describe("event triggered (multicast) functions", () => { + it("should trigger auth triggered functions in response to auth events", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const response = await test.writeToAuth(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + expect(test.authTriggerCount).to.equal(1); + }); + + it("should trigger storage triggered functions in response to storage events across codebases", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.writeToDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + }); }); }); diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 1896ef14bdf6..fcf8a3f80c26 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -292,20 +292,18 @@ export class FunctionsEmulator implements EmulatorInstance { const { host, port } = this.getInfo(); triggers.forEach((triggerId) => { this.workQueue.submit(() => { - return new Promise((resolve, reject) => { - const trigReq = http.request( - { - host: connectableHostname(host), - port, - method: req.method, - path: `/functions/projects/${projectId}/triggers/${triggerId}`, - headers: req.headers, - }, - resolve - ); + return new Promise((resolve, reject) => { + const trigReq = http.request({ + host: connectableHostname(host), + port, + method: req.method, + path: `/functions/projects/${projectId}/triggers/${triggerId}`, + headers: req.headers, + }); trigReq.on("error", reject); trigReq.write(rawBody); trigReq.end(); + resolve(); }); }); }); From 577bd7eee65e08685c66bd28c05044e2b65b97f4 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Tue, 8 Nov 2022 15:57:32 -0800 Subject: [PATCH 085/115] Updating link to docs and massaging some text. (#5131) Co-authored-by: Bryan Kendall --- templates/init/functions/golang/functions.go | 2 +- templates/init/functions/javascript/index.js | 4 ++-- templates/init/functions/typescript/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/init/functions/golang/functions.go b/templates/init/functions/golang/functions.go index 9b44a27868b4..b8795b7a4d31 100644 --- a/templates/init/functions/golang/functions.go +++ b/templates/init/functions/golang/functions.go @@ -1,7 +1,7 @@ package PACKAGE // Welcome to Cloud Functions for Firebase for Golang! -// To get started, simply uncomment the below code or create your own. +// To get started, uncomment the below code or create your own. // Deploy with `firebase deploy` /* diff --git a/templates/init/functions/javascript/index.js b/templates/init/functions/javascript/index.js index 081873b3f9b0..0d3ca7f46ee9 100644 --- a/templates/init/functions/javascript/index.js +++ b/templates/init/functions/javascript/index.js @@ -1,7 +1,7 @@ const functions = require("firebase-functions"); -// // Create and Deploy Your First Cloud Functions -// // https://firebase.google.com/docs/functions/write-firebase-functions +// // Create and deploy your first functions +// // https://firebase.google.com/docs/functions/get-started // // exports.helloWorld = functions.https.onRequest((request, response) => { // functions.logger.info("Hello logs!", {structuredData: true}); diff --git a/templates/init/functions/typescript/index.ts b/templates/init/functions/typescript/index.ts index 10c30843a624..079282359d21 100644 --- a/templates/init/functions/typescript/index.ts +++ b/templates/init/functions/typescript/index.ts @@ -1,6 +1,6 @@ import * as functions from "firebase-functions"; -// // Start writing Firebase Functions +// // Start writing functions // // https://firebase.google.com/docs/functions/typescript // // export const helloWorld = functions.https.onRequest((request, response) => { From be10d72656614f2dfdcea8a8d5f54decde45e4de Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Tue, 8 Nov 2022 17:27:33 -0800 Subject: [PATCH 086/115] Update pubsub version/md5/byte size. (#5205) * Update pubsub version/md5/byte size. * Update some download logic to pause for 2s to allow the unzip to complete before chmod. --- CHANGELOG.md | 2 ++ src/emulator/download.ts | 6 +++++- src/emulator/downloadableEmulators.ts | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec1619b56d8..e1424f12fa2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- Updated the pubsub emulator to v0.7.1. +- Updated some emulator download logic to pause after unzipping to avoid a file not found issue. - Fixes gzipped file handling in Storage Emulator. - Add support for object list using certain Admin SDKs (#5208) - Fixes source token expiration issue by acquiring new source token upon expiration. diff --git a/src/emulator/download.ts b/src/emulator/download.ts index 32398cdb2b33..0841482ce489 100644 --- a/src/emulator/download.ts +++ b/src/emulator/download.ts @@ -37,6 +37,10 @@ export async function downloadEmulator(name: DownloadableEmulators): Promise setTimeout(f, 2000)); + const executablePath = emulator.binaryPath || emulator.downloadPath; fs.chmodSync(executablePath, 0o755); @@ -79,7 +83,7 @@ function unzip(zipPath: string, unzipDir: string): Promise { fs.createReadStream(zipPath) .pipe(unzipper.Extract({ path: unzipDir })) // eslint-disable-line new-cap .on("error", reject) - .on("finish", resolve); + .on("close", resolve); }); } diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index 95821f8b304f..d307cc5567f3 100644 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -50,9 +50,9 @@ const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDet expectedChecksum: "a4944414518be206280b495f526f18bf", }, pubsub: { - version: "0.1.0", - expectedSize: 36623622, - expectedChecksum: "81704b24737d4968734d3e175f4cde71", + version: "0.7.1", + expectedSize: 65137179, + expectedChecksum: "b59a6e705031a54a69e5e1dced7ca9bf", }, }; From dc44460d5a2d5f75e6ba013c484589856dfd359c Mon Sep 17 00:00:00 2001 From: Victor Fan Date: Wed, 9 Nov 2022 00:15:39 -0800 Subject: [PATCH 087/115] Adds an omit parametrized configuration option for functions to skip deploy (#5117) --- src/deploy/functions/build.ts | 6 ++++ .../functions/runtimes/discovery/v1alpha1.ts | 3 ++ src/test/deploy/functions/build.spec.ts | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 40d884b3d76c..0032602da6b1 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -220,6 +220,9 @@ export const AllIngressSettings: IngressSetting[] = [ ]; export type Endpoint = Triggered & { + // Defaults to false. If true, the function will be ignored during the deploy process. + omit?: Field; + // Defaults to "gcfv2". "Run" will be an additional option defined later platform?: "gcfv1" | "gcfv2"; @@ -413,6 +416,9 @@ export function toBackend( const bkEndpoints: Array = []; for (const endpointId of Object.keys(build.endpoints)) { const bdEndpoint = build.endpoints[endpointId]; + if (r.resolveBoolean(bdEndpoint.omit || false)) { + continue; + } let regions = bdEndpoint.region; if (typeof regions === "undefined") { diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.ts index be232b4cf06f..5b2369c76a44 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.ts @@ -44,6 +44,7 @@ export type WireEndpoint = build.Triggered & Partial & Partial & Partial<{ scheduleTrigger: WireScheduleTrigger }> & { + omit?: build.Field; labels?: Record | null; environmentVariables?: Record | null; availableMemoryMb?: build.MemoryOption | build.Expression | null; @@ -124,6 +125,7 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { region: "array", platform: (platform) => build.AllFunctionsPlatforms.includes(platform), entryPoint: "string", + omit: "Field?", availableMemoryMb: (mem) => mem === null || isCEL(mem) || build.isValidMemoryOption(mem), maxInstances: "Field?", minInstances: "Field?", @@ -395,6 +397,7 @@ function parseEndpointForBuild( copyIfPresent( parsed, ep, + "omit", "availableMemoryMb", "cpu", "maxInstances", diff --git a/src/test/deploy/functions/build.spec.ts b/src/test/deploy/functions/build.spec.ts index 21cf8b7e350b..873c9a2a3b7e 100644 --- a/src/test/deploy/functions/build.spec.ts +++ b/src/test/deploy/functions/build.spec.ts @@ -44,6 +44,35 @@ describe("toBackend", () => { } }); + it("doesn't populate if omit is set on the build", () => { + const desiredBuild: build.Build = build.of({ + func: { + omit: true, + platform: "gcfv1", + region: ["us-central1"], + project: "project", + runtime: "nodejs16", + entryPoint: "func", + maxInstances: 42, + minInstances: 1, + serviceAccount: "service-account-1@", + vpc: { + connector: "projects/project/locations/region/connectors/connector", + egressSettings: "PRIVATE_RANGES_ONLY", + }, + ingressSettings: "ALLOW_ALL", + labels: { + test: "testing", + }, + httpsTrigger: { + invoker: ["public"], + }, + }, + }); + const backend = build.toBackend(desiredBuild, {}); + expect(Object.keys(backend.endpoints).length).to.equal(0); + }); + it("populates multiple specified invokers correctly", () => { const desiredBuild: build.Build = build.of({ func: { From 95799e1db587371ba800b2578d51358c1657e898 Mon Sep 17 00:00:00 2001 From: Yuangwang Date: Thu, 10 Nov 2022 12:32:57 -0500 Subject: [PATCH 088/115] Fix storage admin sdk content type (#5229) * Fix storage admin sdk content type * lint * lint * rename variables * lint --- .../conformance/gcs-js-sdk.test.ts | 7 ++++ .../conformance/gcs.endpoints.test.ts | 37 +++++++++++++++++++ src/emulator/storage/apis/firebase.ts | 4 +- src/emulator/storage/apis/gcloud.ts | 7 ++-- src/emulator/storage/upload.ts | 8 ++-- src/test/emulators/storage/files.spec.ts | 6 +-- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts index af10db916932..18703e6b7751 100644 --- a/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts +++ b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts @@ -123,6 +123,13 @@ describe("GCS Javascript SDK conformance tests", () => { expect(fileMetadata).to.deep.include(metadata); }); + it("should upload with proper content type", async () => { + const jpgFile = createRandomFile("small_file.jpg", SMALL_FILE_SIZE, tmpDir); + const [, fileMetadata] = await testBucket.upload(jpgFile); + + expect(fileMetadata.contentType).to.equal("image/jpeg"); + }); + it("should handle firebaseStorageDownloadTokens", async () => { const testFileName = "public/file"; await testBucket.upload(smallFilePath, { diff --git a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts index 1f772f5248a3..11e57fd3d16f 100644 --- a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts +++ b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts @@ -251,6 +251,26 @@ describe("GCS endpoint conformance tests", () => { expect(returnedMetadata.contentType).to.equal(customMetadata.contentType); expect(returnedMetadata.contentDisposition).to.equal(customMetadata.contentDisposition); }); + + it("should upload content type properly from x-upload-content-type headers", async () => { + const uploadURL = await supertest(storageHost) + .post( + `/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}&uploadType=resumable` + ) + .set(authHeader) + .set({ + "x-upload-content-type": "image/png", + }) + .expect(200) + .then((res) => new URL(res.header["location"])); + + const returnedMetadata = await supertest(storageHost) + .put(uploadURL.pathname + uploadURL.search) + .expect(200) + .then((res) => res.body); + + expect(returnedMetadata.contentType).to.equal("image/png"); + }); }); describe("multipart upload", () => { @@ -292,6 +312,23 @@ describe("GCS endpoint conformance tests", () => { expect(res.text).to.include("Bad content type."); }); + + it("should upload content type properly from x-upload headers", async () => { + const returnedMetadata = await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?uploadType=multipart`) + .set(authHeader) + .set({ + "content-type": "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e", + }) + .set({ + "x-upload-content-type": "text/plain", + }) + .send(MULTIPART_REQUEST_BODY) + .expect(200) + .then((res) => res.body); + + expect(returnedMetadata.contentType).to.equal("text/plain"); + }); }); }); diff --git a/src/emulator/storage/apis/firebase.ts b/src/emulator/storage/apis/firebase.ts index 5a0c88369de2..762a28bd3bd8 100644 --- a/src/emulator/storage/apis/firebase.ts +++ b/src/emulator/storage/apis/firebase.ts @@ -232,7 +232,7 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { const upload = uploadService.startResumableUpload({ bucketId, objectId, - metadataRaw: JSON.stringify(req.body), + metadata: req.body, // Store auth header for use in the finalize request authorization: req.header("authorization"), }); @@ -355,7 +355,7 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { const upload = uploadService.multipartUpload({ bucketId, objectId, - metadataRaw, + metadata: JSON.parse(metadataRaw), dataRaw: dataRaw, authorization: req.header("authorization"), }); diff --git a/src/emulator/storage/apis/gcloud.ts b/src/emulator/storage/apis/gcloud.ts index 1efc031052c2..a51070e35b2e 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -261,10 +261,11 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { res.sendStatus(400); return; } + const contentType = req.header("x-upload-content-type"); const upload = uploadService.startResumableUpload({ bucketId: req.params.bucketId, objectId: name, - metadataRaw: JSON.stringify(req.body), + metadata: { contentType, ...req.body }, authorization: req.header("authorization"), }); @@ -292,6 +293,7 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { // Multipart upload protocol. if (uploadType === "multipart") { const contentTypeHeader = req.header("content-type") || req.header("x-upload-content-type"); + const contentType = req.header("x-upload-content-type"); if (!contentTypeHeader) { return res.sendStatus(400); } @@ -319,11 +321,10 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { res.sendStatus(400); return; } - const upload = uploadService.multipartUpload({ bucketId: req.params.bucketId, objectId: name, - metadataRaw: metadataRaw, + metadata: { contentType, ...JSON.parse(metadataRaw) }, dataRaw: dataRaw, authorization: req.header("authorization"), }); diff --git a/src/emulator/storage/upload.ts b/src/emulator/storage/upload.ts index d60e95e16d83..5d0b0dd7a810 100644 --- a/src/emulator/storage/upload.ts +++ b/src/emulator/storage/upload.ts @@ -44,7 +44,7 @@ export type MediaUploadRequest = { export type MultipartUploadRequest = { bucketId: string; objectId: string; - metadataRaw: string; + metadata: object; dataRaw: Buffer; authorization?: string; }; @@ -53,7 +53,7 @@ export type MultipartUploadRequest = { export type StartResumableUploadRequest = { bucketId: string; objectId: string; - metadataRaw: string; + metadata: object; authorization?: string; }; @@ -117,7 +117,7 @@ export class UploadService { objectId: request.objectId, uploadType: UploadType.MULTIPART, dataRaw: request.dataRaw, - metadata: JSON.parse(request.metadataRaw), + metadata: request.metadata, authorization: request.authorization, }); this._persistence.deleteFile(upload.path, /* failSilently = */ true); @@ -155,7 +155,7 @@ export class UploadService { type: UploadType.RESUMABLE, path: this.getStagingFileName(id, request.bucketId, request.objectId), status: UploadStatus.ACTIVE, - metadata: JSON.parse(request.metadataRaw), + metadata: request.metadata, size: 0, authorization: request.authorization, }; diff --git a/src/test/emulators/storage/files.spec.ts b/src/test/emulators/storage/files.spec.ts index 2140a2269468..3fead95d1739 100644 --- a/src/test/emulators/storage/files.spec.ts +++ b/src/test/emulators/storage/files.spec.ts @@ -83,7 +83,7 @@ describe("files", () => { bucketId, objectId: encodeURIComponent(objectId), dataRaw: Buffer.from(opts?.data ?? "hello world"), - metadataRaw: JSON.stringify(opts?.metadata ?? {}), + metadata: opts?.metadata ?? {}, }); await storageLayer.uploadObject(upload); } @@ -99,7 +99,7 @@ describe("files", () => { const upload = _uploadService.startResumableUpload({ bucketId: "bucket", objectId: "dir%2Fobject", - metadataRaw: "{}", + metadata: {}, }); expect(storageLayer.uploadObject(upload)).to.be.rejectedWith("Unexpected upload status"); @@ -110,7 +110,7 @@ describe("files", () => { const uploadId = _uploadService.startResumableUpload({ bucketId: "bucket", objectId: "dir%2Fobject", - metadataRaw: "{}", + metadata: {}, }).id; _uploadService.continueResumableUpload(uploadId, Buffer.from("hello world")); const upload = _uploadService.finalizeResumableUpload(uploadId); From d6871aabb9760e25d5936d5db668b529902d213c Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 15 Nov 2022 13:04:17 -0800 Subject: [PATCH 089/115] Fixes a bug where extensions emulator was not appearing in the registry (#5242) * Fixes a bug where extensions emualtor was not appearing in the registry * changelog --- CHANGELOG.md | 3 ++- src/emulator/extensionsEmulator.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1424f12fa2a..3d7e779fa0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,5 @@ - Fixes gzipped file handling in Storage Emulator. - Add support for object list using certain Admin SDKs (#5208) - Fixes source token expiration issue by acquiring new source token upon expiration. -- Fix bug where emulated event triggered function broke in debug mode (#5211) +- Fixes bug where emulated event triggered function broke in debug mode (#5211) +- Fixes bug that caused the Extensions Emulator to always appear to be inactive in the Emulator UI. diff --git a/src/emulator/extensionsEmulator.ts b/src/emulator/extensionsEmulator.ts index 3d69232be73e..04233bfe57da 100644 --- a/src/emulator/extensionsEmulator.ts +++ b/src/emulator/extensionsEmulator.ts @@ -67,7 +67,7 @@ export class ExtensionsEmulator implements EmulatorInstance { "Extensions Emulator is running but Functions emulator is not. This should never happen." ); } - return functionsEmulator.getInfo(); + return { ...functionsEmulator.getInfo(), name: this.getName() }; } public getName(): Emulators { From a5bfe2481c393a4df441d1eff1fda162659f5325 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 15 Nov 2022 22:52:52 +0000 Subject: [PATCH 090/115] 11.16.1 --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index db6a738cd421..054c70742744 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.16.0", + "version": "11.16.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.16.0", + "version": "11.16.1", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index 3ccb1fc8118d..274da8d69431 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.16.0", + "version": "11.16.1", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 392d8a8565eeb1f7018170148baee15ad169359a Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 15 Nov 2022 22:53:03 +0000 Subject: [PATCH 091/115] [firebase-release] Removed change log and reset repo after 11.16.1 release --- CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7e779fa0cf..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +0,0 @@ -- Updated the pubsub emulator to v0.7.1. -- Updated some emulator download logic to pause after unzipping to avoid a file not found issue. -- Fixes gzipped file handling in Storage Emulator. -- Add support for object list using certain Admin SDKs (#5208) -- Fixes source token expiration issue by acquiring new source token upon expiration. -- Fixes bug where emulated event triggered function broke in debug mode (#5211) -- Fixes bug that caused the Extensions Emulator to always appear to be inactive in the Emulator UI. From 2354100aca96eb927dd931a5afbe4e0a380a728e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 16 Nov 2022 12:46:45 -0800 Subject: [PATCH 092/115] Fix bug where disabling background triggers did nothing. (#5221) Fixes https://github.com/firebase/firebase-tools/issues/5026 --- CHANGELOG.md | 1 + scripts/integration-helpers/framework.ts | 13 +++++ scripts/triggers-end-to-end-tests/tests.ts | 62 ++++++++++++++++++++++ src/emulator/functionsEmulator.ts | 5 ++ 4 files changed, 81 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..66abef19ff35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Fix bug where disabling background triggers did nothing. (#5221) diff --git a/scripts/integration-helpers/framework.ts b/scripts/integration-helpers/framework.ts index a5aeb59df3fa..e0a906029447 100644 --- a/scripts/integration-helpers/framework.ts +++ b/scripts/integration-helpers/framework.ts @@ -53,6 +53,7 @@ interface ConnectionInfo { export interface FrameworkOptions { emulators?: { + hub: ConnectionInfo; database: ConnectionInfo; firestore: ConnectionInfo; functions: ConnectionInfo; @@ -63,6 +64,7 @@ export interface FrameworkOptions { } export class EmulatorEndToEndTest { + emulatorHubPort = 0; rtdbEmulatorHost = "localhost"; rtdbEmulatorPort = 0; firestoreEmulatorHost = "localhost"; @@ -87,6 +89,7 @@ export class EmulatorEndToEndTest { if (!config.emulators) { return; } + this.emulatorHubPort = config.emulators.hub?.port; this.rtdbEmulatorPort = config.emulators.database?.port; this.firestoreEmulatorPort = config.emulators.firestore?.port; this.functionsEmulatorPort = config.emulators.functions?.port; @@ -409,4 +412,14 @@ export class TriggerEndToEndTest extends EmulatorEndToEndTest { } }, interval); } + + disableBackgroundTriggers(): Promise { + const url = `http://localhost:${this.emulatorHubPort}/functions/disableBackgroundTriggers`; + return fetch(url, { method: "PUT" }); + } + + enableBackgroundTriggers(): Promise { + const url = `http://localhost:${this.emulatorHubPort}/functions/enableBackgroundTriggers`; + return fetch(url, { method: "PUT" }); + } } diff --git a/scripts/triggers-end-to-end-tests/tests.ts b/scripts/triggers-end-to-end-tests/tests.ts index a0dc6928a120..fc0ee330e21f 100755 --- a/scripts/triggers-end-to-end-tests/tests.ts +++ b/scripts/triggers-end-to-end-tests/tests.ts @@ -429,4 +429,66 @@ describe("function triggers", () => { const v2response = await test.invokeHttpFunction("onreqv2timeout"); expect(v2response.status).to.equal(500); }); + + describe("disable/enableBackgroundTriggers", () => { + before(() => { + test.resetCounts(); + }); + + it("should disable all background triggers", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.disableBackgroundTriggers(); + expect(response.status).to.equal(200); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + await Promise.all([ + test.writeToRtdb(), + test.writeToFirestore(), + test.writeToPubsub(), + test.writeToAuth(), + test.writeToDefaultStorage(), + ]); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 2)); + + expect(test.rtdbTriggerCount).to.equal(0); + expect(test.rtdbV2TriggerCount).to.eq(0); + expect(test.firestoreTriggerCount).to.equal(0); + expect(test.pubsubTriggerCount).to.equal(0); + expect(test.pubsubV2TriggerCount).to.equal(0); + expect(test.authTriggerCount).to.equal(0); + expect(test.storageFinalizedTriggerCount).to.equal(0); + expect(test.storageV2FinalizedTriggerCount).to.equal(0); + }); + + it("should re-enable all background triggers", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.enableBackgroundTriggers(); + expect(response.status).to.equal(200); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + await Promise.all([ + test.writeToRtdb(), + test.writeToFirestore(), + test.writeToPubsub(), + test.writeToAuth(), + test.writeToDefaultStorage(), + ]); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 3)); + + expect(test.rtdbTriggerCount).to.equal(1); + expect(test.rtdbV2TriggerCount).to.eq(1); + expect(test.firestoreTriggerCount).to.equal(1); + expect(test.pubsubTriggerCount).to.equal(1); + expect(test.pubsubV2TriggerCount).to.equal(1); + expect(test.authTriggerCount).to.equal(1); + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + }); + }); }); diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index fcf8a3f80c26..fb47df888fd5 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -1395,6 +1395,11 @@ export class FunctionsEmulator implements EmulatorInstance { } const record = this.getTriggerRecordByKey(triggerId); + // If trigger is disabled, exit early + if (!record.enabled) { + res.status(204).send("Background triggers are currently disabled."); + return; + } const trigger = record.def; logger.debug(`Accepted request ${method} ${req.url} --> ${triggerId}`); From a71d911c0d62b47296bf6f296c32397f3aa606f8 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Thu, 17 Nov 2022 15:49:01 -0800 Subject: [PATCH 093/115] Change signInWithPassword error handling for empty string emails (#5258) --- CHANGELOG.md | 1 + src/emulator/auth/operations.ts | 3 ++- src/test/emulators/auth/password.spec.ts | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66abef19ff35..553b7ea33142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Fix bug where disabling background triggers did nothing. (#5221) +- Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index d72d6faca3d3..783754e42f2d 100644 --- a/src/emulator/auth/operations.ts +++ b/src/emulator/auth/operations.ts @@ -1708,7 +1708,8 @@ async function signInWithPassword( ): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED"); - assert(reqBody.email, "MISSING_EMAIL"); + assert(reqBody.email !== undefined, "MISSING_EMAIL"); + assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL"); assert(reqBody.password, "MISSING_PASSWORD"); if (reqBody.captchaResponse || reqBody.captchaChallenge) { throw new NotImplementedError("captcha unimplemented"); diff --git a/src/test/emulators/auth/password.spec.ts b/src/test/emulators/auth/password.spec.ts index 064ca3c8541e..29314547fc86 100644 --- a/src/test/emulators/auth/password.spec.ts +++ b/src/test/emulators/auth/password.spec.ts @@ -101,6 +101,25 @@ describeAuthEmulator("accounts:signInWithPassword", ({ authApi, getClock }) => { }); }); + it("should error if email is invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "ill-formatted-email", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_EMAIL"); + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_EMAIL"); + }); + }); + it("should error if email is not found", async () => { await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") From b2d23b685a12a9f98deb125940f4dc419fb5ab5f Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Mon, 21 Nov 2022 10:35:46 -0800 Subject: [PATCH 094/115] Show prereleases when resolving Extension versions. (#5161) --- src/deploy/extensions/planner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deploy/extensions/planner.ts b/src/deploy/extensions/planner.ts index 6559f3eb782d..c48d04224e7c 100644 --- a/src/deploy/extensions/planner.ts +++ b/src/deploy/extensions/planner.ts @@ -209,7 +209,7 @@ export async function want(args: { */ export async function resolveVersion(ref: refs.Ref): Promise { const extensionRef = refs.toExtensionRef(ref); - const versions = await extensionsApi.listExtensionVersions(extensionRef); + const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true); if (versions.length === 0) { throw new FirebaseError(`No versions found for ${extensionRef}`); } From e984a6c3a82f43eec505b9a923a8d8be93b7aae7 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Mon, 21 Nov 2022 12:46:56 -0800 Subject: [PATCH 095/115] Add createdAt time for signInWithIdp new users (#5260) --- CHANGELOG.md | 1 + src/emulator/auth/operations.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553b7ea33142..aa5c30b100a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Fix bug where disabling background triggers did nothing. (#5221) - Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) +- Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index 783754e42f2d..f40c1beea10e 100644 --- a/src/emulator/auth/operations.ts +++ b/src/emulator/auth/operations.ts @@ -1608,9 +1608,11 @@ async function signInWithIdp( oauthExpiresIn: coercePrimitiveToString(response.oauthExpireIn), }; if (response.isNewUser) { + const timestamp = new Date(); let updates: Partial = { ...accountUpdates.fields, - lastLoginAt: Date.now().toString(), + createdAt: timestamp.getTime().toString(), + lastLoginAt: timestamp.getTime().toString(), providerUserInfo: [providerUserInfo], tenantId: state instanceof TenantProjectState ? state.tenantId : undefined, }; From e968d50f2b0f138bc5fbded19dd8a44a32a7caf6 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Dec 2022 09:05:57 -0800 Subject: [PATCH 096/115] Temporarily disable storage emulator test. (#5281) --- .github/workflows/node-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 5e72bc66f0fc..f276fe2381f7 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -89,7 +89,8 @@ jobs: # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. - npm run test:import-export - npm run test:storage-deploy - - npm run test:storage-emulator-integration + # Temporarily disable broken storage emulator integration test. + # - npm run test:storage-emulator-integration - npm run test:triggers-end-to-end - npm run test:triggers-end-to-end:inspect steps: From 76450efc702f60b3f7a23b9dd8a7bb92c3315310 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Thu, 1 Dec 2022 09:25:51 -0800 Subject: [PATCH 097/115] Support x-goog-api-key in Auth Emulator. Fix #5249. (#5263) * Update Auth Emulator API spec. * Add x-goog-api-key in API spec. * Support x-goog-api-key in Auth Emulator. Fix #5249. --- CHANGELOG.md | 1 + scripts/gen-auth-api-spec.ts | 17 +- src/emulator/auth/apiSpec.ts | 766 ++++++++++++++++++++------- src/emulator/auth/schema.ts | 93 +++- src/emulator/auth/server.ts | 28 +- src/test/emulators/auth/rest.spec.ts | 28 +- 6 files changed, 708 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5c30b100a0..33ff5550bcaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ - Fix bug where disabling background triggers did nothing. (#5221) - Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) - Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) +- Support the x-goog-api-key header in auth emulator. (#5249) diff --git a/scripts/gen-auth-api-spec.ts b/scripts/gen-auth-api-spec.ts index b64d11d8656a..d5d9cc545097 100644 --- a/scripts/gen-auth-api-spec.ts +++ b/scripts/gen-auth-api-spec.ts @@ -252,13 +252,20 @@ function patchSecurity(openapi3: any, apiKeyDescription: string): void { securitySchemes = openapi3.components.securitySchemes = {}; } - // Add the missing apiKey method here. - securitySchemes.apiKey = { + // Add the missing apiKeyQuery and apiKeyHeader schemes here. + // https://cloud.google.com/docs/authentication/api-keys#using-with-rest + securitySchemes.apiKeyQuery = { type: "apiKey", name: "key", in: "query", description: apiKeyDescription, }; + securitySchemes.apiKeyHeader = { + type: "apiKey", + name: "x-goog-api-key", + in: "header", + description: apiKeyDescription, + }; forEachOperation(openapi3, (operation) => { if (!operation.security) { @@ -271,9 +278,9 @@ function patchSecurity(openapi3: any, apiKeyDescription: string): void { delete alt.Oauth2c; }); - // Forcibly add API Key as an alternative auth method. Note that some - // operations may not support it, but those can be handled within impl. - operation.security.push({ apiKey: [] }); + // Add alternative auth schemes (query OR header) for API key. Note that + // some operations may not support it, but those can be handled within impl. + operation.security.push({ apiKeyQuery: [] }, { apiKeyHeader: [] }); }); } diff --git a/src/emulator/auth/apiSpec.ts b/src/emulator/auth/apiSpec.ts index 310a071cd21e..125afaa542db 100644 --- a/src/emulator/auth/apiSpec.ts +++ b/src/emulator/auth/apiSpec.ts @@ -17,7 +17,7 @@ export default { termsOfService: "https://developers.google.com/terms/", }, servers: [{ url: "https://identitytoolkit.googleapis.com" }], - externalDocs: { url: "https://firebase.google.com/docs/auth/" }, + externalDocs: { url: "https://cloud.google.com/identity-platform" }, tags: [ { name: "accounts" }, { name: "projects" }, @@ -33,7 +33,7 @@ export default { "If an email identifier is specified, checks and returns if any user account is registered with the email. If there is a registered account, fetches all providers associated with the account's email. If the provider ID of an Identity Provider (IdP) is specified, creates an authorization URI for the IdP. The user can be directed to this URI to sign in with the IdP. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.createAuthUri", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -53,7 +53,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -73,7 +77,7 @@ export default { description: "Deletes a user's account.", operationId: "identitytoolkit.accounts.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -87,7 +91,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1DeleteAccountRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -107,7 +115,7 @@ export default { description: "Experimental", operationId: "identitytoolkit.accounts.issueSamlResponse", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -127,7 +135,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -148,7 +160,7 @@ export default { "Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria.", operationId: "identitytoolkit.accounts.lookup", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -162,7 +174,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -183,7 +199,7 @@ export default { "Resets the password of an account either using an out-of-band code generated by sendOobCode or by specifying the email and password of the account to be modified. Can also check the purpose of an out-of-band code without consuming it.", operationId: "identitytoolkit.accounts.resetPassword", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -203,7 +219,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -224,7 +244,7 @@ export default { "Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it.", operationId: "identitytoolkit.accounts.sendOobCode", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -238,7 +258,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetOobCodeRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -259,7 +283,7 @@ export default { "Sends a SMS verification code for phone number sign-in. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.sendVerificationCode", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -279,7 +303,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -300,7 +328,7 @@ export default { "Signs in or signs up a user by exchanging a custom Auth token. Upon a successful sign-in or sign-up, a new Identity Platform ID token and refresh token are issued for the user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signInWithCustomToken", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -320,7 +348,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -341,7 +373,7 @@ export default { "Signs in or signs up a user with a out-of-band code from an email link. If a user does not exist with the given email address, a user record will be created. If the sign-in succeeds, an Identity Platform ID and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signInWithEmailLink", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -361,7 +393,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -382,7 +418,7 @@ export default { "Signs in or signs up a user with iOS Game Center credentials. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signInWithGameCenter", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -402,7 +438,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -423,7 +463,7 @@ export default { 'Signs in or signs up a user using credentials from an Identity Provider (IdP). This is done by manually providing an IdP credential, or by providing the authorization response obtained via the authorization request from CreateAuthUri. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. A new Identity Platform user account will be created if the user has not previously signed in to the IdP with the same account. In addition, when the "One account per email address" setting is enabled, there should not be an existing Identity Platform user account with the same email address for a new user account to be created. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.', operationId: "identitytoolkit.accounts.signInWithIdp", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -443,7 +483,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -464,7 +508,7 @@ export default { "Signs in a user with email and password. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signInWithPassword", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -484,7 +528,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -505,7 +553,7 @@ export default { "Completes a phone number authentication attempt. If a user already exists with the given phone number, an ID token is minted for that user. Otherwise, a new user is created and associated with the phone number. This method may also be used to link a phone number to an existing user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signInWithPhoneNumber", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -525,7 +573,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -546,7 +598,7 @@ export default { "Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.signUp", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -558,7 +610,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SignUpRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -579,7 +635,7 @@ export default { "Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported.", operationId: "identitytoolkit.accounts.update", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -593,7 +649,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -611,10 +671,10 @@ export default { "/v1/accounts:verifyIosClient": { post: { description: - "Verifies an iOS client is a real iOS device. If the request is valid, a reciept will be sent in the response and a secret will be sent via Apple Push Notification Service. The client should send both of them back to certain Identity Platform APIs in a later call (for example, /accounts:sendVerificationCode), in order to verify the client. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", + "Verifies an iOS client is a real iOS device. If the request is valid, a receipt will be sent in the response and a secret will be sent via Apple Push Notification Service. The client should send both of them back to certain Identity Platform APIs in a later call (for example, /accounts:sendVerificationCode), in order to verify the client. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.accounts.verifyIosClient", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -634,7 +694,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -655,7 +719,7 @@ export default { "Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.projects.accounts", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -677,7 +741,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SignUpRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -698,7 +766,7 @@ export default { "Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state.", operationId: "identitytoolkit.projects.createSessionCookie", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -721,7 +789,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -742,7 +814,7 @@ export default { "Looks up user accounts within a project or a tenant based on conditions in the request.", operationId: "identitytoolkit.projects.queryAccounts", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -768,7 +840,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -790,7 +863,7 @@ export default { "Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).", operationId: "identitytoolkit.projects.accounts.batchCreate", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -817,7 +890,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -836,10 +910,10 @@ export default { "/v1/projects/{targetProjectId}/accounts:batchDelete": { post: { description: - "Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper permissions. (https://cloud.google.com/identity-platform/docs/access-control)", + "Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control).", operationId: "identitytoolkit.projects.accounts.batchDelete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -866,7 +940,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -888,7 +963,7 @@ export default { "Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted.", operationId: "identitytoolkit.projects.accounts.batchGet", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -933,7 +1008,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -954,7 +1030,7 @@ export default { description: "Deletes a user's account.", operationId: "identitytoolkit.projects.accounts.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -978,7 +1054,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1DeleteAccountRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -999,7 +1079,7 @@ export default { "Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria.", operationId: "identitytoolkit.projects.accounts.lookup", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1023,7 +1103,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1044,7 +1128,7 @@ export default { "Looks up user accounts within a project or a tenant based on conditions in the request.", operationId: "identitytoolkit.projects.accounts.query", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1070,7 +1154,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -1092,7 +1177,7 @@ export default { "Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it.", operationId: "identitytoolkit.projects.accounts.sendOobCode", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1116,7 +1201,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetOobCodeRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1137,7 +1226,7 @@ export default { "Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported.", operationId: "identitytoolkit.projects.accounts.update", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1153,7 +1242,7 @@ export default { name: "targetProjectId", in: "path", description: - "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", + "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", required: true, schema: { type: "string" }, }, @@ -1161,7 +1250,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1182,7 +1275,7 @@ export default { "Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project.", operationId: "identitytoolkit.projects.tenants.accounts", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1212,7 +1305,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SignUpRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1233,7 +1330,7 @@ export default { "Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state.", operationId: "identitytoolkit.projects.tenants.createSessionCookie", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1263,7 +1360,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1284,7 +1385,7 @@ export default { "Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).", operationId: "identitytoolkit.projects.tenants.accounts.batchCreate", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1318,7 +1419,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -1337,10 +1439,10 @@ export default { "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:batchDelete": { post: { description: - "Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper permissions. (https://cloud.google.com/identity-platform/docs/access-control)", + "Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control).", operationId: "identitytoolkit.projects.tenants.accounts.batchDelete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1375,7 +1477,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -1397,7 +1500,7 @@ export default { "Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted.", operationId: "identitytoolkit.projects.tenants.accounts.batchGet", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1443,7 +1546,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -1464,7 +1568,7 @@ export default { description: "Deletes a user's account.", operationId: "identitytoolkit.projects.tenants.accounts.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1496,7 +1600,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1DeleteAccountRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1517,7 +1625,7 @@ export default { "Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria.", operationId: "identitytoolkit.projects.tenants.accounts.lookup", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1549,7 +1657,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1570,7 +1682,7 @@ export default { "Looks up user accounts within a project or a tenant based on conditions in the request.", operationId: "identitytoolkit.projects.tenants.accounts.query", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1603,7 +1715,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -1625,7 +1738,7 @@ export default { "Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it.", operationId: "identitytoolkit.projects.tenants.accounts.sendOobCode", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1656,7 +1769,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1GetOobCodeRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1677,7 +1794,7 @@ export default { "Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported.", operationId: "identitytoolkit.projects.tenants.accounts.update", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1693,7 +1810,7 @@ export default { name: "targetProjectId", in: "path", description: - "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", + "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", required: true, schema: { type: "string" }, }, @@ -1709,7 +1826,11 @@ export default { requestBody: { $ref: "#/components/requestBodies/GoogleCloudIdentitytoolkitV1SetAccountInfoRequest", }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, parameters: [ @@ -1730,7 +1851,7 @@ export default { "Gets a project's public Identity Toolkit configuration. (Legacy) This method also supports authenticated calls from a developer to retrieve non-public configuration.", operationId: "identitytoolkit.getProjects", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1798,7 +1919,11 @@ export default { schema: { type: "string" }, }, ], - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["v1"], }, parameters: [ @@ -1818,7 +1943,7 @@ export default { description: "Gets parameters needed for generating a reCAPTCHA challenge.", operationId: "identitytoolkit.getRecaptchaParams", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1829,7 +1954,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["v1"], }, parameters: [ @@ -1850,7 +1979,7 @@ export default { "Retrieves the set of public keys of the session cookie JSON Web Token (JWT) signer that can be used to validate the session cookie created through createSessionCookie.", operationId: "identitytoolkit.getSessionCookiePublicKeys", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1862,7 +1991,7 @@ export default { }, }, tags: ["v1"], - security: [{ apiKey: [] }], + security: [{ apiKeyQuery: [] }, { apiKeyHeader: [] }], }, parameters: [ { $ref: "#/components/parameters/access_token" }, @@ -1881,7 +2010,7 @@ export default { description: "Finishes enrolling a second factor for the user.", operationId: "identitytoolkit.accounts.mfaEnrollment.finalize", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1901,7 +2030,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -1922,7 +2055,7 @@ export default { "Step one of the MFA enrollment process. In SMS case, this sends an SMS verification code to the user.", operationId: "identitytoolkit.accounts.mfaEnrollment.start", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1942,7 +2075,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -1962,7 +2099,7 @@ export default { description: "Revokes one second factor from the enrolled second factors for an account.", operationId: "identitytoolkit.accounts.mfaEnrollment.withdraw", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -1982,7 +2119,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -2002,7 +2143,7 @@ export default { description: "Verifies the MFA challenge and performs sign-in", operationId: "identitytoolkit.accounts.mfaSignIn.finalize", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2022,7 +2163,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -2042,7 +2187,7 @@ export default { description: "Sends the MFA challenge", operationId: "identitytoolkit.accounts.mfaSignIn.start", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2062,7 +2207,11 @@ export default { }, }, }, - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["accounts"], }, parameters: [ @@ -2082,7 +2231,7 @@ export default { description: "List all default supported Idps.", operationId: "identitytoolkit.defaultSupportedIdps.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2110,7 +2259,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["defaultSupportedIdps"], }, @@ -2131,7 +2281,7 @@ export default { description: "Retrieve an Identity Toolkit project configuration.", operationId: "identitytoolkit.projects.getConfig", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2143,14 +2293,18 @@ export default { parameters: [ { name: "targetProjectId", in: "path", required: true, schema: { type: "string" } }, ], - security: [{ Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { apiKey: [] }], + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], tags: ["projects"], }, patch: { description: "Update an Identity Toolkit project configuration.", operationId: "identitytoolkit.projects.updateConfig", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2179,7 +2333,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2201,7 +2356,7 @@ export default { "Create a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.defaultSupportedIdpConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2228,7 +2383,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2237,7 +2393,7 @@ export default { "List all default supported Idp configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.defaultSupportedIdpConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2266,7 +2422,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2288,7 +2445,7 @@ export default { "Delete a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.defaultSupportedIdpConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -2305,7 +2462,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2314,7 +2472,7 @@ export default { "Retrieve a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.defaultSupportedIdpConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2337,7 +2495,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2346,7 +2505,7 @@ export default { "Update a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.defaultSupportedIdpConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2379,7 +2538,56 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, + ], + tags: ["projects"], + }, + parameters: [ + { $ref: "#/components/parameters/access_token" }, + { $ref: "#/components/parameters/alt" }, + { $ref: "#/components/parameters/callback" }, + { $ref: "#/components/parameters/fields" }, + { $ref: "#/components/parameters/oauth_token" }, + { $ref: "#/components/parameters/prettyPrint" }, + { $ref: "#/components/parameters/quotaUser" }, + { $ref: "#/components/parameters/uploadType" }, + { $ref: "#/components/parameters/upload_protocol" }, + ], + }, + "/v2/projects/{targetProjectId}/identityPlatform:initializeAuth": { + post: { + description: + "Initialize Identity Platform for a Cloud project. Identity Platform is an end-to-end authentication system for third-party users to access your apps and services. These could include mobile/web apps, games, APIs and beyond. This is the publicly available variant of EnableIdentityPlatform that is only available to billing-enabled projects.", + operationId: "identitytoolkit.projects.identityPlatform.initializeAuth", + responses: { + "200": { + description: "Successful response", + content: { + "*/*": { + schema: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformResponse", + }, + }, + }, + }, + }, + parameters: [ + { name: "targetProjectId", in: "path", required: true, schema: { type: "string" } }, + ], + requestBody: { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformRequest", + }, + }, + }, + }, + security: [ + { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2400,7 +2608,7 @@ export default { description: "Create an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.inboundSamlConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2426,7 +2634,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2434,7 +2643,7 @@ export default { description: "List all inbound SAML configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.inboundSamlConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2463,7 +2672,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2484,7 +2694,7 @@ export default { description: "Delete an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.inboundSamlConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -2496,7 +2706,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2504,7 +2715,7 @@ export default { description: "Retrieve an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.inboundSamlConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2522,7 +2733,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2530,7 +2742,7 @@ export default { description: "Update an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.inboundSamlConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2558,7 +2770,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2579,7 +2792,7 @@ export default { description: "Create an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.oauthIdpConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2605,7 +2818,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2613,7 +2827,7 @@ export default { description: "List all Oidc Idp configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.oauthIdpConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2642,7 +2856,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2663,7 +2878,7 @@ export default { description: "Delete an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.oauthIdpConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -2675,7 +2890,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2683,7 +2899,7 @@ export default { description: "Retrieve an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.oauthIdpConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2701,7 +2917,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2709,7 +2926,7 @@ export default { description: "Update an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.oauthIdpConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2737,7 +2954,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2758,7 +2976,7 @@ export default { description: "Create a tenant. Requires write permission on the Agent project.", operationId: "identitytoolkit.projects.tenants.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2774,7 +2992,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2783,7 +3002,7 @@ export default { "List tenants under the given agent project. Requires read permission on the Agent project.", operationId: "identitytoolkit.projects.tenants.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2813,7 +3032,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2834,7 +3054,7 @@ export default { description: "Delete a tenant. Requires write permission on the Agent project.", operationId: "identitytoolkit.projects.tenants.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -2846,7 +3066,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2854,7 +3075,7 @@ export default { description: "Get a tenant. Requires read permission on the Tenant resource.", operationId: "identitytoolkit.projects.tenants.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2870,7 +3091,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2878,7 +3100,7 @@ export default { description: "Update a tenant. Requires write permission on the Tenant resource.", operationId: "identitytoolkit.projects.tenants.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -2902,7 +3124,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2924,7 +3147,7 @@ export default { "Gets the access control policy for a resource. An error is returned if the resource does not exist. An empty policy is returned if the resource exists but does not have a policy set on it. Caller must have the right Google IAM permission on the resource.", operationId: "identitytoolkit.projects.tenants.getIamPolicy", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleIamV1Policy" } } }, }, @@ -2943,7 +3166,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -2965,7 +3189,7 @@ export default { "Sets the access control policy for a resource. If the policy exists, it is replaced. Caller must have the right Google IAM permission on the resource.", operationId: "identitytoolkit.projects.tenants.setIamPolicy", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleIamV1Policy" } } }, }, @@ -2984,7 +3208,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3006,7 +3231,7 @@ export default { "Returns the caller's permissions on a resource. An error is returned if the resource does not exist. A caller is not required to have Google IAM permission to make this request.", operationId: "identitytoolkit.projects.tenants.testIamPermissions", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3029,7 +3254,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3051,7 +3277,7 @@ export default { "Create a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3079,7 +3305,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3088,7 +3315,7 @@ export default { "List all default supported Idp configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3118,7 +3345,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3141,7 +3369,7 @@ export default { "Delete a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -3159,7 +3387,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3168,7 +3397,7 @@ export default { "Retrieve a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3192,7 +3421,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3201,7 +3431,7 @@ export default { "Update a default supported Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3235,7 +3465,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3256,7 +3487,7 @@ export default { description: "Create an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.inboundSamlConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3283,7 +3514,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3291,7 +3523,7 @@ export default { description: "List all inbound SAML configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.inboundSamlConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3321,7 +3553,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3342,7 +3575,7 @@ export default { description: "Delete an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.inboundSamlConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -3355,7 +3588,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3363,7 +3597,7 @@ export default { description: "Retrieve an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.inboundSamlConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3382,7 +3616,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3390,7 +3625,7 @@ export default { description: "Update an inbound SAML configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.inboundSamlConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3419,7 +3654,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3440,7 +3676,7 @@ export default { description: "Create an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.oauthIdpConfigs.create", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3467,7 +3703,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3475,7 +3712,7 @@ export default { description: "List all Oidc Idp configurations for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.oauthIdpConfigs.list", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3505,7 +3742,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3526,7 +3764,7 @@ export default { description: "Delete an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.oauthIdpConfigs.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GoogleProtobufEmpty" } } }, }, @@ -3539,7 +3777,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3547,7 +3786,7 @@ export default { description: "Retrieve an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.oauthIdpConfigs.get", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3566,7 +3805,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3574,7 +3814,7 @@ export default { description: "Update an Oidc Idp configuration for an Identity Toolkit project.", operationId: "identitytoolkit.projects.tenants.oauthIdpConfigs.patch", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { @@ -3603,7 +3843,8 @@ export default { security: [ { Oauth2: ["https://www.googleapis.com/auth/cloud-platform"] }, { Oauth2: ["https://www.googleapis.com/auth/firebase"] }, - { apiKey: [] }, + { apiKeyQuery: [] }, + { apiKeyHeader: [] }, ], tags: ["projects"], }, @@ -3625,7 +3866,7 @@ export default { "The Token Service API lets you exchange either an ID token or a refresh token for an access token and a new refresh token. You can use the access token to securely call APIs that require user authorization.", operationId: "securetoken.token", responses: { - 200: { + "200": { description: "Successful response", content: { "*/*": { schema: { $ref: "#/components/schemas/GrantTokenResponse" } } }, }, @@ -3639,7 +3880,7 @@ export default { }, }, tags: ["secureToken"], - security: [{ apiKey: [] }], + security: [{ apiKeyQuery: [] }, { apiKeyHeader: [] }], }, parameters: [ { $ref: "#/components/parameters/access_token" }, @@ -3669,7 +3910,7 @@ export default { description: "Remove all accounts in the project, regardless of state.", operationId: "emulator.projects.accounts.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { schema: { type: "object" } } }, }, @@ -3701,7 +3942,7 @@ export default { description: "Remove all accounts in the project, regardless of state.", operationId: "emulator.projects.accounts.delete", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { schema: { type: "object" } } }, }, @@ -3725,7 +3966,7 @@ export default { description: "Get emulator-specific configuration for the project.", operationId: "emulator.projects.config.get", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -3748,7 +3989,7 @@ export default { }, }, responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -3776,7 +4017,7 @@ export default { description: "List all pending confirmation codes for the project.", operationId: "emulator.projects.oobCodes.list", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -3812,7 +4053,7 @@ export default { description: "List all pending confirmation codes for the project.", operationId: "emulator.projects.oobCodes.list", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -3840,7 +4081,7 @@ export default { description: "List all pending phone verification codes for the project.", operationId: "emulator.projects.verificationCodes.list", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -3876,7 +4117,7 @@ export default { description: "List all pending phone verification codes for the project.", operationId: "emulator.projects.verificationCodes.list", responses: { - 200: { + "200": { description: "Successful response", content: { "application/json": { @@ -4892,7 +5133,7 @@ export default { }, customAttributes: { description: - "JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control).", + "JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control).", type: "string", }, delegatedProjectNumber: { format: "int64", type: "string" }, @@ -4934,7 +5175,7 @@ export default { }, emailVerified: { description: - "Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control).", + "Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control).", type: "boolean", }, idToken: { @@ -4953,7 +5194,7 @@ export default { }, localId: { description: - "The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead.", + "The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead.", type: "string", }, mfa: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitV1MfaInfo" }, @@ -4987,7 +5228,7 @@ export default { }, targetProjectId: { description: - "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", + "The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead.", type: "string", }, tenantId: { @@ -5770,12 +6011,7 @@ export default { format: "int32", type: "integer", }, - delegatedProjectNumber: { - description: - "If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped.", - format: "int64", - type: "string", - }, + delegatedProjectNumber: { format: "int64", type: "string" }, dkLen: { description: "The desired key length for the STANDARD_SCRYPT hashing function. Must be at least 1.", @@ -5816,7 +6052,11 @@ export default { format: "byte", type: "string", }, - sanityCheck: { type: "boolean" }, + sanityCheck: { + description: + "If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped.", + type: "boolean", + }, signerKey: { description: "The signer key used to hash the password. Required for the following hashing functions: * SCRYPT, * HMAC_MD5, * HMAC_SHA1, * HMAC_SHA256, * HMAC_SHA512", @@ -5828,7 +6068,8 @@ export default { type: "string", }, users: { - description: "A list of accounts to upload.", + description: + "A list of accounts to upload. `local_id` is required for each user; everything else is optional.", items: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitV1UserInfo" }, type: "array", }, @@ -6019,6 +6260,32 @@ export default { }, type: "object", }, + GoogleCloudIdentitytoolkitAdminV2AllowByDefault: { + description: + "Defines a policy of allowing every region by default and adding disallowed regions to a disallow list.", + properties: { + disallowedRegions: { + description: + "Two letter unicode region codes to disallow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json", + items: { type: "string" }, + type: "array", + }, + }, + type: "object", + }, + GoogleCloudIdentitytoolkitAdminV2AllowlistOnly: { + description: + "Defines a policy of only allowing regions by explicitly adding them to an allowlist.", + properties: { + allowedRegions: { + description: + "Two letter unicode region codes to allow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json", + items: { type: "string" }, + type: "array", + }, + }, + type: "object", + }, GoogleCloudIdentitytoolkitAdminV2Anonymous: { description: "Configuration options related to authenticating an anonymous user.", properties: { @@ -6081,6 +6348,33 @@ export default { }, type: "object", }, + GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig: { + description: + "Options related to how clients making requests on behalf of a tenant should be configured.", + properties: { + permissions: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2ClientPermissions", + }, + }, + type: "object", + }, + GoogleCloudIdentitytoolkitAdminV2ClientPermissions: { + description: + "Configuration related to restricting a user's ability to affect their account.", + properties: { + disabledUserDeletion: { + description: + "When true, end users cannot delete their account on the associated project through any of our API methods", + type: "boolean", + }, + disabledUserSignup: { + description: + "When true, end users cannot sign up for a new account on the associated project through any of our API methods", + type: "boolean", + }, + }, + type: "object", + }, GoogleCloudIdentitytoolkitAdminV2CodeFlowConfig: { description: "Additional config for Apple for code flow.", properties: { @@ -6101,10 +6395,17 @@ export default { items: { type: "string" }, type: "array", }, + autodeleteAnonymousUsers: { + description: "Whether anonymous users will be auto-deleted after a period of 30 days.", + type: "boolean", + }, blockingFunctions: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig", }, client: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2ClientConfig" }, + emailPrivacyConfig: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig", + }, mfa: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig", }, @@ -6125,6 +6426,9 @@ export default { }, quota: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2QuotaConfig" }, signIn: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2SignInConfig" }, + smsRegionConfig: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig", + }, subtype: { description: "Output only. The subtype of this config.", enum: ["SUBTYPE_UNSPECIFIED", "IDENTITY_PLATFORM", "FIREBASE_AUTH"], @@ -6217,6 +6521,18 @@ export default { }, type: "object", }, + GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig: { + description: + "Configuration for settings related to email privacy and public visibility. Settings in this config protect against email enumeration, but may make some trade-offs in user-friendliness.", + properties: { + enableImprovedEmailPrivacy: { + description: + "Migrates the project to a state of improved email privacy. For example certain error codes are more generic to avoid giving away information on whether the account exists. In addition, this disables certain features that as a side-effect allow user enumeration. Enabling this toggle disables the fetchSignInMethodsForEmail functionality and changing the user's email to an unverified email. It is recommended to remove dependence on this functionality and enable this toggle to improve user privacy.", + type: "boolean", + }, + }, + type: "object", + }, GoogleCloudIdentitytoolkitAdminV2EmailTemplate: { description: "Email template. The subject and body fields can contain the following placeholders which will be replaced with the appropriate values: %LINK% - The link to use to redeem the send OOB code. %EMAIL% - The email where the email is being sent. %NEW_EMAIL% - The new email being set for the account (when applicable). %APP_NAME% - The GCP project's display name. %DISPLAY_NAME% - The user's display name.", @@ -6368,6 +6684,16 @@ export default { }, type: "object", }, + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformRequest: { + description: "Request for InitializeIdentityPlatform.", + properties: {}, + type: "object", + }, + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformResponse: { + description: "Response for InitializeIdentityPlatform. Empty for now.", + properties: {}, + type: "object", + }, GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpConfigsResponse: { description: "Response for DefaultSupportedIdpConfigs", properties: { @@ -6663,6 +6989,19 @@ export default { }, type: "object", }, + GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig: { + description: + "Configures the regions where users are allowed to send verification SMS for the project or tenant. This is based on the calling code of the destination phone number.", + properties: { + allowByDefault: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2AllowByDefault", + }, + allowlistOnly: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2AllowlistOnly", + }, + }, + type: "object", + }, GoogleCloudIdentitytoolkitAdminV2SmsTemplate: { description: "The template to use when sending an SMS.", properties: { @@ -6751,12 +7090,22 @@ export default { description: "Whether to allow email/password user authentication.", type: "boolean", }, + autodeleteAnonymousUsers: { + description: "Whether anonymous users will be auto-deleted after a period of 30 days.", + type: "boolean", + }, + client: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig", + }, disableAuth: { description: "Whether authentication is disabled for the tenant. If true, the users under the disabled tenant are not allowed to sign-in. Admins of the disabled tenant are not able to manage its users.", type: "boolean", }, displayName: { description: "Display name of the tenant.", type: "string" }, + emailPrivacyConfig: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig", + }, enableAnonymousUser: { description: "Whether to enable anonymous user authentication.", type: "boolean", @@ -6772,12 +7121,18 @@ export default { mfaConfig: { $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig", }, + monitoring: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2MonitoringConfig", + }, name: { description: 'Output only. Resource name of a tenant. For example: "projects/{project-id}/tenants/{tenant-id}"', readOnly: true, type: "string", }, + smsRegionConfig: { + $ref: "#/components/schemas/GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig", + }, testPhoneNumbers: { additionalProperties: { type: "string" }, description: @@ -7077,7 +7432,7 @@ export default { condition: { $ref: "#/components/schemas/GoogleTypeExpr" }, members: { description: - "Specifies the principals requesting access for a Google Cloud resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. ", + "Specifies the principals requesting access for a Google Cloud resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. Does not include identities that come from external identity providers (IdPs) through identity federation. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a Google service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `serviceAccount:{projectid}.svc.id.goog[{namespace}/{kubernetes-sa}]`: An identifier for a [Kubernetes service account](https://cloud.google.com/kubernetes-engine/docs/how-to/kubernetes-service-accounts). For example, `my-project.svc.id.goog[my-namespace/my-kubernetes-sa]`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. ", items: { type: "string" }, type: "array", }, @@ -7488,13 +7843,20 @@ export default { }, }, }, - apiKey: { + apiKeyQuery: { type: "apiKey", name: "key", in: "query", description: "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", }, + apiKeyHeader: { + type: "apiKey", + name: "x-goog-api-key", + in: "header", + description: + "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + }, }, }, }; diff --git a/src/emulator/auth/schema.ts b/src/emulator/auth/schema.ts index 31c2144929f8..2f977a49c4fd 100644 --- a/src/emulator/auth/schema.ts +++ b/src/emulator/auth/schema.ts @@ -879,7 +879,7 @@ export interface components { */ createdAt?: string; /** - * JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). + * JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ customAttributes?: string; delegatedProjectNumber?: string; @@ -912,7 +912,7 @@ export interface components { */ email?: string; /** - * Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). + * Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ emailVerified?: boolean; /** @@ -926,7 +926,7 @@ export interface components { lastLoginAt?: string; linkProviderUserInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1ProviderUserInfo"]; /** - * The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead. + * The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead. */ localId?: string; mfa?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaInfo"]; @@ -955,7 +955,7 @@ export interface components { */ returnSecureToken?: boolean; /** - * The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. + * The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. */ targetProjectId?: string; /** @@ -1674,9 +1674,6 @@ export interface components { * The CPU memory cost parameter to be used by the STANDARD_SCRYPT hashing function. This parameter, along with block_size and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. */ cpuMemCost?: number; - /** - * If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped. - */ delegatedProjectNumber?: string; /** * The desired key length for the STANDARD_SCRYPT hashing function. Must be at least 1. @@ -1706,6 +1703,9 @@ export interface components { * One or more bytes to be inserted between the salt and plain text password. For stronger security, this should be a single non-printable character. */ saltSeparator?: string; + /** + * If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped. + */ sanityCheck?: boolean; /** * The signer key used to hash the password. Required for the following hashing functions: * SCRYPT, * HMAC_MD5, * HMAC_SHA1, * HMAC_SHA256, * HMAC_SHA512 @@ -1716,7 +1716,7 @@ export interface components { */ tenantId?: string; /** - * A list of accounts to upload. + * A list of accounts to upload. `local_id` is required for each user; everything else is optional. */ users?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserInfo"][]; }; @@ -1869,6 +1869,24 @@ export interface components { */ suggestedTimeout?: string; }; + /** + * Defines a policy of allowing every region by default and adding disallowed regions to a disallow list. + */ + GoogleCloudIdentitytoolkitAdminV2AllowByDefault: { + /** + * Two letter unicode region codes to disallow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + disallowedRegions?: string[]; + }; + /** + * Defines a policy of only allowing regions by explicitly adding them to an allowlist. + */ + GoogleCloudIdentitytoolkitAdminV2AllowlistOnly: { + /** + * Two letter unicode region codes to allow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + allowedRegions?: string[]; + }; /** * Configuration options related to authenticating an anonymous user. */ @@ -1914,6 +1932,25 @@ export interface components { firebaseSubdomain?: string; permissions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Permissions"]; }; + /** + * Options related to how clients making requests on behalf of a tenant should be configured. + */ + GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig: { + permissions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientPermissions"]; + }; + /** + * Configuration related to restricting a user's ability to affect their account. + */ + GoogleCloudIdentitytoolkitAdminV2ClientPermissions: { + /** + * When true, end users cannot delete their account on the associated project through any of our API methods + */ + disabledUserDeletion?: boolean; + /** + * When true, end users cannot sign up for a new account on the associated project through any of our API methods + */ + disabledUserSignup?: boolean; + }; /** * Additional config for Apple for code flow. */ @@ -1939,8 +1976,13 @@ export interface components { * List of domains authorized for OAuth redirects */ authorizedDomains?: string[]; + /** + * Whether anonymous users will be auto-deleted after a period of 30 days. + */ + autodeleteAnonymousUsers?: boolean; blockingFunctions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig"]; client?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientConfig"]; + emailPrivacyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"]; mfa?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"]; monitoring?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MonitoringConfig"]; multiTenant?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiTenantConfig"]; @@ -1951,6 +1993,7 @@ export interface components { notification?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2NotificationConfig"]; quota?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2QuotaConfig"]; signIn?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SignInConfig"]; + smsRegionConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig"]; /** * Output only. The subtype of this config. */ @@ -2034,6 +2077,15 @@ export interface components { */ passwordRequired?: boolean; }; + /** + * Configuration for settings related to email privacy and public visibility. Settings in this config protect against email enumeration, but may make some trade-offs in user-friendliness. + */ + GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig: { + /** + * Migrates the project to a state of improved email privacy. For example certain error codes are more generic to avoid giving away information on whether the account exists. In addition, this disables certain features that as a side-effect allow user enumeration. Enabling this toggle disables the fetchSignInMethodsForEmail functionality and changing the user's email to an unverified email. It is recommended to remove dependence on this functionality and enable this toggle to improve user privacy. + */ + enableImprovedEmailPrivacy?: boolean; + }; /** * Email template. The subject and body fields can contain the following placeholders which will be replaced with the appropriate values: %LINK% - The link to use to redeem the send OOB code. %EMAIL% - The email where the email is being sent. %NEW_EMAIL% - The new email being set for the account (when applicable). %APP_NAME% - The GCP project's display name. %DISPLAY_NAME% - The user's display name. */ @@ -2181,6 +2233,14 @@ export interface components { */ emailSendingConfig?: boolean; }; + /** + * Request for InitializeIdentityPlatform. + */ + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformRequest: { [key: string]: any }; + /** + * Response for InitializeIdentityPlatform. Empty for now. + */ + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformResponse: { [key: string]: any }; /** * Response for DefaultSupportedIdpConfigs */ @@ -2420,6 +2480,13 @@ export interface components { hashConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2HashConfig"]; phoneNumber?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2PhoneNumber"]; }; + /** + * Configures the regions where users are allowed to send verification SMS for the project or tenant. This is based on the calling code of the destination phone number. + */ + GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig: { + allowByDefault?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2AllowByDefault"]; + allowlistOnly?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2AllowlistOnly"]; + }; /** * The template to use when sending an SMS. */ @@ -2513,6 +2580,11 @@ export interface components { * Whether to allow email/password user authentication. */ allowPasswordSignup?: boolean; + /** + * Whether anonymous users will be auto-deleted after a period of 30 days. + */ + autodeleteAnonymousUsers?: boolean; + client?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig"]; /** * Whether authentication is disabled for the tenant. If true, the users under the disabled tenant are not allowed to sign-in. Admins of the disabled tenant are not able to manage its users. */ @@ -2521,6 +2593,7 @@ export interface components { * Display name of the tenant. */ displayName?: string; + emailPrivacyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"]; /** * Whether to enable anonymous user authentication. */ @@ -2532,10 +2605,12 @@ export interface components { hashConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2HashConfig"]; inheritance?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Inheritance"]; mfaConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"]; + monitoring?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MonitoringConfig"]; /** * Output only. Resource name of a tenant. For example: "projects/{project-id}/tenants/{tenant-id}" */ name?: string; + smsRegionConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig"]; /** * A map of pairs that can be used for MFA. The phone number should be in E.164 format (https://www.itu.int/rec/T-REC-E.164/) and a maximum of 10 pairs can be added (error will be thrown once exceeded). */ @@ -2802,7 +2877,7 @@ export interface components { GoogleIamV1Binding: { condition?: components["schemas"]["GoogleTypeExpr"]; /** - * Specifies the principals requesting access for a Google Cloud resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. + * Specifies the principals requesting access for a Google Cloud resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. Does not include identities that come from external identity providers (IdPs) through identity federation. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a Google service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `serviceAccount:{projectid}.svc.id.goog[{namespace}/{kubernetes-sa}]`: An identifier for a [Kubernetes service account](https://cloud.google.com/kubernetes-engine/docs/how-to/kubernetes-service-accounts). For example, `my-project.svc.id.goog[my-namespace/my-kubernetes-sa]`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. */ members?: string[]; /** diff --git a/src/emulator/auth/server.ts b/src/emulator/auth/server.ts index e63eef89086a..f7c61b6c9d2d 100644 --- a/src/emulator/auth/server.ts +++ b/src/emulator/auth/server.ts @@ -166,14 +166,25 @@ export async function createApp( ); const apiKeyAuthenticator: PromiseAuthenticator = (ctx, info) => { - if (info.in !== "query") { - throw new Error('apiKey must be defined as in: "query" in API spec.'); - } if (!info.name) { - throw new Error("apiKey param name is undefined in API spec."); + throw new Error("apiKey param/header name is undefined in API spec."); + } + + let key: string | undefined; + const req = ctx.req as express.Request; + switch (info.in) { + case "header": + key = req.get(info.name); + break; + case "query": { + const q = req.query[info.name]; + key = typeof q === "string" ? q : undefined; + break; + } + default: + throw new Error('apiKey must be defined as in: "query" or "header" in API spec.'); } - const key = (ctx.req as express.Request).query[info.name]; - if (typeof key === "string" && key.length > 0) { + if (key) { return { type: "success", user: getProjectIdByApiKey(key) }; } else { return undefined; @@ -218,7 +229,8 @@ export async function createApp( const apis = await exegesisExpress.middleware(specForRouter(), { controllers: { auth: toExegesisController(authOperations, getProjectStateById) }, authenticators: { - apiKey: apiKeyAuthenticator, + apiKeyQuery: apiKeyAuthenticator, + apiKeyHeader: apiKeyAuthenticator, Oauth2: oauth2Authenticator, }, autoHandleHttpErrors(err) { @@ -305,7 +317,7 @@ export async function createApp( if (ctx.res.statusCode === 401) { // Normalize unauthenticated responses to match production. const requirements = (ctx.api.operationObject as OperationObject).security; - if (requirements?.some((req) => req.apiKey)) { + if (requirements?.some((req) => req.apiKeyQuery || req.apiKeyHeader)) { throw new PermissionDeniedError("The request is missing a valid API key."); } else { throw new UnauthenticatedError( diff --git a/src/test/emulators/auth/rest.spec.ts b/src/test/emulators/auth/rest.spec.ts index b2adb5efeb49..8e2494e968fc 100644 --- a/src/test/emulators/auth/rest.spec.ts +++ b/src/test/emulators/auth/rest.spec.ts @@ -93,6 +93,28 @@ describeAuthEmulator("authentication", ({ authApi }) => { }); }); + it("should accept API key as a query parameter", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should accept API key in HTTP Header x-goog-api-key", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("x-goog-api-key", "fake-api-key") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + it("should ignore non-Bearer Authorization headers", async () => { await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") @@ -143,7 +165,11 @@ describeAuthEmulator("authentication", ({ authApi }) => { .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") // This authenticates as owner of the default projectId. The exact value // and expiry don't matter -- the Emulator only checks for the format. - .set("Authorization", "Bearer ya29.AHES6ZRVmB7fkLtd1XTmq6mo0S1wqZZi3-Lh_s-6Uw7p8vtgSwg") + .set( + "Authorization", + // Not an actual token. Breaking it down to avoid linter false positives. + "Bearer ya" + "29.AHES0ZZZZZ0fff" + "ff0XXXX0mmmm0wwwww0-LL_l-0bb0b0bbbbbb" + ) .send({ // This field requires OAuth 2 and should work correctly. targetProjectId: "example2", From 6ed818b747f8847948759639bf2c1c013d8e9cc1 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 1 Dec 2022 12:40:21 -0500 Subject: [PATCH 098/115] Detect Google Cloud Workstations (#5283) Default to --no-localhost when calling login from Cloud Workstations --- CHANGELOG.md | 1 + src/test/utils.spec.ts | 26 ++++++++++++++++++++++++++ src/utils.ts | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ff5550bcaf..29b20fd25a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ - Fix bug where disabling background triggers did nothing. (#5221) - Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) - Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) +- Default to --no-localhost when calling login from Google Cloud Workstations - Support the x-goog-api-key header in auth emulator. (#5249) diff --git a/src/test/utils.spec.ts b/src/test/utils.spec.ts index a6dc65709068..384ca44ef49c 100644 --- a/src/test/utils.spec.ts +++ b/src/test/utils.spec.ts @@ -70,6 +70,32 @@ describe("utils", () => { }); }); + describe("isCloudEnvironment", () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("should return false by default", () => { + expect(utils.isCloudEnvironment()).to.be.false; + }); + + it("should return true when in codespaces", () => { + process.env.CODESPACES = "true"; + expect(utils.isCloudEnvironment()).to.be.true; + }); + + it("should return true when in Cloud Workstations", () => { + process.env.GOOGLE_CLOUD_WORKSTATIONS = "true"; + expect(utils.isCloudEnvironment()).to.be.true; + }); + }); + describe("getDatabaseUrl", () => { it("should create a url for prod", () => { expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/")).to.equal( diff --git a/src/utils.ts b/src/utils.ts index e14d43ef73f0..39c517587b22 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -577,7 +577,7 @@ export function datetimeString(d: Date): string { * Indicates whether the end-user is running the CLI from a cloud-based environment. */ export function isCloudEnvironment() { - return !!process.env.CODESPACES; + return !!process.env.CODESPACES || !!process.env.GOOGLE_CLOUD_WORKSTATIONS; } /** From d59f265bd4c8ae563a5e42deb5f3e6472656da8c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Dec 2022 10:15:43 -0800 Subject: [PATCH 099/115] Fix bug using --only filter to disable no-op deploy fails deploy because function source isn't uploaded (#5280) Fixes https://github.com/firebase/firebase-tools/issues/5270. --- CHANGELOG.md | 1 + src/deploy/functions/deploy.ts | 8 +++++- src/test/deploy/functions/deploy.spec.ts | 31 +++++++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b20fd25a69..074bef811847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,4 @@ - Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) - Default to --no-localhost when calling login from Google Cloud Workstations - Support the x-goog-api-key header in auth emulator. (#5249) +- Fix bug where function deployments using --only filter sometimes failed deployments. (#5280) diff --git a/src/deploy/functions/deploy.ts b/src/deploy/functions/deploy.ts index a432dc4caa5b..ecdc7b680501 100644 --- a/src/deploy/functions/deploy.ts +++ b/src/deploy/functions/deploy.ts @@ -112,7 +112,7 @@ export async function deploy( await checkHttpIam(context, options, payload); const uploads: Promise[] = []; for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) { - if (shouldUploadBeSkipped(wantBackend, haveBackend)) { + if (shouldUploadBeSkipped(context, wantBackend, haveBackend)) { continue; } uploads.push(uploadCodebase(context, codebase, wantBackend)); @@ -124,9 +124,15 @@ export async function deploy( * @return True IFF wantBackend + haveBackend are the same */ export function shouldUploadBeSkipped( + context: args.Context, wantBackend: backend.Backend, haveBackend: backend.Backend ): boolean { + // If function targets are specified by --only flag, assume that function will be deployed + // and go ahead and upload the source. + if (context.filters && context.filters.length > 0) { + return false; + } const wantEndpoints = backend.allEndpoints(wantBackend); const haveEndpoints = backend.allEndpoints(haveBackend); diff --git a/src/test/deploy/functions/deploy.spec.ts b/src/test/deploy/functions/deploy.spec.ts index 78e3ab5feaa9..1fb4e1030901 100644 --- a/src/test/deploy/functions/deploy.spec.ts +++ b/src/test/deploy/functions/deploy.spec.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; +import * as args from "../../../deploy/functions/args"; import * as backend from "../../../deploy/functions/backend"; import * as deploy from "../../../deploy/functions/deploy"; @@ -17,6 +18,11 @@ describe("deploy", () => { ...ENDPOINT_BASE, httpsTrigger: {}, }; + + const CONTEXT: args.Context = { + projectId: "project", + }; + describe("shouldUploadBeSkipped", () => { let endpoint1InWantBackend: backend.Endpoint; let endpoint2InWantBackend: backend.Endpoint; @@ -63,7 +69,7 @@ describe("deploy", () => { endpoint2InHaveBackend.hash = endpoint2InWantBackend.hash; // Execute - const result = deploy.shouldUploadBeSkipped(wantBackend, haveBackend); + const result = deploy.shouldUploadBeSkipped(CONTEXT, wantBackend, haveBackend); // Expect expect(result).to.be.true; @@ -76,7 +82,7 @@ describe("deploy", () => { endpoint2InHaveBackend.hash = "No_match"; // Execute - const result = deploy.shouldUploadBeSkipped(wantBackend, haveBackend); + const result = deploy.shouldUploadBeSkipped(CONTEXT, wantBackend, haveBackend); // Expect expect(result).to.be.false; @@ -92,7 +98,7 @@ describe("deploy", () => { haveBackend = backend.of(endpoint1InHaveBackend); // Execute - const result = deploy.shouldUploadBeSkipped(wantBackend, haveBackend); + const result = deploy.shouldUploadBeSkipped(CONTEXT, wantBackend, haveBackend); // Expect expect(result).to.be.false; @@ -108,7 +114,24 @@ describe("deploy", () => { haveBackend = backend.of(endpoint1InHaveBackend, endpoint2InHaveBackend); // Execute - const result = deploy.shouldUploadBeSkipped(wantBackend, haveBackend); + const result = deploy.shouldUploadBeSkipped(CONTEXT, wantBackend, haveBackend); + + // Expect + expect(result).to.be.false; + }); + + it("should not skip if endpoint filter is specified", () => { + endpoint1InWantBackend.hash = "1"; + endpoint2InWantBackend.hash = "2"; + endpoint1InHaveBackend.hash = endpoint1InWantBackend.hash; + endpoint2InHaveBackend.hash = endpoint2InWantBackend.hash; + + // Execute + const result = deploy.shouldUploadBeSkipped( + { ...CONTEXT, filters: [{ idChunks: ["foobar"] }] }, + wantBackend, + haveBackend + ); // Expect expect(result).to.be.false; From 10e022bb87ab7ddad1a81c08e2ad4ad8b6dc575a Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 1 Dec 2022 21:30:48 -0500 Subject: [PATCH 100/115] Fix/next js predeploy hooks (#5288) * Fix predeploy hooks not running for framework deploy Co-authored-by: SirFreakness --- CHANGELOG.md | 1 + src/deploy/lifecycleHooks.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074bef811847..2d018fea5c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,5 @@ - Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) - Default to --no-localhost when calling login from Google Cloud Workstations - Support the x-goog-api-key header in auth emulator. (#5249) +- Fix bug in deploying web frameworks when a predeploy hook was configured in firebase.json (#5199) - Fix bug where function deployments using --only filter sometimes failed deployments. (#5280) diff --git a/src/deploy/lifecycleHooks.ts b/src/deploy/lifecycleHooks.ts index fca96fb33704..0c08e09a8cb0 100644 --- a/src/deploy/lifecycleHooks.ts +++ b/src/deploy/lifecycleHooks.ts @@ -48,7 +48,7 @@ function getChildEnvironment(target: string, overallOptions: any, config: any) { let resourceDir; switch (target) { case "hosting": - resourceDir = overallOptions.config.path(config.public); + resourceDir = overallOptions.config.path(config.public ?? config.source); break; case "functions": resourceDir = overallOptions.config.path(config.source); From 7b070b015b3d2caedc331269ff59f6859d354246 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Fri, 2 Dec 2022 11:16:40 -0500 Subject: [PATCH 101/115] Add int tests for test lab, remote config, and fireperf (#5200) * bumping functions package * revert package * updated to 4.0.2 * increase the function count * try testlab only * testlab might have a bug, trying out perf & remote config * adding in preserveExternalChanges and RESET_VALUE * formatter * trying run with functions binary * adding in required package to root & cleaning up reset test case * fixing imports * adding firebase/types * yanking tar * bumping functions package * revert to RuntimeOptions * remove @firebase/logger --- npm-shrinkwrap.json | 2055 ++++++++--------- package.json | 5 +- .../functionsEmulatorRuntime.spec.ts | 2 +- .../functions-deploy-tests/functions/fns.js | 3 + .../functions/package.json | 2 +- scripts/functions-deploy-tests/tests.ts | 81 +- 6 files changed, 960 insertions(+), 1188 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 054c70742744..cac6a02c3f37 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -90,6 +90,7 @@ "@types/cross-spawn": "^6.0.1", "@types/express": "^4.17.0", "@types/express-serve-static-core": "^4.17.8", + "@types/firebase": "^3.2.1", "@types/fs-extra": "^9.0.13", "@types/glob": "^7.1.1", "@types/inquirer": "^8.1.3", @@ -134,8 +135,8 @@ "eslint-plugin-jsdoc": "^39.2.9", "eslint-plugin-prettier": "^4.0.0", "firebase": "^7.24.0", - "firebase-admin": "^9.4.2", - "firebase-functions": "^3.23.0", + "firebase-admin": "^10.0.0", + "firebase-functions": "^4.1.0", "google-discovery-to-swagger": "^2.1.0", "googleapis": "^105.0.0", "mocha": "^9.1.3", @@ -812,6 +813,18 @@ "integrity": "sha512-W98NvvOe/Med3o66xTO03pd7a2omZebH79PV64gSE+ceDdU8uxQhFTa7ISiD1kseyqyOrMyW5/MNdsGEU02i3Q==", "dev": true }, + "node_modules/@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dev": true, + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/@firebase/analytics": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.6.0.tgz", @@ -836,6 +849,22 @@ "integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA==", "dev": true }, + "node_modules/@firebase/analytics/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/analytics/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "node_modules/@firebase/analytics/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -860,75 +889,32 @@ "xmlhttprequest": "1.8.0" } }, - "node_modules/@firebase/app-compat": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", - "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", - "dev": true, - "peer": true, - "dependencies": { - "@firebase/app": "0.7.22", - "@firebase/component": "0.5.13", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "node_modules/@firebase/app-compat/node_modules/@firebase/app": { - "version": "0.7.22", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", - "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", - "dev": true, - "peer": true, - "dependencies": { - "@firebase/component": "0.5.13", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "node_modules/@firebase/app-compat/node_modules/@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", - "dev": true, - "peer": true, - "dependencies": { - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } + "node_modules/@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==", + "dev": true }, - "node_modules/@firebase/app-compat/node_modules/@firebase/logger": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", - "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/@firebase/app/node_modules/@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true }, - "node_modules/@firebase/app-compat/node_modules/@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "node_modules/@firebase/app/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", "dev": true, - "peer": true, "dependencies": { - "tslib": "^2.1.0" + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" } }, - "node_modules/@firebase/app-compat/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - }, - "node_modules/@firebase/app-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", - "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "node_modules/@firebase/app/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", "dev": true }, "node_modules/@firebase/app/node_modules/@firebase/util": { @@ -953,16 +939,16 @@ } }, "node_modules/@firebase/auth-interop-types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", - "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", "dev": true, "peerDependencies": { "@firebase/app-types": "0.x", - "@firebase/util": "0.x" + "@firebase/util": "1.x" } }, - "node_modules/@firebase/auth-types": { + "node_modules/@firebase/auth/node_modules/@firebase/auth-types": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", @@ -972,141 +958,81 @@ "@firebase/util": "0.x" } }, - "node_modules/@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "dependencies": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "node_modules/@firebase/component/node_modules/@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "node_modules/@firebase/auth/node_modules/@firebase/util": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.4.1.tgz", + "integrity": "sha512-XhYCOwq4AH+YeQBEnDQvigz50WiiBU4LnJh2+//VMt4J2Ybsk0eTgUHNngUzXsmp80EJrwal3ItODg55q1ajWg==", "dev": true, + "peer": true, "dependencies": { - "tslib": "^1.11.1" + "tslib": "^2.1.0" } }, - "node_modules/@firebase/database": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", - "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", + "node_modules/@firebase/auth/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true, - "dependencies": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.19", - "@firebase/database-types": "0.5.2", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "faye-websocket": "0.11.3", - "tslib": "^1.11.1" - } + "peer": true }, - "node_modules/@firebase/database-compat": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.5.tgz", - "integrity": "sha512-UVxkHL24sZfsjsjs+yiKIdYdrWXHrLxSFCYNdwNXDlTkAc0CWP9AAY3feLhBVpUKk+4Cj0I4sGnyIm2C1ltAYg==", + "node_modules/@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", "dev": true, "dependencies": { - "@firebase/component": "0.5.10", - "@firebase/database": "0.12.5", - "@firebase/database-types": "0.9.4", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.4.3", + "@firebase/util": "1.7.3", "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" } }, - "node_modules/@firebase/database-compat/node_modules/@firebase/app-types": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", - "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==", + "node_modules/@firebase/component/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, - "node_modules/@firebase/database-compat/node_modules/@firebase/auth-interop-types": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", - "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", - "dev": true, - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/database-compat/node_modules/@firebase/component": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", - "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", - "dev": true, - "dependencies": { - "@firebase/util": "1.4.3", - "tslib": "^2.1.0" - } - }, - "node_modules/@firebase/database-compat/node_modules/@firebase/database": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.5.tgz", - "integrity": "sha512-1Pd2jYqvqZI7SQWAiXbTZxmsOa29PyOaPiUtr8pkLSfLp4AeyMBegYAXCLYLW6BNhKn3zNKFkxYDxYHq4q+Ixg==", + "node_modules/@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", "dev": true, "dependencies": { - "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.10", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.4.3", + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, - "node_modules/@firebase/database-compat/node_modules/@firebase/database-types": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.4.tgz", - "integrity": "sha512-uAQuc6NUZ5Oh/cWZPeMValtcZ+4L1stgKOeYvz7mLn8+s03tnCDL2N47OLCHdntktVkhImQTwGNARgqhIhtNeA==", - "dev": true, - "dependencies": { - "@firebase/app-types": "0.7.0", - "@firebase/util": "1.4.3" - } - }, - "node_modules/@firebase/database-compat/node_modules/@firebase/logger": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", - "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@firebase/database-compat/node_modules/@firebase/util": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", - "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", + "node_modules/@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", "dev": true, "dependencies": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", "tslib": "^2.1.0" } }, - "node_modules/@firebase/database-compat/node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "node_modules/@firebase/database-compat/node_modules/@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", "dev": true, "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" } }, "node_modules/@firebase/database-compat/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, "node_modules/@firebase/database-types": { @@ -1118,14 +1044,17 @@ "@firebase/app-types": "0.6.1" } }, - "node_modules/@firebase/database/node_modules/@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "dependencies": { - "tslib": "^1.11.1" - } + "node_modules/@firebase/database-types/node_modules/@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + }, + "node_modules/@firebase/database/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true }, "node_modules/@firebase/firestore": { "version": "1.18.0", @@ -1160,6 +1089,22 @@ "@firebase/app-types": "0.x" } }, + "node_modules/@firebase/firestore/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/firestore/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "node_modules/@firebase/firestore/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -1169,6 +1114,19 @@ "tslib": "^1.11.1" } }, + "node_modules/@firebase/firestore/node_modules/@grpc/proto-loader": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", + "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", + "dev": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@firebase/firestore/node_modules/node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -1201,6 +1159,25 @@ "integrity": "sha512-DGR4i3VI55KnYk4IxrIw7+VG7Q3gA65azHnZxo98Il8IvYLr2UTBlSh72dTLlDf25NW51HqvJgYJDKvSaAeyHQ==", "dev": true }, + "node_modules/@firebase/functions/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/functions/node_modules/@firebase/util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", + "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "dev": true, + "dependencies": { + "tslib": "^1.11.1" + } + }, "node_modules/@firebase/functions/node_modules/node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -1236,6 +1213,16 @@ "@firebase/app-types": "0.x" } }, + "node_modules/@firebase/installations/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, "node_modules/@firebase/installations/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -1246,9 +1233,18 @@ } }, "node_modules/@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/logger/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, "node_modules/@firebase/messaging": { @@ -1278,6 +1274,16 @@ "@firebase/app-types": "0.x" } }, + "node_modules/@firebase/messaging/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, "node_modules/@firebase/messaging/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -1311,6 +1317,22 @@ "integrity": "sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA==", "dev": true }, + "node_modules/@firebase/performance/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/performance/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "node_modules/@firebase/performance/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -1355,10 +1377,26 @@ "integrity": "sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA==", "dev": true }, - "node_modules/@firebase/remote-config/node_modules/@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "node_modules/@firebase/remote-config/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/remote-config/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, + "node_modules/@firebase/remote-config/node_modules/@firebase/util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", + "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", "dev": true, "dependencies": { "tslib": "^1.11.1" @@ -1380,7 +1418,17 @@ "@firebase/app-types": "0.x" } }, - "node_modules/@firebase/storage-types": { + "node_modules/@firebase/storage/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/@firebase/storage/node_modules/@firebase/storage-types": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.13.tgz", "integrity": "sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog==", @@ -1400,21 +1448,19 @@ } }, "node_modules/@firebase/util": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.4.1.tgz", - "integrity": "sha512-XhYCOwq4AH+YeQBEnDQvigz50WiiBU4LnJh2+//VMt4J2Ybsk0eTgUHNngUzXsmp80EJrwal3ItODg55q1ajWg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", "dev": true, - "peer": true, "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@firebase/util/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true }, "node_modules/@firebase/webchannel-wrapper": { "version": "0.4.0", @@ -1428,81 +1474,17 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, - "node_modules/@google-cloud/common": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", - "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", - "dev": true, - "optional": true, - "dependencies": { - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^6.1.1", - "retry-request": "^4.1.1", - "teeny-request": "^7.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/common/node_modules/google-auth-library": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", - "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", - "dev": true, - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/common/node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dev": true, - "optional": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/@google-cloud/common/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dev": true, - "optional": true, - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "node_modules/@google-cloud/firestore": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.8.0.tgz", - "integrity": "sha512-cBPo7QQG+aUhS7AIr6fDlA9KIX0/U26rKZyL2K/L68LArDQzgBk1/xOiMoflHRNDQARwCQ0PAZmw8V8CXg7vTg==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", "dev": true, "optional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^2.9.2" + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" }, "engines": { "node": ">=10.10.0" @@ -1583,24 +1565,6 @@ "node": ">=12.0.0" } }, - "node_modules/@google-cloud/pubsub/node_modules/@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@google-cloud/pubsub/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1748,48 +1712,51 @@ } }, "node_modules/@google-cloud/storage": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.7.0.tgz", - "integrity": "sha512-6nPTylNaYWsVo5yHDdjQfUSh9qP/DFwahhyvOAf9CSDKfeoOys8+PAyHsoKyL29uyYoC6ymws7uJDO48y/SzBA==", + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.20.5.tgz", + "integrity": "sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==", "dev": true, "optional": true, "dependencies": { - "@google-cloud/common": "^3.5.0", - "@google-cloud/paginator": "^3.0.0", + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", "arrify": "^2.0.0", + "async-retry": "^1.3.3", "compressible": "^2.0.12", - "date-and-time": "^0.14.0", + "configstore": "^5.0.0", "duplexify": "^4.0.0", + "ent": "^2.2.0", "extend": "^3.0.2", "gaxios": "^4.0.0", - "gcs-resumable-upload": "^3.1.0", - "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", + "mime": "^3.0.0", "mime-types": "^2.0.8", - "onetime": "^5.1.0", "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", + "retry-request": "^4.2.2", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "uuid": "^8.0.0", "xdg-basedir": "^4.0.0" }, "engines": { "node": ">=10" } }, - "node_modules/@google-cloud/storage/node_modules/get-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", - "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "optional": true, - "engines": { - "node": ">=10" + "bin": { + "mime": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=10.0.0" } }, "node_modules/@google-cloud/storage/node_modules/p-limit": { @@ -1829,15 +1796,15 @@ "node": "^8.13.0 || >=10.10.0" } }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.10.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "bin": { @@ -1847,19 +1814,6 @@ "node": ">=6" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.1.tgz", - "integrity": "sha512-3y0FhacYAwWvyXshH18eDkUI40wT/uGio7MAegzY8lO5+wVsc19+1A7T0pPptae4kl7bdITL+0cHpnAPmryBjQ==", - "dev": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", @@ -2624,6 +2578,16 @@ "@types/express": "*" } }, + "node_modules/@types/firebase": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/firebase/-/firebase-3.2.1.tgz", + "integrity": "sha512-G8XgHMu2jHlElfc2xVNaYP50F0qrqeTCjgeG1v5b4SRwWG4XKC4fCuEdVZuZaMRmVygcnbRZBAo9O7RsDvmkGQ==", + "deprecated": "This is a stub types definition for Firebase API (https://www.firebase.com/docs/javascript/firebase). Firebase API provides its own type definitions, so you don't need @types/firebase installed!", + "dev": true, + "dependencies": { + "firebase": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3875,6 +3839,16 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==" }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5199,13 +5173,6 @@ "node": ">= 6" } }, - "node_modules/date-and-time": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", - "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", - "dev": true, - "optional": true - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5378,18 +5345,6 @@ "kuler": "1.0.x" } }, - "node_modules/dicer": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", - "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -5563,7 +5518,7 @@ "node_modules/ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true, "optional": true }, @@ -6595,9 +6550,9 @@ } }, "node_modules/faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "dependencies": { "websocket-driver": ">=0.5.1" @@ -6749,63 +6704,58 @@ } }, "node_modules/firebase-admin": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.12.0.tgz", - "integrity": "sha512-AtA7OH5RbIFGoc0gZOQgaYC6cdjdhZv4w3XgWoupkPKO1HY+0GzixOuXDa75kFeoVyhIyo4PkLg/GAC1dC1P6w==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-10.3.0.tgz", + "integrity": "sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==", "dev": true, "dependencies": { - "@firebase/database-compat": "^0.1.1", - "@firebase/database-types": "^0.7.2", + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.0", + "@firebase/database-types": "^0.9.7", "@types/node": ">=12.12.47", - "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.2", - "node-forge": "^0.10.0" + "node-forge": "^1.3.1", + "uuid": "^8.3.2" }, "engines": { - "node": ">=10.13.0" + "node": ">=12.7.0" }, "optionalDependencies": { - "@google-cloud/firestore": "^4.5.0", - "@google-cloud/storage": "^5.3.0" + "@google-cloud/firestore": "^4.15.1", + "@google-cloud/storage": "^5.18.3" } }, - "node_modules/firebase-admin/node_modules/@firebase/app-types": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.3.tgz", - "integrity": "sha512-/M13DPPati7FQHEQ9Minjk1HGLm/4K4gs9bR4rzLCWJg64yGtVC0zNg9gDpkw9yc2cvol/mNFxqTtd4geGrwdw==", - "dev": true - }, "node_modules/firebase-admin/node_modules/@firebase/database-types": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.3.tgz", - "integrity": "sha512-dSOJmhKQ0nL8O4EQMRNGpSExWCXeHtH57gGg0BfNAdWcKhC8/4Y+qfKLfWXzyHvrSecpLmO0SmAi/iK2D5fp5A==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", "dev": true, "dependencies": { - "@firebase/app-types": "0.6.3" + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" } }, "node_modules/firebase-functions": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.0.tgz", - "integrity": "sha512-YKZm/AxjnWTP9VbxAyjs7ImWfMydleQAiHB2T6li3imRCcwC4+h6BXU/Jf2uELz9AkCb+UabWbdVrklk3b+70Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.1.0.tgz", + "integrity": "sha512-brbww5lGQVm8+d4KFmHF+O8wJBthws1NGXgphy7UDguMbUoW0fq6bL0NI442w+3nDE8IYUbnR4p3U8/cLAhnOA==", "dev": true, "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", - "lodash": "^4.17.14", "node-fetch": "^2.6.7" }, "bin": { "firebase-functions": "lib/bin/firebase-functions.js" }, "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=14.10.0" }, "peerDependencies": { - "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + "firebase-admin": "^10.0.0 || ^11.0.0" } }, "node_modules/firebase-functions/node_modules/@types/express": { @@ -6819,6 +6769,53 @@ "@types/serve-static": "*" } }, + "node_modules/firebase/node_modules/@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + }, + "node_modules/firebase/node_modules/@firebase/auth-interop-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", + "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "0.x" + } + }, + "node_modules/firebase/node_modules/@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "dependencies": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "node_modules/firebase/node_modules/@firebase/database": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", + "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", + "dev": true, + "dependencies": { + "@firebase/auth-interop-types": "0.1.5", + "@firebase/component": "0.1.19", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.2", + "faye-websocket": "0.11.3", + "tslib": "^1.11.1" + } + }, + "node_modules/firebase/node_modules/@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "node_modules/firebase/node_modules/@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -6828,6 +6825,18 @@ "tslib": "^1.11.1" } }, + "node_modules/firebase/node_modules/faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -7166,131 +7175,21 @@ "node": ">=8" } }, - "node_modules/gcs-resumable-upload": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", - "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "optional": true, - "dependencies": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "extend": "^3.0.2", - "gaxios": "^3.0.0", - "google-auth-library": "^6.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "bin": { - "gcs-upload": "build/src/cli.js" - }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/gcs-resumable-upload/node_modules/gaxios": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", - "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", - "dev": true, - "optional": true, - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { - "node": ">=10" - } - }, - "node_modules/gcs-resumable-upload/node_modules/google-auth-library": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", - "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", - "dev": true, - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcs-resumable-upload/node_modules/google-auth-library/node_modules/gaxios": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", - "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", - "dev": true, - "optional": true, - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcs-resumable-upload/node_modules/is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gcs-resumable-upload/node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dev": true, - "optional": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/gcs-resumable-upload/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dev": true, - "optional": true, - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-func-name": { @@ -7574,26 +7473,6 @@ "node": ">=10" } }, - "node_modules/google-gax/node_modules/@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", - "dev": true, - "optional": true, - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/google-p12-pem": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", @@ -7608,14 +7487,6 @@ "node": ">=10" } }, - "node_modules/google-p12-pem/node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/googleapis": { "version": "105.0.0", "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-105.0.0.tgz", @@ -7756,15 +7627,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/googleapis-common/node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/googleapis-common/node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -7884,15 +7746,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/googleapis/node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -8139,9 +7992,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "node_modules/http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", "dev": true }, "node_modules/http-proxy-agent": { @@ -10481,12 +10334,11 @@ } }, "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true, + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "engines": { - "node": ">= 6.0.0" + "node": ">= 6.13.0" } }, "node_modules/node-gyp": { @@ -12374,22 +12226,23 @@ } }, "node_modules/retry-request": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", - "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", "dev": true, "optional": true, "dependencies": { - "debug": "^4.1.1" + "debug": "^4.1.1", + "extend": "^3.0.2" }, "engines": { "node": ">=8.10.0" } }, "node_modules/retry-request/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "optional": true, "dependencies": { @@ -12827,13 +12680,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/snakeize": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", - "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", - "dev": true, - "optional": true - }, "node_modules/socks": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", @@ -13072,15 +12918,6 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13175,7 +13012,7 @@ "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "dev": true, "optional": true }, @@ -13780,13 +13617,13 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/teeny-request": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", - "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", "dev": true, "optional": true, "dependencies": { - "http-proxy-agent": "^4.0.0", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.1", "stream-events": "^1.0.5", @@ -13796,6 +13633,56 @@ "node": ">=10" } }, + "node_modules/teeny-request/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + }, "node_modules/term-size": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", @@ -13849,6 +13736,12 @@ "node": ">=8" } }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", + "dev": true + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -14729,12 +14622,12 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "node_modules/websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "dependencies": { - "http-parser-js": ">=0.4.0 <0.4.11", + "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" }, @@ -14955,7 +14848,7 @@ "node_modules/xmlhttprequest": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -15602,6 +15495,15 @@ "integrity": "sha512-W98NvvOe/Med3o66xTO03pd7a2omZebH79PV64gSE+ceDdU8uxQhFTa7ISiD1kseyqyOrMyW5/MNdsGEU02i3Q==", "dev": true }, + "@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dev": true, + "requires": { + "text-decoding": "^1.0.0" + } + }, "@firebase/analytics": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.6.0.tgz", @@ -15616,6 +15518,22 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -15648,6 +15566,28 @@ "xmlhttprequest": "1.8.0" }, "dependencies": { + "@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + }, + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -15659,58 +15599,32 @@ } } }, - "@firebase/app-compat": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", - "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", + "@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==", + "dev": true + }, + "@firebase/auth": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.15.0.tgz", + "integrity": "sha512-IFuzhxS+HtOQl7+SZ/Mhaghy/zTU7CENsJFWbC16tv2wfLZbayKF5jYGdAU3VFLehgC8KjlcIWd10akc3XivfQ==", "dev": true, - "peer": true, "requires": { - "@firebase/app": "0.7.22", - "@firebase/component": "0.5.13", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" + "@firebase/auth-types": "0.10.1" }, "dependencies": { - "@firebase/app": { - "version": "0.7.22", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", - "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", - "dev": true, - "peer": true, - "requires": { - "@firebase/component": "0.5.13", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", - "dev": true, - "peer": true, - "requires": { - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "@firebase/logger": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", - "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", + "@firebase/auth-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", + "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", "dev": true, - "peer": true, - "requires": { - "tslib": "^2.1.0" - } + "requires": {} }, "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.4.1.tgz", + "integrity": "sha512-XhYCOwq4AH+YeQBEnDQvigz50WiiBU4LnJh2+//VMt4J2Ybsk0eTgUHNngUzXsmp80EJrwal3ItODg55q1ajWg==", "dev": true, "peer": true, "requires": { @@ -15718,182 +15632,89 @@ } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true, "peer": true } } }, - "@firebase/app-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", - "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", - "dev": true - }, - "@firebase/auth": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.15.0.tgz", - "integrity": "sha512-IFuzhxS+HtOQl7+SZ/Mhaghy/zTU7CENsJFWbC16tv2wfLZbayKF5jYGdAU3VFLehgC8KjlcIWd10akc3XivfQ==", - "dev": true, - "requires": { - "@firebase/auth-types": "0.10.1" - } - }, "@firebase/auth-interop-types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", - "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", - "dev": true, - "requires": {} - }, - "@firebase/auth-types": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", - "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", "dev": true, "requires": {} }, "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", "dev": true, "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" }, "dependencies": { - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true } } }, "@firebase/database": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", - "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", "dev": true, "requires": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.19", - "@firebase/database-types": "0.5.2", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "faye-websocket": "0.11.3", - "tslib": "^1.11.1" + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" }, "dependencies": { - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true } } }, "@firebase/database-compat": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.5.tgz", - "integrity": "sha512-UVxkHL24sZfsjsjs+yiKIdYdrWXHrLxSFCYNdwNXDlTkAc0CWP9AAY3feLhBVpUKk+4Cj0I4sGnyIm2C1ltAYg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", "dev": true, "requires": { - "@firebase/component": "0.5.10", - "@firebase/database": "0.12.5", - "@firebase/database-types": "0.9.4", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.4.3", + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", "tslib": "^2.1.0" }, "dependencies": { - "@firebase/app-types": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", - "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==", - "dev": true - }, - "@firebase/auth-interop-types": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", - "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", - "dev": true, - "requires": {} - }, - "@firebase/component": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", - "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", - "dev": true, - "requires": { - "@firebase/util": "1.4.3", - "tslib": "^2.1.0" - } - }, - "@firebase/database": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.5.tgz", - "integrity": "sha512-1Pd2jYqvqZI7SQWAiXbTZxmsOa29PyOaPiUtr8pkLSfLp4AeyMBegYAXCLYLW6BNhKn3zNKFkxYDxYHq4q+Ixg==", - "dev": true, - "requires": { - "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.10", - "@firebase/logger": "0.3.2", - "@firebase/util": "1.4.3", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - } - }, "@firebase/database-types": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.4.tgz", - "integrity": "sha512-uAQuc6NUZ5Oh/cWZPeMValtcZ+4L1stgKOeYvz7mLn8+s03tnCDL2N47OLCHdntktVkhImQTwGNARgqhIhtNeA==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", "dev": true, "requires": { - "@firebase/app-types": "0.7.0", - "@firebase/util": "1.4.3" - } - }, - "@firebase/logger": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", - "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "@firebase/util": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", - "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" } }, "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true } } @@ -15905,6 +15726,14 @@ "dev": true, "requires": { "@firebase/app-types": "0.6.1" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + } } }, "@firebase/firestore": { @@ -15924,6 +15753,22 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -15933,6 +15778,16 @@ "tslib": "^1.11.1" } }, + "@grpc/proto-loader": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", + "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", + "dev": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -15961,6 +15816,25 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", + "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", + "dev": true, + "requires": { + "tslib": "^1.11.1" + } + }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -15988,6 +15862,16 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -16007,10 +15891,21 @@ "requires": {} }, "@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", - "dev": true + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } }, "@firebase/messaging": { "version": "0.7.1", @@ -16026,6 +15921,16 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -16058,6 +15963,22 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -16100,6 +16021,22 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -16129,6 +16066,23 @@ "tslib": "^1.11.1" }, "dependencies": { + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/storage-types": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.13.tgz", + "integrity": "sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog==", + "dev": true, + "requires": {} + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -16140,115 +16094,46 @@ } } }, - "@firebase/storage-types": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.13.tgz", - "integrity": "sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog==", - "dev": true, - "requires": {} - }, "@firebase/util": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.4.1.tgz", - "integrity": "sha512-XhYCOwq4AH+YeQBEnDQvigz50WiiBU4LnJh2+//VMt4J2Ybsk0eTgUHNngUzXsmp80EJrwal3ItODg55q1ajWg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", "dev": true, - "peer": true, "requires": { "tslib": "^2.1.0" }, "dependencies": { "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true } } }, - "@firebase/webchannel-wrapper": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz", - "integrity": "sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ==", - "dev": true - }, - "@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "optional": true - }, - "@google-cloud/common": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", - "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", - "dev": true, - "optional": true, - "requires": { - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^6.1.1", - "retry-request": "^4.1.1", - "teeny-request": "^7.0.0" - }, - "dependencies": { - "google-auth-library": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", - "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", - "dev": true, - "optional": true, - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - } - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dev": true, - "optional": true, - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dev": true, - "optional": true, - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - } - } + "@firebase/webchannel-wrapper": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz", + "integrity": "sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true }, "@google-cloud/firestore": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.8.0.tgz", - "integrity": "sha512-cBPo7QQG+aUhS7AIr6fDlA9KIX0/U26rKZyL2K/L68LArDQzgBk1/xOiMoflHRNDQARwCQ0PAZmw8V8CXg7vTg==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", "dev": true, "optional": true, "requires": { "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^2.9.2" + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" } }, "@google-cloud/paginator": { @@ -16308,18 +16193,6 @@ "extend": "^3.0.2" } }, - "@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -16434,38 +16307,41 @@ } }, "@google-cloud/storage": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.7.0.tgz", - "integrity": "sha512-6nPTylNaYWsVo5yHDdjQfUSh9qP/DFwahhyvOAf9CSDKfeoOys8+PAyHsoKyL29uyYoC6ymws7uJDO48y/SzBA==", + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.20.5.tgz", + "integrity": "sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==", "dev": true, "optional": true, "requires": { - "@google-cloud/common": "^3.5.0", - "@google-cloud/paginator": "^3.0.0", + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", "arrify": "^2.0.0", + "async-retry": "^1.3.3", "compressible": "^2.0.12", - "date-and-time": "^0.14.0", + "configstore": "^5.0.0", "duplexify": "^4.0.0", + "ent": "^2.2.0", "extend": "^3.0.2", "gaxios": "^4.0.0", - "gcs-resumable-upload": "^3.1.0", - "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", + "mime": "^3.0.0", "mime-types": "^2.0.8", - "onetime": "^5.1.0", "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", + "retry-request": "^4.2.2", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "uuid": "^8.0.0", "xdg-basedir": "^4.0.0" }, "dependencies": { - "get-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", - "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "optional": true }, @@ -16494,30 +16370,18 @@ "requires": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" - }, - "dependencies": { - "@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - } - } } }, "@grpc/proto-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.1.tgz", - "integrity": "sha512-3y0FhacYAwWvyXshH18eDkUI40wT/uGio7MAegzY8lO5+wVsc19+1A7T0pPptae4kl7bdITL+0cHpnAPmryBjQ==", - "dev": true, + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "requires": { + "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" } }, "@humanwhocodes/config-array": { @@ -17156,6 +17020,15 @@ "@types/express": "*" } }, + "@types/firebase": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/firebase/-/firebase-3.2.1.tgz", + "integrity": "sha512-G8XgHMu2jHlElfc2xVNaYP50F0qrqeTCjgeG1v5b4SRwWG4XKC4fCuEdVZuZaMRmVygcnbRZBAo9O7RsDvmkGQ==", + "dev": true, + "requires": { + "firebase": "*" + } + }, "@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -18180,6 +18053,16 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==" }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -19170,13 +19053,6 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" }, - "date-and-time": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", - "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", - "dev": true, - "optional": true - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -19321,15 +19197,6 @@ "kuler": "1.0.x" } }, - "dicer": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", - "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", - "dev": true, - "requires": { - "streamsearch": "^1.1.0" - } - }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -19486,7 +19353,7 @@ "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true, "optional": true }, @@ -20249,9 +20116,9 @@ } }, "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "requires": { "websocket-driver": ">=0.5.1" @@ -20363,6 +20230,50 @@ "@firebase/util": "0.3.2" }, "dependencies": { + "@firebase/app-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", + "dev": true + }, + "@firebase/auth-interop-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", + "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", + "dev": true, + "requires": {} + }, + "@firebase/component": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", + "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", + "dev": true, + "requires": { + "@firebase/util": "0.3.2", + "tslib": "^1.11.1" + } + }, + "@firebase/database": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", + "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", + "dev": true, + "requires": { + "@firebase/auth-interop-types": "0.1.5", + "@firebase/component": "0.1.19", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.2", + "faye-websocket": "0.11.3", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true + }, "@firebase/util": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", @@ -20371,54 +20282,58 @@ "requires": { "tslib": "^1.11.1" } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } } } }, "firebase-admin": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.12.0.tgz", - "integrity": "sha512-AtA7OH5RbIFGoc0gZOQgaYC6cdjdhZv4w3XgWoupkPKO1HY+0GzixOuXDa75kFeoVyhIyo4PkLg/GAC1dC1P6w==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-10.3.0.tgz", + "integrity": "sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==", "dev": true, "requires": { - "@firebase/database-compat": "^0.1.1", - "@firebase/database-types": "^0.7.2", - "@google-cloud/firestore": "^4.5.0", - "@google-cloud/storage": "^5.3.0", + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.0", + "@firebase/database-types": "^0.9.7", + "@google-cloud/firestore": "^4.15.1", + "@google-cloud/storage": "^5.18.3", "@types/node": ">=12.12.47", - "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.2", - "node-forge": "^0.10.0" + "node-forge": "^1.3.1", + "uuid": "^8.3.2" }, "dependencies": { - "@firebase/app-types": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.3.tgz", - "integrity": "sha512-/M13DPPati7FQHEQ9Minjk1HGLm/4K4gs9bR4rzLCWJg64yGtVC0zNg9gDpkw9yc2cvol/mNFxqTtd4geGrwdw==", - "dev": true - }, "@firebase/database-types": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.3.tgz", - "integrity": "sha512-dSOJmhKQ0nL8O4EQMRNGpSExWCXeHtH57gGg0BfNAdWcKhC8/4Y+qfKLfWXzyHvrSecpLmO0SmAi/iK2D5fp5A==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", "dev": true, "requires": { - "@firebase/app-types": "0.6.3" + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" } } } }, "firebase-functions": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.0.tgz", - "integrity": "sha512-YKZm/AxjnWTP9VbxAyjs7ImWfMydleQAiHB2T6li3imRCcwC4+h6BXU/Jf2uELz9AkCb+UabWbdVrklk3b+70Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.1.0.tgz", + "integrity": "sha512-brbww5lGQVm8+d4KFmHF+O8wJBthws1NGXgphy7UDguMbUoW0fq6bL0NI442w+3nDE8IYUbnR4p3U8/cLAhnOA==", "dev": true, "requires": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", - "lodash": "^4.17.14", "node-fetch": "^2.6.7" }, "dependencies": { @@ -20696,102 +20611,6 @@ } } }, - "gcs-resumable-upload": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", - "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", - "dev": true, - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "extend": "^3.0.2", - "gaxios": "^3.0.0", - "google-auth-library": "^6.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "dependencies": { - "gaxios": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", - "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", - "dev": true, - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "google-auth-library": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", - "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", - "dev": true, - "optional": true, - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "dependencies": { - "gaxios": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", - "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", - "dev": true, - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.1" - } - } - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true, - "optional": true - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dev": true, - "optional": true, - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dev": true, - "optional": true, - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - } - } - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21031,22 +20850,6 @@ "proto3-json-serializer": "^0.1.8", "protobufjs": "6.11.3", "retry-request": "^4.0.0" - }, - "dependencies": { - "@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", - "dev": true, - "optional": true, - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - } - } } }, "google-p12-pem": { @@ -21055,13 +20858,6 @@ "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", "requires": { "node-forge": "^1.0.0" - }, - "dependencies": { - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" - } } }, "googleapis": { @@ -21159,12 +20955,6 @@ "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } - }, - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true } } }, @@ -21268,12 +21058,6 @@ "safe-buffer": "^5.0.1" } }, - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true - }, "uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -21482,9 +21266,9 @@ } }, "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", "dev": true }, "http-proxy-agent": { @@ -23296,10 +23080,9 @@ } }, "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, "node-gyp": { "version": "9.1.0", @@ -24753,19 +24536,20 @@ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" }, "retry-request": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", - "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", "dev": true, "optional": true, "requires": { - "debug": "^4.1.1" + "debug": "^4.1.1", + "extend": "^3.0.2" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "optional": true, "requires": { @@ -25120,13 +24904,6 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, - "snakeize": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", - "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", - "dev": true, - "optional": true - }, "socks": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", @@ -25324,12 +25101,6 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -25404,7 +25175,7 @@ "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "dev": true, "optional": true }, @@ -25861,17 +25632,55 @@ } }, "teeny-request": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", - "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", "dev": true, "optional": true, "requires": { - "http-proxy-agent": "^4.0.0", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.1", "stream-events": "^1.0.5", "uuid": "^8.0.0" + }, + "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "optional": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } } }, "term-size": { @@ -25914,6 +25723,12 @@ "minimatch": "^3.0.4" } }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", + "dev": true + }, "text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -26524,12 +26339,12 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", + "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } @@ -26701,7 +26516,7 @@ "xmlhttprequest": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==", "dev": true }, "xregexp": { diff --git a/package.json b/package.json index 274da8d69431..c7d22aaeb873 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "@types/cross-spawn": "^6.0.1", "@types/express": "^4.17.0", "@types/express-serve-static-core": "^4.17.8", + "@types/firebase": "^3.2.1", "@types/fs-extra": "^9.0.13", "@types/glob": "^7.1.1", "@types/inquirer": "^8.1.3", @@ -215,8 +216,8 @@ "eslint-plugin-jsdoc": "^39.2.9", "eslint-plugin-prettier": "^4.0.0", "firebase": "^7.24.0", - "firebase-admin": "^9.4.2", - "firebase-functions": "^3.23.0", + "firebase-admin": "^10.0.0", + "firebase-functions": "^4.1.0", "google-discovery-to-swagger": "^2.1.0", "googleapis": "^105.0.0", "mocha": "^9.1.3", diff --git a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts index 2c41ff599e2b..7c473bf4efcc 100644 --- a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts +++ b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts @@ -8,7 +8,7 @@ import { ChildProcess } from "child_process"; import * as express from "express"; import { Change } from "firebase-functions"; -import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; +import { DocumentSnapshot } from "firebase-functions/v1/firestore"; import { FunctionRuntimeBundles, TIMEOUT_LONG, MODULE_ROOT } from "./fixtures"; import { diff --git a/scripts/functions-deploy-tests/functions/fns.js b/scripts/functions-deploy-tests/functions/fns.js index cdbad3c3c25f..d5c746331ca1 100644 --- a/scripts/functions-deploy-tests/functions/fns.js +++ b/scripts/functions-deploy-tests/functions/fns.js @@ -42,3 +42,6 @@ export const v2tq = v2.tasks.onTaskDispatched(v2TqOpts, () => {}); // export const v2custom = v2.eventarc.onCustomEventPublished("custom.event", () => {}); export const v2secret = v2.pubsub.onMessagePublished({ topic: "foo", secrets: ["TOP"] }, () => {}); export const v2scheduled = v2.scheduler.onSchedule(v2ScheduleOpts, () => {}); +export const v2testlab = v2.testLab.onTestMatrixCompleted(() => {}); +export const v2rc = v2.remoteConfig.onConfigUpdated(() => {}); +export const v2perf = v2.alerts.performance.onThresholdAlertPublished(() => {}); diff --git a/scripts/functions-deploy-tests/functions/package.json b/scripts/functions-deploy-tests/functions/package.json index d000995efab4..affb998fa58c 100644 --- a/scripts/functions-deploy-tests/functions/package.json +++ b/scripts/functions-deploy-tests/functions/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "firebase-admin": "^11.0.0", - "firebase-functions": "^3.23.0" + "firebase-functions": "^4.1.0" }, "engines": { "node": "16" diff --git a/scripts/functions-deploy-tests/tests.ts b/scripts/functions-deploy-tests/tests.ts index 2f18b924aa99..cd36095d5758 100644 --- a/scripts/functions-deploy-tests/tests.ts +++ b/scripts/functions-deploy-tests/tests.ts @@ -15,7 +15,7 @@ import { requireAuth } from "../../src/requireAuth"; const FIREBASE_PROJECT = process.env.GCLOUD_PROJECT || ""; const FIREBASE_DEBUG = process.env.FIREBASE_DEBUG || ""; const FUNCTIONS_DIR = path.join(__dirname, "functions"); -const FNS_COUNT = 17; +const FNS_COUNT = 20; function genRandomId(n = 10): string { const charset = "abcdefghijklmnopqrstuvwxyz"; @@ -143,6 +143,7 @@ describe("firebase deploy", function (this) { memory: "128MB", maxInstances: 42, timeoutSeconds: 42, + preserveExternalChanges: true, }, v2Opts: { memory: "128MiB", @@ -150,6 +151,7 @@ describe("firebase deploy", function (this) { timeoutSeconds: 42, cpu: 2, concurrency: 42, + preserveExternalChanges: true, }, v1TqOpts: { retryConfig: { @@ -260,10 +262,10 @@ describe("firebase deploy", function (this) { } }); - it("skips duplicate deploys functions with runtime options", async () => { + it("skips duplicate deploys functions with runtime options when preserveExternalChanges is set", async () => { const opts: Opts = { - v1Opts: {}, - v2Opts: {}, + v1Opts: { preserveExternalChanges: true }, + v2Opts: { preserveExternalChanges: true }, v1TqOpts: {}, v2TqOpts: {}, v1IdpOpts: {}, @@ -279,10 +281,10 @@ describe("firebase deploy", function (this) { expect(result2.stdout, "deploy result").to.match(/Skipped \(No changes detected\)/); }); - it("leaves existing options when unspecified", async () => { + it("leaves existing options when unspecified and preserveExternalChanges is set", async () => { const opts: Opts = { - v1Opts: {}, - v2Opts: {}, + v1Opts: { preserveExternalChanges: true }, + v2Opts: { preserveExternalChanges: true }, v1TqOpts: {}, v2TqOpts: {}, v1IdpOpts: {}, @@ -352,65 +354,16 @@ describe("firebase deploy", function (this) { // BUGBUG: Setting options to null SHOULD restore their values to default, but this isn't correctly implemented in // the CLI. - it.skip("restores default values if options are explicitly cleared out", async () => { + it.skip("restores default values when unspecified and preserveExternalChanges is not set", async () => { const opts: Opts = { - v1Opts: { - memory: undefined, - maxInstances: undefined, - timeoutSeconds: undefined, - }, - v2Opts: { - memory: undefined, - maxInstances: undefined, - timeoutSeconds: undefined, - cpu: undefined, - concurrency: undefined, - }, - v1TqOpts: { - retryConfig: { - maxAttempts: undefined, - maxRetrySeconds: undefined, - maxBackoffSeconds: undefined, - maxDoublings: undefined, - minBackoffSeconds: undefined, - }, - rateLimits: { - maxDispatchesPerSecond: undefined, - maxConcurrentDispatches: undefined, - }, - }, - v2TqOpts: { - retryConfig: { - maxAttempts: undefined, - maxRetrySeconds: undefined, - maxBackoffSeconds: undefined, - maxDoublings: undefined, - minBackoffSeconds: undefined, - }, - rateLimits: { - maxDispatchesPerSecond: undefined, - maxConcurrentDispatches: undefined, - }, - }, - v1IdpOpts: { - blockingOptions: {}, - }, + v1Opts: {}, + v2Opts: {}, + v1TqOpts: {}, + v2TqOpts: {}, + v1IdpOpts: { blockingOptions: {} }, v2IdpOpts: {}, - v1ScheduleOpts: { - retryCount: undefined, - maxDoublings: undefined, - maxBackoffDuration: undefined, - maxRetryDuration: undefined, - minBackoffDuration: undefined, - }, - v2ScheduleOpts: { - schedule: "every 30 minutes", - retryCount: undefined, - maxDoublings: undefined, - maxBackoffSeconds: undefined, - maxRetrySeconds: undefined, - minBackoffSeconds: undefined, - }, + v1ScheduleOpts: {}, + v2ScheduleOpts: { schedule: "every 30 minutes" }, }; const result = await setOptsAndDeploy(opts); From e1872a4298cc1cbfe60551027dc645f4070ba477 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Fri, 2 Dec 2022 12:13:59 -0500 Subject: [PATCH 102/115] Add region warning for emulated database functions (#5143) * adding error message * add changelog entry --- CHANGELOG.md | 1 + src/emulator/functionsEmulator.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d018fea5c2b..543bbc97096c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ - Fix bug where disabling background triggers did nothing. (#5221) - Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) - Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) +- Add region warning for emulated database functions (#5143) - Default to --no-localhost when calling login from Google Cloud Workstations - Support the x-goog-api-key header in auth emulator. (#5249) - Fix bug in deploying web frameworks when a predeploy hook was configured in firebase.json (#5199) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index fb47df888fd5..296e0015d082 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -589,6 +589,7 @@ export class FunctionsEmulator implements EmulatorInstance { case Constants.SERVICE_REALTIME_DATABASE: added = await this.addRealtimeDatabaseTrigger( this.args.projectId, + definition.id, key, definition.eventTrigger, signature, @@ -745,6 +746,7 @@ export class FunctionsEmulator implements EmulatorInstance { private getV2DatabaseApiAttributes( projectId: string, + id: string, key: string, eventTrigger: EventTrigger, region: string @@ -760,6 +762,15 @@ export class FunctionsEmulator implements EmulatorInstance { throw new FirebaseError("A database reference must be supplied."); } + // TODO(colerogers): yank/change if RTDB emulator ever supports multiple regions + if (region !== "us-central1") { + this.logger.logLabeled( + "WARN", + `functions[${id}]`, + `function region is defined outside the database region, will not trigger.` + ); + } + // The 'namespacePattern' determines that we are using the v2 interface const bundle = JSON.stringify({ name: `projects/${projectId}/locations/${region}/triggers/${key}`, @@ -777,6 +788,7 @@ export class FunctionsEmulator implements EmulatorInstance { async addRealtimeDatabaseTrigger( projectId: string, + id: string, key: string, eventTrigger: EventTrigger, signature: SignatureType, @@ -788,7 +800,7 @@ export class FunctionsEmulator implements EmulatorInstance { const { bundle, apiPath, instance } = signature === "cloudevent" - ? this.getV2DatabaseApiAttributes(projectId, key, eventTrigger, region) + ? this.getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region) : this.getV1DatabaseApiAttributes(projectId, key, eventTrigger); logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); From 1ebb9f84e35f016bf55497c8832d156985477310 Mon Sep 17 00:00:00 2001 From: Yuangwang Date: Mon, 5 Dec 2022 10:37:47 -0500 Subject: [PATCH 103/115] Fix storage integration tests (#5287) * turn back on integration tests * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test * test --- .github/workflows/node-test.yml | 3 +- npm-shrinkwrap.json | 254 +++++++++++++++++++++++--------- package.json | 2 +- 3 files changed, 186 insertions(+), 73 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index f276fe2381f7..5e72bc66f0fc 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -89,8 +89,7 @@ jobs: # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. - npm run test:import-export - npm run test:storage-deploy - # Temporarily disable broken storage emulator integration test. - # - npm run test:storage-emulator-integration + - npm run test:storage-emulator-integration - npm run test:triggers-end-to-end - npm run test:triggers-end-to-end:inspect steps: diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cac6a02c3f37..503e5045da13 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -147,7 +147,7 @@ "openapi-merge": "^1.0.23", "prettier": "^2.5.1", "proxy": "^1.0.2", - "puppeteer": "^9.0.0", + "puppeteer": "^19.0.0", "sinon": "^9.2.3", "sinon-chai": "^3.6.0", "source-map-support": "^0.5.9", @@ -2755,6 +2755,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "node_modules/@types/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.3.tgz", @@ -3022,9 +3028,9 @@ } }, "node_modules/@types/yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", "dev": true, "optional": true, "dependencies": { @@ -5029,6 +5035,22 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -5089,6 +5111,15 @@ "node": ">=4.8" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5320,9 +5351,9 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "node_modules/devtools-protocol": { - "version": "0.0.869402", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz", - "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==", + "version": "0.0.1056733", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1056733.tgz", + "integrity": "sha512-CmTu6SQx2g3TbZzDCAV58+LTxVdKplS7xip0g5oDXpZ+isr0rv5dDP8ToyVRywzPHkCCPKgKgScEcwz4uPWDIA==", "dev": true }, "node_modules/dezalgo": { @@ -6435,9 +6466,9 @@ } }, "node_modules/extract-zip/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -6564,7 +6595,7 @@ "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "dependencies": { "pend": "~1.2.0" @@ -8052,9 +8083,9 @@ "dev": true }, "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dependencies": { "agent-base": "6", "debug": "4" @@ -11282,7 +11313,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "node_modules/performance-now": { @@ -11729,33 +11760,48 @@ } }, "node_modules/puppeteer": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.0.0.tgz", - "integrity": "sha512-Avu8SKWQRC1JKNMgfpH7d4KzzHOL/A65jRYrjNU46hxnOYGwqe4zZp/JW8qulaH0Pnbm5qyO3EbSKvqBUlfvkg==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.3.0.tgz", + "integrity": "sha512-WJbi/ULaeuFOz7cfMgJlJCBAZiyqIFeQ6os4h5ex3PVTt2qosXgwI9eruFZqFAwJRv8x5pOuMhWR0aSRgyDqEg==", "dev": true, "hasInstallScript": true, "dependencies": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.869402", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" + "cosmiconfig": "7.0.1", + "devtools-protocol": "0.0.1056733", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.3.0" }, "engines": { - "node": ">=10.18.1" + "node": ">=14.1.0" } }, - "node_modules/puppeteer/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "node_modules/puppeteer-core": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.3.0.tgz", + "integrity": "sha512-P8VAAOBnBJo/7DKJnj1b0K9kZBF2D8lkdL94CjJ+DZKCp182LQqYemPI9omUSZkh4bgykzXjZhaVR1qtddTTQg==", + "dev": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1056733", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.10.0" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -11769,12 +11815,33 @@ } } }, - "node_modules/puppeteer/node_modules/ms": { + "node_modules/puppeteer-core/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz", + "integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -14965,7 +15032,7 @@ "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", @@ -17197,6 +17264,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "@types/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.3.tgz", @@ -17463,9 +17536,9 @@ } }, "@types/yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", "dev": true, "optional": true, "requires": { @@ -18948,6 +19021,19 @@ "vary": "^1" } }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, "crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -18991,6 +19077,15 @@ } } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -19172,9 +19267,9 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "devtools-protocol": { - "version": "0.0.869402", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz", - "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==", + "version": "0.0.1056733", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1056733.tgz", + "integrity": "sha512-CmTu6SQx2g3TbZzDCAV58+LTxVdKplS7xip0g5oDXpZ+isr0rv5dDP8ToyVRywzPHkCCPKgKgScEcwz4uPWDIA==", "dev": true }, "dezalgo": { @@ -20017,9 +20112,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -20127,7 +20222,7 @@ "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "requires": { "pend": "~1.2.0" @@ -21313,9 +21408,9 @@ "dev": true }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "requires": { "agent-base": "6", "debug": "4" @@ -23807,7 +23902,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "performance-now": { @@ -24162,29 +24257,41 @@ } }, "puppeteer": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.0.0.tgz", - "integrity": "sha512-Avu8SKWQRC1JKNMgfpH7d4KzzHOL/A65jRYrjNU46hxnOYGwqe4zZp/JW8qulaH0Pnbm5qyO3EbSKvqBUlfvkg==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.3.0.tgz", + "integrity": "sha512-WJbi/ULaeuFOz7cfMgJlJCBAZiyqIFeQ6os4h5ex3PVTt2qosXgwI9eruFZqFAwJRv8x5pOuMhWR0aSRgyDqEg==", "dev": true, "requires": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.869402", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" + "cosmiconfig": "7.0.1", + "devtools-protocol": "0.0.1056733", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.3.0" + } + }, + "puppeteer-core": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.3.0.tgz", + "integrity": "sha512-P8VAAOBnBJo/7DKJnj1b0K9kZBF2D8lkdL94CjJ+DZKCp182LQqYemPI9omUSZkh4bgykzXjZhaVR1qtddTTQg==", + "dev": true, + "requires": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1056733", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.10.0" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -24195,6 +24302,13 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "ws": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz", + "integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==", + "dev": true, + "requires": {} } } }, @@ -26601,7 +26715,7 @@ "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "requires": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index c7d22aaeb873..c13a1334c231 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "openapi-merge": "^1.0.23", "prettier": "^2.5.1", "proxy": "^1.0.2", - "puppeteer": "^9.0.0", + "puppeteer": "^19.0.0", "sinon": "^9.2.3", "sinon-chai": "^3.6.0", "source-map-support": "^0.5.9", From ab11ced04a75353e17c1138177f8295e33082752 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Mon, 5 Dec 2022 16:10:09 -0500 Subject: [PATCH 104/115] Fix eventarc emulator (#5304) Convert events from proto to JSON format before delegating to the trigger function. The eventarc admin SDK sends events in proto format: https://github.com/firebase/firebase-admin-node/blob/9fc8e84b8f496f12141e611bf3075e94f633c117/src/eventarc/eventarc-client-internal.ts#L96 --- src/emulator/eventarcEmulator.ts | 3 +- src/emulator/eventarcEmulatorUtils.ts | 62 +++++++++ .../emulators/eventarcEmulatorUtils.spec.ts | 126 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/emulator/eventarcEmulatorUtils.ts create mode 100644 src/test/emulators/eventarcEmulatorUtils.spec.ts diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index e08f9c358c54..b1f487a89143 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -8,6 +8,7 @@ import { EventTrigger } from "./functionsEmulatorShared"; import { CloudEvent } from "./events/types"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; +import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; interface CustomEventTrigger { projectId: string; @@ -123,7 +124,7 @@ export class EventarcEmulator implements EmulatorInstance { .request, NodeJS.ReadableStream>({ method: "POST", path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, - body: JSON.stringify(event), + body: JSON.stringify(cloudEventFromProtoToJson(event)), responseType: "stream", resolveOnHTTPError: true, }) diff --git a/src/emulator/eventarcEmulatorUtils.ts b/src/emulator/eventarcEmulatorUtils.ts new file mode 100644 index 000000000000..3cc06f5a539c --- /dev/null +++ b/src/emulator/eventarcEmulatorUtils.ts @@ -0,0 +1,62 @@ +import { CloudEvent } from "./events/types"; +import { FirebaseError } from "../error"; + +const BUILT_IN_ATTRS: string[] = ["time", "datacontenttype", "subject"]; + +export function cloudEventFromProtoToJson(ce: any): CloudEvent { + if (ce["id"] === undefined) { + throw new FirebaseError("CloudEvent 'id' is required."); + } + if (ce["type"] === undefined) { + throw new FirebaseError("CloudEvent 'type' is required."); + } + if (ce["specVersion"] === undefined) { + throw new FirebaseError("CloudEvent 'specVersion' is required."); + } + if (ce["source"] === undefined) { + throw new FirebaseError("CloudEvent 'source' is required."); + } + const out: CloudEvent = { + id: ce["id"], + type: ce["type"], + specversion: ce["specVersion"], + source: ce["source"], + subject: getOptionalAttribute(ce, "subject", "ceString"), + time: getRequiredAttribute(ce, "time", "ceTimestamp"), + data: getData(ce), + datacontenttype: getRequiredAttribute(ce, "datacontenttype", "ceString"), + }; + for (const attr in ce["attributes"]) { + if (BUILT_IN_ATTRS.includes(attr)) { + continue; + } + out[attr] = getRequiredAttribute(ce, attr, "ceString"); + } + return out; +} + +function getOptionalAttribute(ce: any, attr: string, type: string): string | undefined { + return ce["attributes"][attr][type]; +} + +function getRequiredAttribute(ce: any, attr: string, type: string): string { + const val = ce["attributes"][attr][type]; + if (val === undefined) { + throw new FirebaseError("CloudEvent must contain " + attr + " attribute"); + } + return val; +} + +function getData(ce: any): any { + const contentType = getRequiredAttribute(ce, "datacontenttype", "ceString"); + switch (contentType) { + case "application/json": + return JSON.parse(ce["textData"]); + case "text/plain": + return ce["textData"]; + case undefined: + return undefined; + default: + throw new FirebaseError("Unsupported content type: " + contentType); + } +} diff --git a/src/test/emulators/eventarcEmulatorUtils.spec.ts b/src/test/emulators/eventarcEmulatorUtils.spec.ts new file mode 100644 index 000000000000..d7015a118297 --- /dev/null +++ b/src/test/emulators/eventarcEmulatorUtils.spec.ts @@ -0,0 +1,126 @@ +import { expect } from "chai"; + +import { cloudEventFromProtoToJson } from "../../emulator/eventarcEmulatorUtils"; + +describe("eventarcEmulatorUtils", () => { + describe("cloudEventFromProtoToJson", () => { + it("converts cloud event from proto format", () => { + expect( + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }) + ).to.deep.eq({ + type: "some.custom.event", + specversion: "1.0", + subject: "context", + datacontenttype: "application/json", + id: "user-provided-id", + data: { + hello: "world", + }, + source: "/my/functions", + time: "2022-03-16T20:20:42.212Z", + customattr: "custom value", + }); + }); + + it("throws invalid argument when source not set", () => { + expect(() => + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }) + ).throws("CloudEvent 'source' is required."); + }); + + it("populates converts object data to JSON and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("application/json"); + expect(got.data).to.deep.eq({ hello: "world" }); + }); + + it("populates string data and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "text/plain", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: "hello world", + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("text/plain"); + expect(got.data).to.eq("hello world"); + }); + }); +}); From 57129783400a89a757c88e49012acbde2cb49fa7 Mon Sep 17 00:00:00 2001 From: kazuwombat Date: Tue, 6 Dec 2022 08:02:49 +0900 Subject: [PATCH 105/115] Add return type of findAvailableLogFile (#5189) Co-authored-by: Bryan Kendall --- src/bin/firebase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/firebase.ts b/src/bin/firebase.ts index f46fbe7c731a..155b6d80b430 100755 --- a/src/bin/firebase.ts +++ b/src/bin/firebase.ts @@ -38,7 +38,7 @@ import * as winston from "winston"; let args = process.argv.slice(2); let cmd: Command; -function findAvailableLogFile() { +function findAvailableLogFile(): string { const candidates = ["firebase-debug.log"]; for (let i = 1; i < 10; i++) { candidates.push(`firebase-debug.${i}.log`); From 4c9214904bb3eb79edb21380e1dc11a6259ab1c8 Mon Sep 17 00:00:00 2001 From: joehan Date: Tue, 6 Dec 2022 10:27:56 -0800 Subject: [PATCH 106/115] Improve handling of bad versions during ext:install (#5305) * Improve handling of bad versions during ext:install * Add changelog * Better variable names --- CHANGELOG.md | 1 + src/deploy/extensions/planner.ts | 1 + src/extensions/extensionsHelper.ts | 16 ++++++++++------ src/test/deploy/extensions/planner.spec.ts | 17 ++++++++--------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543bbc97096c..80f64a9fe86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,4 @@ - Support the x-goog-api-key header in auth emulator. (#5249) - Fix bug in deploying web frameworks when a predeploy hook was configured in firebase.json (#5199) - Fix bug where function deployments using --only filter sometimes failed deployments. (#5280) +- Fix bug where `ext:install` would sometimes fail if no version was specified. (#5305) diff --git a/src/deploy/extensions/planner.ts b/src/deploy/extensions/planner.ts index c48d04224e7c..4aa473370f84 100644 --- a/src/deploy/extensions/planner.ts +++ b/src/deploy/extensions/planner.ts @@ -215,6 +215,7 @@ export async function resolveVersion(ref: refs.Ref): Promise { } if (!ref.version || ref.version === "latest") { return versions + .filter((ev) => ev.spec.version !== undefined) .map((ev) => ev.spec.version) .sort(semver.compare) .pop()!; diff --git a/src/extensions/extensionsHelper.ts b/src/extensions/extensionsHelper.ts index 1fa1e1dad7e3..11b15b3e69dd 100644 --- a/src/extensions/extensionsHelper.ts +++ b/src/extensions/extensionsHelper.ts @@ -833,14 +833,18 @@ export async function diagnoseAndFixProject(options: any): Promise { * 1. Infer firebase publisher if not provided * 2. Infer "latest" as the version if not provided */ -export async function canonicalizeRefInput(extensionName: string): Promise { - // Infer firebase if publisher ID not provided. - if (extensionName.split("/").length < 2) { - const [extensionID, version] = extensionName.split("@"); - extensionName = `firebase/${extensionID}@${version || "latest"}`; +export async function canonicalizeRefInput(refInput: string): Promise { + let inferredRef = refInput; + // Infer 'firebase' if publisher ID not provided. + if (refInput.split("/").length < 2) { + inferredRef = `firebase/${inferredRef}`; + } + // Infer 'latest' if no version provided. + if (refInput.split("@").length < 2) { + inferredRef = `${inferredRef}@latest`; } // Get the correct version for a given extension reference from the Registry API. - const ref = refs.parse(extensionName); + const ref = refs.parse(inferredRef); ref.version = await resolveVersion(ref); return refs.toExtensionVersionRef(ref); } diff --git a/src/test/deploy/extensions/planner.spec.ts b/src/test/deploy/extensions/planner.spec.ts index 2b1519454226..1d4aef349178 100644 --- a/src/test/deploy/extensions/planner.spec.ts +++ b/src/test/deploy/extensions/planner.spec.ts @@ -3,9 +3,9 @@ import * as sinon from "sinon"; import * as planner from "../../../deploy/extensions/planner"; import * as extensionsApi from "../../../extensions/extensionsApi"; -import { ExtensionInstance, ExtensionVersion } from "../../../extensions/types"; +import { ExtensionInstance } from "../../../extensions/types"; -function extensionVersion(version: string): ExtensionVersion { +function extensionVersion(version?: string): any { return { name: `publishers/test/extensions/test/versions/${version}`, ref: `test/test@${version}`, @@ -26,13 +26,12 @@ describe("Extensions Deployment Planner", () => { let listExtensionVersionsStub: sinon.SinonStub; before(() => { - listExtensionVersionsStub = sinon - .stub(extensionsApi, "listExtensionVersions") - .resolves([ - extensionVersion("0.1.0"), - extensionVersion("0.1.1"), - extensionVersion("0.2.0"), - ]); + listExtensionVersionsStub = sinon.stub(extensionsApi, "listExtensionVersions").resolves([ + extensionVersion("0.1.0"), + extensionVersion("0.1.1"), + extensionVersion("0.2.0"), + extensionVersion(), // Explicitly test that this doesn't break on bad data + ]); }); after(() => { From ff9497ee14a0631e22ef54a48e8908c89bed6a58 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Tue, 6 Dec 2022 15:46:27 -0500 Subject: [PATCH 107/115] Added support for CF3v2 functions in the Extensions emulator (#5306) --- src/emulator/extensions/validation.ts | 2 + src/extensions/billingMigrationHelper.ts | 3 +- src/extensions/displayExtensionInfo.ts | 7 +- src/extensions/emulator/specHelper.ts | 6 +- src/extensions/emulator/triggerHelper.ts | 101 ++++++++++++++---- src/extensions/extensionsHelper.ts | 3 - src/extensions/types.ts | 34 +++++- src/extensions/utils.ts | 23 +++- .../emulators/extensions/validation.spec.ts | 7 ++ .../extensions/emulator/triggerHelper.spec.ts | 89 +++++++++++++++ 10 files changed, 242 insertions(+), 33 deletions(-) diff --git a/src/emulator/extensions/validation.ts b/src/emulator/extensions/validation.ts index 158ba0232e2d..0a7d84d35280 100644 --- a/src/emulator/extensions/validation.ts +++ b/src/emulator/extensions/validation.ts @@ -80,6 +80,8 @@ export function checkForUnemulatedTriggerTypes( return !shouldStart(options, Emulators.AUTH); case Constants.SERVICE_STORAGE: return !shouldStart(options, Emulators.STORAGE); + case Constants.SERVICE_EVENTARC: + return !shouldStart(options, Emulators.EVENTARC); default: return true; } diff --git a/src/extensions/billingMigrationHelper.ts b/src/extensions/billingMigrationHelper.ts index 16c7d1a0ef40..6152861e1b24 100644 --- a/src/extensions/billingMigrationHelper.ts +++ b/src/extensions/billingMigrationHelper.ts @@ -7,6 +7,7 @@ import { ExtensionSpec } from "./types"; import { logPrefix } from "./extensionsHelper"; import { promptOnce } from "../prompt"; import * as utils from "../utils"; +import { getResourceRuntime } from "./utils"; marked.setOptions({ renderer: new TerminalRenderer(), @@ -41,7 +42,7 @@ function hasRuntime(spec: ExtensionSpec, runtime: string): boolean { const specVersion = spec.specVersion || defaultSpecVersion; const defaultRuntime = defaultRuntimes[specVersion]; const resources = spec.resources || []; - return resources.some((r) => runtime === (r.properties?.runtime || defaultRuntime)); + return resources.some((r) => runtime === (getResourceRuntime(r) || defaultRuntime)); } /** diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index f996762cca75..381d03b548b0 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -7,7 +7,7 @@ import * as utils from "../utils"; import { logPrefix } from "./extensionsHelper"; import { logger } from "../logger"; import { FirebaseError } from "../error"; -import { Api, ExtensionSpec, Role, Resource } from "./types"; +import { Api, ExtensionSpec, Role, Resource, FUNCTIONS_RESOURCE_TYPE } from "./types"; import * as iam from "../gcp/iam"; import { SECRET_ROLE, usesSecrets } from "./secretsUtils"; @@ -113,7 +113,10 @@ function displayApis(apis: Api[]): string { } function usesTasks(spec: ExtensionSpec): boolean { - return spec.resources.some((r: Resource) => r.properties?.taskQueueTrigger !== undefined); + return spec.resources.some( + (r: Resource) => + r.type === FUNCTIONS_RESOURCE_TYPE && r.properties?.taskQueueTrigger !== undefined + ); } function impliedRoles(spec: ExtensionSpec): Role[] { diff --git a/src/extensions/emulator/specHelper.ts b/src/extensions/emulator/specHelper.ts index f7eda1891c34..139a1c2c60e2 100644 --- a/src/extensions/emulator/specHelper.ts +++ b/src/extensions/emulator/specHelper.ts @@ -5,12 +5,14 @@ import * as fs from "fs-extra"; import { ExtensionSpec, Resource } from "../types"; import { FirebaseError } from "../../error"; import { substituteParams } from "../extensionsHelper"; +import { getResourceRuntime } from "../utils"; import { parseRuntimeVersion } from "../../emulator/functionsEmulatorUtils"; const SPEC_FILE = "extension.yaml"; const POSTINSTALL_FILE = "POSTINSTALL.md"; const validFunctionTypes = [ "firebaseextensions.v1beta.function", + "firebaseextensions.v1beta.v2function", "firebaseextensions.v1beta.scheduledFunction", ]; @@ -95,8 +97,8 @@ export function getFunctionProperties(resources: Resource[]) { export function getNodeVersion(resources: Resource[]): number { const invalidRuntimes: string[] = []; const versions = resources.map((r: Resource) => { - if (r.properties?.runtime) { - const runtimeName = r.properties?.runtime as string; + if (getResourceRuntime(r)) { + const runtimeName = getResourceRuntime(r) as string; const runtime = parseRuntimeVersion(runtimeName); if (!runtime) { invalidRuntimes.push(runtimeName); diff --git a/src/extensions/emulator/triggerHelper.ts b/src/extensions/emulator/triggerHelper.ts index 914ef71ac00b..192d117208f2 100644 --- a/src/extensions/emulator/triggerHelper.ts +++ b/src/extensions/emulator/triggerHelper.ts @@ -4,8 +4,14 @@ import { } from "../../emulator/functionsEmulatorShared"; import { EmulatorLogger } from "../../emulator/emulatorLogger"; import { Emulators } from "../../emulator/types"; -import { Resource } from "../../extensions/types"; +import { + Resource, + FUNCTIONS_RESOURCE_TYPE, + FUNCTIONS_V2_RESOURCE_TYPE, +} from "../../extensions/types"; +import * as backend from "../../deploy/functions/backend"; import * as proto from "../../gcp/proto"; +import { FirebaseError } from "../../error"; /** * Convert a Resource into a ParsedTriggerDefinition @@ -13,29 +19,78 @@ import * as proto from "../../gcp/proto"; export function functionResourceToEmulatedTriggerDefintion( resource: Resource ): ParsedTriggerDefinition { - const etd: ParsedTriggerDefinition = { - name: resource.name, - entryPoint: resource.name, - platform: "gcfv1", - }; - const properties = resource.properties || {}; - proto.convertIfPresent(etd, properties, "timeoutSeconds", "timeout", proto.secondsFromDuration); - proto.convertIfPresent(etd, properties, "regions", "location", (str: string) => [str]); - proto.copyIfPresent(etd, properties, "availableMemoryMb"); - if (properties.httpsTrigger) { - etd.httpsTrigger = properties.httpsTrigger; + const resourceType = resource.type; + if (resource.type === FUNCTIONS_RESOURCE_TYPE) { + const etd: ParsedTriggerDefinition = { + name: resource.name, + entryPoint: resource.name, + platform: "gcfv1", + }; + const properties = resource.properties || {}; + proto.convertIfPresent(etd, properties, "timeoutSeconds", "timeout", proto.secondsFromDuration); + proto.convertIfPresent(etd, properties, "regions", "location", (str: string) => [str]); + proto.copyIfPresent(etd, properties, "availableMemoryMb"); + if (properties.httpsTrigger) { + etd.httpsTrigger = properties.httpsTrigger; + } + if (properties.eventTrigger) { + etd.eventTrigger = { + eventType: properties.eventTrigger.eventType, + resource: properties.eventTrigger.resource, + service: getServiceFromEventType(properties.eventTrigger.eventType), + }; + } else { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( + "WARN", + `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.` + ); + } + return etd; } - if (properties.eventTrigger) { - etd.eventTrigger = { - eventType: properties.eventTrigger.eventType, - resource: properties.eventTrigger.resource, - service: getServiceFromEventType(properties.eventTrigger.eventType), + if (resource.type === FUNCTIONS_V2_RESOURCE_TYPE) { + const etd: ParsedTriggerDefinition = { + name: resource.name, + entryPoint: resource.name, + platform: "gcfv2", }; - } else { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( - "WARN", - `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.` - ); + const properties = resource.properties || {}; + proto.convertIfPresent(etd, properties, "regions", "location", (str: string) => [str]); + if (properties.serviceConfig) { + proto.copyIfPresent(etd, properties.serviceConfig, "timeoutSeconds"); + proto.convertIfPresent( + etd, + properties.serviceConfig, + "availableMemoryMb", + "availableMemory", + (mem: string) => parseInt(mem) as backend.MemoryOptions + ); + } + if (properties.eventTrigger) { + etd.eventTrigger = { + eventType: properties.eventTrigger.eventType, + service: getServiceFromEventType(properties.eventTrigger.eventType), + }; + proto.copyIfPresent(etd.eventTrigger, properties.eventTrigger, "channel"); + if (properties.eventTrigger.eventFilters) { + const eventFilters: Record = {}; + const eventFilterPathPatterns: Record = {}; + for (const filter of properties.eventTrigger.eventFilters) { + if (filter.operator === undefined) { + eventFilters[filter.attribute] = filter.value; + } else if (filter.operator === "match-path-pattern") { + eventFilterPathPatterns[filter.attribute] = filter.value; + } + } + etd.eventTrigger.eventFilters = eventFilters; + etd.eventTrigger.eventFilterPathPatterns = eventFilterPathPatterns; + } + } else { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( + "WARN", + `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.` + ); + } + return etd; } - return etd; + throw new FirebaseError("Unexpected resource type " + resourceType); } diff --git a/src/extensions/extensionsHelper.ts b/src/extensions/extensionsHelper.ts index 11b15b3e69dd..51f66fe03591 100644 --- a/src/extensions/extensionsHelper.ts +++ b/src/extensions/extensionsHelper.ts @@ -85,9 +85,6 @@ export const AUTOPOULATED_PARAM_PLACEHOLDERS = { DATABASE_INSTANCE: "project-id-default-rtdb", DATABASE_URL: "https://project-id-default-rtdb.firebaseio.com", }; -export const resourceTypeToNiceName: Record = { - "firebaseextensions.v1beta.function": "Cloud Function", -}; export type ReleaseStage = "stable" | "alpha" | "beta" | "rc"; /** diff --git a/src/extensions/types.ts b/src/extensions/types.ts index 82abaeb524f2..103963658366 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -160,9 +160,41 @@ export interface FunctionResourceProperties { }; } +export const FUNCTIONS_V2_RESOURCE_TYPE = "firebaseextensions.v1beta.v2function"; +export interface FunctionV2ResourceProperties { + type: typeof FUNCTIONS_V2_RESOURCE_TYPE; + properties?: { + location?: string; + sourceDirectory?: string; + buildConfig?: { + runtime?: Runtime; + }; + serviceConfig?: { + availableMemory?: string; + timeoutSeconds?: number; + minInstanceCount?: number; + maxInstanceCount?: number; + }; + eventTrigger?: { + eventType: string; + triggerRegion?: string; + channel?: string; + pubsubTopic?: string; + retryPolicy?: string; + eventFilters?: FunctionV2EventFilter[]; + }; + }; +} + +export interface FunctionV2EventFilter { + attribute: string; + value: string; + operator?: string; +} + // Union of all valid property types so we can have a strongly typed "property" // field depending on the actual value of "type" -type ResourceProperties = FunctionResourceProperties; +type ResourceProperties = FunctionResourceProperties | FunctionV2ResourceProperties; export type Resource = ResourceProperties & { name: string; diff --git a/src/extensions/utils.ts b/src/extensions/utils.ts index a656cefb8ed8..7ffc31526b65 100644 --- a/src/extensions/utils.ts +++ b/src/extensions/utils.ts @@ -1,5 +1,10 @@ import { promptOnce } from "../prompt"; -import { ParamOption } from "./types"; +import { + ParamOption, + Resource, + FUNCTIONS_RESOURCE_TYPE, + FUNCTIONS_V2_RESOURCE_TYPE, +} from "./types"; import { RegistryEntry } from "./resolveSource"; // Modified version of the once function from prompt, to return as a joined string. @@ -67,3 +72,19 @@ export function formatTimestamp(timestamp: string): string { const withoutMs = timestamp.split(".")[0]; return withoutMs.replace("T", " "); } + +/** + * Returns the runtime for the resource. The resource may be v1 or v2 function, + * etc, and this utility will do its best to identify the runtime specified for + * this resource. + */ +export function getResourceRuntime(resource: Resource): string | undefined { + switch (resource.type) { + case FUNCTIONS_RESOURCE_TYPE: + return resource.properties?.runtime; + case FUNCTIONS_V2_RESOURCE_TYPE: + return resource.properties?.buildConfig?.runtime; + default: + return undefined; + } +} diff --git a/src/test/emulators/extensions/validation.spec.ts b/src/test/emulators/extensions/validation.spec.ts index c19e7972c081..8be9e7eebd42 100644 --- a/src/test/emulators/extensions/validation.spec.ts +++ b/src/test/emulators/extensions/validation.spec.ts @@ -144,6 +144,7 @@ describe("ExtensionsEmulator validation", () => { const shouldStartStub = sandbox.stub(controller, "shouldStart"); shouldStartStub.withArgs(sinon.match.any, Emulators.STORAGE).returns(true); shouldStartStub.withArgs(sinon.match.any, Emulators.DATABASE).returns(true); + shouldStartStub.withArgs(sinon.match.any, Emulators.EVENTARC).returns(true); shouldStartStub.withArgs(sinon.match.any, Emulators.FIRESTORE).returns(false); shouldStartStub.withArgs(sinon.match.any, Emulators.AUTH).returns(false); }); @@ -220,6 +221,12 @@ describe("ExtensionsEmulator validation", () => { eventType: "providers/google.firebase.database/eventTypes/ref.write", }, }), + getTestParsedTriggerDefinition({ + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/us-central1/channels/firebase", + }, + }), ], want: [], }, diff --git a/src/test/extensions/emulator/triggerHelper.spec.ts b/src/test/extensions/emulator/triggerHelper.spec.ts index 6b392a834664..b2fb3d90ffaa 100644 --- a/src/test/extensions/emulator/triggerHelper.spec.ts +++ b/src/test/extensions/emulator/triggerHelper.spec.ts @@ -134,5 +134,94 @@ describe("triggerHelper", () => { expect(result).to.eql(expected); }); + + it("should handle v2 custom event triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.v2function", + properties: { + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/bar/channels/baz", + }, + }, + }; + const expected = { + platform: "gcfv2", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + service: "", + channel: "projects/foo/locations/bar/channels/baz", + eventType: "test.custom.event", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle fully packed v2 triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.v2function", + properties: { + buildConfig: { + runtime: "node16", + }, + location: "us-cental1", + serviceConfig: { + availableMemory: "100MB", + minInstanceCount: 1, + maxInstanceCount: 10, + timeoutSeconds: 66, + }, + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/bar/channels/baz", + pubsubTopic: "pubsub.topic", + eventFilters: [ + { + attribute: "basic", + value: "attr", + }, + { + attribute: "mattern", + value: "patch", + operator: "match-path-pattern", + }, + ], + retryPolicy: "RETRY", + triggerRegion: "us-cental1", + }, + }, + }; + const expected = { + platform: "gcfv2", + entryPoint: "test-resource", + name: "test-resource", + availableMemoryMb: 100, + timeoutSeconds: 66, + eventTrigger: { + service: "", + channel: "projects/foo/locations/bar/channels/baz", + eventType: "test.custom.event", + eventFilters: { + basic: "attr", + }, + eventFilterPathPatterns: { + mattern: "patch", + }, + }, + regions: ["us-cental1"], + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); }); }); From 78c70b253fab222235eb9e063c435aefa6996a0f Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 6 Dec 2022 13:32:09 -0800 Subject: [PATCH 108/115] [firebase-release] Removed change log and reset repo after 11.17.0 release --- CHANGELOG.md | 10 +--------- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f64a9fe86d..8b137891791f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1 @@ -- Fix bug where disabling background triggers did nothing. (#5221) -- Fix bug in auth emulator where empty string should throw invalid email instead of missing email. (#3898) -- Fix bug in auth emulator in which createdAt was not set for signInWithIdp new users. (#5203) -- Add region warning for emulated database functions (#5143) -- Default to --no-localhost when calling login from Google Cloud Workstations -- Support the x-goog-api-key header in auth emulator. (#5249) -- Fix bug in deploying web frameworks when a predeploy hook was configured in firebase.json (#5199) -- Fix bug where function deployments using --only filter sometimes failed deployments. (#5280) -- Fix bug where `ext:install` would sometimes fail if no version was specified. (#5305) + diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 503e5045da13..80682016da21 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "11.16.1", + "version": "11.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "11.16.1", + "version": "11.17.0", "license": "MIT", "dependencies": { "@google-cloud/pubsub": "^3.0.1", diff --git a/package.json b/package.json index c13a1334c231..51d0349eb307 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "11.16.1", + "version": "11.17.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From f83fc6e544273826d205cce489fd55f31870deb6 Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Tue, 6 Dec 2022 13:40:10 -0800 Subject: [PATCH 109/115] [firebase-release] Removed change log and reset repo after 11.17.0 release, fixed formatting --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b137891791f..e69de29bb2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ - From 27def0a80fce714b0e64aaca7303bd685ce808f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Fabricio=20Urbina=20Gonz=C3=A1lez?= Date: Tue, 6 Dec 2022 17:15:50 -0600 Subject: [PATCH 110/115] Add support for Firestore TTL (#5267) * Add support for Firestore TTL * Add support for Firestore TTL * Remove unnecessary TTL check * Remove unnecessary TTL check * Add support for Firestore TTL --- CHANGELOG.md | 1 + src/firestore/README.md | 3 + src/firestore/indexes-api.ts | 14 +++ src/firestore/indexes-sort.ts | 8 ++ src/firestore/indexes-spec.ts | 1 + src/firestore/indexes.ts | 44 +++++++-- src/firestore/util.ts | 7 ++ src/firestore/validator.ts | 10 ++ src/test/firestore/indexes.spec.ts | 148 +++++++++++++++++++++++++++++ 9 files changed, 230 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d1..237eed9fca2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add support for Firestore TTL (#5267) diff --git a/src/firestore/README.md b/src/firestore/README.md index 882c08504ab2..bdc86baed8f1 100644 --- a/src/firestore/README.md +++ b/src/firestore/README.md @@ -61,9 +61,12 @@ The schema for one object in the `fieldOverrides` array is as follows. Optional Note that Cloud Firestore document fields can only be indexed in one [mode](https://firebase.google.com/docs/firestore/query-data/index-overview#index_modes), thus a field object cannot contain both the `order` and `arrayConfig` properties. +For more information about time-to-live (TTL) policies review the [official documention](https://cloud.google.com/firestore/docs/ttl). + ```javascript collectionGroup: string // Labeled "Collection ID" in the Firebase console fieldPath: string + ttl?: boolean // Set specified field to have TTL policy and be eligible for deletion indexes: array // Set empty array to disable indexes on this collectionGroup + fieldPath queryScope: string // One of "COLLECTION", "COLLECTION_GROUP" order?: string // One of "ASCENDING", "DESCENDING"; excludes arrayConfig property diff --git a/src/firestore/indexes-api.ts b/src/firestore/indexes-api.ts index 39f3556130e6..d8127f7a75d3 100644 --- a/src/firestore/indexes-api.ts +++ b/src/firestore/indexes-api.ts @@ -30,6 +30,12 @@ export enum State { NEEDS_REPAIR = "NEEDS_REPAIR", } +export enum StateTtl { + CREATING = "CREATING", + ACTIVE = "ACTIVE", + NEEDS_REPAIR = "NEEDS_REPAIR", +} + /** * An Index as it is represented in the Firestore v1beta2 indexes API. */ @@ -49,6 +55,13 @@ export interface IndexField { arrayConfig?: ArrayConfig; } +/** + * TTL policy configuration for a field + */ +export interface TtlConfig { + state: StateTtl; +} + /** * Represents a single field in the database. * @@ -58,6 +71,7 @@ export interface IndexField { export interface Field { name: string; indexConfig: IndexConfig; + ttlConfig?: TtlConfig; } /** diff --git a/src/firestore/indexes-sort.ts b/src/firestore/indexes-sort.ts index be2179bbe3ab..8539eb002cf9 100644 --- a/src/firestore/indexes-sort.ts +++ b/src/firestore/indexes-sort.ts @@ -92,6 +92,7 @@ export function compareApiField(a: API.Field, b: API.Field): number { * Comparisons: * 1) The collection group. * 2) The field path. + * 3) The ttl. * 3) The list of indexes. */ export function compareFieldOverride(a: Spec.FieldOverride, b: Spec.FieldOverride): number { @@ -99,6 +100,13 @@ export function compareFieldOverride(a: Spec.FieldOverride, b: Spec.FieldOverrid return a.collectionGroup.localeCompare(b.collectionGroup); } + // The ttl override can be undefined, we only guarantee that true values will + // come last since those overrides should be executed after disabling TTL per collection. + const compareTtl = Number(!!a.ttl) - Number(!!b.ttl); + if (compareTtl) { + return compareTtl; + } + if (a.fieldPath !== b.fieldPath) { return a.fieldPath.localeCompare(b.fieldPath); } diff --git a/src/firestore/indexes-spec.ts b/src/firestore/indexes-spec.ts index a85ea6ee31b2..6fb10f23f7ed 100644 --- a/src/firestore/indexes-spec.ts +++ b/src/firestore/indexes-spec.ts @@ -21,6 +21,7 @@ export interface Index { export interface FieldOverride { collectionGroup: string; fieldPath: string; + ttl?: boolean; indexes: FieldIndex[]; } diff --git a/src/firestore/indexes.ts b/src/firestore/indexes.ts index c19774e03c98..711bd674c2b0 100644 --- a/src/firestore/indexes.ts +++ b/src/firestore/indexes.ts @@ -140,7 +140,10 @@ export class FirestoreIndexes { } } - for (const field of fieldOverridesToDeploy) { + // Disabling TTL must be executed first in case another field is enabled for + // the same collection in the same deployment. + const sortedFieldOverridesToDeploy = fieldOverridesToDeploy.sort(sort.compareFieldOverride); + for (const field of sortedFieldOverridesToDeploy) { const exists = existingFieldOverrides.some((x) => this.fieldMatchesSpec(x, field)); if (exists) { logger.debug(`Skipping existing field override: ${JSON.stringify(field)}`); @@ -195,7 +198,7 @@ export class FirestoreIndexes { */ async listFieldOverrides(project: string): Promise { const parent = `projects/${project}/databases/(default)/collectionGroups/-`; - const url = `/${parent}/fields?filter=indexConfig.usesAncestorConfig=false`; + const url = `/${parent}/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:*`; const res = await this.apiClient.get<{ fields?: API.Field[] }>(url); const fields = res.body.fields; @@ -236,6 +239,7 @@ export class FirestoreIndexes { return { collectionGroup: parsedName.collectionGroupId, fieldPath: parsedName.fieldPath, + ttl: !!field.ttlConfig, indexes: fieldIndexes.map((index) => { const firstField = index.fields[0]; @@ -339,6 +343,10 @@ export class FirestoreIndexes { validator.assertHas(field, "fieldPath"); validator.assertHas(field, "indexes"); + if (typeof field.ttl !== "undefined") { + validator.assertType("ttl", field.ttl, "boolean"); + } + field.indexes.forEach((index: any) => { validator.assertHasOneOf(index, ["arrayConfig", "order"]); @@ -379,23 +387,33 @@ export class FirestoreIndexes { }; }); - const data = { + let data = { indexConfig: { indexes, }, }; - await this.apiClient.patch(url, data); + if (spec.ttl) { + data = Object.assign(data, { + ttlConfig: {}, + }); + } + + if (typeof spec.ttl !== "undefined") { + await this.apiClient.patch(url, data); + } else { + await this.apiClient.patch(url, data, { queryParams: { updateMask: "indexConfig" } }); + } } /** - * Delete an existing index on the specified project. + * Delete an existing field overrides on the specified project. */ deleteField(field: API.Field): Promise { const url = field.name; const data = {}; - return this.apiClient.patch(`/${url}`, data, { queryParams: { updateMask: "indexConfig" } }); + return this.apiClient.patch(`/${url}`, data); } /** @@ -471,6 +489,16 @@ export class FirestoreIndexes { return false; } + if (typeof spec.ttl !== "undefined" && util.booleanXOR(!!field.ttlConfig, spec.ttl)) { + return false; + } else if (!!field.ttlConfig && typeof spec.ttl === "undefined") { + utils.logLabeledBullet( + "firestore", + `there are TTL field overrides for collection ${spec.collectionGroup} defined in your project that are not present in your ` + + "firestore indexes file. The TTL policy won't be deleted since is not specified as false." + ); + } + const fieldIndexes = field.indexConfig.indexes || []; if (fieldIndexes.length !== spec.indexes.length) { return false; @@ -619,6 +647,10 @@ export class FirestoreIndexes { } else { result += " (no indexes)"; } + const fieldTtl = field.ttlConfig; + if (fieldTtl) { + result += ` TTL(${fieldTtl.state})`; + } return result; } diff --git a/src/firestore/util.ts b/src/firestore/util.ts index 669eb1663072..d084246f4b61 100644 --- a/src/firestore/util.ts +++ b/src/firestore/util.ts @@ -55,3 +55,10 @@ export function parseFieldName(name: string): FieldName { fieldPath: m[3], }; } + +/** + * Performs XOR operator between two boolean values + */ +export function booleanXOR(a: boolean, b: boolean): boolean { + return !!(Number(a) - Number(b)); +} diff --git a/src/firestore/validator.ts b/src/firestore/validator.ts index 7647ab13318b..87a3169c1411 100644 --- a/src/firestore/validator.ts +++ b/src/firestore/validator.ts @@ -39,3 +39,13 @@ export function assertEnum(obj: any, prop: string, valid: any[]): void { throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); } } + +/** + * Throw an error if the value of the property 'prop' differs against type + * guard. + */ +export function assertType(prop: string, propValue: any, type: string): void { + if (typeof propValue !== type) { + throw new FirebaseError(`Property "${prop}" must be of type ${type}`); + } +} diff --git a/src/test/firestore/indexes.spec.ts b/src/test/firestore/indexes.spec.ts index 8866678bfb20..ab8a899ddd3a 100644 --- a/src/test/firestore/indexes.spec.ts +++ b/src/test/firestore/indexes.spec.ts @@ -200,6 +200,85 @@ describe("IndexSpecMatching", () => { expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); }); + it("should identify a positive field spec match with ttl specified as false", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", order: "ASCENDING" }], + }, + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "abc123", + ttl: false, + indexes: [ + { order: "ASCENDING", queryScope: "COLLECTION" }, + { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, + ], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a positive ttl field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }], + }, + ], + }, + ttlConfig: { + state: "ACTIVE", + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: true, + indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a negative ttl field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: true, + indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); + }); + it("should match a field spec with all indexes excluded", () => { const apiField = { name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", @@ -215,6 +294,25 @@ describe("IndexSpecMatching", () => { expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); }); + it("should match a field spec with only ttl", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/ttlField", + ttlConfig: { + state: "ACTIVE", + }, + indexConfig: {}, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "ttlField", + ttl: true, + indexes: [], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + it("should identify a negative field spec match", () => { const apiField = { name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", @@ -244,6 +342,27 @@ describe("IndexSpecMatching", () => { // The second spec contains "DESCENDING" where the first contains "ASCENDING" expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); }); + + it("should identify a negative field spec match with ttl as false", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + ttlConfig: { + state: "ACTIVE", + }, + indexConfig: {}, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: false, + indexes: [], + } as Spec.FieldOverride; + + // The second spec contains "false" for ttl where the first contains "true" + // for ttl + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); + }); }); describe("IndexSorting", () => { @@ -360,6 +479,35 @@ describe("IndexSorting", () => { expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]); }); + it("should sort ttl true to be last in an array of Spec field overrides", () => { + // Sorts first because of collectionGroup + const a: Spec.FieldOverride = { + collectionGroup: "collectionA", + fieldPath: "fieldA", + ttl: false, + indexes: [], + }; + const b: Spec.FieldOverride = { + collectionGroup: "collectionA", + fieldPath: "fieldB", + ttl: true, + indexes: [], + }; + const c: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldA", + ttl: false, + indexes: [], + }; + const d: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldB", + ttl: true, + indexes: [], + }; + expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]); + }); + it("should correctly sort an array of API indexes", () => { // Sorts first because of collectionGroup const a: API.Index = { From 97ce5505af95ea9e3cffcdf8b2d460c68786cde7 Mon Sep 17 00:00:00 2001 From: joehan Date: Wed, 7 Dec 2022 10:22:37 -0800 Subject: [PATCH 111/115] Load secrets when emulating functions with --inspect-function flag (#5308) * Load secrets when emulating functions with --inspect-function flag * add changeloag --- CHANGELOG.md | 1 + src/emulator/extensionsEmulator.ts | 2 +- src/emulator/functionsEmulator.ts | 84 +++++++++++-------- src/test/emulators/extensionsEmulator.spec.ts | 2 +- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237eed9fca2a..94af501af7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ - Add support for Firestore TTL (#5267) +- Fix bug where secrets were not loaded when emulating functions with `--inpsect-functions`. (#4605) diff --git a/src/emulator/extensionsEmulator.ts b/src/emulator/extensionsEmulator.ts index 04233bfe57da..c1fd847d7dc8 100644 --- a/src/emulator/extensionsEmulator.ts +++ b/src/emulator/extensionsEmulator.ts @@ -234,7 +234,7 @@ export class ExtensionsEmulator implements EmulatorInstance { const emulatableBackend: EmulatableBackend = { functionsDir, env: nonSecretEnv, - codebase: "", + codebase: instance.instanceId, // Give each extension its own codebase name so that they don't share workerPools. secretEnv: secretEnvVariables, predefinedTriggers: extensionTriggers, nodeMajorVersion: nodeMajorVersion, diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 296e0015d082..44ff3151ee32 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -653,10 +653,22 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled("SUCCESS", `functions[${definition.id}]`, msg); } } - - // In debug mode, we eagerly start a runtime process to allow debuggers to attach + // In debug mode, we eagerly start the runtime processes to allow debuggers to attach // before invoking a function. if (this.args.debugPort) { + // Since we're about to start a runtime to be shared by all the functions in this codebase, + // we need to make sure it has all the secrets used by any function in the codebase. + emulatableBackend.secretEnv = Object.values( + toSetup.reduce( + (acc: Record, curr: EmulatedTriggerDefinition) => { + for (const secret of curr.secretEnvironmentVariables || []) { + acc[secret.key] = secret; + } + return acc; + }, + {} + ) + ); await this.startRuntime(emulatableBackend); } } @@ -1206,42 +1218,42 @@ export class FunctionsEmulator implements EmulatorInstance { ); } } + // Note - if trigger is undefined, we are loading in 'sequential' mode. + // In that case, we need to load all secrets for that codebase. + const secrets: backend.SecretEnvVar[] = + trigger?.secretEnvironmentVariables || backend.secretEnv; + const accesses = secrets + .filter((s) => !secretEnvs[s.key]) + .map(async (s) => { + this.logger.logLabeled("INFO", "functions", `Trying to access secret ${s.secret}@latest`); + const value = await accessSecretVersion( + this.getProjectId(), + s.secret, + s.version ?? "latest" + ); + return [s.key, value]; + }); + const accessResults = await allSettled(accesses); - if (trigger) { - const secrets: backend.SecretEnvVar[] = trigger.secretEnvironmentVariables || []; - const accesses = secrets - .filter((s) => !secretEnvs[s.key]) - .map(async (s) => { - this.logger.logLabeled("INFO", "functions", `Trying to access secret ${s.secret}@latest`); - const value = await accessSecretVersion( - this.getProjectId(), - s.secret, - s.version ?? "latest" - ); - return [s.key, value]; - }); - const accessResults = await allSettled(accesses); - - const errs: string[] = []; - for (const result of accessResults) { - if (result.status === "rejected") { - errs.push(result.reason as string); - } else { - const [k, v] = result.value; - secretEnvs[k] = v; - } + const errs: string[] = []; + for (const result of accessResults) { + if (result.status === "rejected") { + errs.push(result.reason as string); + } else { + const [k, v] = result.value; + secretEnvs[k] = v; } + } - if (errs.length > 0) { - this.logger.logLabeled( - "ERROR", - "functions", - "Unable to access secret environment variables from Google Cloud Secret Manager. " + - "Make sure the credential used for the Functions Emulator have access " + - `or provide override values in ${secretPath}:\n\t` + - errs.join("\n\t") - ); - } + if (errs.length > 0) { + this.logger.logLabeled( + "ERROR", + "functions", + "Unable to access secret environment variables from Google Cloud Secret Manager. " + + "Make sure the credential used for the Functions Emulator have access " + + `or provide override values in ${secretPath}:\n\t` + + errs.join("\n\t") + ); } return secretEnvs; @@ -1280,7 +1292,6 @@ export class FunctionsEmulator implements EmulatorInstance { "See https://yarnpkg.com/getting-started/migration#step-by-step for more information." ); } - const runtimeEnv = this.getRuntimeEnvs(backend, trigger); const secretEnvs = await this.resolveSecretEnvs(backend, trigger); const socketPath = getTemporarySocketPath(); @@ -1307,6 +1318,7 @@ export class FunctionsEmulator implements EmulatorInstance { instanceId: backend.extensionInstanceId, ref: backend.extensionVersion?.ref, }; + const pool = this.workerPools[backend.codebase]; const worker = pool.addWorker(trigger?.id, runtime, extensionLogInfo); await worker.waitForSocketReady(); diff --git a/src/test/emulators/extensionsEmulator.spec.ts b/src/test/emulators/extensionsEmulator.spec.ts index 2a4ff08dfaa5..59231fd36365 100644 --- a/src/test/emulators/extensionsEmulator.spec.ts +++ b/src/test/emulators/extensionsEmulator.spec.ts @@ -119,7 +119,7 @@ describe("Extensions Emulator", () => { ], extension: TEST_EXTENSION, extensionVersion: TEST_EXTENSION_VERSION, - codebase: "", + codebase: "ext-test", }, }, ]; From d7f0186256a213697bcf205600abcc6dab5159b2 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 8 Dec 2022 22:46:14 -0500 Subject: [PATCH 112/115] Warning for custom build scripts in project's package.json (#5240) Warn if the build command in the source directory's package.json contains anything other than the default build command. Direct users towards the Express.js / custom integration --- CHANGELOG.md | 1 + src/frameworks/angular/index.ts | 4 +++ src/frameworks/next/index.ts | 5 +++ src/frameworks/nuxt/index.ts | 6 ++++ src/frameworks/utils.ts | 22 +++++++++++++ src/frameworks/vite/index.ts | 6 ++++ src/test/frameworks/utils.spec.ts | 53 +++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+) create mode 100644 src/frameworks/utils.ts create mode 100644 src/test/frameworks/utils.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 94af501af7f5..52890686b4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Add support for Firestore TTL (#5267) - Fix bug where secrets were not loaded when emulating functions with `--inpsect-functions`. (#4605) +- Warn if a web framework's package.json contains anything other than the framework default build command. diff --git a/src/frameworks/angular/index.ts b/src/frameworks/angular/index.ts index db5ccbf0fb8c..3d24a8f6023f 100644 --- a/src/frameworks/angular/index.ts +++ b/src/frameworks/angular/index.ts @@ -14,12 +14,14 @@ import { } from ".."; import { promptOnce } from "../../prompt"; import { proxyRequestHandler } from "../../hosting/proxy"; +import { warnIfCustomBuildScript } from "../utils"; export const name = "Angular"; export const support = SupportLevel.Experimental; export const type = FrameworkType.Framework; const CLI_COMMAND = join("node_modules", ".bin", process.platform === "win32" ? "ng.cmd" : "ng"); +const DEFAULT_BUILD_SCRIPT = ["ng build"]; export async function discover(dir: string): Promise { if (!(await pathExists(join(dir, "package.json")))) return; @@ -57,6 +59,8 @@ export async function build(dir: string): Promise { if (!success) throw new Error(error); }; + await warnIfCustomBuildScript(dir, name, DEFAULT_BUILD_SCRIPT); + if (!browserTarget) throw new Error("No build target..."); if (prerenderTarget) { diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index e13ba65b00e2..884bdbb5cc97 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -22,6 +22,7 @@ import { IncomingMessage, ServerResponse } from "http"; import { logger } from "../../logger"; import { FirebaseError } from "../../error"; import { fileExistsSync } from "../../fsutils"; +import { warnIfCustomBuildScript } from "../utils"; // Next.js's exposed interface is incomplete here // TODO see if there's a better way to grab this @@ -45,6 +46,8 @@ const CLI_COMMAND = join( process.platform === "win32" ? "next.cmd" : "next" ); +const DEFAULT_BUILD_SCRIPT = ["next build"]; + export const name = "Next.js"; export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; @@ -73,6 +76,8 @@ export async function discover(dir: string) { export async function build(dir: string): Promise { const { default: nextBuild } = relativeRequire(dir, "next/dist/build"); + await warnIfCustomBuildScript(dir, name, DEFAULT_BUILD_SCRIPT); + const reactVersion = getReactVersion(dir); if (reactVersion && gte(reactVersion, "18.0.0")) { // This needs to be set for Next build to succeed with React 18 diff --git a/src/frameworks/nuxt/index.ts b/src/frameworks/nuxt/index.ts index aba1129bcc75..3fce4387193a 100644 --- a/src/frameworks/nuxt/index.ts +++ b/src/frameworks/nuxt/index.ts @@ -3,11 +3,14 @@ import { readFile } from "fs/promises"; import { basename, join } from "path"; import { gte } from "semver"; import { BuildResult, findDependency, FrameworkType, relativeRequire, SupportLevel } from ".."; +import { warnIfCustomBuildScript } from "../utils"; export const name = "Nuxt"; export const support = SupportLevel.Experimental; export const type = FrameworkType.Toolchain; +const DEFAULT_BUILD_SCRIPT = ["nuxt build"]; + export async function discover(dir: string) { if (!(await pathExists(join(dir, "package.json")))) return; const nuxtDependency = findDependency("nuxt", { cwd: dir, depth: 0, omitDev: false }); @@ -23,6 +26,9 @@ export async function discover(dir: string) { export async function build(root: string): Promise { const { buildNuxt } = await relativeRequire(root, "@nuxt/kit"); const nuxtApp = await getNuxtApp(root); + + await warnIfCustomBuildScript(root, name, DEFAULT_BUILD_SCRIPT); + await buildNuxt(nuxtApp); return { wantsBackend: true }; } diff --git a/src/frameworks/utils.ts b/src/frameworks/utils.ts new file mode 100644 index 000000000000..d45174a03b19 --- /dev/null +++ b/src/frameworks/utils.ts @@ -0,0 +1,22 @@ +import { readFile } from "fs/promises"; +import { join } from "path"; + +/** + * Prints a warning if the build script in package.json + * contains anything other than allowedBuildScripts. + */ +export async function warnIfCustomBuildScript( + dir: string, + framework: string, + defaultBuildScripts: string[] +): Promise { + const packageJsonBuffer = await readFile(join(dir, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + const buildScript = packageJson.scripts?.build; + + if (buildScript && !defaultBuildScripts.includes(buildScript)) { + console.warn( + `\nWARNING: Your package.json contains a custom build that is being ignored. Only the ${framework} default build script (e.g, "${defaultBuildScripts[0]}") is respected. If you have a more advanced build process you should build a custom integration https://firebase.google.com/docs/hosting/express\n` + ); + } +} diff --git a/src/frameworks/vite/index.ts b/src/frameworks/vite/index.ts index d68c4177c4be..6586c1ff5886 100644 --- a/src/frameworks/vite/index.ts +++ b/src/frameworks/vite/index.ts @@ -5,6 +5,7 @@ import { join } from "path"; import { findDependency, FrameworkType, relativeRequire, SupportLevel } from ".."; import { proxyRequestHandler } from "../../hosting/proxy"; import { promptOnce } from "../../prompt"; +import { warnIfCustomBuildScript } from "../utils"; export const name = "Vite"; export const support = SupportLevel.Experimental; @@ -16,6 +17,8 @@ const CLI_COMMAND = join( process.platform === "win32" ? "vite.cmd" : "vite" ); +export const DEFAULT_BUILD_SCRIPT = ["vite build", "tsc && vite build"]; + export const initViteTemplate = (template: string) => async (setup: any) => await init(setup, template); @@ -61,6 +64,9 @@ export async function discover(dir: string, plugin?: string, npmDependency?: str export async function build(root: string) { const { build } = relativeRequire(root, "vite"); + + await warnIfCustomBuildScript(root, name, DEFAULT_BUILD_SCRIPT); + await build({ root }); } diff --git a/src/test/frameworks/utils.spec.ts b/src/test/frameworks/utils.spec.ts new file mode 100644 index 000000000000..9d0c15c00cd7 --- /dev/null +++ b/src/test/frameworks/utils.spec.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs"; + +import { warnIfCustomBuildScript } from "../../frameworks/utils"; + +describe("Frameworks utils", () => { + describe("warnIfCustomBuildScript", () => { + const framework = "Next.js"; + let sandbox: sinon.SinonSandbox; + let consoleLogSpy: sinon.SinonSpy; + const packageJson = { + scripts: { + build: "", + }, + }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + consoleLogSpy = sandbox.spy(console, "warn"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should not print warning when a default build script is found.", async () => { + const buildScript = "next build"; + const defaultBuildScripts = ["next build"]; + packageJson.scripts.build = buildScript; + + sandbox.stub(fs.promises, "readFile").resolves(JSON.stringify(packageJson)); + + await warnIfCustomBuildScript("fakedir/", framework, defaultBuildScripts); + + expect(consoleLogSpy.callCount).to.equal(0); + }); + + it("should print warning when a custom build script is found.", async () => { + const buildScript = "echo 'Custom build script' && next build"; + const defaultBuildScripts = ["next build"]; + packageJson.scripts.build = buildScript; + + sandbox.stub(fs.promises, "readFile").resolves(JSON.stringify(packageJson)); + + await warnIfCustomBuildScript("fakedir/", framework, defaultBuildScripts); + + expect(consoleLogSpy).to.be.calledOnceWith( + `\nWARNING: Your package.json contains a custom build that is being ignored. Only the ${framework} default build script (e.g, "${defaultBuildScripts[0]}") is respected. If you have a more advanced build process you should build a custom integration https://firebase.google.com/docs/hosting/express\n` + ); + }); + }); +}); From cbe579059ef7fdb8e61783a926a2406d9b3c6f3e Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 8 Dec 2022 23:26:54 -0500 Subject: [PATCH 113/115] Handle Next.js rewrites/redirects/headers (#5212) * Handle Next.js rewrites/redirects/headers incompatible with `firebase.json` in Cloud Functions * Filter out Next.js prerendered routes that matches rewrites/redirects/headers rules from SSG content directory --- CHANGELOG.md | 2 + src/frameworks/next/index.ts | 141 +++++++-- src/frameworks/next/interfaces.ts | 31 ++ src/frameworks/next/utils.ts | 114 +++++++ src/frameworks/utils.ts | 21 +- src/test/frameworks/next/helpers/headers.ts | 292 ++++++++++++++++++ src/test/frameworks/next/helpers/index.ts | 4 + src/test/frameworks/next/helpers/paths.ts | 90 ++++++ src/test/frameworks/next/helpers/redirects.ts | 107 +++++++ src/test/frameworks/next/helpers/rewrites.ts | 85 +++++ src/test/frameworks/next/utils.spec.ts | 145 +++++++++ src/test/frameworks/utils.spec.ts | 23 ++ 12 files changed, 1020 insertions(+), 35 deletions(-) create mode 100644 src/frameworks/next/interfaces.ts create mode 100644 src/frameworks/next/utils.ts create mode 100644 src/test/frameworks/next/helpers/headers.ts create mode 100644 src/test/frameworks/next/helpers/index.ts create mode 100644 src/test/frameworks/next/helpers/paths.ts create mode 100644 src/test/frameworks/next/helpers/redirects.ts create mode 100644 src/test/frameworks/next/helpers/rewrites.ts create mode 100644 src/test/frameworks/next/utils.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 52890686b4bf..6149438f5a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ - Add support for Firestore TTL (#5267) - Fix bug where secrets were not loaded when emulating functions with `--inpsect-functions`. (#4605) +- Handle Next.js rewrites/redirects/headers incompatible with `firebase.json` in Cloud Functions (#5212) +- Filter out Next.js prerendered routes that matches rewrites/redirects/headers rules from SSG content directory (#5212) - Warn if a web framework's package.json contains anything other than the framework default build command. diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index 884bdbb5cc97..6a71d5dbfe0e 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -1,7 +1,6 @@ import { execSync } from "child_process"; import { readFile, mkdir, copyFile } from "fs/promises"; import { dirname, join } from "path"; -import type { Header, Rewrite, Redirect } from "next/dist/lib/load-custom-routes"; import type { NextConfig } from "next"; import { copy, mkdirp, pathExists } from "fs-extra"; import { pathToFileURL, parse } from "url"; @@ -22,24 +21,17 @@ import { IncomingMessage, ServerResponse } from "http"; import { logger } from "../../logger"; import { FirebaseError } from "../../error"; import { fileExistsSync } from "../../fsutils"; +import { + cleanEscapedChars, + getNextjsRewritesToUse, + isHeaderSupportedByFirebase, + isRedirectSupportedByFirebase, + isRewriteSupportedByFirebase, +} from "./utils"; +import type { Manifest } from "./interfaces"; +import { readJSON } from "../utils"; import { warnIfCustomBuildScript } from "../utils"; -// Next.js's exposed interface is incomplete here -// TODO see if there's a better way to grab this -interface Manifest { - distDir?: string; - basePath?: string; - headers?: (Header & { regex: string })[]; - redirects?: (Redirect & { regex: string })[]; - rewrites?: - | (Rewrite & { regex: string })[] - | { - beforeFiles?: (Rewrite & { regex: string })[]; - afterFiles?: (Rewrite & { regex: string })[]; - fallback?: (Rewrite & { regex: string })[]; - }; -} - const CLI_COMMAND = join( "node_modules", ".bin", @@ -140,27 +132,56 @@ export async function build(dir: string): Promise { } } - const manifestBuffer = await readFile(join(dir, distDir, "routes-manifest.json")); - const manifest: Manifest = JSON.parse(manifestBuffer.toString()); + const manifest = await readJSON(join(dir, distDir, "routes-manifest.json")); + const { headers: nextJsHeaders = [], redirects: nextJsRedirects = [], rewrites: nextJsRewrites = [], } = manifest; - const headers = nextJsHeaders.map(({ source, headers }) => ({ source, headers })); + + const isEveryHeaderSupported = nextJsHeaders.every(isHeaderSupportedByFirebase); + if (!isEveryHeaderSupported) wantsBackend = true; + + const headers = nextJsHeaders.filter(isHeaderSupportedByFirebase).map(({ source, headers }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + headers, + })); + + const isEveryRedirectSupported = nextJsRedirects.every(isRedirectSupportedByFirebase); + if (!isEveryRedirectSupported) wantsBackend = true; + const redirects = nextJsRedirects - .filter(({ internal }: any) => !internal) - .map(({ source, destination, statusCode: type }) => ({ source, destination, type })); - const nextJsRewritesToUse = Array.isArray(nextJsRewrites) - ? nextJsRewrites - : nextJsRewrites.beforeFiles || []; + .filter(isRedirectSupportedByFirebase) + .map(({ source, destination, statusCode: type }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + destination, + type, + })); + + const nextJsRewritesToUse = getNextjsRewritesToUse(nextJsRewrites); + + // rewrites.afterFiles / rewrites.fallback are not supported by firebase.json + if ( + !Array.isArray(nextJsRewrites) && + (nextJsRewrites.afterFiles?.length || nextJsRewrites.fallback?.length) + ) { + wantsBackend = true; + } else { + const isEveryRewriteSupported = nextJsRewritesToUse.every(isRewriteSupportedByFirebase); + if (!isEveryRewriteSupported) wantsBackend = true; + } + + // Can we change i18n into Firebase settings? const rewrites = nextJsRewritesToUse - .map(({ source, destination, has }) => { - // Can we change i18n into Firebase settings? - if (has) return undefined; - return { source, destination }; - }) - .filter((it) => it); + .filter(isRewriteSupportedByFirebase) + .map(({ source, destination }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + destination, + })); return { wantsBackend, headers, redirects, rewrites }; } @@ -215,10 +236,47 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin } } - const prerenderManifestBuffer = await readFile( - join(sourceDir, distDir, "prerender-manifest.json") + const [prerenderManifest, routesManifest] = await Promise.all([ + readJSON( + join( + sourceDir, + distDir, + "prerender-manifest.json" // TODO: get this from next/constants + ) + ), + readJSON( + join( + sourceDir, + distDir, + "routes-manifest.json" // TODO: get this from next/constants + ) + ), + ]); + + const { redirects = [], rewrites = [], headers = [] } = routesManifest; + + const rewritesToUse = getNextjsRewritesToUse(rewrites); + const rewritesNotSupportedByFirebase = rewritesToUse.filter( + (rewrite) => !isRewriteSupportedByFirebase(rewrite) + ); + const rewritesRegexesNotSupportedByFirebase = rewritesNotSupportedByFirebase.map( + (rewrite) => new RegExp(rewrite.regex) + ); + + const redirectsNotSupportedByFirebase = redirects.filter( + (redirect) => !isRedirectSupportedByFirebase(redirect) + ); + const redirectsRegexesNotSupportedByFirebase = redirectsNotSupportedByFirebase.map( + (redirect) => new RegExp(redirect.regex) + ); + + const headersNotSupportedByFirebase = headers.filter( + (header) => !isHeaderSupportedByFirebase(header) ); - const prerenderManifest = JSON.parse(prerenderManifestBuffer.toString()); + const headersRegexesNotSupportedByFirebase = headersNotSupportedByFirebase.map( + (header) => new RegExp(header.regex) + ); + for (const path in prerenderManifest.routes) { if (prerenderManifest.routes[path]) { // Skip ISR in the deploy to hosting @@ -227,6 +285,21 @@ export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: strin continue; } + const routeMatchUnsupportedRewrite = rewritesRegexesNotSupportedByFirebase.some( + (rewriteRegex) => rewriteRegex.test(path) + ); + if (routeMatchUnsupportedRewrite) continue; + + const routeMatchUnsupportedRedirect = redirectsRegexesNotSupportedByFirebase.some( + (redirectRegex) => redirectRegex.test(path) + ); + if (routeMatchUnsupportedRedirect) continue; + + const routeMatchUnsupportedHeader = headersRegexesNotSupportedByFirebase.some( + (headerRegex) => headerRegex.test(path) + ); + if (routeMatchUnsupportedHeader) continue; + // TODO(jamesdaniels) explore oppertunity to simplify this now that we // are defaulting cleanURLs to true for frameworks diff --git a/src/frameworks/next/interfaces.ts b/src/frameworks/next/interfaces.ts new file mode 100644 index 000000000000..e012402b9fb4 --- /dev/null +++ b/src/frameworks/next/interfaces.ts @@ -0,0 +1,31 @@ +import type { Header, Rewrite, Redirect } from "next/dist/lib/load-custom-routes"; + +export interface RoutesManifestRewrite extends Rewrite { + regex: string; +} + +export interface RoutesManifestRewriteObject { + beforeFiles?: RoutesManifestRewrite[]; + afterFiles?: RoutesManifestRewrite[]; + fallback?: RoutesManifestRewrite[]; +} + +export interface RoutesManifestHeader extends Header { + regex: string; +} + +// Next.js's exposed interface is incomplete here +// TODO see if there's a better way to grab this +// TODO: rename to RoutesManifest as Next.js has other types of manifests +export interface Manifest { + distDir?: string; + basePath?: string; + headers?: RoutesManifestHeader[]; + redirects?: Array< + Redirect & { + regex: string; + internal?: boolean; + } + >; + rewrites?: RoutesManifestRewrite[] | RoutesManifestRewriteObject; +} diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts new file mode 100644 index 000000000000..040f06adedb9 --- /dev/null +++ b/src/frameworks/next/utils.ts @@ -0,0 +1,114 @@ +import type { Header, Redirect, Rewrite } from "next/dist/lib/load-custom-routes"; +import type { Manifest, RoutesManifestRewrite } from "./interfaces"; +import { isUrl } from "../utils"; + +/** + * Whether the given path has a regex or not. + * According to the Next.js documentation: + * ```md + * To match a regex path you can wrap the regex in parentheses + * after a parameter, for example /post/:slug(\\d{1,}) will match /post/123 + * but not /post/abc. + * ``` + * See: https://nextjs.org/docs/api-reference/next.config.js/redirects#regex-path-matching + */ +export function pathHasRegex(path: string): boolean { + // finds parentheses that are not preceded by double backslashes + return /(? b); +} + +/** + * Whether a Next.js rewrite is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#rewrites + * + * Next.js unsupported rewrites includes: + * - Rewrites with the `has` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/rewrites#header-cookie-and-query-matching + * + * - Rewrites using regex for path matching. + * - https://nextjs.org/docs/api-reference/next.config.js/rewrites#regex-path-matching + * + * - Rewrites to external URLs + */ +export function isRewriteSupportedByFirebase(rewrite: Rewrite): boolean { + return !("has" in rewrite || pathHasRegex(rewrite.source) || isUrl(rewrite.destination)); +} + +/** + * Whether a Next.js redirect is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#redirects + * + * Next.js unsupported redirects includes: + * - Redirects with the `has` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/redirects#header-cookie-and-query-matching + * + * - Redirects using regex for path matching. + * - https://nextjs.org/docs/api-reference/next.config.js/redirects#regex-path-matching + * + * - Next.js internal redirects + */ +export function isRedirectSupportedByFirebase(redirect: Redirect): boolean { + return !("has" in redirect || pathHasRegex(redirect.source) || "internal" in redirect); +} + +/** + * Whether a Next.js custom header is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#headers + * + * Next.js unsupported headers includes: + * - Custom header with the `has` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/headers#header-cookie-and-query-matching + * + * - Custom header using regex for path matching. + * - https://nextjs.org/docs/api-reference/next.config.js/headers#regex-path-matching + */ +export function isHeaderSupportedByFirebase(header: Header): boolean { + return !("has" in header || pathHasRegex(header.source)); +} + +/** + * Get which Next.js rewrites will be used before checking supported items individually. + * + * Next.js rewrites can be arrays or objects: + * - For arrays, all supported items can be used. + * - For objects only `beforeFiles` can be used. + * + * See: https://nextjs.org/docs/api-reference/next.config.js/rewrites + */ +export function getNextjsRewritesToUse( + nextJsRewrites: Manifest["rewrites"] +): RoutesManifestRewrite[] { + if (Array.isArray(nextJsRewrites)) { + return nextJsRewrites; + } + + if (nextJsRewrites?.beforeFiles) { + return nextJsRewrites.beforeFiles; + } + + return []; +} diff --git a/src/frameworks/utils.ts b/src/frameworks/utils.ts index d45174a03b19..2024df5e1b66 100644 --- a/src/frameworks/utils.ts +++ b/src/frameworks/utils.ts @@ -1,5 +1,24 @@ -import { readFile } from "fs/promises"; +import { readJSON as originalReadJSON } from "fs-extra"; +import type { ReadOptions } from "fs-extra"; import { join } from "path"; +import { readFile } from "fs/promises"; + +/** + * Whether the given string starts with http:// or https:// + */ +export function isUrl(url: string): boolean { + return /^https?:\/\//.test(url); +} + +/** + * add type to readJSON + */ +export function readJSON( + file: string, + options?: ReadOptions | BufferEncoding | string +): Promise { + return originalReadJSON(file, options) as Promise; +} /** * Prints a warning if the build script in package.json diff --git a/src/test/frameworks/next/helpers/headers.ts b/src/test/frameworks/next/helpers/headers.ts new file mode 100644 index 000000000000..8f8bb4470061 --- /dev/null +++ b/src/test/frameworks/next/helpers/headers.ts @@ -0,0 +1,292 @@ +import type { RoutesManifestHeader } from "../../../../frameworks/next/interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedHeaders: RoutesManifestHeader[] = [ + ...supportedPaths.map((path) => ({ + source: path, + regex: "", + headers: [ + { + key: "x-path", + value: ":path", + }, + { + key: "some:path", + value: "hi", + }, + { + key: "x-test", + value: "some:value*", + }, + { + key: "x-test-2", + value: "value*", + }, + { + key: "x-test-3", + value: ":value?", + }, + { + key: "x-test-4", + value: ":value+", + }, + { + key: "x-test-5", + value: "something https:", + }, + { + key: "x-test-6", + value: ":hello(world)", + }, + { + key: "x-test-7", + value: "hello(world)", + }, + { + key: "x-test-8", + value: "hello{1,}", + }, + { + key: "x-test-9", + value: ":hello{1,2}", + }, + { + key: "content-security-policy", + value: + "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path", + }, + ], + })), + { + regex: "", + source: "/add-header", + headers: [ + { + key: "x-custom-header", + value: "hello world", + }, + { + key: "x-another-header", + value: "hello again", + }, + ], + }, + { + regex: "", + source: "/my-other-header/:path", + headers: [ + { + key: "x-path", + value: ":path", + }, + { + key: "some:path", + value: "hi", + }, + { + key: "x-test", + value: "some:value*", + }, + { + key: "x-test-2", + value: "value*", + }, + { + key: "x-test-3", + value: ":value?", + }, + { + key: "x-test-4", + value: ":value+", + }, + { + key: "x-test-5", + value: "something https:", + }, + { + key: "x-test-6", + value: ":hello(world)", + }, + { + key: "x-test-7", + value: "hello(world)", + }, + { + key: "x-test-8", + value: "hello{1,}", + }, + { + key: "x-test-9", + value: ":hello{1,2}", + }, + { + key: "content-security-policy", + value: + "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path", + }, + ], + }, + { + regex: "", + source: "/without-params/url", + headers: [ + { + key: "x-origin", + value: "https://example.com", + }, + ], + }, + { + regex: "", + source: "/with-params/url/:path*", + headers: [ + { + key: "x-url", + value: "https://example.com/:path*", + }, + ], + }, + { + regex: "", + source: "/with-params/url2/:path*", + headers: [ + { + key: "x-url", + value: "https://example.com:8080?hello=:path*", + }, + ], + }, + { + regex: "", + source: "/:path*", + headers: [ + { + key: "x-something", + value: "applied-everywhere", + }, + ], + }, + { + regex: "", + source: "/catchall-header/:path*", + headers: [ + { + key: "x-value", + value: ":path*", + }, + ], + }, +]; + +export const unsupportedHeaders: RoutesManifestHeader[] = [ + ...unsupportedPaths.map((path) => ({ + source: path, + regex: "", + headers: [ + { + key: "x-custom-header", + value: "hello world", + }, + { + key: "x-another-header", + value: "hello again", + }, + ], + })), + { + regex: "", + source: "/named-pattern/:path(.*)", + headers: [ + { + key: "x-something", + value: "value=:path", + }, + { + key: "path-:path", + value: "end", + }, + ], + }, + + { + regex: "", + source: "/my-headers/(.*)", + headers: [ + { + key: "x-first-header", + value: "first", + }, + { + key: "x-second-header", + value: "second", + }, + ], + }, + + { + regex: "", + source: "/has-header-1", + has: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + headers: [ + { + key: "x-another", + value: "header", + }, + ], + }, + { + regex: "", + source: "/has-header-2", + has: [ + { + type: "query", + key: "my-query", + }, + ], + headers: [ + { + key: "x-added", + value: "value", + }, + ], + }, + { + regex: "", + source: "/has-header-3", + has: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + headers: [ + { + key: "x-is-user", + value: "yuuuup", + }, + ], + }, + { + regex: "", + source: "/has-header-4", + has: [ + { + type: "host", + value: "example.com", + }, + ], + headers: [ + { + key: "x-is-host", + value: "yuuuup", + }, + ], + }, +]; diff --git a/src/test/frameworks/next/helpers/index.ts b/src/test/frameworks/next/helpers/index.ts new file mode 100644 index 000000000000..83772e4ea987 --- /dev/null +++ b/src/test/frameworks/next/helpers/index.ts @@ -0,0 +1,4 @@ +export * from "./paths"; +export * from "./headers"; +export * from "./redirects"; +export * from "./rewrites"; diff --git a/src/test/frameworks/next/helpers/paths.ts b/src/test/frameworks/next/helpers/paths.ts new file mode 100644 index 000000000000..2b5c3e3f881c --- /dev/null +++ b/src/test/frameworks/next/helpers/paths.ts @@ -0,0 +1,90 @@ +export const pathsWithRegex = [ + "/(.*)", + "/post/:slug(\\d{1,})", + "/:path((?!another-page$).*)", + "/api-hello-regex/:first(.*)", + "/unnamed-params/nested/(.*)/:test/(.*)", +] as const; + +export const pathsWithEscapedChars = [ + `/post\\(someStringBetweenParentheses\\)/:slug`, + `/english\\(default\\)/:slug`, + `/post/\\(es\\?cap\\Wed\\*p\\{ar\\}en\\:th\\eses\\)`, +] as const; + +export const pathsWithRegexAndEscapedChars = [ + `/post/\\(escapedparentheses\\)/:slug(\\d{1,})`, + `/post/\\(es\\?cap\\Wed\\*p\\{ar\\}en\\:th\\eses\\)/:slug(\\d{1,})`, +] as const; + +export const pathsAsGlobs = [ + "/specific/:path*", + "/another/:path*", + "/about", + "/", + "/old-blog/:path*", + "/blog/:path*", + "/to-websocket", + "/to-nowhere", + "/rewriting-to-auto-export", + "/rewriting-to-another-auto-export/:path*", + "/to-another", + "/another/one", + "/nav", + "/404", + "/hello-world", + "/static/hello.txt", + "/another", + "/multi-rewrites", + "/first", + "/hello", + "/second", + "/hello-again", + "/to-hello", + "/hello", + "/blog/post-1", + "/blog/post-2", + "/test/:path", + "/:path", + "/test-overwrite/:something/:another", + "/params/this-should-be-the-value", + "/params/:something", + "/with-params", + "/query-rewrite/:section/:name", + "/hidden/_next/:path*", + "/_next/:path*", + "/proxy-me/:path*", + "/api-hello", + "/api/hello", + "/api/hello?name=:first*", + "/api-hello-param/:name", + "/api/hello?hello=:name", + "/api-dynamic-param/:name", + "/api/dynamic/:name?hello=:name", + "/:path/post-321", + "/with-params", + "/with-params", + "/catchall-rewrite/:path*", + "/with-params", + "/catchall-query/:path*", + "/has-rewrite-1", + "/has-rewrite-2", + "/has-rewrite-3", + "/has-rewrite-4", + "/has-rewrite-5", + "/:hasParam", + "/has-rewrite-6", + "/with-params", + "/has-rewrite-7", + "/has-rewrite-8", + "/blog-catchall/:post", + "/missing-rewrite-1", + "/with-params", + "/missing-rewrite-2", + "/with-params", + "/missing-rewrite-3", + "/overridden/:path*", +] as const; + +export const supportedPaths = [...pathsWithEscapedChars, ...pathsAsGlobs] as const; +export const unsupportedPaths = [...pathsWithRegex, ...pathsWithRegexAndEscapedChars] as const; diff --git a/src/test/frameworks/next/helpers/redirects.ts b/src/test/frameworks/next/helpers/redirects.ts new file mode 100644 index 000000000000..d67394951f29 --- /dev/null +++ b/src/test/frameworks/next/helpers/redirects.ts @@ -0,0 +1,107 @@ +import type { Manifest } from "../../../../frameworks/next/interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedRedirects: NonNullable = supportedPaths.map( + (path) => ({ + source: path, + destination: `${path}/redirect`, + regex: "", + statusCode: 301, + }) +); + +export const unsupportedRedirects: NonNullable = [ + ...unsupportedPaths.map((path) => ({ + source: path, + destination: `/${path}/redirect`, + regex: "", + statusCode: 301, + })), + { + source: "/has-redirect-1", + has: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + destination: "/another?myHeader=:myHeader", + permanent: false, + regex: "", + }, + { + source: "/has-redirect-2", + has: [ + { + type: "query", + key: "my-query", + }, + ], + destination: "/another?value=:myquery", + permanent: false, + regex: "", + }, + { + source: "/has-redirect-3", + has: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + destination: "/another?authorized=1", + permanent: false, + regex: "", + }, + { + source: "/has-redirect-4", + has: [ + { + type: "host", + value: "example.com", + }, + ], + destination: "/another?host=1", + permanent: false, + regex: "", + }, + { + source: "/:path/has-redirect-5", + has: [ + { + type: "header", + key: "x-test-next", + }, + ], + destination: "/somewhere", + permanent: false, + regex: "", + }, + { + source: "/has-redirect-6", + has: [ + { + type: "host", + value: "(?.*)-test.example.com", + }, + ], + destination: "https://:subdomain.example.com/some-path/end?a=b", + permanent: false, + regex: "", + }, + { + source: "/has-redirect-7", + has: [ + { + type: "query", + key: "hello", + value: "(?.*)", + }, + ], + destination: "/somewhere?value=:hello", + permanent: false, + regex: "", + }, +]; diff --git a/src/test/frameworks/next/helpers/rewrites.ts b/src/test/frameworks/next/helpers/rewrites.ts new file mode 100644 index 000000000000..cc55dde96692 --- /dev/null +++ b/src/test/frameworks/next/helpers/rewrites.ts @@ -0,0 +1,85 @@ +import type { + RoutesManifestRewrite, + RoutesManifestRewriteObject, +} from "../../../../frameworks/next/interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedRewritesArray: RoutesManifestRewrite[] = supportedPaths.map((path) => ({ + source: path, + destination: `${path}/rewrite`, + regex: "", +})); + +export const unsupportedRewritesArray: RoutesManifestRewrite[] = [ + ...unsupportedPaths.map((path) => ({ + source: path, + destination: `/${path}/rewrite`, + regex: "", + })), + // external http URL + { + source: "/:path*", + destination: "http://firebase.google.com", + regex: "", + }, + // external https URL + { + source: "/:path*", + destination: "https://firebase.google.com", + regex: "", + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { type: "query", key: "overrideMe" }, + { + type: "header", + key: "x-rewrite-me", + }, + ], + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { + type: "query", + key: "page", + // the page value will not be available in the + // destination since value is provided and doesn't + // use a named capture group e.g. (?home) + value: "home", + }, + ], + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { + type: "cookie", + key: "authorized", + value: "true", + }, + ], + }, +]; + +export const supportedRewritesObject: RoutesManifestRewriteObject = { + afterFiles: unsupportedRewritesArray, // should be ignored, only beforeFiles is used + beforeFiles: supportedRewritesArray, + fallback: unsupportedRewritesArray, // should be ignored, only beforeFiles is used +}; + +export const unsupportedRewritesObject: RoutesManifestRewriteObject = { + afterFiles: unsupportedRewritesArray, // should be ignored, only beforeFiles is used + beforeFiles: unsupportedRewritesArray, + fallback: unsupportedRewritesArray, // should be ignored, only beforeFiles is used +}; diff --git a/src/test/frameworks/next/utils.spec.ts b/src/test/frameworks/next/utils.spec.ts new file mode 100644 index 000000000000..06c0e995b25a --- /dev/null +++ b/src/test/frameworks/next/utils.spec.ts @@ -0,0 +1,145 @@ +import { expect } from "chai"; + +import { + pathHasRegex, + cleanEscapedChars, + isRewriteSupportedByFirebase, + isRedirectSupportedByFirebase, + isHeaderSupportedByFirebase, + getNextjsRewritesToUse, +} from "../../../frameworks/next/utils"; +import { + pathsAsGlobs, + pathsWithEscapedChars, + pathsWithRegex, + pathsWithRegexAndEscapedChars, + supportedHeaders, + supportedRedirects, + supportedRewritesArray, + supportedRewritesObject, + unsupportedHeaders, + unsupportedRedirects, + unsupportedRewritesArray, +} from "./helpers"; + +describe("Next.js utils", () => { + describe("pathHasRegex", () => { + it("should identify regex", () => { + for (const path of pathsWithRegex) { + expect(pathHasRegex(path)).to.be.true; + } + }); + + it("should not identify escaped parentheses as regex", () => { + for (const path of pathsWithEscapedChars) { + expect(pathHasRegex(path)).to.be.false; + } + }); + + it("should identify regex along with escaped chars", () => { + for (const path of pathsWithRegexAndEscapedChars) { + expect(pathHasRegex(path)).to.be.true; + } + }); + + it("should not identify globs as regex", () => { + for (const path of pathsAsGlobs) { + expect(pathHasRegex(path)).to.be.false; + } + }); + }); + + describe("cleanEscapedChars", () => { + it("should clean escaped chars", () => { + // path containing all escaped chars + const testPath = "/\\(\\)\\{\\}\\:\\+\\?\\*/:slug"; + + expect(testPath.includes("\\(")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\(")).to.be.false; + + expect(testPath.includes("\\)")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\)")).to.be.false; + + expect(testPath.includes("\\{")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\{")).to.be.false; + + expect(testPath.includes("\\}")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\}")).to.be.false; + + expect(testPath.includes("\\:")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\:")).to.be.false; + + expect(testPath.includes("\\+")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\+")).to.be.false; + + expect(testPath.includes("\\?")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\?")).to.be.false; + + expect(testPath.includes("\\*")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\*")).to.be.false; + }); + }); + + describe("isRewriteSupportedByFirebase", () => { + it("should allow supported rewrites", () => { + for (const rewrite of supportedRewritesArray) { + expect(isRewriteSupportedByFirebase(rewrite)).to.be.true; + } + }); + + it("should disallow unsupported rewrites", () => { + for (const rewrite of unsupportedRewritesArray) { + expect(isRewriteSupportedByFirebase(rewrite)).to.be.false; + } + }); + }); + + describe("isRedirectSupportedByFirebase", () => { + it("should allow supported redirects", () => { + for (const redirect of supportedRedirects) { + expect(isRedirectSupportedByFirebase(redirect)).to.be.true; + } + }); + + it("should disallow unsupported redirects", () => { + for (const redirect of unsupportedRedirects) { + expect(isRedirectSupportedByFirebase(redirect)).to.be.false; + } + }); + }); + + describe("isHeaderSupportedByFirebase", () => { + it("should allow supported headers", () => { + for (const header of supportedHeaders) { + expect(isHeaderSupportedByFirebase(header)).to.be.true; + } + }); + + it("should disallow unsupported headers", () => { + for (const header of unsupportedHeaders) { + expect(isHeaderSupportedByFirebase(header)).to.be.false; + } + }); + }); + + describe("getNextjsRewritesToUse", () => { + it("should use only beforeFiles", () => { + if (!supportedRewritesObject?.beforeFiles?.length) { + throw new Error("beforeFiles must have rewrites"); + } + + const rewritesToUse = getNextjsRewritesToUse(supportedRewritesObject); + + for (const [i, rewrite] of supportedRewritesObject.beforeFiles.entries()) { + expect(rewrite.source).to.equal(rewritesToUse[i].source); + expect(rewrite.destination).to.equal(rewritesToUse[i].destination); + } + }); + + it("should return all rewrites if in array format", () => { + const rewritesToUse = getNextjsRewritesToUse(supportedRewritesArray); + + expect(rewritesToUse).to.have.length(supportedRewritesArray.length); + }); + }); +}); diff --git a/src/test/frameworks/utils.spec.ts b/src/test/frameworks/utils.spec.ts index 9d0c15c00cd7..ebb675aadbca 100644 --- a/src/test/frameworks/utils.spec.ts +++ b/src/test/frameworks/utils.spec.ts @@ -3,8 +3,31 @@ import * as sinon from "sinon"; import * as fs from "fs"; import { warnIfCustomBuildScript } from "../../frameworks/utils"; +import { isUrl } from "../../frameworks/utils"; describe("Frameworks utils", () => { + describe("isUrl", () => { + it("should identify http URL", () => { + expect(isUrl("http://firebase.google.com")).to.be.true; + }); + + it("should identify https URL", () => { + expect(isUrl("https://firebase.google.com")).to.be.true; + }); + + it("should ignore URL within path", () => { + expect(isUrl("path/?url=https://firebase.google.com")).to.be.false; + }); + + it("should ignore path starting with http but without protocol", () => { + expect(isUrl("httpendpoint/foo/bar")).to.be.false; + }); + + it("should ignore path starting with https but without protocol", () => { + expect(isUrl("httpsendpoint/foo/bar")).to.be.false; + }); + }); + describe("warnIfCustomBuildScript", () => { const framework = "Next.js"; let sandbox: sinon.SinonSandbox; From 934e4e51e1ddacb9eb0acbccd86e37f7da04a7ee Mon Sep 17 00:00:00 2001 From: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Date: Fri, 9 Dec 2022 15:32:11 -0800 Subject: [PATCH 114/115] Update the download_emulators default to true. The user has indicated they want to use the emulators we should download them right away to speed up their use and enable the suite to work better offline. (#5123) --- src/init/features/emulators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init/features/emulators.ts b/src/init/features/emulators.ts index 84e6ee97ad99..3c8bf3ec2451 100644 --- a/src/init/features/emulators.ts +++ b/src/init/features/emulators.ts @@ -95,7 +95,7 @@ export async function doSetup(setup: any, config: any) { name: "download", type: "confirm", message: "Would you like to download the emulators now?", - default: false, + default: true, }, ]); } From 26b97ba19b3996fc118e7a86f58841a059ae86c5 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 9 Dec 2022 16:14:38 -0800 Subject: [PATCH 115/115] Add support for nodejs18 (in preview). (#5319) --- CHANGELOG.md | 1 + schema/firebase-config.json | 6 ++++-- src/deploy/functions/runtimes/index.ts | 3 ++- .../functions/runtimes/node/parseRuntimeAndValidateSDK.ts | 1 + src/firebaseConfig.ts | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6149438f5a53..cc6969380cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,4 @@ - Handle Next.js rewrites/redirects/headers incompatible with `firebase.json` in Cloud Functions (#5212) - Filter out Next.js prerendered routes that matches rewrites/redirects/headers rules from SSG content directory (#5212) - Warn if a web framework's package.json contains anything other than the framework default build command. +- Add support for nodejs18 for Cloud Functions for Firebase (#5319) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 6a20dd6d5009..839508468e60 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -388,7 +388,8 @@ "nodejs10", "nodejs12", "nodejs14", - "nodejs16" + "nodejs16", + "nodejs18" ], "type": "string" }, @@ -442,7 +443,8 @@ "nodejs10", "nodejs12", "nodejs14", - "nodejs16" + "nodejs16", + "nodejs18" ], "type": "string" }, diff --git a/src/deploy/functions/runtimes/index.ts b/src/deploy/functions/runtimes/index.ts index 537d8141238e..68bfb4125a0e 100644 --- a/src/deploy/functions/runtimes/index.ts +++ b/src/deploy/functions/runtimes/index.ts @@ -5,7 +5,7 @@ import * as validate from "../validate"; import { FirebaseError } from "../../../error"; /** Supported runtimes for new Cloud Functions. */ -const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14", "nodejs16"]; +const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14", "nodejs16", "nodejs18"]; // Experimental runtimes are part of the Runtime type, but are in a // different list to help guard against some day accidentally iterating over // and printing a hidden runtime to the user. @@ -33,6 +33,7 @@ const MESSAGE_FRIENDLY_RUNTIMES: Record = { nodejs12: "Node.js 12", nodejs14: "Node.js 14", nodejs16: "Node.js 16", + nodejs18: "Node.js 18", go113: "Go 1.13", }; diff --git a/src/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.ts b/src/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.ts index f1eeecefb6b9..048d74f58ee1 100644 --- a/src/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.ts +++ b/src/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.ts @@ -16,6 +16,7 @@ const ENGINE_RUNTIMES: Record