diff --git a/.gitignore b/.gitignore index c31fcb3ea6..6fcfe4198b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules out .tmp .env*.local +.env .vercel _temp-migrations/ src/data/_redirects.generated.json diff --git a/.npmrc b/.npmrc index b6f27f1359..a32230ed05 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ engine-strict=true +@hashicorp:registry=https://registry.npmjs.org/ diff --git a/config/base.json b/config/base.json index 7c0e5392de..5037fea747 100644 --- a/config/base.json +++ b/config/base.json @@ -28,7 +28,10 @@ "clientToken": "pubd5cf774aa1cbbd001accd50cb463925b", "service": "non-prod.developer.hashicorp.com" }, - "product_slugs_with_integrations": ["vault", "nomad", "packer"] + "product_slugs_with_integrations": ["vault", "nomad", "packer"], + "sandbox": { + "instruqt_base_url": "https://play.instruqt.com/embed" + } }, "learn": { "max_static_paths": 10 diff --git a/config/development.json b/config/development.json index a424fa0f83..be9acacde7 100644 --- a/config/development.json +++ b/config/development.json @@ -4,7 +4,10 @@ "max_static_paths": 1 }, "dev_dot": { - "max_static_paths": 1 + "max_static_paths": 1, + "sandbox": { + "instruqt_base_url": "https://play.instruqt.com/embed" + } }, "learn": { "max_static_paths": 1 diff --git a/package-lock.json b/package-lock.json index 39177c5389..ebf3b02a1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,8 +149,7 @@ "@types/google-spreadsheet": "^3.2.2", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^8.5.9", - "@types/node": "^18.18.9", - "@types/react": "^18.2.6", + "@types/node": "^20.19.0", "@types/react-dom": "^18.2.4", "@types/semver": "^7.3.9", "@types/shortid": "^0.0.29", @@ -168,7 +167,7 @@ "next-remote-watch": "^2.0.0", "nextjs-bundle-analysis": "^0.5.0-canary.1", "nock": "^14.0.0-beta.5", - "postcss": "^8.4.5", + "postcss": "^8.5.6", "postcss-flexbugs-fixes": "^5.0.2", "postcss-normalize": "^10.0.1", "postcss-preset-env": "^6.7.0", @@ -176,6 +175,7 @@ "rimraf": "^5.0.5", "sass": "^1.75.0", "simple-git-hooks": "^2.7.0", + "source-map": "^0.6.1", "stylelint": "^13.8.0", "typescript": "^5.4.5", "typescript-eslint": "^8.25.0", @@ -2718,16 +2718,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/@hashicorp/next-optimized-images/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@hashicorp/next-optimized-images/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4246,16 +4236,6 @@ "node": ">=8.0.0" } }, - "node_modules/@hashicorp/platform-cli/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@hashicorp/platform-cli/node_modules/ts-jest": { "version": "26.5.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", @@ -9851,6 +9831,15 @@ "semver": "bin/semver" } }, + "node_modules/@mdx-js/mdx/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@mdx-js/mdx/node_modules/unist-util-is": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", @@ -15974,14 +15963,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/@types/cssnano/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -16125,11 +16106,12 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "18.18.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", - "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-fetch": { @@ -18102,15 +18084,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/autoprefixer/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/autoprefixer/node_modules/supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -21443,15 +21416,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/css-blank-pseudo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -21536,15 +21500,6 @@ "node": ">=4" } }, - "node_modules/css-has-pseudo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-prefers-color-scheme": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", @@ -21583,15 +21538,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/css-prefers-color-scheme/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", @@ -21620,14 +21566,6 @@ "node": ">=8.0.0" } }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -21787,14 +21725,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/cssnano-util-raw-cache/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cssnano-util-same-parent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", @@ -21842,14 +21772,6 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -23257,18 +23179,6 @@ "source-map": "~0.6.1" } }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -28855,16 +28765,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", @@ -30203,16 +30103,6 @@ "node": ">=8" } }, - "node_modules/jest-jasmine2/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jest-jasmine2/node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -35760,15 +35650,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -36065,6 +35956,34 @@ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.1.tgz", "integrity": "sha512-qsHJle3GU3CmVx7pUoXcghX4sRN+vINkbLdH611T8ZlsP//grzqVW87BSUgOZeSAD4q7ZdZicdwNe/20U2janA==" }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/nextjs-bundle-analysis": { "version": "0.5.0-canary.1", "resolved": "https://registry.npmjs.org/nextjs-bundle-analysis/-/nextjs-bundle-analysis-0.5.0-canary.1.tgz", @@ -38404,9 +38323,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -38421,10 +38340,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -38463,15 +38383,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-attribute-case-insensitive/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-browser-comments": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", @@ -38516,14 +38427,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-calc/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-clamp": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", @@ -38690,15 +38593,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-color-functional-notation/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-color-gray": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", @@ -38750,15 +38644,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-color-gray/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-color-hex-alpha": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", @@ -38809,15 +38694,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-color-hex-alpha/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-color-mod-function": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", @@ -38869,15 +38745,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-color-mod-function/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-color-rebeccapurple": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", @@ -38928,15 +38795,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-color-rebeccapurple/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-colormin": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", @@ -39004,15 +38862,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-custom-media/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-custom-properties": { "version": "8.0.11", "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", @@ -39063,15 +38912,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-custom-properties/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-custom-selectors": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", @@ -39134,15 +38974,6 @@ "node": ">=4" } }, - "node_modules/postcss-custom-selectors/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-dir-pseudo-class": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", @@ -39205,15 +39036,6 @@ "node": ">=4" } }, - "node_modules/postcss-dir-pseudo-class/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-discard-comments": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", @@ -39311,15 +39133,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-double-position-gradients/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-env-function": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", @@ -39370,15 +39183,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-env-function/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", @@ -39423,15 +39227,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-focus-visible/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-focus-within": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", @@ -39467,15 +39262,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-focus-within/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-font-variant": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", @@ -39508,15 +39294,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-font-variant/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-gap-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", @@ -39552,15 +39329,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-gap-properties/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-html": { "version": "0.36.0", "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", @@ -39624,15 +39392,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-image-set-function/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-import": { "version": "16.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz", @@ -39681,15 +39440,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-initial/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-lab-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", @@ -39741,15 +39491,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-lab-function/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-less": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", @@ -39785,15 +39526,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-load-config": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", @@ -39878,15 +39610,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-logical/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-media-minmax": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", @@ -39922,15 +39645,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-media-minmax/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -40120,15 +39834,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-nesting/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-normalize": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", @@ -40343,15 +40048,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-overflow-shorthand/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-page-break": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", @@ -40384,15 +40080,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-page-break/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-place": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", @@ -40443,15 +40130,6 @@ "node": ">=6.14.4" } }, - "node_modules/postcss-place/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-preset-env": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", @@ -40523,15 +40201,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-preset-env/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-pseudo-class-any-link": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", @@ -40594,15 +40263,6 @@ "node": ">=4" } }, - "node_modules/postcss-pseudo-class-any-link/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-reduce-initial": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", @@ -40664,15 +40324,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-replace-overflow-wrap/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-reporter": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz", @@ -40739,15 +40390,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-safe-parser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-sass": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", @@ -40781,15 +40423,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-sass/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-scss": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", @@ -40825,15 +40458,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-scss/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-selector-matches": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", @@ -40867,15 +40491,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-selector-matches/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-selector-not": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", @@ -40909,15 +40524,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-selector-not/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", @@ -40966,15 +40572,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-sorting/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss-svgo": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", @@ -41226,15 +40823,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss-values-parser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/posthog-js": { "version": "1.249.1", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.249.1.tgz", @@ -43579,6 +43167,15 @@ "semver": "bin/semver" } }, + "node_modules/remark-mdx/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/remark-parse": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", @@ -46498,6 +46095,16 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -46530,17 +46137,19 @@ "peer": true }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "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==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -46568,14 +46177,6 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", @@ -47859,15 +47460,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/stylelint-order/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stylelint-use-nesting": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stylelint-use-nesting/-/stylelint-use-nesting-3.0.0.tgz", @@ -48165,15 +47757,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stylelint/node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -48239,15 +47822,6 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/sugarss/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -49997,9 +49571,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "node_modules/unfetch": { "version": "4.2.0", @@ -51155,34 +50730,6 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/vite/node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/vitest": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", diff --git a/package.json b/package.json index 87faddd658..9d28d7c00d 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,7 @@ "@types/google-spreadsheet": "^3.2.2", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^8.5.9", - "@types/node": "^18.18.9", - "@types/react": "^18.2.6", + "@types/node": "^20.19.0", "@types/react-dom": "^18.2.4", "@types/semver": "^7.3.9", "@types/shortid": "^0.0.29", @@ -80,7 +79,7 @@ "next-remote-watch": "^2.0.0", "nextjs-bundle-analysis": "^0.5.0-canary.1", "nock": "^14.0.0-beta.5", - "postcss": "^8.4.5", + "postcss": "^8.5.6", "postcss-flexbugs-fixes": "^5.0.2", "postcss-normalize": "^10.0.1", "postcss-preset-env": "^6.7.0", @@ -88,6 +87,7 @@ "rimraf": "^5.0.5", "sass": "^1.75.0", "simple-git-hooks": "^2.7.0", + "source-map": "^0.6.1", "stylelint": "^13.8.0", "typescript": "^5.4.5", "typescript-eslint": "^8.25.0", diff --git a/public/img/sandbox/hashicorp.png b/public/img/sandbox/hashicorp.png new file mode 100644 index 0000000000..7c7104bb6a Binary files /dev/null and b/public/img/sandbox/hashicorp.png differ diff --git a/src/components/dev-dot-content/mdx-components/mdx-alert/index.test.tsx b/src/components/dev-dot-content/mdx-components/mdx-alert/index.test.tsx index cb19b9b715..5a2eb704c3 100644 --- a/src/components/dev-dot-content/mdx-components/mdx-alert/index.test.tsx +++ b/src/components/dev-dot-content/mdx-components/mdx-alert/index.test.tsx @@ -38,11 +38,8 @@ describe('`MdxInlineAlert` Component', () => { const { queryByText, queryByTestId } = render( {data.body} ) - // icon renders expect(queryByTestId('icon')).toBeInTheDocument() - // default title renders expect(queryByText(data.title)).toBeInTheDocument() - // body text renders expect(queryByText(data.body)).toBeInTheDocument() }) @@ -51,11 +48,8 @@ describe('`MdxInlineAlert` Component', () => { const { queryByText, queryByTestId } = render( {data.body} ) - // icon renders expect(queryByTestId('icon')).toBeInTheDocument() - // default title renders expect(queryByText(data.title)).toBeInTheDocument() - // body text renders expect(queryByText(data.body)).toBeInTheDocument() }) @@ -64,11 +58,8 @@ describe('`MdxInlineAlert` Component', () => { const { queryByText, queryByTestId } = render( {data.body} ) - // icon renders expect(queryByTestId('icon')).toBeInTheDocument() - // default title renders expect(queryByText(data.title)).toBeInTheDocument() - // body text renders expect(queryByText(data.body)).toBeInTheDocument() }) @@ -77,11 +68,8 @@ describe('`MdxInlineAlert` Component', () => { const { queryByText, queryByTestId } = render( {data.body} ) - // icon renders expect(queryByTestId('icon')).toBeInTheDocument() - // default title renders expect(queryByText(data.title)).toBeInTheDocument() - // body text renders expect(queryByText(data.body)).toBeInTheDocument() }) @@ -94,32 +82,42 @@ describe('`MdxInlineAlert` Component', () => { expect(queryByText(TEST_DATA.customTitle)).toBeInTheDocument() }) - it('throws when children are not passed', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => render()).toThrowError( - TEST_DATA.errors.noChildren - ) + it('renders error fallback when children are empty', () => { + const { getByText } = render({''}) + + // Should render the error fallback UI instead of throwing + expect(getByText('Alert Error')).toBeInTheDocument() + expect( + getByText( + 'There was an error rendering this alert. Please check the alert configuration.' + ) + ).toBeInTheDocument() }) - it('throws when type is invalid', () => { - expect(() => - render( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - I am an MdxInlineAlert + it('renders error fallback when type is invalid', () => { + const invalidType = 'doughnut' as 'tip' | 'highlight' | 'note' | 'warning' + const { getByText } = render( + I am an MdxInlineAlert + ) + + // Should render the error fallback UI instead of throwing + expect(getByText('Alert Error')).toBeInTheDocument() + expect( + getByText( + 'There was an error rendering this alert. Please check the alert configuration.' ) - ).toThrowError(TEST_DATA.errors.invalidType) + ).toBeInTheDocument() }) it('renders multiple children', () => { - expect(() => - render( - -

