diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index fc80666..f9e6837 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [24.x] steps: - name: Checkout 🛎️ diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 167b2c6..49bd4ed 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [24.x] steps: - name: Checkout 🛎️ diff --git a/package-lock.json b/package-lock.json index 7268400..db62953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,127 +8,67 @@ "name": "codestar-website-next", "version": "1.0.0", "dependencies": { - "@mdx-js/loader": "^3.1.0", - "@mdx-js/react": "^3.1.0", - "@next/mdx": "^15.1.7", - "fast-xml-parser": "^5.0.1", - "html-to-text": "^9.0.5", + "@mdx-js/loader": "3.1.0", + "@mdx-js/react": "3.1.0", + "@next/mdx": "15.1.7", + "fast-xml-parser": "5.0.1", + "graphql-request": "7.2.0", + "html-to-text": "9.0.5", "next": "15.1.7", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "remark-frontmatter": "^5.0.0", - "remark-mdx-frontmatter": "^5.0.0" + "react": "19.0.0", + "react-dom": "19.0.0", + "remark-frontmatter": "5.0.0", + "remark-mdx-frontmatter": "5.0.0" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/html-to-text": "^9.0.4", - "@types/mdx": "^2.0.13", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", + "@eslint/eslintrc": "3.2.0", + "@types/html-to-text": "9.0.4", + "@types/mdx": "2.0.13", + "@types/node": "20.17.19", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "eslint": "9.20.1", "eslint-config-next": "15.1.7", - "sass": "^1.85.0", - "typescript": "^5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "optional": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "sass": "1.85.0", + "typescript": "5.7.3" } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "license": "MIT", "optional": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", - "optional": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "optional": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "optional": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -148,13 +88,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -244,6 +185,16 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -258,27 +209,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -356,27 +309,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "optional": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", "optional": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", "optional": true, "engines": { "node": ">=6.9.0" @@ -397,25 +353,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1537,54 +1495,48 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", "optional": true, "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1718,6 +1670,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2122,17 +2083,25 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", "optional": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2144,21 +2113,12 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "optional": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2171,9 +2131,10 @@ "optional": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "optional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2256,6 +2217,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -2840,8 +2802,8 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2851,17 +2813,18 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -2928,6 +2891,7 @@ "version": "19.0.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2980,6 +2944,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", @@ -3179,8 +3144,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -3190,29 +3155,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3223,15 +3188,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3243,8 +3208,8 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -3253,8 +3218,8 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -3263,15 +3228,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3287,8 +3252,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -3301,8 +3266,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -3314,8 +3279,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -3329,8 +3294,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -3340,20 +3305,22 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "optional": true, - "peer": true + "license": "BSD-3-Clause", + "optional": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "optional": true, - "peer": true + "license": "Apache-2.0", + "optional": true }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3361,6 +3328,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3374,6 +3354,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "devOptional": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3389,8 +3370,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -3407,8 +3388,8 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3424,8 +3405,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -3858,6 +3839,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3889,38 +3880,6 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "optional": true, - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3995,9 +3954,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001700", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", - "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "funding": [ { "type": "opencollective", @@ -4011,7 +3970,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -4093,8 +4053,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=6.0" } @@ -4181,8 +4141,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/commondir": { "version": "1.0.1", @@ -4474,9 +4434,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.102", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", - "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", + "version": "1.5.255", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", + "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", + "license": "ISC", "optional": true }, "node_modules/emoji-regex": { @@ -4629,11 +4590,11 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "optional": true, - "peer": true + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "optional": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -4725,6 +4686,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -4747,6 +4709,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4913,6 +4876,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -5252,8 +5216,8 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=0.8.x" } @@ -5310,9 +5274,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -5323,8 +5287,8 @@ "url": "https://opencollective.com/fastify" } ], - "optional": true, - "peer": true + "license": "BSD-3-Clause", + "optional": true }, "node_modules/fast-xml-parser": { "version": "5.0.1", @@ -5596,8 +5560,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "optional": true, - "peer": true + "license": "BSD-2-Clause", + "optional": true }, "node_modules/globals": { "version": "14.0.0", @@ -5651,6 +5615,28 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.2.0.tgz", + "integrity": "sha512-0GR7eQHBFYz372u9lxS16cOtEekFlZYB2qOyq8wDvzRmdRSJ0mgUVX1tzNcIzk3G+4NY+mGtSz411wZdeDF/+A==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6369,8 +6355,8 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -6384,8 +6370,8 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6436,8 +6422,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -6536,13 +6522,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -6863,8 +6853,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7477,8 +7467,8 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -7487,8 +7477,8 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7549,8 +7539,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/next": { "version": "15.1.7", @@ -7613,9 +7603,10 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT", "optional": true }, "node_modules/object-assign": { @@ -8090,8 +8081,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -8100,6 +8091,7 @@ "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8108,6 +8100,7 @@ "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -8409,8 +8402,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8523,8 +8516,8 @@ "url": "https://feross.org/support" } ], - "optional": true, - "peer": true + "license": "MIT", + "optional": true }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -8564,6 +8557,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "devOptional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -8629,8 +8623,8 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -9085,23 +9079,28 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -9113,11 +9112,11 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -9147,29 +9146,12 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "optional": true, - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9177,19 +9159,12 @@ "ajv": "^8.8.2" } }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "optional": true, - "peer": true - }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9239,6 +9214,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -9401,6 +9377,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9567,9 +9544,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "funding": [ { "type": "opencollective", @@ -9584,6 +9561,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "optional": true, "dependencies": { "escalade": "^3.2.0", @@ -9632,11 +9610,11 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -9645,144 +9623,16 @@ "node": ">=10.13.0" } }, - "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", - "optional": true, - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "optional": true, - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "optional": true, - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "optional": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "optional": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "optional": true, - "peer": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", - "optional": true, - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 69e1294..156eebc 100644 --- a/package.json +++ b/package.json @@ -10,27 +10,28 @@ "lint": "next lint" }, "dependencies": { - "@mdx-js/loader": "^3.1.0", - "@mdx-js/react": "^3.1.0", - "@next/mdx": "^15.1.7", - "fast-xml-parser": "^5.0.1", - "html-to-text": "^9.0.5", + "@mdx-js/loader": "3.1.0", + "@mdx-js/react": "3.1.0", + "@next/mdx": "15.1.7", + "fast-xml-parser": "5.0.1", + "graphql-request": "7.2.0", + "html-to-text": "9.0.5", "next": "15.1.7", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "remark-frontmatter": "^5.0.0", - "remark-mdx-frontmatter": "^5.0.0" + "react": "19.0.0", + "react-dom": "19.0.0", + "remark-frontmatter": "5.0.0", + "remark-mdx-frontmatter": "5.0.0" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/html-to-text": "^9.0.4", - "@types/mdx": "^2.0.13", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", + "@eslint/eslintrc": "3.2.0", + "@types/html-to-text": "9.0.4", + "@types/mdx": "2.0.13", + "@types/node": "20.17.19", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "eslint": "9.20.1", "eslint-config-next": "15.1.7", - "sass": "^1.85.0", - "typescript": "^5" + "sass": "1.85.0", + "typescript": "5.7.3" } } diff --git a/public/articles/images/16mEccGy9WNOJ5zU_8a35DA.png b/public/articles/images/16mEccGy9WNOJ5zU_8a35DA.png new file mode 100644 index 0000000..507fd90 Binary files /dev/null and b/public/articles/images/16mEccGy9WNOJ5zU_8a35DA.png differ diff --git a/public/articles/images/1Aj8UgvxNf7r4zTjs-JqL7A.jpeg b/public/articles/images/1Aj8UgvxNf7r4zTjs-JqL7A.jpeg new file mode 100644 index 0000000..dbe68e2 Binary files /dev/null and b/public/articles/images/1Aj8UgvxNf7r4zTjs-JqL7A.jpeg differ diff --git a/public/articles/images/1JmOikDAspLJuHPY07W1-Lw.png b/public/articles/images/1JmOikDAspLJuHPY07W1-Lw.png new file mode 100644 index 0000000..68434d0 Binary files /dev/null and b/public/articles/images/1JmOikDAspLJuHPY07W1-Lw.png differ diff --git a/public/articles/images/1LPCG2xlLiUdqcYJulzxvmw.png b/public/articles/images/1LPCG2xlLiUdqcYJulzxvmw.png new file mode 100644 index 0000000..75f4724 Binary files /dev/null and b/public/articles/images/1LPCG2xlLiUdqcYJulzxvmw.png differ diff --git a/public/articles/images/1NtVSxnFcGi9P5ddosigsSA.png b/public/articles/images/1NtVSxnFcGi9P5ddosigsSA.png new file mode 100644 index 0000000..044829e Binary files /dev/null and b/public/articles/images/1NtVSxnFcGi9P5ddosigsSA.png differ diff --git a/public/articles/images/1P93TyWUqN0foFjmMaB0ehQ.jpeg b/public/articles/images/1P93TyWUqN0foFjmMaB0ehQ.jpeg new file mode 100644 index 0000000..bbfe981 Binary files /dev/null and b/public/articles/images/1P93TyWUqN0foFjmMaB0ehQ.jpeg differ diff --git a/public/articles/images/1QJZ-f4Ptx1_eZ24nLZ4Kow.png b/public/articles/images/1QJZ-f4Ptx1_eZ24nLZ4Kow.png new file mode 100644 index 0000000..4cf7d6c Binary files /dev/null and b/public/articles/images/1QJZ-f4Ptx1_eZ24nLZ4Kow.png differ diff --git a/public/articles/images/1QvLkMzKtFw0DxqZ-pWiL8g.jpeg b/public/articles/images/1QvLkMzKtFw0DxqZ-pWiL8g.jpeg new file mode 100644 index 0000000..ab5e5d0 Binary files /dev/null and b/public/articles/images/1QvLkMzKtFw0DxqZ-pWiL8g.jpeg differ diff --git a/public/articles/images/1WHpL6IJd_gUROUmBUlXWnQ.png b/public/articles/images/1WHpL6IJd_gUROUmBUlXWnQ.png new file mode 100644 index 0000000..ad18596 Binary files /dev/null and b/public/articles/images/1WHpL6IJd_gUROUmBUlXWnQ.png differ diff --git a/public/articles/images/1WqOZsQ8xnX1iJg3PPHcilg.png b/public/articles/images/1WqOZsQ8xnX1iJg3PPHcilg.png new file mode 100644 index 0000000..7d34c73 Binary files /dev/null and b/public/articles/images/1WqOZsQ8xnX1iJg3PPHcilg.png differ diff --git a/public/articles/images/1ZEGWmf7hHjjJsDOXVB_c9A.png b/public/articles/images/1ZEGWmf7hHjjJsDOXVB_c9A.png new file mode 100644 index 0000000..9389af9 Binary files /dev/null and b/public/articles/images/1ZEGWmf7hHjjJsDOXVB_c9A.png differ diff --git a/public/articles/images/1cQGXudGNVjRGDM2uB-BuGA.jpeg b/public/articles/images/1cQGXudGNVjRGDM2uB-BuGA.jpeg new file mode 100644 index 0000000..c5c0733 Binary files /dev/null and b/public/articles/images/1cQGXudGNVjRGDM2uB-BuGA.jpeg differ diff --git a/public/articles/images/1g_5XkBVLaWIkvs7W_XHv8g.png b/public/articles/images/1g_5XkBVLaWIkvs7W_XHv8g.png new file mode 100644 index 0000000..bddc5c7 Binary files /dev/null and b/public/articles/images/1g_5XkBVLaWIkvs7W_XHv8g.png differ diff --git a/public/articles/images/1iBVlr8lOKvIzbqA8-qvHoQ.gif b/public/articles/images/1iBVlr8lOKvIzbqA8-qvHoQ.gif new file mode 100644 index 0000000..56537b3 Binary files /dev/null and b/public/articles/images/1iBVlr8lOKvIzbqA8-qvHoQ.gif differ diff --git a/public/articles/images/1lY5WPHYbCz_PDY-PaxTa0A.png b/public/articles/images/1lY5WPHYbCz_PDY-PaxTa0A.png new file mode 100644 index 0000000..74a98f2 Binary files /dev/null and b/public/articles/images/1lY5WPHYbCz_PDY-PaxTa0A.png differ diff --git a/public/articles/images/1pql8S-NvSjqcqruEhI9mSw.png b/public/articles/images/1pql8S-NvSjqcqruEhI9mSw.png new file mode 100644 index 0000000..a33141e Binary files /dev/null and b/public/articles/images/1pql8S-NvSjqcqruEhI9mSw.png differ diff --git a/public/articles/images/1sJHpfDClwKyFoaIpNp6sgg.png b/public/articles/images/1sJHpfDClwKyFoaIpNp6sgg.png new file mode 100644 index 0000000..0c83ebe Binary files /dev/null and b/public/articles/images/1sJHpfDClwKyFoaIpNp6sgg.png differ diff --git a/public/articles/images/1upB28P_sCBlqPhXgWO15Zw.png b/public/articles/images/1upB28P_sCBlqPhXgWO15Zw.png new file mode 100644 index 0000000..2953efa Binary files /dev/null and b/public/articles/images/1upB28P_sCBlqPhXgWO15Zw.png differ diff --git a/public/articles/images/1zlgGPvL70VCc9cOf5VFBsA.jpeg b/public/articles/images/1zlgGPvL70VCc9cOf5VFBsA.jpeg new file mode 100644 index 0000000..0e495ab Binary files /dev/null and b/public/articles/images/1zlgGPvL70VCc9cOf5VFBsA.jpeg differ diff --git a/public/articles/images/corona-web.jpg b/public/articles/images/corona-web.jpg new file mode 100644 index 0000000..3f8b8c9 Binary files /dev/null and b/public/articles/images/corona-web.jpg differ diff --git a/public/articles/images/gV0cxPU.jpg b/public/articles/images/gV0cxPU.jpg new file mode 100644 index 0000000..b45066c Binary files /dev/null and b/public/articles/images/gV0cxPU.jpg differ diff --git a/public/articles/images/nmsg8s.jpg b/public/articles/images/nmsg8s.jpg new file mode 100644 index 0000000..40b450d Binary files /dev/null and b/public/articles/images/nmsg8s.jpg differ diff --git a/public/articles/images/rq3ptj8i75b21.jpg b/public/articles/images/rq3ptj8i75b21.jpg new file mode 100644 index 0000000..87ddd4b Binary files /dev/null and b/public/articles/images/rq3ptj8i75b21.jpg differ diff --git a/public/articles/images/smNksSJ.jpg b/public/articles/images/smNksSJ.jpg new file mode 100644 index 0000000..ee1efbb Binary files /dev/null and b/public/articles/images/smNksSJ.jpg differ diff --git a/src/articles/20190403-leibniz-equality-in-typescript-2aeff1303749.mdx b/src/articles/20190403-leibniz-equality-in-typescript-2aeff1303749.mdx new file mode 100644 index 0000000..58b12e0 --- /dev/null +++ b/src/articles/20190403-leibniz-equality-in-typescript-2aeff1303749.mdx @@ -0,0 +1,370 @@ +--- +title: "Leibniz equality in TypeScript" +original_url: "https://medium.com/codestar-blog/leibniz-equality-in-typescript-2aeff1303749" +source: "medium" +author: Werner de Groot +publishedAt: 2019-04-03 +--- + +In this post I’ll explain how you can use Leibniz equality to safely type your higher-order components in React, although it can be used in many other places (outside the React ecosystem) too. + +## Introduction + +At the client I’m currently working for we use a lot of different charts to visualize processes over time. We have line charts, Gantt charts, you name it. Each of those charts features buttons which allows users to zoom in or out. + +I’d like to use a simplified version of one of those graphs to explain what Leibniz, a German mathematician who lived well over 300 years ago, has to do with TypeScript. + +## Motivating example + +Let's suppose our graph looks a bit like this: + + + +I use a component `Graph` which takes the following props: + +`[GraphProps.ts](https://gist.github.com/wernerdegroot-blogs/233ddd104b1e58f14382de69080c7f9f#file-GraphProps-ts)` + +``` +type GraphProps = { + activities: Activity[] + dayStart: number + dayEnd: number + onZoomIn: () => void + onZoomOut: () => void +} +``` + +What do we need to show a graph? We need: + +* `activities` to show; +* `dayStart` and `dayEnd`, which specify the range of the time axis; +* `onZoomIn` and `onZoomOut` to control the range of the time axis from inside the component. + +## Zooming in and out + +In this aside, I’d like to show you the function that handles zooming in or out. It’s not really relevant to the rest of the story (and you can skip this if you like) but it might come in handy if you wish to code along with this blog post. + +`[zoom.ts](https://gist.github.com/wernerdegroot-blogs/ba3ebc50d0c63c54bd89d6672b7edaa7#file-zoom-ts)` + +``` +function zoom(dayStart: number, dayEnd: number, zoomFactor: number): [number, number] { + // What is the middle of the time range? + // When zooming in or out, the middle of the time range should stay the middle. + const dayMiddle = (dayEnd + dayStart) / 2 + + // Determine what the new time range should be using the `zoomFactor`. + const oldTimeRange = dayEnd - dayStart + const newTimeRange = oldTimeRange * zoomFactor + + // Calculate the new boundaries of the time range: + const newDayStart = dayMiddle - newTimeRange / 2 + const newDayEnd = dayMiddle + newTimeRange / 2 + + return [newDayStart, newDayEnd] +} +``` + +## Higher-order component + +Although it is tempting to let this component manage its own `dayStart` and `dayEnd` (especially now that we can use hooks), it has two benefits to manage that state externally: + +* I can easily expose the component to different states, which makes it easy to test; +* I can change that state from the outside if I need to (for instance to ensure that all visible charts share the same time axis). + +If I would create a higher-order component (HOC) to manage that state for me, I would get the best of both worlds. I get an easy to use component which manages its own state if I wrap `Graph` in this HOC, but I get a lot of power if I choose not to. + +Furthermore, I can apply this HOC to many other components which have a time axis and support some form of zooming in and out. + +What should this HOC look like? What is the input? And what is the output? + +* The component that we pass to the HOC (the “inner component”) should have some props provided automatically by the component that the HOC produces (the “outer component”). +* The outer component should forward any other props to the inner component. Those are the only props that we need to provide to the outer component. +* TypeScript should be able to infer all of this automatically. + +The HOC we will write will provide the following props to the inner component (which we’ll call `Inner` in what follows): + +`[leibniz-03-TimeAxisProps.ts](https://gist.github.com/wernerdegroot-blogs/d2ab9d1b04b4170309dd81b5b85db6c5#file-leibniz-03-TimeAxisProps-ts)` + +``` +type TimeAxisProps = { + dayStart: number + dayEnd: number + onZoomIn: () => void + onZoomOut: () => void +} +``` + +It will produce a component (which we’ll call `Outer` from now on) that takes the following props¹: + +`[leibniz-04-OuterProps.ts](https://gist.github.com/wernerdegroot-blogs/f8dc13b757ddbba78bcd5d01d14e8c1a#file-leibniz-04-OuterProps-ts)` + +``` +type OuterProps = Omit +``` + +This might be a bit intimidating. What this says is that we can determine the props to the outer component (`OuterProps`) from the props to the inner component (`InnerProps`) by removing all values that are shared with `TimeAxisProps` (`dayStart`, `dayEnd`, `onZoomIn` and `onZoomOut` to be precise)². + +Now that we know what the HOC should do, we can focus on _how_ it should do it: + +`[leibniz-05-WithTimeAxis.tsx](https://gist.github.com/wernerdegroot-blogs/5a37a833eed02e85f937572087bd09fc#file-leibniz-05-WithTimeAxis-tsx)` + +``` +// The state that we want to manage for `Inner`: +type OuterState = { + dayStart: number + dayEnd: number +} + +function WithTimeAxis(Inner: React.ComponentType) { + return class Outer extends React.Component, OuterState> { + constructor(props: OuterProps) { + super(props) + + // Choose a sensible default state: + this.state = { + dayStart: 0, + dayEnd: 10 + } + } + + public render() { + // Construct `TimeAxisProps`. + const timeAxisProps: TimeAxisProps = { + dayStart: this.state.dayStart, + dayEnd: this.state.dayEnd, + onZoomIn: this.handleZoomIn, + onZoomOut: this.handleZoomOut + } + + // Combine `OuterProps` and `TimeAxisProps` + // to get `InnerProps`. + const innerProps: InnerProps = { + ...this.props, + ...timeAxisProps + } + + // Render... + return + } + + private handleZoomOut = () => { + const [newDayStart, newDayEnd] = zoom( + this.state.dayStart, + this.state.dayEnd, + 2 // Make the time range twice as big + ) + this.setState({ dayStart: newDayStart, dayEnd: newDayEnd }) + } + + private handleZoomIn = () => { + const [newDayStart, newDayEnd] = zoom( + this.state.dayStart, + this.state.dayEnd, + 0.5 // Make the time range half as big (twice as small) + ) + this.setState({ dayStart: newDayStart, dayEnd: newDayEnd }) + } + } +} +``` + +That’s a big piece of code! We can see how to handle zooming in and zooming out. We can also see how we can combine both the `OuterProps` and the `TimeAxisProps` to render the `Inner`\-component. You might also have noticed that `InnerProps extends TimeAxisProps`. Constraining our generic type parameter it this way ensures that we can only apply this HOC on components that have at least the props `dayStart`, `dayEnd`, `onZoomIn` and `onZoomOut` that we’d like to provide to it. If that component doesn’t have these four props, why even apply `WithTimeAxis`, right? + +## Trouble + +There is, however, a tiny problem… It doesn't compile! + + + +But why doesn't it? TypeScript has trouble figuring out that the combination of `OuterProps` and `TimeAxisProps` is equal to `InnerProps`. Although this is true for the case with `GraphProps`, it isn't true in general. + +To give you an example in which this isn’t true, let’s suppose that we try to apply the HOC to a component `CounterExample` with the following props: + +`[leibniz-06-CounterExampleProps.ts](https://gist.github.com/wernerdegroot-blogs/17879a042e2df6fbf7be765fb07fc6b8#file-leibniz-06-CounterExampleProps-ts)` + +``` +type CounterExampleProps = { + activities: Activity[] + dayStart: 0 // Can only start at zero! + dayEnd: number + onZoomIn: () => void + onZoomOut: () => void +} +``` + +where I’d like to point your attention to the `dayStart: 0`. + +I admit, this is a bit farfetched, but it does illustrate the point. We shouldn’t apply `WithTimeAxis` to `CounterExample` as the HOC might provide a `dayStart` that is not equal to zero. In fact, changing the zoom level multiple times ensures that `dayStart` will eventually be non-zero, even if it was equal to zero initially. + +The TypeScript isn’t complaining about this when we do try to apply `WithTimeAxis` to `CounterExample`, as `CounterExampleProps` nicely extends `TimeAxisProps` as I required. `CounterExampleProps` is more specific than `TimeAxisProps` (because the type `0` is more specific than `number`) but that is allowed for subtypes. Instead, the compiler has noticed this possibility even before we did, and that is why our HOC doesn’t compile! + +The root of our issues is with the `InnerProps extends TimeAxisProps` constraint. What we try to express is that all properties of `TimeAxisProps` are shared with `InnerProps` without allowing for subtypes. Unfortunately `extends` is currently the best we can do. In fact, it’s the only type of constraint we can express on our generic type parameters in TypeScript. + +## Hope on the horizon + +We can solve this problem by pushing the burden of proof up a level. We ask the user for a function `convert` that is able to convert the combination of `OuterProps` and `TimeAxisProps` (which can be expressed in TypeScript as `OuterProps & TimeAxisProps`) to `InnerProps`. If the user can do that, we can call `Inner` with the right props: + +`[leibniz-07-WithTimeAxis.tsx](https://gist.github.com/wernerdegroot-blogs/b0ce2eb9479c9d548b4ce3495bb211a5#file-leibniz-07-WithTimeAxis-tsx)` + +``` +function WithTimeAxis( + Inner: React.ComponentType, + convert: (p: OuterProps & TimeAxisProps) => InnerProps +) { + return class Outer extends React.Component, OuterState> { + + ... + + public render() { + + ... + + // Combine `OuterProps` and `TimeAxisProps` + // to get `InnerProps`. + const innerProps: InnerProps = convert({ + ...this.props, + ...timeAxisProps + }) + + // Render... + return + } + + ... + + } +} +``` + +What does this conversion function look like in the example of `GraphProps`? It’s not very difficult at all! In the example of `GraphProps` we can see that: + +* The parameter type is `Omit & TimeAxisProps`, which the compiler knows is just a fancy way to write `GraphProps`; +* The return type is `GraphProps`. + +What it boils down to is that we are asked to provide a function that makes this very trivial conversion: + +`[leibniz-08-trivial.ts](https://gist.github.com/wernerdegroot-blogs/9e53339b4ed8a0485174812ddea9823e#file-leibniz-08-trivial-ts)` + +``` +function trivial(graphProps: GraphProps): GraphProps { + return graphProps +} + +const GraphWithTimeAxis = WithTimeAxis(Graph, trivial) +``` + +We can even use the identity function if we’d like: + +`[leibniz-09-identity.ts](https://gist.github.com/wernerdegroot-blogs/0c0d992f20045903fc0e50233d7f64dc#file-leibniz-09-identity-ts)` + +``` +function identity(t: T): T { + return t +} + +const GraphWithTimeAxis = WithTimeAxis(Graph, identity) +``` + +For `CounterExample` we are asked to provide a conversion function that takes an object with `dayStart: number` to `dayStart: 0`. We could simply provide a conversion function that maps every `dayStart` (whether it is 1, 2, 99 or something else) to 0 but that would clearly not be in the spirit of `WithTimeAxis`. If I would instead try to use something `identity` in this case, TypeScript would complain. + + + +which is a rather nice way of hearing about this compilation error I think. (Especially the note at the bottom that says "Type 'number' is not assignable to type '0'" points you in the right direction immediately.) + +As we’ve concluded earlier, `OuterProps & TimeAxisProps` is not equal to `CounterExampleProps`, and the compiler can tell you that. If you cannot use something like `identity` or `trivial`, that means you probably shouldn’t use this HOC. + +This is really the crucial step of this blog, so take some time to digest this. We’ve pushed the burden of proving that `OuterProps & TimeAxisProps` to `InnerProps` from `Outer` (where that’s hard or even impossible to do) to the consumers of this component (where that is easy or even trivial to do). We can’t prove this in general, but we can do it case-by-case every time we apply `WithTimeAxis`. + +## Leibnizian equality + +A famous mathematician called Leibniz described a form of equality in which two things (_a_ and _b_) can be considered to be equal if every predicate that holds for _a_ also holds for _b_ (and vice versa). + +In TypeScript, we can express this as + +`[leibniz-10-Leibniz.ts](https://gist.github.com/wernerdegroot-blogs/9e26e278c02c700af1845d37632fc28b#file-leibniz-10-Leibniz-ts)` + +``` +type Leibniz = ((a: A) => B) & ((b: B) => A) +``` + +Two types `A` and `B` are equal if every function that maps `A` to `B` is also a mapping from `B` to `A`. You can see that it’s only possible to construct such a function if `A` is equal to `B`. In that case `Leibniz` collapses to `type Leibniz = (a: A) => A` (in other words, it is our `identity` function). + +`Leibniz` is a formalization of the technique we used in the previous section with a HOC: + +`[leibniz-11-WithTimeAxis.tsx](https://gist.github.com/wernerdegroot-blogs/c584c3e77ed95b7fc5c3bcf39f4460ee#file-leibniz-11-WithTimeAxis-tsx)` + +``` +function WithTimeAxis( + Inner: React.ComponentType, + leibniz: Leibniz & TimeAxisProps, InnerProps> +) { + return class Outer extends React.Component, OuterState> { + + ... + + public render() { + + ... + + // Combine `OuterProps` and `TimeAxisProps` + // to get `InnerProps`. + const innerProps: InnerProps = leibniz({ + ...this.props, + ...timeAxisProps + }) + + // Render... + return + } + + ... + + } +} +``` + +By requiring a `Leibniz & TimeAxisProps, InnerProps>` this function expresses that it can only do its job if `OuterProps & TimeAxisProps` and `InnerProps` are equal. + +Because `Leibniz<...>` serves as our type constraint, we can even drop the `extends` from `InnerProps extends TimeAxisProps`. This is no real loss as that `extends` wasn’t doing a very good job anyways. + +## Conclusion + +Sometimes we need something stricter than `extends`, or we’d like to constrict the type parameter in the other direction (`number extends T` instead of `T extends number`). In those cases `Leibniz<...>` can be your friend. In my experience using a `Leibniz<...>` improves the readability of your type constraints when those constraints get more complicated (or include three or more different types). + +## Afterthoughts + +This technique was first used in [Typing Dynamic Typing (Baars and Swierstra, ICFP 2002)](http://portal.acm.org/citation.cfm?id=583852.581494) but I haven’t seen it used in TypeScript anywhere yet. I’m really interested to hear how you would tackle the problem addressed in this post without using a `Leibniz<...>` or if you’ve seen it used in similar (or different!) places. Let me know! + +\[1\]: `Omit` will be introduced in TypeScript 3.5. In the meantime, you can define it yourself as `type Omit = Pick>` . + +\[2\]: We’ve defined `OuterProps` in terms of `InnerProps`. Like in mathematics, where you can express _y_ in terms of _x_ (_y_ = 2_x_) or _x_ in terms of _y_ (_x_ = _y_ / 2), TypeScript allows me to reverse this relationship. We get + +`[leibniz-12-InnerProps.ts](https://gist.github.com/wernerdegroot-blogs/260c0917f0f805fb3406f8dbff051cbe#file-leibniz-12-InnerProps-ts)` + +``` +type InnerProps = OuterProps & TimeAxisProps +``` + +No need for complicated tricks like `Omit<...>`. Unfortunately, this doesn’t work. Because we start out with an `Inner`\-component, from which we generate an `Outer`\-component, we should start out with an `InnerProps`, from which we derive the `OuterProps`. If we would reverse this relationship by writing + +`[leibniz-13-WithTimeAxis.tsx](https://gist.github.com/wernerdegroot-blogs/21a456a053cec1f303a5f6f2a4a26506#file-leibniz-13-WithTimeAxis-tsx)` + +``` +type InnerProps = OuterProps & TimeAxisProps + +function WithTimeAxis(Inner: React.ComponentType>) { + return class Outer extends React.Component { + + ... + + } +} +``` + +we'd lose the ability for TypeScript to correctly infer the right types: + + + +In our example, the compiler would infer `OuterProps` to be equal to `GraphProps`, which includes `dayStart`, `dayEnd`, `onZoomIn` and `onZoomOut` so when you try to use the resulting component you are still asked to provide those props (even though they will by overwritten by the ones the HOC provides). + +If you don’t mind helping the compiler a hand by providing the type yourself (instead of letting TypeScript infer it) then this is a very nice way of writing HOC’s and you needn’t read the rest of the blog. diff --git a/src/articles/20190412-using-generative-art-to-create-a-pulsating-svg-star-cd7456268dc5.mdx b/src/articles/20190412-using-generative-art-to-create-a-pulsating-svg-star-cd7456268dc5.mdx new file mode 100644 index 0000000..d3cfcfa --- /dev/null +++ b/src/articles/20190412-using-generative-art-to-create-a-pulsating-svg-star-cd7456268dc5.mdx @@ -0,0 +1,82 @@ +--- +title: "Using generative art to create a pulsating SVG star" +original_url: "https://medium.com/codestar-blog/using-generative-art-to-create-a-pulsating-svg-star-cd7456268dc5" +source: "medium" +author: Hamza Haiken +publishedAt: 2019-04-12 +--- + +### Part 1 — Intro + +I recently created a new design for our recruitment campaign at Codestar. It represents a burning star, with [corona-like features](/articles/images/corona-web.jpg), represented in an abstract way, aiming to make it feel like a pulsating stream of data (The original artist for the star is [Garry Killian](https://www.shutterstock.com/g/GarryKillian)). + +We're very big on only hiring Latin speakers + +The rest of the team enjoyed it, and we nicknamed this design _"the code star"_. It was then suggested that we could try and recreate this as an animation for our website, or to simply just to generate as many different static stars as we want: a teammate's name could be used as a random seed, and they would be given their own special code star. + +In this series of blog posts, I will endeavor to explain my process in taking on this challenge, going through various subjects, in particular _generative art_ and _SVG animation._ + +Our goal in this series will be to re-create the above visual in SVG, and animate it to make it slowly pulsate, like the sun’s corona. + +## What is generative art? + +The term [“generative art”](https://en.wikipedia.org/wiki/Generative_art) (or also “procedural art”) refers the making of art algorithmically, typically relying on fractals and randomness. + +Famous examples of this include: [that one album cover that everybody wears on a t-shirt](/articles/images/gV0cxPU.jpg), [Minecraft's infinitely expanding worlds](/articles/images/smNksSJ.jpg), No Man's Sky's universe and planets, and many more. + +For further learning about getting started in generative art (after reading this of course!), I recommend as starting points [Generative Artistry](https://generativeartistry.com) and [The Coding Train](https://www.youtube.com/channel/UCvjgXvBlbQiydffZU7m1_aw) YouTube channel, which often takes on generative art challenges. + +## Noise + +One commonly used tool in the field of generative art is _noise_ — in particular, [Perlin](https://en.wikipedia.org/wiki/Perlin_noise), or [Simplex noise](https://en.wikipedia.org/wiki/Simplex_noise) (which is more suited for animations since it has a lower overhead). It is used for a variety of results: terrain, smoke, clouds, textures. + +This kind of noise is obtained by overlaying noise at different frequencies on top of each other, forming a cloud-like texture that expands infinitely in any direction. The nice thing about this method of constructing noise is that it can be expanded to support any number of required dimensions (in our case, a 3D space). + +In a nutshell, Perlin noise is constructed by averaging noise rendered at several scales (bilinear interpolation is used to smooth out the lower frequencies): + +Image source: https://medium.com/100-days-of-algorithms/day-88-perlin-noise-96d23158a44c + +When averaging all of these (using some kind of weighted distribution), the following natural, cloudy texture is obtained: + +Could this image just be the cloud filter in Photoshop? Who knows + +Used creatively, this noise can be used to create impressive results, like this [wood texture](https://skybase.wordpress.com/2012/01/26/how-to-creating-super-simple-procedural-wood-textures-in-filter-forge/) for example (and again, given the nature of Perlin noise, this texture can expand seamlessly in any direction). + +Sitting atop a mathematically generated hill is a big dream of mine + +[And here](/articles/images/rq3ptj8i75b21.jpg) is an example from the game "No Man's Sky". The mountains in such a landscape are created using low-frequency noise (big features), the smaller hills on top of that come from the middle range of noise frequencies, and if you zoom in even closer the small dirt bumps are added on top of the hills by adding in the values of a higher frequency (this is like a fractal). + +## Using noise + +One of the big advantages of Perlin noise: by traveling along its plane, the intensity values increase and decrease **continuously**. This is not only very useful for terrain generation for creating hills and valleys, but also for _animation_, providing offsets that will smoothly increase or decrease randomly in a natural way. + +We will use [simplex-noise.js](https://github.com/jwagner/simplex-noise.js) for generating our noise. This is not Perlin but Simplex noise; it looks less detailed, and is faster to compute. The library provides a simple API: just instantiate a noise object with a [random seed](https://en.wikipedia.org/wiki/Random_seed), which you can then use for getting noise values in 2D, 3D or 4D: + +let simplex = new SimplexNoise("tutorial seed"); +let value = simplex.noise2D(0.42, 13.37); + +Precision can be as small as needed, effectively zooming in on the noise, and the whole 2D plane that can be represented with JavaScript numbers is available to us. + +Here is a simple demo on how to use Simplex noise. Feel free to play around with the sliders, particularly with the z-axis (slowly). + +[Embedded media omitted] +✍︎Try moving in the X and Y directions, then slowly scroll the Z slider + +Moving along the X- and Y-axes feels natural to us humans — it just looks like standard translation — but it shows us that the noise is indeed continuous. + +Moving along the Z-axis, however, gives a totally different feeling, exposing the vertical continuity of the noise by showing us slices of what seems to be an animation. + +The color values at a given pixel coordinates change continuously and smoothly while navigating along the Z direction. + +This is a big clue; by using the Z-axis to represent time, we can now animate things. + +But before we can animate, we will first need a drawing! + +## Coming up in this series + +Stick around on the Codestar blog to catch the next parts in this series: + +* Drawing the basis for the SVG star: basic [Snap.svg](http://snapsvg.io/) tutorial +* Mapping star element coordinates using the Simplex noise as a displacement map: some trigonometry and calculus +* Animation: bringing everything together, and using [Tweakpane](https://cocopon.github.io/tweakpane) for playing around with the settings +* Making a tool for outputting static SVG images using a string seed for generating a random star diff --git a/src/articles/20190501-sharing-is-caring-domain-objects-in-both-scala-and-r-with-graalvm-polyglot-bindings-b561e8cfbcfa.mdx b/src/articles/20190501-sharing-is-caring-domain-objects-in-both-scala-and-r-with-graalvm-polyglot-bindings-b561e8cfbcfa.mdx new file mode 100644 index 0000000..002e358 --- /dev/null +++ b/src/articles/20190501-sharing-is-caring-domain-objects-in-both-scala-and-r-with-graalvm-polyglot-bindings-b561e8cfbcfa.mdx @@ -0,0 +1,342 @@ +--- +title: "Sharing is Caring! Domain objects in BOTH Scala and R with GraalVM Polyglot bindings." +original_url: "https://medium.com/codestar-blog/sharing-is-caring-domain-objects-in-both-scala-and-r-with-graalvm-polyglot-bindings-b561e8cfbcfa" +source: "medium" +author: Nathan Perdijk +publishedAt: 2019-05-01 +--- + +**In any domain that goes beyond a sample project, it becomes almost inevitable that you want to use objects that accurately represent that domain. GraalVM does an adequate job of converting datastructures from R to JVM languages and back by using sensible defaults, but what do you do when the sensible defaults are not sufficient? Given that GraalVM can perform translation between its multitude of supported languages, is it possible to define a “Domain” that can be accessed by all?** + +**This is, of course, a rhetorical question and the answer is "Yes".** + + + +In this article I'll demonstrate how to share domain objects between JVM languages and guest languages on the GraalVM platform. I'm using Scala domain objects (because Scala is awesome), but you could do the same with, for instance, Java or Kotlin. + +(If you’re new to GraalVM Polyglot abilities, consider also reading my previous article on the subject: [using GraalVM to execute R files from Scala](https://medium.com/codestar-blog/in-search-of-the-holy-graalvm-putting-the-r-in-scala-or-java-or-b057494f77).) + +## The Problem + +To demonstrate the problem we are trying to solve, we first need a pretend domain. Let’s do something with Weather Forecasts, because people always talk about the weather! + +Creating weather forecasts is the kind of terribly complicated modelling business that could be built in R, but luckily we don’t actually _need_ a working model for this article. So let’s just pretend we already have this awesome R functionality that creates weather forecasts, cleanly abstracted away in a separate file called `fun_MagicHappensHere.R`: + +`[fun_MagicHappensHere.R](https://gist.github.com/NRBPerdijk/4a115fe9e58ba6885f177bf3dd6b7f72#file-fun_MagicHappensHere-R)` + +``` +# Some very impressive R stuff happens here. +# (Well, we're pretending it does, anyway! It's really just a mock...) +# This function returns a data.frame containing a number of +# weather forecasts that we need to bring back to the JVM +magicHappensHere <- function() { + # Omitting mocking code here + (...) + + # Like in Scala, the result of the last statement in an R function is its return. + weatherForecasts +} +``` + +When brought into scope with R’s`source` the above file will yield a `magicHappensHere` function that can be called and returns a `data.frame` with some weather forecast information. We can then return the result to Scala by simply making it the return of our R function: + +`[fun_NoBindingsWeatherForecasts.R](https://gist.github.com/NRBPerdijk/01f617dfdcbed15713aec8b71c2758df#file-fun_NoBindingsWeatherForecasts-R)` + +``` +generateWeatherForecasts <- function(pathToMagicFile) { + + # We're bringing the function contained in the file at the given location into scope + source(paste(pathToMagicFile)) + + # This returns a dataframe, a way for R to store large quantities of data + # in an ordered manner (kind of like a Database Table...) + weatherForecast <- magicHappensHere() + + # Like in Scala, the result of the last statement in an R function is its return. + weatherForecast +} +``` + +Wow, that doesn’t look too bad! This won’t get many complaints from the Data Scientist, I reckon. + +> So, what’s wrong with this? What’s the problem? + +I’m glad you asked, [interlocutor](https://www.dictionary.com/browse/interlocutor)! Let’s take a look on the Scala/JVM side of this equation, to see what the Data Engineer has to deal with: + +`[Main.scala](https://gist.github.com/NRBPerdijk/178b2c01420db29de4863f4bf94e0178#file-Main-scala)` + +``` + // We need to initialise a GraalContext that will do the mediation between + // the JVM languages and R + val context: Context = Context.newBuilder("R").allowAllAccess(true).build() + + // Next, we need to create a Source which needs to know what language it features + // and where to find the code. + val sourceNoBindings: Source = + Source + .newBuilder("R", Main.getClass.getResource("fun_NoBindingsWeatherForecasts.R")) + .build() + + /* + * We use the graal context to convert the source into a function. + * Because R is dynamically typed, the compiler cannot help you here: + * it trusts that you give it correct instructions! + * This also means that you may ( => DEFINITELY) want to wrap any call + * to this function in a Try to prevent explosions! + * + * We need to tell our compiler what kind of function this new Source represents. + * In this case it is a function that takes one argument: + * - a path to another R function (which mocks the magic that R is good at) + * And it returns... something rather complex: a Map of Strings, that refer to Lists + * that contain... something we can't usefully Type because it will actually be + * different things! + */ + val rNoBindingsWeatherForecasts: String => util.Map[String, util.ArrayList[_]] = + context.eval(sourceNoBindings).as(classOf[String => util.Map[String, util.ArrayList[_]]]) +``` + +Whoa… creating the Graal Context and Source is trivial, but look at the nasty type signature on that call to R! Let’s pick it apart for a bit: + +* A `Map` that contains `List`s of each `data.frame` row keyed by its name… That makes sense, well done Graal! It’s just too bad it’s Stringly typed, rather than actual methods on an actual class, so any typo will mess us up at runtime. +* Unknown content type of the Lists?… That’s unfortunate, we know that some rows should only contain `String`, while others contain `Int` but this information is lost in conversion… We have to do a bunch of casting! +* The returned Collections are Java? That’s just sad! The polyglot representation of collections doesn’t transfer to Scala, but Scala `Map` and `List` are much more powerful than their Java equivalent, so we’ll have to convert the Java equivalents! +* Every element of each `List` doesn’t actually belong to the rest of the `List`, but instead should be combined with each corresponding position in every other `List` to actually make a `WeatherReport`… (The first entry of “humidities”, should be paired with the first entry of “temperatures” etc.) + +Let’s see what this means when we try to use the output of this function: + +`[Main.scala](https://gist.github.com/NRBPerdijk/9e36ef4ba3fd89f633b49b55c2c9c745#file-Main-scala)` + +``` + private val path = Main.getClass.getResource("fun_MagicHappensHere.R").getPath + Try(rNoBindingsWeatherForecasts(path)) match { + case Failure(f) => print(f) + case Success(s) => + // turning the Java Map into an immutable Scala Map, same for the Java List. + val resultAsScala: Map[String, List[_]] = + s.asScala.toMap.map(entry => entry._1 -> entry._2.asScala.toList) + + // We need to do a bunch of nasty casting, because the returntype is not uniform + val humidities: List[Int] = resultAsScala("humidity").asInstanceOf[List[Int]] + val temperatures: List[Int] = resultAsScala("temperature").asInstanceOf[List[Int]] + val temperatureScale: List[String] = + resultAsScala("temperatureScale").asInstanceOf[List[String]] + /* + * We are omitting a bunch of things here: + * - there are more return values that need to be extracted from the map + * (which won't tell us if we're being exhaustive or not) + * - these return values need to be fit inside proper domain objects for further + * typesafe treatment, so we'll need to stitch elements from each list + * together... + * + * But already, we can see that this is: + * - very verbose + * - very error prone (it takes a lot of trial and error to get it right) + * - very brittle (it is very easy for a change somewhere else to break this + * parsing in half) + * - annoying to do! + * If something is wrong (say, a column is missing), we get errors when parsing, + * NOT where the actual mistake is made! + */ + } +``` + +I don’t know about you, but I’d feel quite uncomfortable at the thought of maintaining the code above. It’s verbose, error prone, brittle, annoying and it fails at the wrong spot if any mistakes are introduced (namely at the place of conversion, rather than the place of programming error). I wish the R function would just return a `Set` of `WeatherForecast`! + +Whoops, hold on… Wait a minute… + +Why don’t we just _**make it do that?**_ + +## **The Solution: Bindings** + +GraalVM comes with an option that makes it possible to explicitly share instances of code across the language divide. It makes it possible to add symbols to bindings that are accessible to other languages. The Graal Context has two functions that can be used to do this in a very similar way: + +* [`getPolyglotBindings()`](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Context.html#getPolyglotBindings--) +* [`getBindings(“nameOfLanguage")`](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Context.html#getBindings-java.lang.String-) + +In this article I will be using `getBindings`, because it doesn’t require an explicit import on the side of the using language and it allows you to limit which languages you are exposing each binding to. Using `getPolyglotBindings()` is almost identical from a coding perspective though, so pick the one you like best. + +### Using Domain objects on both sides of the language divide + +This is what our Domain object looks like: + +`[Domain.scala](https://gist.github.com/NRBPerdijk/4c64c4966621cbd6567b39e15208bc47#file-Domain-scala)` + +``` + /* + * Our Domain object functions as a factory for our domain-related classes. + * It has methods that create new instances of these classes, which can then safely be used from another Context. + */ +class Domain { + def weatherForecastList(): WeatherForecastList = WeatherForecastList(List()) + def percentage(percent: Int): Percentage = Percentage(percent: Double) + def chanceOfRain(chance: Percentage): ChanceOfRain = ChanceOfRain(chance: Percentage) + + def temperature(degrees: Int, temperatureScale: String): Temperature = + Temperature(degrees: Int, temperatureScale: String) + + def windSpeed(scale: String, speed: Int): WindSpeed = + WindSpeed(scale: String, speed: Int) + + def windForecast(windSpeed: WindSpeed, direction: String): WindForecast = + WindForecast(windSpeed: WindSpeed, direction: String) + + def weatherForecast( + humidity: Percentage, + windForecast: WindForecast, + sunshine: Percentage, + temperature: Temperature, + chanceOfRain: ChanceOfRain): WeatherForecast = + WeatherForecast(humidity, windForecast, sunshine, temperature, chanceOfRain) +} +``` + +`Domain` is basically a factory that can be used to spawn new instances of all the domain classes that we want to share. The class`Domain` itself is immutable! (As it happens, the spawned instances are too.) + +> **WARNING**: You probably don’t want to put a mutable object into bindings. If you do, this object can be mutated **from any language that can reach it**. Just as you don’t want multiple threads to tangle with the same mutable object, you don’t want multiple languages to access the same mutable state! (Really! Imagine having to debug race conditions across language boundaries...) + +Any instance of the `Domain` class provides methods to spawn new instances of the following domain case classes: + +`[WeatherForecast.scala](https://gist.github.com/NRBPerdijk/eae681fac5c1e060f48d5521c7743d01#file-WeatherForecast-scala)` + +``` +case class WeatherForecast( + humidity: Percentage, + windForecast: WindForecast, + sunshine: Percentage, + temperature: Temperature, + chanceOfRain: ChanceOfRain +) +case class Temperature(degrees: Int, temperatureScale: String) +case class Percentage(percent: Double) +case class WindForecast(windSpeed: WindSpeed, direction: String) +case class WindSpeed(scale: String, speed: Int) +case class ChanceOfRain(chance: Percentage) +case class WeatherForecastList(asScalaList: List[WeatherForecast]) { + def add(weatherForecast: WeatherForecast): WeatherForecastList = + this.copy(weatherForecast :: asScalaList) +} +``` + +Let’s put an instance of our `Domain` class into the bindings for R, so it can be accessed from the R guest language context: + +`[Main.scala](https://gist.github.com/NRBPerdijk/badcd84759c7bf7154c3303d56cf19c0#file-Main-scala)` + +``` +/* + * Exposing bindings is an interesting way to share functionality between languages. + * This command makes an instance of the Domain class available under the "Domain" + * accessor. + */ + context.getBindings("R").putMember("Domain", new Domain) + +``` + +Easy peasy. From R, the new object will simply be known as `Domain` and its methods will be accessible like this: `Domain$methodName(arguments)` + +We turn a new R file, that uses this binding, into our newest `Source`: + +`[Main.scala](https://gist.github.com/NRBPerdijk/72f9552fe0267d5fac9581d5b4e81470#file-Main-scala)` + +``` + // This source will use the provided Domain instance to create objects as they have been + // defined in the Scala domain. + val sourceWithBindings: Source = + Source + .newBuilder("R", Main.getClass.getResource("fun_WithBindingsWeatherForecasts.R")) + .build() +``` + +And then we define the function: + +`[Main.scala](https://gist.github.com/NRBPerdijk/c093d7d313727e2f527fc8891f569f59#file-Main-scala)` + +``` + // This function signature is a lot cleaner than the one that doesn't use bindings. + // It is also completely Scala, meaning we do not have to do ANY parsing. + val rMagicWithBindings: String => WeatherForecastList = + context.eval(sourceWithBindings).as(classOf[String => WeatherForecastList]) +``` + +Now that this is our return type, all we need to do to work with the returned `WeatherForecast`s is this: + +`[Main.scala](https://gist.github.com/NRBPerdijk/446b9d3ba6c1ee37ebac2b88ef8a719d#file-Main-scala)` + +``` +// Remember to always put a call to R in a Try block, because +// R often resorts to throwing RuntimeExceptions. + Try(rMagicWithBindings(path)) match { + case Failure(f) => print(f) + case Success(weatherForecastList) => + // We get back a WeatherForecastList, which is a wrapper for List[WeatherForecast]. + // Now we can work with the results WITHOUT any parsing: + // simply take out the List and do your operations (here we print them one by one). + weatherForecastList.asScalaList.foreach(forecast => println(forecast)) + } +``` + +That is one very happy Data Engineer! (Don’t forget to compare with the incomplete parsing above.) + +Now, let’s see the impact on the DataScientist side: + +`[fun_WithBindingsWeatherForecasts.R](https://gist.github.com/NRBPerdijk/da927c196ef81aece09b5561c0e8b6ba#file-fun_WithBindingsWeatherForecasts-R)` + +``` +generateWeatherForecasts <- function(pathToMagicFile) { + # We're bringing the function contained in the file at the given location into scope + source(paste(pathToMagicFile)) + + # This returns a dataframe, a way for R to store large quantities of data in an ordered + # manner (kind of like a Database Table...) + weatherForecast <- magicHappensHere() + + # We use the Scala Domain object provided through GraalVM bindings to get ourselves an + # instance of the Scala wrapper containing a List of WeatherForecast + weatherForecastList <- Domain$weatherForecastList() + + # We're looping over all the entries in the dataframe and getting the corresponding + # elements from the proper columns/rows + for (count in seq(weatherForecast$humidity)) { + # Here we use the provided add method to add a new WeatherForecast to the List. + # Just like R, this Scala class returns a new, updated instance (rather than + # updating the old), so we're reassigning the variable to this new instance. + weatherForecastList <- weatherForecastList$add( + # We are using Domain to construct properly Typed Scala instances of Domain classes. + # Anything illegal (like putting a String in an Int, or Percentage) will cause an + # exception at the location of insertion! (Instead of after parsing!) + # Yay for proper stacktraces! + Domain$weatherForecast( + Domain$percentage(weatherForecast$humidity[count]), + Domain$windForecast( + Domain$windSpeed(weatherForecast$windScale[count], weatherForecast$windSpeed[count]), + weatherForecast$windDirection[count] + ), + Domain$percentage(weatherForecast$sunshine[count]), + Domain$temperature( + weatherForecast$temperature[count], + weatherForecast$temperatureScale[count] + ), + Domain$chanceOfRain(Domain$percentage(weatherForecast$chanceOfRain[count])) + ) + ) + } + + #like in Scala, the result of the last statement in an R function is its return. + weatherForecastList +} +``` + +As we can see, the code has become more verbose (although it’s actually quite efficient still, if you take out all the clarifying comments I put in), but not quite as bad as in the previous solution: + +In this R file, we now need to convert the `data.frame` to proper `WeatherForecast` instances to be added to the `WeatherForecastList` we also got from `Domain`. But rather than doing a Parse & Pray, as we had to do with the no-bindings solution, we can now use proper constructors that will fail with intelligible errors if we make a mistake. (Sadly still only at runtime, because this is still R.) Cleanly taking values out of the `data.frame` is also better supported by its native language and we could add more convenience methods to more succinctly create the domain classes if we wanted to. If we have direct control over the function that creates the weather forecasts, we can even skip the `data.frame` altogether and exclusively use `WeatherForecastList`, which eliminates the extra code seen above. + +The biggest advantage, though, is that we now have a very clearly defined interface. Any user can open up the `Domain.scala` file to see what methods are available, what parameters they take and what things they return. + +## **Conclusion** + +Using Bindings to provide a clean shared domain between guest languages (like R or Python) and JVM languages (like Scala, Java or Kotlin) in GraalVM is pretty easy and gets rid of a lot of ugly and fault-sensitive parsing. It also provides a crucial stepping stone for further integration of functionalities across language boundaries. + +**PS**: I could have added a factory for each separate domain class to the bindings, instead of giving them a shared factory. This can make the code on the R side a little shorter, but creates a less clean interface (at least to my taste). + +## **Sourcecode** + +I have reused the example project from my previous article on [using GraalVM to execute R files from Scala](https://medium.com/codestar-blog/in-search-of-the-holy-graalvm-putting-the-r-in-scala-or-java-or-b057494f77)) and branched it for this article. The source code can be found [here](https://github.com/NRBPerdijk/example-graalvm-r-scala/tree/usingBindingsToShareDomain). The snippets above are taken from the linked project and altered to better fit the sizing of the article. diff --git a/src/articles/20190502-apollo-client-in-practice-f81434f6f8d7.mdx b/src/articles/20190502-apollo-client-in-practice-f81434f6f8d7.mdx new file mode 100644 index 0000000..6832cfb --- /dev/null +++ b/src/articles/20190502-apollo-client-in-practice-f81434f6f8d7.mdx @@ -0,0 +1,97 @@ +--- +title: "Apollo Client in Practice" +original_url: "https://medium.com/codestar-blog/apollo-client-in-practice-f81434f6f8d7" +source: "medium" +author: mdworld +publishedAt: 2019-05-02 +--- + +Some time ago I joined a team that is working on a search application. The application takes search terms and displays the results in a table with potentially dozens of columns and hundreds of rows, even before pagination. It is implemented in React and uses [Apollo](https://www.apollographql.com/) for GraphQL calls. I was surprised to find it noticeably slow when a lot of search results were retrieved. React is well-known for leveraging virtual DOM to optimize performance and GraphQL should even be able to add caching to further optimize performance on the side of network requests. + +Looking into the performance tab of Chrome dev tools lead me to believe the performance problems were caused by computations in the bottom components (e.g. formatting in cells). Because there are so many and they are re-rendered quite often, this approach is quite intensive on resources. + +Besides that, the application had obvious state synchronization problems. When moving between views it was not maintaining the same state of selected rows. Even though Redux was used to store application state and communicate it between components, it was not used consistently. There were still plenty of React class components that stored some parts of the state locally. + +To summarize, there were two issues that needed to be solved: + +1. Poor performance due to excessive re-rendering +2. Loss of application state when navigating views due to decentralized state stores + +Since both issues were caused by (a lack of) architecture, we redesigned the structure of the application. The original implementation used: + +* [Apollo Client](https://www.apollographql.com) as a GraphQL client +* [Axios](https://github.com/axios/axios) as an HTTP client for REST endpoints +* [Redux](https://redux.js.org) and [React local state](https://reactjs.org/docs/hooks-reference.html#usestate) to manage the state between components + +It used Apollo, but by [manually firing](https://www.apollographql.com/docs/react/essentials/queries#manual-query) `client.query()` and after processing the response, it stored the result in the Redux store. + +## Fixing application state with Apollo Local State + +When restructuring the application, Apollo Client was updated to 2.5. This version has a built-in [local state manager](https://www.apollographql.com/docs/react/essentials/local-state) (formerly _apollo-link-state_) and it supports REST calls with the [apollo-link-rest](https://www.apollographql.com/docs/link/links/rest) plugin. The [apollo-boost](https://github.com/apollographql/apollo-client/tree/master/packages/apollo-boost) package contains the client and several useful plugins. Adopting these means that both Redux and Axios can be removed and Apollo will be used as a single source of truth. If there is a single store for the data, there is no need for synchronization and with that one of the issues is solved. + +The way we used Apollo Client was also updated, to create a better separation of UI and data. Instead of using `client.query()` directly in the component lifecycle methods, components are split into a presentational component and enhanced with the [graphql()](https://www.apollographql.com/docs/react/api/react-apollo#graphql) HOC to add data from remote (i.e. GraphQL back-end) or local fields. Both utilize the Apollo cache, which fulfills multiple functions, one of them an application local state store. + +Example of wrapping a component in a Query HOC: + +``` +const Books = ({ data: { books } }) => ( +
    + {books.map(book =>
  • {book.title}
  • } +
); + +export graphql({ query: gql\` + query($author: String!) { + books(author: $author) { + title + } + }\`, + variables: { author: "Mickiewicz" })(Books); +``` + +Apollo reactively updates when using `Query` as a container, basically like the `connect` HOC in Redux. When the `variables` prop on the `Query` component is updated, it will automatically re-query. It uses the cache if possible and falls back to a network call if needed, although this behavior can be configured. + +## Improving performance with Local Field Resolvers + +Having a single source of truth fixes the state synchronization problem. It also paves the way for improving the performance. In general, when a lot of data enters the application, it is a good idea to format it once and cascade the formatted data down to the components and it’s descendants with as little transformations to the data itself. This reduces the amount of computations in the lower components, which solves our other issue. + +When using Redux, a common way to transform data in the store is using [Reselect](https://github.com/reduxjs/reselect), which computes derived data from the Redux store with selectors. For Apollo this is done by: + +* wrapping the table in a `Query` that queries a local prop `rows @client`, using the [@client](https://www.apollographql.com/docs/react/essentials/local-state) directive +* making client side resolvers for rows that queries GraphQL endpoint +* mapping the data in a resolver from a raw format to a format ready for the table components, e.g.: + +from a data object + +``` +{ + author: "Mickiewicz", + publications: \[ + { + title: "Pan Tadeusz", + date: -4291747200 + } + \] +} +``` + +to an array rows of cells + +``` +\[ + \[ "Mickiewicz", "Pan Tadeusz", "January 1834" \] +\] +``` + +## Next steps + +Apollo is excellent for merging data from multiple sources (in this case GraphQL, REST, local state and cache) and functions as a “single source of truth” which should solve the state synchronization problems. The local fields that Apollo uses in its local state manager can derive data, moving expensive operations from component render functions to resolvers in its application level cache. Although the issues mentioned in the introduction are now dealt with, we did encounter plenty of other issues I may dive into later. However, these are some things that you might want to take into account when working with Apollo Client: + +Outside of restructuring the application, we improved performance with [react-virtualized](https://github.com/bvaughn/react-virtualized) which speeds up rendering large tables. Apollo also offers GraphQL pagination. We did not use that, as we have to do our pagination on the client side to keep the sorting feature of react-virtualized in tact. + +Apollo Client offers support for TypeScript, it is even possible to generate queries and typed React components from GraphQL schemas with [@graphql-codegen/cli](https://graphql-code-generator.com/). + +Also definitely use the [JS GraphQL IntelliJ Plugin](https://jimkyndemeyer.github.io/js-graphql-intellij-plugin/) because it will not only auto complete queries, but it will help you think about (client side) schema’s. + +When the `Query` component mounts, it creates an observable that subscribes to the query in the query prop. This encourages reactive behavior like RxJS (which can also be used as a [state store](https://github.com/mdvanes/realtime-planner)). However, it seems that Apollo offers much less fine-grained control over the observables than what RxJS provides. And considering observables, Apollo Client effortlessly [scales to web sockets](https://www.apollographql.com/docs/link/links/ws)! + +Are you looking for inspiration on how Apollo client can be applied? I can recommend [this talk by Uri Goldshtein](https://www.youtube.com/watch?v=g6Mhm9W76jY) and this [introduction to Apollo state management by Sara Vieira](https://www.youtube.com/watch?v=2RvRcnD8wHY). diff --git a/src/articles/20190521-how-we-automated-our-angular-updates-9790212aa211.mdx b/src/articles/20190521-how-we-automated-our-angular-updates-9790212aa211.mdx new file mode 100644 index 0000000..a9d9efa --- /dev/null +++ b/src/articles/20190521-how-we-automated-our-angular-updates-9790212aa211.mdx @@ -0,0 +1,29 @@ +--- +title: "How we automated our Angular updates" +original_url: "https://medium.com/codestar-blog/how-we-automated-our-angular-updates-9790212aa211" +source: "medium" +author: Bjorn 'Bjeaurn' +publishedAt: 2019-05-21 +--- + +### How we keep our developers actively involved with the ever-changing world around our own development. + + + +> This article was written with the help of [Jan-Hendrik Kuperus](https://medium.com/u/3fb0a5abe9b8) and [Nathan Perdijk](https://medium.com/u/a2ee7eceb76f). + +Upgrading your Angular applications is quite easy with the Angular CLI. We have been faithfully upgrading to major releases usually within a week of release, without hesitation or issues since Angular 4. + +This process has been delightful. You get a compile error with some breaking changes and maybe there’s some manual work to be done, but other than that, it’s been quite effortless and easy to maintain. + +So much so, that we have automated our Angular upgrade process using our CI solution. + +## How we did it + +We have 5 steps in our CI pipeline. For our example we used Jenkins, but there’s no reason this wouldn’t work with any other CI pipeline. + +Our 5 steps consist of: + +* Git checkout, workspace cleanup and creating a new branch. +* Using the Angular CLI to run our updates. +* If the updates cannot be applied automatically, or there were no updates available; we send a message to our communication platform of choice. diff --git a/src/articles/20190607-event-sourcing-with-akka-persistence-6a3f4b167852.mdx b/src/articles/20190607-event-sourcing-with-akka-persistence-6a3f4b167852.mdx new file mode 100644 index 0000000..7b422a6 --- /dev/null +++ b/src/articles/20190607-event-sourcing-with-akka-persistence-6a3f4b167852.mdx @@ -0,0 +1,288 @@ +--- +title: "Event sourcing with Akka Persistence" +original_url: "https://medium.com/codestar-blog/event-sourcing-with-akka-persistence-6a3f4b167852" +source: "medium" +author: "Nick ten Veen" +publishedAt: 2019-06-07 +--- + +### Asynchronous pains + +In one of our projects at the Port of Rotterdam we do a lot of stream processing where we require intermediate state. We are using Event Sourcing with [Akka Persistence](https://doc.akka.io/docs/akka/current/typed/persistence.html). It allows us to create robust stateful streaming applications that can maintain state between application restarts. We were struggling a bit writing our command handlers since we do a bunch of asynchronous operations. Akka Persistence does not allow you to handle command asynchronously which means you need to deal with this yourself. Let us explore the problem in a simplified event sourcing application. + +### Event Sourcing + +Let us create a simple implementation of an event sourcing system. A simple calculator that can add and subtract values. First we define our state, which is simply an integer value: + +`[block1.scala](https://gist.github.com/besuikerd/f860cbd65e5c4c0cc1f8f22ecfda0b94#file-block1-scala)` + +``` +case class State(n: Int) +``` + +The state can only be modified by firing events. Let us create two possible operations, adding and subtracting from the state: + +`[block2.scala](https://gist.github.com/besuikerd/08af6b2f0526d6edeae5a7288b6f2f2b#file-block2-scala)` + +``` +sealed trait Event +case class Added(n: Int) extends Event +case class Subtracted(n: Int) extends Event +``` + +Now that we have a definition for our state and possible events, we can write a handler that will process these events: + +`[block3.scala](https://gist.github.com/besuikerd/b9265ac0a285d7e096ce27332cbdf18e#file-block3-scala)` + +``` +type EventHandler = (State, Event) => State + +val eventHandler: EventHandler = + (state, event) => + event match { + case Added(n) => state.copy(n = state.n + n) + case Subtracted(n) => state.copy(n = state.n - n) + } +``` + +We can test the event handler to verify that the events are processed correctly: + +`[block4.scala](https://gist.github.com/besuikerd/a63482d631107621c5d3b4e3142dff9b#file-block4-scala)` + +``` +eventHandler(State(2), Added(2)) shouldBe State(4) +eventHandler(State(42), Subtracted(21)) shouldBe State(21) +``` + +### Commands + +In event sourcing, events are immutable facts that happened. These events should be handled deterministically without any side effect. However, sometimes we need to perform side effects. For example when we need to query a database to check if an operation is allowed. We can use the command abstraction for this purpose. A command is a request to do something. Requests can be accepted or denied, or even transformed. They are also allowed to perform side effects. We can define commands for addition and subtraction: + +`[block5.scala](https://gist.github.com/besuikerd/7ad7221547e7cbc05b73a7a333bf737c#file-block5-scala)` + +``` +sealed trait Command + +case class Add(n: Int) extends Command +case class Sub(n: Int) extends Command +``` + +A command handler can process these commands and decide to fire zero or more events: + +`[block6.scala](https://gist.github.com/besuikerd/161e166621d79da4cc2769a09b2e5aa6#file-block6-scala)` + +``` +type CommandHandler = (State, Command) => Seq[Event] + +val commandHandler: CommandHandler = + (state, command) => command match { + case Add(n) => Seq(Added(n)) + case Sub(n) => Seq(Subtracted(n)) + } +``` + +We can test the command handler to verify it will fire events accordingly: + +`[block7.scala](https://gist.github.com/besuikerd/0f08b31ca39ca0bac9a16dfdd6c0e993#file-block7-scala)` + +``` +commandHandler(State(0), Add(2)) shouldBe Seq(Added(2)) +commandHandler(State(0), Subtract(42)) shouldBe Seq(Subtracted(42)) +``` + +The command handler and event handler can be folded together to calculate the state for a given list of commands: + +`[block8.scala](https://gist.github.com/besuikerd/c115e2df5c28c50797abd0fa951ccbe5#file-block8-scala)` + +``` +def combined(commands: Seq[Command]): State = + commands.foldLeft(State(0)) { + case (state, command) => + val events = commandHandler(state, command) + events.foldLeft(state)(eventHandler) + } +``` + +This all works fine, but if we want to recover the state during a crash or restart, we also need to store the events that we persist. We need a function that accumulates the events while calculating the state: + +`[block9.scala](https://gist.github.com/besuikerd/5a9e4ca0ef7e199caddb3b16c105aef3#file-block9-scala)` + +``` +def combinedWithEvents(commands: Seq[Command]): (State, Seq[Event]) = + commands.foldLeft((State(0), Seq.empty[Event])) { + case ((state, accumulatedEvents), command) => + val events = commandHandler(state, command) + val nextState = events.foldLeft(state)(eventHandler) + (nextState, accumulatedEvents ++ events) + } +``` + +We can keep the accumulated state in memory during processing and at the same time persist the generated events somewhere. On restarts we can replay these events with the `eventHandler` to restore our state. + +### Akka Persistence + +This pattern is encoded in Akka Persistence and allows us to have actors with state that can be recovered after crashes and restarts. The command handler is a little bit different. Instead of returning a list of events that happened, you can specify an `Effect`. These effects are simply an encoding of possible actions a persistent actor can do after receiving a command: + +* Persist an event +* Stop the actor +* Stash the command +* Do nothing + +These effects can be composed together to (for example) persist multiple events. In our example we can write a simple command handler: + +`[block10.scala](https://gist.github.com/besuikerd/94f2c3c255bdff421c5a3de1ad0ae6a9#file-block10-scala)` + +``` +type CommandHandler[Command, Event, State] = (State, Command) ⇒ Effect[Event, State] + +val commandHandler: CommandHandler[Command, Event, State] = (state, command) => + command match { + case Add(n) => Effect.persist(Added(n)) + case Sub(n) => Effect.persist(Subtracted(n)) + } +``` + +### Asynchronous command handling + +One issue about the `commandHandler` is that it is synchronous. [There are currently no plans for aynchronous command handlers in akka](https://github.com/akka/akka/issues/25650) persistence. If you want to do some asynchronous processing before deciding to persist an event, you need to introduce extra commands. For example, lets say we want to have a check if a specific addition or subtraction is allowed before we emit an event. We really need to do this asynchronously for some reason, so lets create a definition of our permission check: + +`[block11.scala](https://gist.github.com/besuikerd/582ff0a0dafe23067676abe807466311#file-block11-scala)` + +``` +type CheckPermission = Int => Future[Boolean] + +val trivialCheck: CheckPermission = _ => Future.successful(true) +``` + +To be able to add this to our command handler, we need an extra command that is fired after validation. We also group our previous commands into a subtype so it can be a parameter of our new command: + +`[block12.scala](https://gist.github.com/besuikerd/9169c542dd68d396b4b30189f260ffc1#file-block12-scala)` + +``` +sealed trait Command + +sealed trait AlgebraicCommand extends Command { def n: Int } +case class Add(n: Int) extends AlgebraicCommand +case class Sub(n: Int) extends AlgebraicCommand + +case class OperationAllowed(algebraicCommand: AlgebraicCommand) extends Command +``` + +With this definition we can rewrite our event handler to take this check into account: + +`[block13.scala](https://gist.github.com/besuikerd/fb292d22d42a9a767bb2296da0562a2e#file-block13-scala)` + +``` +def commandHandler(self: ActorRef[Command], checkPermission: CheckPermission): CommandHandler[Command, Event, State] = + (state, command) => + command match { + case algebraicCommand: AlgebraicCommand => + checkPermission(algebraicCommand.n).map { + case true => + ctx.self ! OperationAllowed(op) + case _ => () + } + Effect.none + case OperationAllowed(algebraicCommand) => + algebraicCommand match { + case Add(n) => Effect.persist(Added(n)) + case Sub(n) => Effect.persist(Subtracted(n)) + } + } +``` + +This does work, however we lost a property that might be important to us. The order in which the algebraic commands are processed is lost due to the asynchronous boundary. Say our check is really slow for some specific elements. Other elements that arrived later might have been processed already and arrive out of order: + +`[block14.scala](https://gist.github.com/besuikerd/9c4fe71b95328fc79537ff311f8ac986#file-block14-scala)` + +``` +val slowCheck = (n: Int) => + Future { + if (n == 3) { + Thread.sleep(3000) + } + true + } +``` + +If we would process the following commands in order, the outcome might have a different order: + +`[block15.scala](https://gist.github.com/besuikerd/d474236e670ad484003575cc6b170d5a#file-block15-scala)` + +``` +val commands = Seq(Add(2), Add(3), Sub(2)) + +val events = processCommands(commands) + +events shouldBe (Added(2), Subtracted(2), Added(3)) //They arrived in the wrong order +``` + +You could fix this by storing inflight messages in some (non-persistent) state, or by using the ask pattern and waiting for replies before sending each command. Currently (as far as I am aware) you are unable to store volatile state in a persistent actor. This means that if you want to store messages that are in flight, you need to use persistence for this. We can extend the state to store this along with a persistent event to signal inflight messages: + +`[block16.scala](https://gist.github.com/besuikerd/171295d8524864177e7a8604f129d531#file-block16-scala)` + +``` +case class State(n: Int, inFlight: Option[AlgebraicCommand]) + +case class Inflight(command: AlgebraicCommand) extends Event +``` + +Previously we only fired a command if an operation is allowed, but since we also need to unstash if an operation is not allowed, we always need to fire a command for a result. So we need to modify our `OperationAllowed` command: + +`[block17.scala](https://gist.github.com/besuikerd/526911697c11ec573958e92287503493#file-block17-scala)` + +``` +case class CheckPermissionResult( + algebraicCommand: AlgebraicCommand, + allowed: Boolean +) extends Command +``` + +Our event handler is now responsible for handling this extra event and cleaning up after a command has been successfully processed: + +`[block18.scala](https://gist.github.com/besuikerd/060de6dd2909eaacf1a5b6a221b080fa#file-block18-scala)` + +``` +val eventHandler: EventHandler[State, Event] = (state, event) => + event match { + case Added(n) => state.copy(n = state.n + n, inFlight = None) + case Subtracted(n) => state.copy(n = state.n - n, inFlight = None) + case Inflight(command) => state.copy(inFlight = Some(command)) + } +``` + +Finally we need to rewrite our command handler to stash incoming commands as long as there is still a message in flight. After a command is successfully processed, we need to unstash to continue processing potentially stashed commands: + +`[stashing-commandhandler.scala](https://gist.github.com/besuikerd/ff5ae12ad86652802ce1b4747c75f27b#file-stashing-commandhandler-scala)` + +``` +def commandHandler: CommandHandler[Command, Event, State] = + (state, command) => + command match { + case algebraicCommand: AlgebraicCommand => + state.inFlight match { + case Some(inFlight) => Effect.stash() + case None => + Effect.persist[Event, State](Inflight(algebraicCommand)) thenRun { _ => + checkPermission(algebraicCommand.n).map { allowed => + ctx.self ! CheckPermissionResult(algebraicCommand, allowed) + } + } + } + case CheckPermissionResult(algebraicCommand, allowed) => + val effect: EffectBuilder[Event, State] = + if (allowed) { + algebraicCommand match { + case Add(n) => Effect.persist(Added(n)) + case Sub(n) => Effect.persist(Subtracted(n)) + } + } else Effect.none + effect.thenUnstashAll() + } +``` + +After all this there are still a few concerns with this implementation. What if `checkPermission` fails? We would need to extend the example to deal with failing futures as well. Moreover, we persist the state of inflight messages so it survives restarts. However after a restart this message is not in flight and we might wait for eternity for it to resolve. This example illustrates that you can handle commands asynchronously, but in order to ensure messages are processed in the correct order, we needed to add error-prone synchronisation code. + +### Conclusion + +You can do asynchronous command handling with Akka Persistence. It does however require you to write some error-prone boilerplate code. Can we do better? Are persistent actors the correct approach for this problem? Maybe we can express the problem in a different paradigm where we still have the nice property of state recovery, while also allowing us to handle commands asynchronously. Maybe we could use stream processing to have a cleaner solution to our problem. But that is for another blog post. diff --git a/src/articles/20200214-upgrading-to-angular-9-my-experience-65158c284034.mdx b/src/articles/20200214-upgrading-to-angular-9-my-experience-65158c284034.mdx new file mode 100644 index 0000000..a3a0222 --- /dev/null +++ b/src/articles/20200214-upgrading-to-angular-9-my-experience-65158c284034.mdx @@ -0,0 +1,21 @@ +--- +title: "Upgrading to Angular 9: My experience" +original_url: "https://medium.com/codestar-blog/upgrading-to-angular-9-my-experience-65158c284034" +source: "medium" +author: Bjorn 'Bjeaurn' +publishedAt: 2020-02-14 +--- + +Angular with Ivy + +Last week, version 9 of Angular was released. The much anticipated Ivy Renderer was now set to default. The promise of smaller bundle sizes and faster applications was just too much for me to pass over. So Monday first thing, I wanted to get my upgrade going. + +The following story shares my experiences in upgrading a sizable application with some complexity, giving a good indication of the types of issues you may be running into with your application. + +## Upgrading + +As any upgrade starts, I created a new branch and ran the usual `ng update` to see what versions are eligible for automatic upgrades. I tried to run a `ng update --all` which failed cause of dependency issues like TSLint, Codelyzer and `rxjs-tslint-rules` not being updated yet. Normally, you could go for a `ng update --all --force` and see if that sticks, but considering the complexity of our application, I prefer to do it a bit more manually. + +ng update @angular/cli @angular/core + +This works fine, updates the CLI first and then updates and runs all migrations for the Angular application. This may result in some compilation errors, as not all situations and edge cases can be taken care of in the automatic migrations. Especially in larger applications, you’ll run into some edge cases. diff --git a/src/articles/20200410-tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397.mdx b/src/articles/20200410-tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397.mdx new file mode 100644 index 0000000..772505f --- /dev/null +++ b/src/articles/20200410-tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397.mdx @@ -0,0 +1,132 @@ +--- +title: "Tika Tika! Getting started doing OCR with Apache Tika andTesseract from the JVM" +original_url: "https://medium.com/codestar-blog/tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397" +source: "medium" +author: Nathan Perdijk +publishedAt: 2020-04-10 +--- + +## Tika Tika! Getting started doing OCR with Apache Tika andTesseract from the JVM (Scala, Java, Kotlin…). + +_I can do DataScience, mate!_ + +Some things are hard. Some things are not… Turns out that using OCR (Object Character Recognition) using Tesseract from the JVM is… not hard! + + + +The trickiest part, really, is [setting up Tesseract](https://github.com/tesseract-ocr/tesseract/wiki) on the machine you want to do your OCR on. Once you have managed to do that, you can just use the following Scala examples to use **Apache Tika** to do OCR in your own JVM project. + +### **First things first. Taking care of your dependencies…** + +Add these to your `pom.xml` or other build tool equivalent: + +`[pom.xml](https://gist.github.com/NRBPerdijk/111fae4189acafb2d75c7c66ba7f2be8#file-pom-xml)` + +``` + + org.apache.tika + tika-core + 1.24 + + + org.apache.tika + tika-parsers + 1.24 + +``` + +### Then, we need to properly configure a Tika Parser + +We need one in order to do actually do any [parsing](https://tika.apache.org/1.24/parser.html). Because this kind of configuration tends to be ugly, I have put it all inside its own object/class to keep it separate from the rest of the code: + +`[TikaOCRParser.scala](https://gist.github.com/NRBPerdijk/b59332173c9598991f8774d98266e57d#file-TikaOCRParser-scala)` + +``` +package tika.example + +import java.io.InputStream + +import org.apache.tika.config.TikaConfig +import org.apache.tika.metadata.Metadata +import org.apache.tika.parser.ocr.TesseractOCRConfig +import org.apache.tika.parser.pdf.PDFParserConfig +import org.apache.tika.parser.{AutoDetectParser, ParseContext, Parser} +import org.apache.tika.sax.BodyContentHandler + +object TikaOCRParser { + private val pdfConfig: PDFParserConfig = { + val pdfConf = new PDFParserConfig() + pdfConf.setOcrDPI(100) //scalastyle:ignore magic.number + pdfConf.setDetectAngles(true) + pdfConf.setOcrStrategy(PDFParserConfig.OCR_STRATEGY.OCR_ONLY) + + pdfConf + } + + private val tesseractOCRConfig: TesseractOCRConfig = { + val tessConf = new TesseractOCRConfig() + tessConf.setLanguage("eng") + tessConf.setEnableImageProcessing(1) + + tessConf + } + + private val parser = new AutoDetectParser(TikaConfig.getDefaultConfig) + + private val parseContext = { + val parseCont = new ParseContext() + parseCont.set(classOf[Parser], parser) + parseCont.set(classOf[PDFParserConfig], pdfConfig) + parseCont.set(classOf[TesseractOCRConfig], tesseractOCRConfig) + parseCont + } + + def parse(inputStream: InputStream, handler: BodyContentHandler, metadata: Metadata): Unit = parser.parse(inputStream, handler, metadata, parseContext) +} +``` + +Finally, we have to create… + +### The code that provides the file to be OCRed. + +`[TikaOCRApplication.scala](https://gist.github.com/NRBPerdijk/848526c10239f30129e20e8ea9ff6960#file-TikaOCRApplication-scala)` + +``` +package tika.example + +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset + +import org.apache.tika.metadata.Metadata +import org.apache.tika.sax.BodyContentHandler + +import scala.util.{Failure, Success, Using} + +object TikaOCRApplication extends App { + val input = getClass.getResourceAsStream("/ExampleOCR.jpg") + + val outputStream = new ByteArrayOutputStream() + + val attemptedOCR = Using(input) { inputStream => + TikaOCRParser.parse(inputStream, new BodyContentHandler(outputStream), new Metadata()) + }.map { _ => + new String(outputStream.toByteArray, Charset.defaultCharset()) + } + + attemptedOCR match { + case Success(value) => println(s"OCR result was: $value") + case Failure(exception) => println(s"OCR has failed, exception message was: ${exception.getMessage}") + } + +} +``` + +We just turn the file we want to `OCR` into an `InputStream` and hand that off to the `TikaOCRParser` we specified above for parsing. Because using `InputStreams` and doing parsing are two IO processes that can (definitely) throw `Exceptions`, I have delegated the handling of the `InputStream` using Scala’s `Using` functionality, which will automatically wrap the whole operation into a `Try` while also making sure that the `InputStream` is closed when everything is done, _even when exceptions are thrown_. If the result is a Success, I convert it into a regular String, which can then be printed, or otherwise used at your convenience. + +_(The example file is a_ `jpeg`, but lots of different image formats, as well as `PDF`, are supported. Some, like `JPEG2000`_, might require extra supporting software to be installed on the machine.)_ + +So, that’s it. Pretty easy, right? Check out the [Apache Tika documentation](https://tika.apache.org/) to see what other great functionality is available. Tesseract OCR is a pretty tricky field in and off itself, so be sure to check out all the [tweaks](https://tesseract-ocr.github.io/tessdoc/ImproveQuality) you may have to make for your particular dataset. If you want to see the full code for this example, you can [check it out on GitHub](https://github.com/NRBPerdijk/tikascalaexample). Last but not least, kudos to the Apache Software Foundation for their continuing work towards great Open Source solutions. + +Edit: I also wrote a short intro using Apache Tika to do Named-Entity Recognition (NER): [Tika NERding: Getting started using Named-Entity Recognition with OpenNLP on the JVM](https://medium.com/codestar-blog/tika-nerding-getting-started-using-named-entity-recognition-with-opennlp-on-the-jvm-scala-java-befc396d6dc5). + + diff --git a/src/articles/20201106-tika-nerding-getting-started-using-named-entity-recognition-with-opennlp-on-the-jvm-scala-java-befc396d6dc5.mdx b/src/articles/20201106-tika-nerding-getting-started-using-named-entity-recognition-with-opennlp-on-the-jvm-scala-java-befc396d6dc5.mdx new file mode 100644 index 0000000..2636969 --- /dev/null +++ b/src/articles/20201106-tika-nerding-getting-started-using-named-entity-recognition-with-opennlp-on-the-jvm-scala-java-befc396d6dc5.mdx @@ -0,0 +1,117 @@ +--- +title: "Tika NERding: Getting started using Named-Entity Recognition with OpenNLP on the JVM (Scala, Java…" +original_url: "https://medium.com/codestar-blog/tika-nerding-getting-started-using-named-entity-recognition-with-opennlp-on-the-jvm-scala-java-befc396d6dc5" +source: "medium" +author: Nathan Perdijk +publishedAt: 2020-11-06 +--- + +## Tika NERding: Getting started using Named-Entity Recognition with OpenNLP on the JVM (Scala, Java, Kotlin…) + +_For DataScience!_ + +Some things are hard, some things are not… Turns out that doing NER (Named-Entity Recognition) on the JVM is… not! (Wait, _[that sounds familiar](https://medium.com/codestar-blog/tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397)_…) + + + +[NER](https://en.wikipedia.org/wiki/Named-entity_recognition) is the automated process of annotating words and phrases in sentences with relevant entity information, such as marking a word as a `Person`, or a `Location`. This can come in quite handy when doing automated text analysis and is a staple in the DataScience community. As the trouble with DataScience is often getting it into production, it is extremely handy that this technique can be directly used from JVM-languages. Now we can embed this technology in production ready applications built in Java, Scala, Kotlin... + +First things first, the dependencies. These are all the dependencies from the `.pom.xml` file used for this example project: + +`[pom.xml](https://gist.github.com/NRBPerdijk/e902356b5021253cd3fbf728fc922927#file-pom-xml)` + +``` + + + + org.apache.tika + tika-parsers + 1.24.1 + + + + + org.slf4j + slf4j-simple + 1.7.30 + + +``` + +Yes, that’s it. + +For doing NER on `String` this is really all we need, but Apache Tika can also extract text from PDFs or even [perform OCR](https://medium.com/codestar-blog/tika-tika-getting-started-doing-ocr-with-apache-tika-andtesseract-from-the-jvm-f5d2bfe9b397), but you’ll need additional dependencies. + +Then we need to download the models that we want to use and place them in our `resources` folder. You can download suitable OpenNLP models from [http://opennlp.sourceforge.net/models-1.5/](http://opennlp.sourceforge.net/models-1.5/). These are conveniently wrapped in `.bin` format and should NOT be unpacked. + +For this example, we will be using English language Models that can recognise `Date`, `Location`, `Organization` and `Person`, but there are also models available in other languages. Every model you want to use, you’ll need to add to a `Java Map` (or you can use `SystemProperties` for Tika to “discover”, but that’s a method I don’t like very much): + +`[AddingModels.scala](https://gist.github.com/NRBPerdijk/238f1d9847f4ab2d49816497788f45c0#file-AddingModels-scala)` + +``` + val models = new java.util.HashMap[String, String]() + models.put("Dates", "en-ner-date.bin") + models.put("Locations", "en-ner-location.bin") + models.put("Organisations", "en-ner-organization.bin") + models.put("Persons", "en-ner-person.bin") +``` + +Now `models` contains four models, so let’s feed them to Tika: + +`[OpenNLPNERecogniser.scala](https://gist.github.com/NRBPerdijk/3f47b27e0bb81477213b7228936055d4#file-OpenNLPNERecogniser-scala)` + +``` + val nerRecogniser = new OpenNLPNERecogniser(models) +``` + +Alright, now all we need is to feed a `String` of text to the `nerRecogniser`: + +`[GettingResults.scala](https://gist.github.com/NRBPerdijk/22233389929c1c82636195974c178977#file-GettingResults-scala)` + +``` +val results = nerRecogniser.recognise(articleTextPart1) +``` + +And now you can just go about using the results. Tika will return a `Map`, containing a `key` for each model that has managed to find matching results, and with each key there’s a `value` containing those results. In order to improve the prints, I’ve done a bit of tinkering as it is now DEMO time. + +I’m using the contents of the [Wikipedia](https://en.wikipedia.org/) article on the [Peace of Utrecht](https://en.wikipedia.org/wiki/Peace_of_Utrecht). + +For the first paragraph, this is my input text: + +"The Peace of Utrecht is a series of peace treaties signed by the belligerents in the War of the Spanish Succession, in the Dutch city of Utrecht between April 1713 and February 1715. The war involved three contenders for the vacant throne of Spain, and involved much of Europe for over a decade. The main action saw France as the defender of Spain against a multinational coalition. The war was very expensive and bloody and finally stalemated. Essentially, the treaties allowed Philip V (grandson of King Louis XIV of France) to keep the Spanish throne in return for permanently renouncing his claim to the French throne, along with other necessary guarantees that would ensure that France and Spain should not merge, thus preserving the balance of power in Europe.\\n\\nThe treaties between several European states, including Spain, Great Britain, France, Portugal, Savoy and the Dutch Republic, helped end the war. The treaties were concluded between the representatives of Louis XIV of France and of his grandson Philip on one hand, and representatives of Anne of Great Britain, Victor Amadeus II of Sardinia, John V of Portugal and the United Provinces of the Netherlands on the other. Though the king of France ensured the Spanish crown for his dynasty, the treaties marked the end of French ambitions of hegemony in Europe expressed in the continuous wars of Louis XIV, and paved the way to the European system based on the balance of power.\[1\] British historian G. M. Trevelyan argues:\\n\\nThat Treaty, which ushered in the stable and characteristic period of Eighteenth-Century civilization, marked the end of danger to Europe from the old French monarchy, and it marked a change of no less significance to the world at large, — the maritime, commercial and financial supremacy of Great Britain.\[2\]\\n\\nAnother enduring result was the creation of the Spanish Bourbon Dynasty, still reigning over Spain up to the present while the original House of Bourbon has long since been dethroned in France." + +And these are the results from Tika NER: + +Locations: Britain, Milan, Nova Scotia, Cape Breton, Italy, France, Africa, Sicily, North Sea, North America, Amazon, Spain, Rastatt, Portugal, Sacramento, North + +Organisations: Article XIII, Spain + +Persons: Philip V, Philippe, Philip, Louis XIV's, Louis XV, Charles VI., Oyapock, Saint Kitts + +Date: 1713, 1720, 1713., 1712, 1714 + +And a second example, the second part of the article: + +"The War of the Spanish Succession was occasioned by the failure of the Habsburg king, Charles II of Spain, to produce an heir. Dispute followed the death of Charles II in 1700, and fourteen years of war were the result.\\n\\nFrance and Great Britain had come to terms in October 1711, when the preliminaries of peace had been signed in London. The preliminaries were based on a tacit acceptance of the partition of Spain's European possessions. Following this, the Congress of Utrecht opened on 29 January 1712, with the British representatives being John Robinson, Bishop of Bristol, and Thomas Wentworth, Lord Strafford.\[3\] Reluctantly the United Provinces accepted the preliminaries and sent representatives, but Emperor Charles VI refused to do so until he was assured that the preliminaries were not binding. This assurance was given, and so in February the Imperial representatives made their appearance. As Philip was not yet recognized as its king, Spain did not at first send plenipotentiaries, but the Duke of Savoy sent one, and the Kingdom of Portugal was represented by Luís da Cunha. One of the first questions discussed was the nature of the guarantees to be given by France and Spain that their crowns would be kept separate, and little progress was made until 10 July 1712, when Philip signed a renunciation.\[4\]\\n\\nWith Great Britain, France and Spain having agreed to a \\"suspension of arms\\" (armistice) covering Spain on 19 August in Paris, the pace of negotiation quickened. The first treaty signed at Utrecht was the truce between France and Portugal on 7 November, followed by the truce between France and Savoy on 14 March 1714. That same day, Spain, Great Britain, France and the Empire agreed to the evacuation of Catalonia and an armistice in Italy. The main treaties of peace followed on 11 April 1713. These were five separate treaties between France and Great Britain, the Netherlands, Savoy, Prussia and Portugal. Spain under Philip V signed separate peace treaties with Savoy and Great Britain at Utrecht on 13 July. Negotiations at Utrecht dragged on into the next year, for the peace treaty between Spain and the Netherlands was only signed on 26 June 1714 and that between Spain and Portugal on 6 February 1715.\[5\]\\n\\nSeveral other treaties came out of the congress of Utrecht. France signed treaties of commerce and navigation with Great Britain and the Netherlands (11 April 1713). Great Britain signed a like treaty with Spain (9 December 1713).\[5\]" + +And the results: + +Locations: 1715.\[16\], Britain, Spanish Netherlands, Austrian Netherlands, France + +Organisations: Oxford, House of, United Provinces, Dutch, Austro-Dutch Barrier Treaty, Harley, Duke, Earl, Allied + +Persons: Robert Harley, William III, Earl + +Date: 1710, 1709., May 1711), 1706 + +As you can see, not everything is found, or classified correctly, but it provides a good starting point for further text analysis and it took very little effort to get this working at all! Named-Entity Recognition is a tricky technique, so you may need to preprocess your texts a bit before you get good analysis results for your particular data set, but it’s definitely not difficult to get started. + +You can download suitable OpenNLP models from [http://opennlp.sourceforge.net/models-1.5/](http://opennlp.sourceforge.net/models-1.5/). + +Check out the [Apache Tika documentation](https://tika.apache.org/) to see what other great functionality is available. + +If you want to take a closer look at this example, you can check it out from Github: [https://github.com/NRBPerdijk/examplenertikascala/](https://github.com/NRBPerdijk/examplenertikascala/) + +Last but not least, kudos to the Apache Software Foundation for their continuing work towards great Open Source solutions. + + diff --git a/src/articles/20210928-micro-frontends-in-a-nutshell-cbf6741337d.mdx b/src/articles/20210928-micro-frontends-in-a-nutshell-cbf6741337d.mdx new file mode 100644 index 0000000..3d0f0a7 --- /dev/null +++ b/src/articles/20210928-micro-frontends-in-a-nutshell-cbf6741337d.mdx @@ -0,0 +1,97 @@ +--- +title: "Micro Frontends in A Nutshell" +original_url: "https://medium.com/codestar-blog/micro-frontends-in-a-nutshell-cbf6741337d" +source: "medium" +author: mdworld +publishedAt: 2021-09-28 +--- + +You may have heard of a Micro Frontends recently and felt it a bit difficult to grasp what exactly it is and if it is something you should get involved in. I’ll try to give a summary of the what, why, how, and when of the current state. To provide a high-over summary, I’ll also add some recommended reading if you would like to get more details. + +## Why use Micro Frontends? + +Do you have a very large front-end code-base? + +And by large, I’m talking about 50+ developers in a dozen of teams or more, probably cross-department, working on the same code-base in some manner. Do you have enterprise-scale continuous integration with e.g. GitLab, Bamboo running so many pipelines that the bottleneck is no longer a matter of adding more pods? Do you employ configuration managers or an Ops department to make sure deployments won’t affect each other too much? Do you have considerable codebases in incompatible front-end stacks, e.g. because of a migration from AngularJS to Angular >2? + +Then chances are you need to use Micro Frontends. Or actually, you most probably are _already_ using Micro Frontends. + +## What are Micro Frontends? + +How is it possible you would not know you are using Micro Frontends? And why are we just now hearing so much about them? The truth is that although the term is relatively new it actually covers **any range of solutions to integrate a collection of smaller frontends into one application**. + +Similar to Micro Service architectures, Micro Frontends facilitate large codebases by breaking them up into manageable pieces. This means: + +* Technological stack across Micro Frontends in the same application may differ +* A Micro Frontend has a clear and concise purpose, following the SOLID principles +* Teams of developers maintain one or more Micro Frontend that are isolated in runtime from the rest of the code + +The term is now popularized because of the advent of _Module Federation_ in Webpack 5. Webpack 5 has been released towards the end of 2020, but this new major release is taking some time to be integrated in relevant tooling, e.g. Nx and Ng CLI. + +Note that Micro Frontends (MFEs) are sometimes also referred to as _Micro Apps_. + +## How do you build Micro Frontends? + +Since Micro Frontends as a concept are not new, some solutions that can be classified as Micro Frontends are ancient, considering the speed of development in the frontend ecosystem. Here are some of them, to give an idea of how broad Micro Frontends can be interpreted: + +* Run several frontend applications on different URLs and cross refer them with plain **hyperlinks** +* Run several frontend applications on different URLs on the same page in **iframes** +* Develop frontend components in separate teams and integrate them at build time to be deployed as a **deployment monolith** +* Use macro **Web Components** as an abstraction layer for components +* Develop frontend components in separate teams and integrate them at runtime with **Module Federation** + +I won’t go into all the details about the pros and cons of each of these solutions. Instead I refer you to the recommended reading list below. + +Note that solutions can be combined: you can have a deployment monolith (that expects components that are all using the same stack) but wrap components in Web Components to provide an abstraction layer and use different stacks to produce the Web Components. Additionally, you can use Web Components in combination with Module Federation for instance if you are migrating towards Module Federation as a Micro Frontends solution. Consider this schematic representation of a web application: + +Schematic representation of a web application + +This could be implemented with different platforms like Angular and React by wrapping them in Web Components: + +Web Components diagram + +Module Federation is the newest solution and many libraries are still adapting to it. Last year, Nx 12 released with support for Webpack 5 and Module Federation. See a real working example here [https://code-star.github.io/nx-reference-shell/](https://code-star.github.io/nx-reference-shell/) or its source in [https://github.com/code-star/nx-reference](https://github.com/code-star/nx-reference). + +Because Micro Frontends break up a codebase into smaller, more manageable fragments, they are often mentioned in combination with Monorepo solutions like Nx or yarn/npm workspaces. However, it is perfectly possible to implement Micro Frontends without monorepos! + +## When to use Micro Frontends? + +New technology inspires developers to experiment, but Micro Frontends and with that Module Federation are not worth the upkeep for small to medium applications. All-in solutions like Next or Gatsby are great fits for smaller applications and custom Angular applications, when well organized, scale very well up to enterprise level. + +However, no framework inherently supports older versions of itself. So if a big bang migration from AngularJS to Angular or any other framework for that matter, you’ll end up with some kind of Micro Frontends solution. Plenty of enterprise codebases currently use some combination of hyperlinks and deployment monoliths. + +This could look like a bank that offers a set of public pages (e.g. the general home page, and the landing pages of its departments) referencing each other with hyperlinks and a protected monolith app with many components (e.g. checking account, subscriptions to bank products, investments on one page). + +Hyperlinks and Monolith diagram + +Exploring Module Federation can be worth it if continuous integration is slowed down too much because of the large amounts of tests and compilation of all the involved components. But note that there are other approaches, such as using Nx monorepos with properly set up hierarchy and running only affected tests. + +Another reason to use Module Federation can be the need to support multiple frameworks. Compared to Web Components, Module Federation improves the runtime isolation of components while simultaneously reducing isolation of shared dependencies to reduce the overall footprint of the application. + +Compare to the diagram for the earlier example using macro Web Components, you can see that lodash, Angular and React are only loaded once, despite being used by multiple isolated components: + +Module Federation diagram + +## Want to know more? + +If you want to know more about Micro Frontends, Module Federation or Monorepos, you can contact met at [@mdworldNL](https://twitter.com/mdworldNL) on Twitter or mail codestar@ordina.nl. We have experience with enterprise frontend at all the major banks and many governmental departments in the Netherlands. + +When you want more background information as a developer, you can also read the articles provided below. + +## Recommended in-depth reading + +* Introduction to Micro Frontends: [https://micro-frontends.org/](https://micro-frontends.org/) +* Introduction to Micro Frontends: [https://martinfowler.com/articles/micro-frontends.html](https://martinfowler.com/articles/micro-frontends.html) + +Angular Architects: + +* Micro Frontends introduction: [https://www.angulararchitects.io/en/aktuelles/a-software-architects-approach-towards/](https://www.angulararchitects.io/en/aktuelles/a-software-architects-approach-towards/) +* Micro Frontends series: [https://www.angulararchitects.io/en/aktuelles/micro-apps-with-web-components-using-angular-elements/](https://www.angulararchitects.io/en/aktuelles/micro-apps-with-web-components-using-angular-elements/) +* Module Federation series: [https://www.angulararchitects.io/en/aktuelles/the-microfrontend-revolution-module-federation-in-webpack-5/](https://www.angulararchitects.io/en/aktuelles/the-microfrontend-revolution-module-federation-in-webpack-5/) + +By my colleague Peter Eijgermans: + +* Micro Frontends by Example: [https://dzone.com/articles/micro-frontends-by-example-8](https://dzone.com/articles/micro-frontends-by-example-8) +* (Video) Micro Frontends: The What, the Why and the How by Peter Eijgermans [https://youtu.be/TWcoziCdPkE](https://youtu.be/TWcoziCdPkE) + +This article was originally published at [https://mdworld.nl/micro-frontends-in-a-nutshell](https://mdworld.nl/micro-frontends-in-a-nutshell) diff --git a/src/lib/meetup/getPastMeetups.ts b/src/lib/meetup/getPastMeetups.ts index d20366a..e83a8a8 100644 --- a/src/lib/meetup/getPastMeetups.ts +++ b/src/lib/meetup/getPastMeetups.ts @@ -1,21 +1,31 @@ +import { + eventsDocument, + GQL_ENDPOINT, + GROUP_URL_NAME, + mapResponseToMeetupEvents, +} from "./meetup-graphql"; import { IMeetupEvent, MeetupResponse } from "./meetup.types"; +import { request } from "graphql-request"; -// Meetup API test console: https://secure.meetup.com/meetup_api/console/?path=/:urlname/events -const GET_PAST_EVENTS_URL = - "https://api.meetup.com/Codestar-Night/events?&sign=true&photo-host=public&page=20&desc=true&status=past&fields=featured_photo"; +const pastEventsVariables = { + urlname: GROUP_URL_NAME, + status: "PAST", + sortOrder: "DESC", +}; export const getPastMeetups = async (): Promise | null> => { try { - const response = await fetch(GET_PAST_EVENTS_URL); - if (response.ok) { - const meetupResponse: MeetupResponse = await response.json(); - return meetupResponse.slice(0, 7); - } else { - console.log("not ok"); - return null; - } + const pastMeetupsResponse = await request( + GQL_ENDPOINT, + eventsDocument, + pastEventsVariables + ); + + const meetupEvents = mapResponseToMeetupEvents(pastMeetupsResponse); + + return meetupEvents.slice(0, 7); } catch (err) { - console.log(err); + console.log("getPastMeetups failed:", err); return null; } }; diff --git a/src/lib/meetup/getUpcomingMeetups.ts b/src/lib/meetup/getUpcomingMeetups.ts index 2602ca1..07c9838 100644 --- a/src/lib/meetup/getUpcomingMeetups.ts +++ b/src/lib/meetup/getUpcomingMeetups.ts @@ -1,22 +1,30 @@ +import request from "graphql-request"; +import { + eventsDocument, + GQL_ENDPOINT, + GROUP_URL_NAME, + mapResponseToMeetupEvents, +} from "./meetup-graphql"; import { IMeetupEvent, MeetupResponse } from "./meetup.types"; -// Meetup API test console: https://secure.meetup.com/meetup_api/console/?path=/:urlname/events -const GET_UPCOMING_EVENTS_URL = - "https://api.meetup.com/Codestar-Night/events?&sign=true&photo-host=public&page=3&fields=featured_photo&desc=false"; +const upcomingEventsVariables = { + urlname: GROUP_URL_NAME, + status: "ACTIVE", + sortOrder: "ASC", +}; export const getUpcomingMeetups = async (): Promise | null> => { try { - const response = await fetch(GET_UPCOMING_EVENTS_URL); - if (response.ok) { - const meetupResponse: MeetupResponse = await response.json(); - return meetupResponse; - } else { - console.log("not ok"); - return null; - } + const upcomingMeetupsResponse = await request( + GQL_ENDPOINT, + eventsDocument, + upcomingEventsVariables + ); + + return mapResponseToMeetupEvents(upcomingMeetupsResponse); } catch (err) { - console.log(err); + console.log("getUpcomingMeetups failed:", err); return null; } }; diff --git a/src/lib/meetup/meetup-graphql.ts b/src/lib/meetup/meetup-graphql.ts new file mode 100644 index 0000000..86668d7 --- /dev/null +++ b/src/lib/meetup/meetup-graphql.ts @@ -0,0 +1,37 @@ +import { gql } from "graphql-request"; +import { IMeetupEvent, MeetupResponse } from "./meetup.types"; + +// Meetup API test console: https://secure.meetup.com/meetup_api/console/?path=/:urlname/events +export const GQL_ENDPOINT = "https://api.meetup.com/gql-ext"; + +export const GROUP_URL_NAME = 'codestar-night'; + +export const eventsDocument = gql` + query ($urlname: String!, $status: EventStatus, $sortOrder: SortOrder) { + groupByUrlname(urlname: $urlname) { + events(status: $status, sort: $sortOrder) { + edges { + node { + title + id + eventUrl + dateTime + description + } + } + } + } + } +`; + +export const mapResponseToMeetupEvents = (response: MeetupResponse): Array => { + return response.groupByUrlname.events.edges.map((edge) => { + const { title, dateTime, eventUrl, description } = edge.node; + return { + name: title, + time: dateTime, + link: eventUrl, + description, + } as IMeetupEvent; + }); +}; diff --git a/src/lib/meetup/meetup.types.ts b/src/lib/meetup/meetup.types.ts index 6951540..1017adc 100644 --- a/src/lib/meetup/meetup.types.ts +++ b/src/lib/meetup/meetup.types.ts @@ -9,4 +9,18 @@ export interface IMeetupEvent { }; } -export type MeetupResponse = Array; +export type MeetupResponse = { + groupByUrlname: { + events: { + edges: Array<{ + node: { + title: string; + id: string; + eventUrl: string; + dateTime: string; + description: string; + }; + }>; + }; + }; +} \ No newline at end of file diff --git a/src/lib/publications/getPublications.ts b/src/lib/publications/getPublications.ts index 1ed4764..adfefd0 100644 --- a/src/lib/publications/getPublications.ts +++ b/src/lib/publications/getPublications.ts @@ -129,15 +129,17 @@ export const getBlogPosts = (): IPublication[] => { export const getPublications = async (): Promise => { try { const blogPosts = getBlogPosts(); - const mediumPublications = await getMediumPublications(); - - return blogPosts - .concat(mediumPublications) - .sort((publication, otherPublication) => { - const date = new Date(publication.latestPublishedAt); - const otherDate = new Date(otherPublication.latestPublishedAt); - return otherDate.getTime() - date.getTime(); - }); + // const mediumPublications = await getMediumPublications(); + + return ( + blogPosts + // .concat(mediumPublications) + .sort((publication, otherPublication) => { + const date = new Date(publication.latestPublishedAt); + const otherDate = new Date(otherPublication.latestPublishedAt); + return otherDate.getTime() - date.getTime(); + }) + ); } catch (err) { console.error("error: " + err); return null;