Liquorice cake marzipan danish brownie

-

Lollipop gingerbread bear claw muffin croissant

-
- ) - ).not.toThrowError() + const { getByText } = render( + +

This may render multiple children.

+

This should get multiple children.

+
+ ) + + expect(getByText('This may render multiple children.')).toBeInTheDocument() + expect(getByText('This should get multiple children.')).toBeInTheDocument() }) }) diff --git a/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx b/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx index 7e4fc9a4ff..a3c79a37cf 100644 --- a/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx +++ b/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx @@ -4,6 +4,7 @@ */ import InlineAlert from 'components/inline-alert' +import { withErrorBoundary } from 'components/error-boundary' import { IconInfo24 } from '@hashicorp/flight-icons/svg-react/info-24' import { IconAlertTriangle24 } from '@hashicorp/flight-icons/svg-react/alert-triangle-24' import { IconAlertDiamond24 } from '@hashicorp/flight-icons/svg-react/alert-diamond-24' @@ -22,7 +23,7 @@ const ALERT_DATA: MdxInlineAlertData = { }, } -export function MdxInlineAlert({ +function MdxInlineAlertBase({ children, title, type = 'tip', @@ -55,4 +56,38 @@ export function MdxInlineAlert({ ) } +// Create a fallback UI for when the alert component fails +const AlertErrorFallback = ( +
+ } + title="Alert Error" + description="There was an error rendering this alert. Please check the alert configuration." + color="critical" + className={s.typographyOverride} + /> +
+) + +export const MdxInlineAlert = withErrorBoundary( + MdxInlineAlertBase, + AlertErrorFallback, + (error, errorInfo) => { + if (typeof window !== 'undefined' && window.posthog?.capture) { + window.posthog.capture('mdx_component_error', { + component_name: 'MdxInlineAlert', + error_message: error.message, + error_stack: error.stack, + component_stack: errorInfo?.componentStack, + timestamp: new Date().toISOString(), + page_url: window.location.href, + }) + } + + if (process.env.NODE_ENV === 'development') { + console.warn('MdxInlineAlert validation error:', error.message, errorInfo) + } + } +) + export { MdxHighlight, MdxTip, MdxNote, MdxWarning } diff --git a/src/components/error-boundary/__tests__/error-boundary.test.tsx b/src/components/error-boundary/__tests__/error-boundary.test.tsx new file mode 100644 index 0000000000..1b72108e1a --- /dev/null +++ b/src/components/error-boundary/__tests__/error-boundary.test.tsx @@ -0,0 +1,126 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import React from 'react' +import { render, screen } from '@testing-library/react' +import { vi } from 'vitest' +import { ErrorBoundary, withErrorBoundary } from '../index' + +// Test component that throws an error +const ThrowError = ({ shouldThrow = false }: { shouldThrow?: boolean }) => { + if (shouldThrow) { + throw new Error('Test error') + } + return
No error
+} + +describe('ErrorBoundary', () => { + // Suppress console.error during tests to avoid noise + const originalError = console.error + beforeAll(() => { + console.error = vi.fn() + }) + afterAll(() => { + console.error = originalError + }) + + it('renders children when there is no error', () => { + render( + + + + ) + + expect(screen.getByText('No error')).toBeInTheDocument() + }) + + it('renders default error message when child throws error', () => { + render( + + + + ) + + expect(screen.getByText('Something went wrong.')).toBeInTheDocument() + }) + + it('renders custom fallback when provided', () => { + const customFallback =
Custom error message
+ + render( + + + + ) + + expect(screen.getByText('Custom error message')).toBeInTheDocument() + expect(screen.queryByText('Something went wrong.')).not.toBeInTheDocument() + }) + + it('calls onError callback when error occurs', () => { + const onError = vi.fn() + + render( + + + + ) + + expect(onError).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ + componentStack: expect.any(String) + }) + ) + }) + + it('shows error details in development mode', () => { + // Mock NODE_ENV for this test + vi.stubEnv('NODE_ENV', 'development') + + render( + + + + ) + + expect(screen.getByText('Error details (development only)')).toBeInTheDocument() + + // Restore + vi.unstubAllEnvs() + }) +}) + +describe('withErrorBoundary HOC', () => { + const originalError = console.error + beforeAll(() => { + console.error = vi.fn() + }) + afterAll(() => { + console.error = originalError + }) + + it('wraps component with error boundary', () => { + const WrappedComponent = withErrorBoundary(ThrowError) + + render() + expect(screen.getByText('No error')).toBeInTheDocument() + }) + + it('catches errors in wrapped component', () => { + const WrappedComponent = withErrorBoundary(ThrowError) + + render() + expect(screen.getByText('Something went wrong.')).toBeInTheDocument() + }) + + it('uses custom fallback with HOC', () => { + const customFallback =
HOC custom error
+ const WrappedComponent = withErrorBoundary(ThrowError, customFallback) + + render() + expect(screen.getByText('HOC custom error')).toBeInTheDocument() + }) +}) diff --git a/src/components/error-boundary/index.tsx b/src/components/error-boundary/index.tsx new file mode 100644 index 0000000000..c89937a9ed --- /dev/null +++ b/src/components/error-boundary/index.tsx @@ -0,0 +1,216 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import React, { + Component, + ErrorInfo, + ReactNode, + useCallback, + useState, + useRef, + createContext, + useContext, +} from 'react' + +interface ErrorBoundaryProps { + children: ReactNode + fallback?: ReactNode | ((error: Error, errorInfo: ErrorInfo) => ReactNode) + onError?: (error: Error, errorInfo: ErrorInfo) => void + resetOnPropsChange?: boolean + resetKeys?: Array +} + +interface ErrorBoundaryState { + hasError: boolean + error?: Error + errorInfo?: ErrorInfo +} + +interface ErrorBoundaryContextType { + resetErrorBoundary: () => void + captureError: (error: Error) => void +} + +const ErrorBoundaryContext = createContext( + null +) + +/** + * Error boundary component that catches JavaScript errors anywhere in the child component tree, + * logs those errors, and displays a fallback UI. + */ +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo) + this.setState({ errorInfo }) + + if (this.props.onError) { + this.props.onError(error, errorInfo) + } + } + + componentDidUpdate(prevProps: ErrorBoundaryProps) { + if (this.props.resetKeys && prevProps.resetKeys) { + const hasResetKeyChanged = this.props.resetKeys.some( + (key, index) => key !== prevProps.resetKeys![index] + ) + + if (hasResetKeyChanged && this.state.hasError) { + this.setState({ + hasError: false, + error: undefined, + errorInfo: undefined, + }) + } + } + } + + render(): ReactNode { + if (this.state.hasError && this.state.error) { + if (typeof this.props.fallback === 'function') { + return this.props.fallback(this.state.error, this.state.errorInfo!) + } + + return ( + this.props.fallback || ( +
+ Something went wrong. + {process.env.NODE_ENV === 'development' && this.state.error && ( +
+ Error details (development only) +
+									{this.state.error.message}
+									{this.state.errorInfo && (
+										<>
+											
+
+ Component Stack: + {this.state.errorInfo.componentStack} + + )} +
+
+ )} +
+ ) + ) + } + + return this.props.children + } +} + +export function useErrorBoundary() { + const [resetKey, setResetKey] = useState(0) + const errorRef = useRef(null) + + const resetErrorBoundary = useCallback(() => { + // Increment reset key to trigger error boundary reset + setResetKey((prev) => prev + 1) + errorRef.current = null + }, []) + + const captureError = useCallback((error: Error) => { + errorRef.current = error + throw error + }, []) + + const context = useContext(ErrorBoundaryContext) + + if (context) { + return context + } + + return { + resetErrorBoundary, + captureError, + resetKey, + } +} + +interface ErrorBoundaryWrapperProps + extends Omit { + children: ReactNode +} + +export const ErrorBoundaryWrapper: React.FC = ({ + children, + ...props +}) => { + const [resetKey, setResetKey] = useState(0) + + const resetErrorBoundary = useCallback(() => { + setResetKey((prev) => prev + 1) + }, []) + + const captureError = useCallback((error: Error) => { + throw error + }, []) + + const contextValue: ErrorBoundaryContextType = { + resetErrorBoundary, + captureError, + } + + return ( + + + {children} + + + ) +} + +export function useErrorBoundaryContext() { + const context = useContext(ErrorBoundaryContext) + if (!context) { + throw new Error( + 'useErrorBoundaryContext must be used within an ErrorBoundaryWrapper' + ) + } + return context +} + +export const withErrorBoundary = ( + Component: React.ComponentType, + fallback?: ReactNode | ((error: Error, errorInfo: ErrorInfo) => ReactNode), + onError?: (error: Error, errorInfo: ErrorInfo) => void +) => { + const WrappedComponent = (props: T) => ( + + + + ) + + WrappedComponent.displayName = `withErrorBoundary(${ + Component.displayName || Component.name + })` + + return WrappedComponent +} + +export default ErrorBoundaryWrapper + +export { ErrorBoundary as ErrorBoundaryClass } diff --git a/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx b/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx new file mode 100644 index 0000000000..760f5519de --- /dev/null +++ b/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx @@ -0,0 +1,270 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, act } from '@testing-library/react' +import EmbedElement from '../index' + +import { useInstruqtEmbed } from 'contexts/instruqt-lab' +import { useRouter } from 'next/router' +import { trackSandboxEvent } from 'lib/posthog-events' + +vi.mock('contexts/instruqt-lab', () => ({ + useInstruqtEmbed: vi.fn(), +})) + +vi.mock('next/router', () => ({ + useRouter: vi.fn(), +})) + +vi.mock('lib/posthog-events', () => ({ + trackSandboxEvent: vi.fn(), + SANDBOX_EVENT: { + SANDBOX_LOADED: 'sandbox_loaded', + SANDBOX_ERROR: 'sandbox_error', + SANDBOX_RETRY: 'sandbox_retry', + }, +})) + +const mockUseInstruqtEmbed = vi.mocked(useInstruqtEmbed) +const mockUseRouter = vi.mocked(useRouter) +const mockTrackSandboxEvent = vi.mocked(trackSandboxEvent) + +describe('EmbedElement', () => { + const createMockRouter = () => ({ + asPath: '/test-path', + route: '/test-path', + pathname: '/test-path', + query: {}, + basePath: '', + isLocaleDomain: false, + push: vi.fn().mockResolvedValue(true), + replace: vi.fn().mockResolvedValue(true), + reload: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + prefetch: vi.fn().mockResolvedValue(undefined), + beforePopState: vi.fn(), + events: { + on: vi.fn(), + off: vi.fn(), + emit: vi.fn(), + }, + isFallback: false, + isReady: true, + isPreview: false, + locale: undefined, + locales: undefined, + defaultLocale: undefined, + domainLocales: undefined, + }) + + const createMockInstruqtContext = (overrides = {}) => ({ + labId: 'test-lab-id', + active: true, + setActive: vi.fn(), + openLab: vi.fn(), + closeLab: vi.fn(), + hasConfigError: false, + configErrors: [], + ...overrides, + }) + + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + + mockUseRouter.mockReturnValue(createMockRouter()) + mockUseInstruqtEmbed.mockReturnValue(createMockInstruqtContext()) + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllTimers() + }) + + describe('rendering and props', () => { + it('renders an iframe with the correct props', () => { + render() + + const iframe = screen.getByTitle('Instruqt Lab Environment: test-lab-id') + + expect(iframe).toBeInTheDocument() + expect(iframe.tagName).toBe('IFRAME') + expect(iframe).toHaveAttribute( + 'src', + 'https://play.instruqt.com/embed/test-lab-id' + ) + expect(iframe).toHaveAttribute( + 'sandbox', + 'allow-same-origin allow-scripts allow-popups allow-forms allow-modals' + ) + expect(iframe).toHaveAttribute( + 'aria-label', + 'Interactive lab environment for test-lab-id' + ) + expect(iframe).toHaveAttribute('allow', 'fullscreen') + }) + + it('shows loading state initially', () => { + render() + + expect(screen.getByText('Loading your sandbox...')).toBeInTheDocument() + expect( + screen.getByText('This may take a few moments') + ).toBeInTheDocument() + expect( + screen.getByRole('status', { name: /loading sandbox/i }) + ).toBeInTheDocument() + }) + }) + + describe('state transitions', () => { + it('hides loading state when iframe loads', () => { + render() + + const iframe = screen.getByTitle('Instruqt Lab Environment: test-lab-id') + + fireEvent.load(iframe) + + expect( + screen.queryByText('Loading your sandbox...') + ).not.toBeInTheDocument() + + expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_loaded', { + labId: 'test-lab-id', + page: '/test-path', + }) + }) + + it('shows error state when timeout occurs', () => { + render() + + act(() => { + vi.advanceTimersByTime(30000) + }) + + expect(screen.getByText('Unable to Load Sandbox')).toBeInTheDocument() + expect( + screen.getAllByText(/Failed to load sandbox/).length + ).toBeGreaterThan(0) + expect(screen.getByRole('alert')).toBeInTheDocument() + }) + + it('allows retry when error occurs', () => { + render() + + act(() => { + vi.advanceTimersByTime(30000) + }) + + const retryButton = screen.getByRole('button', { name: /try again/i }) + expect(retryButton).toBeInTheDocument() + + act(() => { + fireEvent.click(retryButton) + }) + + expect(screen.getByText('Loading your sandbox...')).toBeInTheDocument() + + expect(mockTrackSandboxEvent).toHaveBeenCalledWith( + 'sandbox_retry', + expect.objectContaining({ + labId: 'test-lab-id', + page: '/test-path', + }) + ) + }) + }) + + describe('edge cases and error handling', () => { + it('shows no lab selected state when labId is null', () => { + mockUseInstruqtEmbed.mockReturnValue( + createMockInstruqtContext({ + active: true, + labId: null, + }) + ) + + render() + + expect(screen.getByText('No Lab Selected')).toBeInTheDocument() + expect( + screen.getByText(/Please select a lab from the sandbox menu/) + ).toBeInTheDocument() + expect(screen.getByRole('status')).toBeInTheDocument() + }) + + it('has the correct styles when not active', () => { + mockUseInstruqtEmbed.mockReturnValue( + createMockInstruqtContext({ + active: false, + labId: 'test-lab-id', + }) + ) + + render() + + const container = screen + .getByTitle('Instruqt Lab Environment: test-lab-id') + .closest('div') + expect(container).toHaveClass('_hide_4118c4') + }) + + it('shows Try Again button when error occurs', () => { + render() + + // Trigger timeout to get error state + act(() => { + vi.advanceTimersByTime(30000) + }) + + // Verify retry button is present + expect( + screen.getByRole('button', { name: /try again/i }) + ).toBeInTheDocument() + }) + }) + + describe('accessibility', () => { + it('includes proper accessibility attributes', () => { + render() + + const iframe = screen.getByTitle('Instruqt Lab Environment: test-lab-id') + + // Verify accessibility attributes + expect(iframe).toHaveAttribute( + 'aria-label', + 'Interactive lab environment for test-lab-id' + ) + expect(iframe).toHaveAttribute('aria-live', 'polite') + expect(iframe).toHaveAttribute('aria-busy', 'true') // Initially loading + + // Verify screen reader announcements + expect( + screen.getByText('Loading sandbox environment') + ).toBeInTheDocument() + + // Verify loading spinner is hidden from screen readers + const spinner = document.querySelector('[aria-hidden="true"]') + expect(spinner).toBeInTheDocument() + }) + + it('provides proper ARIA roles for different states', () => { + render() + + // Loading state + expect( + screen.getByRole('status', { name: /loading sandbox/i }) + ).toBeInTheDocument() + + act(() => { + vi.advanceTimersByTime(30000) + }) + + expect(screen.getByRole('alert')).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/lab-embed/embed-element/embed-element.module.css b/src/components/lab-embed/embed-element/embed-element.module.css index daafb8ae03..50a100af34 100644 --- a/src/components/lab-embed/embed-element/embed-element.module.css +++ b/src/components/lab-embed/embed-element/embed-element.module.css @@ -11,6 +11,141 @@ } } +.embedContainer { + width: 100%; + height: 100%; + position: relative; +} + .hide { display: none; } + +.loadingContainer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: var(--token-color-surface-primary); + border: 1px solid var(--token-color-border-primary); + border-radius: 8px; + padding: 2rem; + z-index: 1; +} + +.loadingSpinner { + width: 40px; + height: 40px; + border: 3px solid var(--token-color-border-primary); + border-top: 3px solid var(--token-color-palette-blue-200); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loadingText { + text-align: center; + color: var(--token-color-foreground-primary); + + & h3 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 500; + } + + & p { + margin: 0; + color: var(--token-color-foreground-faint); + font-size: 0.875rem; + } +} + +.errorContainer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--token-color-surface-primary); + border: 1px solid var(--token-color-border-critical); + border-radius: 8px; + padding: 2rem; + z-index: 1; + + &.fullHeight { + position: relative; + height: 100%; + } +} + +.errorContent { + text-align: center; + max-width: 400px; + + & h3 { + margin: 0 0 1rem 0; + color: var(--token-color-foreground-critical); + font-size: 1.125rem; + font-weight: 500; + } + + & p { + margin: 0 0 1.5rem 0; + color: var(--token-color-foreground-primary); + font-size: 0.875rem; + line-height: 1.5; + } +} + +.retryButton { + background-color: var(--token-color-palette-blue-200); + color: white; + border: none; + border-radius: 4px; + padding: 0.75rem 1.5rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--token-color-palette-blue-300); + } + + &:focus { + outline: 2px solid var(--token-color-focus-action-internal); + outline-offset: 2px; + } + + &:disabled { + background-color: var(--token-color-surface-faint); + color: var(--token-color-foreground-disabled); + cursor: not-allowed; + } +} + +/* Screen reader only content */ +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} diff --git a/src/components/lab-embed/embed-element/index.tsx b/src/components/lab-embed/embed-element/index.tsx index c22716a85d..6d08d61f8b 100644 --- a/src/components/lab-embed/embed-element/index.tsx +++ b/src/components/lab-embed/embed-element/index.tsx @@ -3,35 +3,265 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { MutableRefObject, useRef, useEffect } from 'react' +import { useRef, useEffect, useState, useCallback, memo } from 'react' import classNames from 'classnames' +import { useRouter } from 'next/router' import { useInstruqtEmbed } from 'contexts/instruqt-lab' +import { trackSandboxEvent, SANDBOX_EVENT } from 'lib/posthog-events' import s from './embed-element.module.css' -export default function EmbedElement(): JSX.Element { - const ref: MutableRefObject = useRef() +interface EmbedState { + isLoading: boolean + hasError: boolean + errorMessage: string + retryCount: number +} + +/** + * EmbedElement component for displaying Instruqt lab environments + * + */ +const EmbedElement = memo(function EmbedElement(): JSX.Element { + const ref = useRef(null) + const { active, labId } = useInstruqtEmbed() + const router = useRouter() + + const [embedState, setEmbedState] = useState({ + isLoading: true, + hasError: false, + errorMessage: '', + retryCount: 0, + }) + + const resetEmbedState = useCallback(() => { + setEmbedState({ + isLoading: true, + hasError: false, + errorMessage: '', + retryCount: 0, + }) + }, []) + + const handleIframeLoad = useCallback(() => { + setEmbedState((prev) => ({ + ...prev, + isLoading: false, + hasError: false, + })) + + if (labId) { + trackSandboxEvent(SANDBOX_EVENT.SANDBOX_LOADED, { + labId, + page: router.asPath, + }) + } + }, [labId, router.asPath]) + + const handleIframeError = useCallback(() => { + setEmbedState((prev) => ({ + ...prev, + isLoading: false, + hasError: true, + errorMessage: + 'Failed to load sandbox. Please check your internet connection and try again.', + })) + + // Track error + if (labId) { + trackSandboxEvent(SANDBOX_EVENT.SANDBOX_ERROR, { + labId, + page: router.asPath, + error: 'iframe_load_error', + retryCount: embedState.retryCount, + }) + } + }, [labId, router.asPath, embedState.retryCount]) + + const handleRetry = useCallback(() => { + if (embedState.retryCount < 3) { + setEmbedState((prev) => ({ + ...prev, + isLoading: true, + hasError: false, + errorMessage: '', + retryCount: prev.retryCount + 1, + })) + + if (labId) { + trackSandboxEvent(SANDBOX_EVENT.SANDBOX_RETRY, { + labId, + page: router.asPath, + retryCount: embedState.retryCount + 1, + }) + } + + const iframe = ref.current + if (iframe) { + const expectedSrc = `https://play.instruqt.com/embed/${labId}` + iframe.src = '' + const timeoutId = setTimeout(() => { + if (ref.current) { + ref.current.src = expectedSrc + } + }, 100) + + return () => clearTimeout(timeoutId) + } + } else { + setEmbedState((prev) => ({ + ...prev, + errorMessage: + 'Maximum retry attempts reached. Please refresh the page or try again later.', + })) + } + }, [embedState.retryCount, labId, router.asPath]) + + // Reset state when labId changes + useEffect(() => { + if (labId) { + resetEmbedState() + } + }, [labId, resetEmbedState]) useEffect(() => { - if (!ref.current) { + if (!labId || !ref.current) { return } - // ensures that focus properly shifts when the lab component is mounted - ref.current.focus() - }, []) + const iframe = ref.current + const expectedSrc = `https://play.instruqt.com/embed/${labId}` - const { active, labId } = useInstruqtEmbed() + if (iframe.src !== expectedSrc) { + iframe.src = expectedSrc + } + }, [labId]) + + useEffect(() => { + if (!embedState.isLoading || !labId) { + return + } + + const timeoutId = setTimeout(() => { + setEmbedState((current) => { + if (current.isLoading) { + return { + ...current, + isLoading: false, + hasError: true, + errorMessage: + 'Failed to load sandbox. Please check your internet connection and try again.', + } + } + return current + }) + }, 30000) + + return () => clearTimeout(timeoutId) + }, [embedState.isLoading, labId]) + + useEffect(() => { + const iframe = ref.current + if (!iframe || !active) { + return + } + + iframe.focus() + }, [active]) + + if (!labId) { + return ( +
+
+

No Lab Selected

+

Please select a lab from the sandbox menu to get started.

+
+
+ ) + } return ( -