diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67bb2aaf..4a2a7e4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: e2e: needs: [changes, lint_unit_tests_and_builds] - if: needs.changes.outputs.renderer == 'true' && (github.event_name == 'push' || github.event.pull_request.draft == false) + if: needs.changes.outputs.renderer == 'true' runs-on: ubuntu-latest defaults: run: @@ -78,6 +78,8 @@ jobs: retention-days: 30 changes: + # nothing runs on a draft; marking it ready is a trigger of its own + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-latest outputs: renderer: ${{ steps.filter.outputs.renderer }} @@ -90,8 +92,10 @@ jobs: filters: | renderer: - '!integrations/**' + - '!examples/integrations/**' integrations: - 'integrations/**' + - 'examples/integrations/**' - '.github/workflows/ci.yml' - '.github/workflows/integrations-matrix.yml' diff --git a/.github/workflows/integrations-matrix.yml b/.github/workflows/integrations-matrix.yml index 75aa221e..a3fa40b6 100644 --- a/.github/workflows/integrations-matrix.yml +++ b/.github/workflows/integrations-matrix.yml @@ -6,7 +6,7 @@ on: - cron: '0 6 * * 1' pull_request: branches: [main, 'release/**'] - types: [labeled, synchronize] + types: [labeled, synchronize, ready_for_review] permissions: contents: read @@ -22,7 +22,7 @@ env: jobs: cells: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests') + if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -53,7 +53,7 @@ jobs: path: integrations/results node-20: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests') + if: github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests')) runs-on: ubuntu-latest continue-on-error: true name: node 20 (end of life, courtesy) diff --git a/README.md b/README.md index 99589f03..6fa886f7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ See [Auto-generate documentation](https://docs.usebruno.com/api-docs/auto-genera | Path | What it is | |---|---| | [`packages/bruno-api-docs`](./packages/bruno-api-docs) | The renderer: a React app built as a library and as the **standalone bundle** (`dist-standalone/`), the artifact the Bruno app's generated HTML loads from the CDN. | -| [`examples/`](./examples) | Consumption patterns: `standalone-html` (how a generated HTML file uses the bundle), `react` and `express-server` (design sketches of the intended embedding API; not shipped packages yet, see Roadmap). | +| [`examples/`](./examples) | `standalone-html`, how a generated HTML page uses the bundle. `integrations/nodejs`, runnable examples for the framework packages, one folder per framework. | ## Roadmap diff --git a/examples/express-server/package-lock.json b/examples/express-server/package-lock.json deleted file mode 100644 index 02c94549..00000000 --- a/examples/express-server/package-lock.json +++ /dev/null @@ -1,951 +0,0 @@ -{ - "name": "opencollection-express-example", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "opencollection-express-example", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "express": "^4.18.2" - }, - "devDependencies": { - "@types/express": "^4.17.21" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", - "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "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", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/examples/express-server/package.json b/examples/express-server/package.json deleted file mode 100644 index 93077f6e..00000000 --- a/examples/express-server/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "opencollection-express-example", - "version": "1.0.0", - "description": "Express server example using OpenCollection playground", - "main": "server.js", - "type": "module", - "scripts": { - "start": "node server.js", - "start:cjs": "node server-cjs.cjs", - "dev": "node --watch server.js", - "dev:cjs": "node --watch server-cjs.cjs", - "test": "node test.js" - }, - "dependencies": { - "express": "^4.18.2" - }, - "devDependencies": { - "@types/express": "^4.17.21" - }, - "keywords": [ - "opencollection", - "express", - "api-docs", - "playground" - ], - "author": "OpenCollection", - "license": "MIT" -} \ No newline at end of file diff --git a/examples/express-server/server-cjs.cjs b/examples/express-server/server-cjs.cjs deleted file mode 100644 index 1df77472..00000000 --- a/examples/express-server/server-cjs.cjs +++ /dev/null @@ -1,117 +0,0 @@ -const express = require('express'); -const { playgroundHandler, setupPlaygroundAssets } = require('../../packages/oc-playground/dist/express.cjs'); - -const app = express(); -const PORT = process.env.PORT || 3001; - -setupPlaygroundAssets(app, express.static, { - playgroundDistPath: '../../packages/oc-playground/dist-standalone' -}); - -const apiCollection = { - name: "My Express API (CommonJS)", - description: "Example API documentation using OpenCollection playground with Express and CommonJS", - docs: "This collection demonstrates different request types, bodies, auth, variables, and assertions for an Express-based API using CommonJS imports.", - environments: [ - { - name: "Development", - description: "Local development environment", - variables: [ - { name: "baseUrl", value: `http://localhost:${PORT}` }, - { name: "authToken", value: "dev-token-123" } - ] - } - ], - base: { - headers: [ - { name: "Content-Type", value: "application/json" } - ], - auth: { - type: "bearer", - token: "{{authToken}}" - } - }, - items: [ - { - name: "Get Users", - type: "http", - url: "{{baseUrl}}/api/users", - method: "GET", - docs: "Retrieve a list of all users", - params: [ - { name: "limit", value: "10", type: "query" } - ], - assertions: [ - { expression: "response.status", operator: "==", value: "200" } - ] - }, - { - name: "Create User", - type: "http", - url: "{{baseUrl}}/api/users", - method: "POST", - docs: "Create a new user", - body: { - type: "json", - data: JSON.stringify({ - name: "Bob Wilson", - email: "bob@example.com" - }, null, 2) - } - } - ] -}; - -app.get('/api-docs', playgroundHandler({ - collection: apiCollection, - theme: 'dark', - title: 'My Express API Documentation (CommonJS)' -})); - -app.get('/simple-docs', playgroundHandler({ - name: "Simple API (CommonJS)", - description: "My awesome API documentation using CommonJS", - items: [{ - type: "http", - method: "GET", - url: "https://api.example.com/users", - docs: "Get all users" - }] -})); - -app.get('/api/users', (req, res) => { - res.json({ - users: [ - { id: 1, name: 'Bob Wilson', email: 'bob@example.com' }, - { id: 2, name: 'Carol Davis', email: 'carol@example.com' } - ] - }); -}); - -app.post('/api/users', express.json(), (req, res) => { - const { name, email } = req.body; - res.status(201).json({ - id: Date.now(), - name, - email, - createdAt: new Date().toISOString() - }); -}); - -app.get('/', (req, res) => { - res.send(` -

OpenCollection Express Example (CommonJS)

-

Visit the API documentation:

- - `); -}); - -app.listen(PORT, () => { - console.log(`🚀 CommonJS Server running on http://localhost:${PORT}`); - console.log(`📚 API docs available at:`); - console.log(` - http://localhost:${PORT}/api-docs`); - console.log(` - http://localhost:${PORT}/simple-docs`); -}); \ No newline at end of file diff --git a/examples/express-server/server.js b/examples/express-server/server.js deleted file mode 100644 index c421bca5..00000000 --- a/examples/express-server/server.js +++ /dev/null @@ -1,247 +0,0 @@ -import express from 'express'; -import { playgroundHandler, setupPlaygroundAssets } from '../../packages/oc-playground/dist/express.js'; - -const app = express(); -const PORT = process.env.PORT || 3000; - -setupPlaygroundAssets(app, express.static, { - playgroundDistPath: '../../packages/oc-playground/dist-standalone' -}); - -const apiCollection = { - name: "My Express API", - description: "Example API documentation using OpenCollection playground with Express", - docs: "This collection demonstrates different request types, bodies, auth, variables, and assertions for an Express-based API.", - environments: [ - { - name: "Development", - description: "Local development environment", - variables: [ - { name: "baseUrl", value: `http://localhost:${PORT}` }, - { name: "authToken", value: "dev-token-123" } - ] - }, - { - name: "Production", - variables: [ - { name: "baseUrl", value: "https://api.example.com" }, - { name: "authToken", value: "prod-token-456" } - ] - } - ], - base: { - headers: [ - { name: "Content-Type", value: "application/json" } - ], - auth: { - type: "bearer", - token: "{{authToken}}" - }, - variables: [ - { name: "globalVar", value: "global-value" } - ] - }, - items: [ - { - name: "Get Users", - type: "http", - url: "{{baseUrl}}/api/users", - method: "GET", - docs: "Retrieve a list of all users", - params: [ - { name: "limit", value: "10", type: "query" }, - { name: "offset", value: "0", type: "query" } - ], - assertions: [ - { expression: "response.status", operator: "==", value: "200" } - ] - }, - { - name: "Get User by ID", - type: "http", - url: "{{baseUrl}}/api/users/:id", - method: "GET", - docs: "Get a specific user by their ID", - params: [ - { name: "id", value: "123", type: "path" } - ], - headers: [ - { name: "X-Custom-Header", value: "CustomValue" } - ] - }, - { - name: "Create User", - type: "http", - url: "{{baseUrl}}/api/users", - method: "POST", - docs: "Create a new user", - body: { - type: "json", - data: JSON.stringify({ - name: "Alice Johnson", - email: "alice@example.com", - role: "user" - }, null, 2) - }, - assertions: [ - { expression: "response.body.name", operator: "==", value: "Alice Johnson" }, - { expression: "response.status", operator: "==", value: "201" } - ] - }, - { - name: "Update User", - type: "http", - url: "{{baseUrl}}/api/users/:id", - method: "PUT", - docs: "Update an existing user", - params: [ - { name: "id", value: "123", type: "path" } - ], - body: [ - { name: "name", value: "Alice Smith" }, - { name: "email", value: "alice.smith@example.com" } - ] - }, - { - name: "Delete User", - type: "http", - url: "{{baseUrl}}/api/users/:id", - method: "DELETE", - docs: "Delete a user by ID", - params: [ - { name: "id", value: "123", type: "path" } - ], - auth: { - type: "apikey", - key: "X-API-Key", - value: "secret-key-999", - placement: "header" - } - }, - { - name: "User Management", - type: "folder", - docs: "Folder containing advanced user management endpoints", - variables: [ - { name: "adminUserId", value: "admin-123" } - ], - headers: [ - { name: "X-Admin-Header", value: "AdminHeaderValue" } - ], - auth: { - type: "basic", - username: "admin", - password: "password" - } - }, - { - name: "Get Admin Dashboard", - type: "http", - url: "{{baseUrl}}/api/admin/dashboard", - method: "GET", - docs: "Access the admin dashboard (requires admin auth)" - } - ] -}; - -app.get('/api-docs', playgroundHandler({ - collection: apiCollection, - theme: 'light', - title: 'My Express API Documentation' -})); - -app.get('/simple-docs', playgroundHandler({ - name: "Simple API", - description: "My awesome API documentation", - items: [{ - type: "http", - method: "GET", - url: "https://api.example.com/users", - docs: "Get all users" - }], - theme: 'dark' -})); - -app.get('/api/users', (req, res) => { - const { limit = 10, offset = 0 } = req.query; - res.json({ - users: [ - { id: 1, name: 'John Doe', email: 'john@example.com' }, - { id: 2, name: 'Jane Smith', email: 'jane@example.com' } - ].slice(offset, offset + parseInt(limit)), - total: 2, - limit: parseInt(limit), - offset: parseInt(offset) - }); -}); - -app.get('/api/users/:id', (req, res) => { - const { id } = req.params; - res.json({ - id: parseInt(id), - name: 'John Doe', - email: 'john@example.com', - role: 'user' - }); -}); - -app.post('/api/users', express.json(), (req, res) => { - const { name, email, role = 'user' } = req.body; - res.status(201).json({ - id: Date.now(), - name, - email, - role, - createdAt: new Date().toISOString() - }); -}); - -app.put('/api/users/:id', express.json(), (req, res) => { - const { id } = req.params; - const { name, email } = req.body; - res.json({ - id: parseInt(id), - name, - email, - updatedAt: new Date().toISOString() - }); -}); - -app.delete('/api/users/:id', (req, res) => { - const { id } = req.params; - res.status(204).send(); -}); - -app.get('/api/admin/dashboard', (req, res) => { - res.json({ - stats: { - totalUsers: 42, - activeUsers: 38, - newUsersToday: 5 - }, - message: 'Admin dashboard data' - }); -}); - -app.get('/', (req, res) => { - res.send(` -

OpenCollection Express Example

-

Visit the API documentation:

- -

Try the API endpoints:

- - `); -}); - -app.listen(PORT, () => { - console.log(`🚀 Server running on http://localhost:${PORT}`); - console.log(`📚 API docs available at:`); - console.log(` - http://localhost:${PORT}/api-docs`); - console.log(` - http://localhost:${PORT}/simple-docs`); -}); \ No newline at end of file diff --git a/examples/integrations/eslint.config.mjs b/examples/integrations/eslint.config.mjs new file mode 100644 index 00000000..dd7c78f5 --- /dev/null +++ b/examples/integrations/eslint.config.mjs @@ -0,0 +1,7 @@ +// eslint lints nothing above its config, so the workspace rules are applied from here +import { ignores, workspace } from '../../integrations/eslint.config.mjs'; + +export default [ + ignores, + { ...workspace, files: ['**/*.{js,ts}'], rules: { ...workspace.rules, 'no-console': 'off' } } +]; diff --git a/integrations/nodejs/express/examples/api-collection/catalog/folder.yml b/examples/integrations/nodejs/express/api-collection/catalog/folder.yml similarity index 100% rename from integrations/nodejs/express/examples/api-collection/catalog/folder.yml rename to examples/integrations/nodejs/express/api-collection/catalog/folder.yml diff --git a/integrations/nodejs/express/examples/api-collection/catalog/get product.yml b/examples/integrations/nodejs/express/api-collection/catalog/get product.yml similarity index 100% rename from integrations/nodejs/express/examples/api-collection/catalog/get product.yml rename to examples/integrations/nodejs/express/api-collection/catalog/get product.yml diff --git a/integrations/nodejs/express/examples/api-collection/catalog/list products.yml b/examples/integrations/nodejs/express/api-collection/catalog/list products.yml similarity index 100% rename from integrations/nodejs/express/examples/api-collection/catalog/list products.yml rename to examples/integrations/nodejs/express/api-collection/catalog/list products.yml diff --git a/integrations/nodejs/express/examples/api-collection/opencollection.yml b/examples/integrations/nodejs/express/api-collection/opencollection.yml similarity index 100% rename from integrations/nodejs/express/examples/api-collection/opencollection.yml rename to examples/integrations/nodejs/express/api-collection/opencollection.yml diff --git a/integrations/nodejs/express/examples/auth.js b/examples/integrations/nodejs/express/auth.js similarity index 90% rename from integrations/nodejs/express/examples/auth.js rename to examples/integrations/nodejs/express/auth.js index 75bba061..0df73d4d 100644 --- a/integrations/nodejs/express/examples/auth.js +++ b/examples/integrations/nodejs/express/auth.js @@ -20,6 +20,6 @@ app.get('/health', (req, res) => res.json({ ok: true })); // the guard runs before the docs on this mount and nowhere else. It covers collection.yml too, // which is the file that carries the collection's own auth and variables. -app.use('/docs', requireToken, apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', requireToken, apiDocs({ collectionPath: './api-collection' })); app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/ (Authorization: Bearer let-me-in)`)); diff --git a/integrations/nodejs/express/examples/csp.js b/examples/integrations/nodejs/express/csp.js similarity index 94% rename from integrations/nodejs/express/examples/csp.js rename to examples/integrations/nodejs/express/csp.js index 369bdbec..c96aa24e 100644 --- a/integrations/nodejs/express/examples/csp.js +++ b/examples/integrations/nodejs/express/csp.js @@ -25,6 +25,6 @@ app.use(helmet({ app.get('/health', (req, res) => res.json({ ok: true })); -app.use('/docs', apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', apiDocs({ collectionPath: './api-collection' })); app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/`)); diff --git a/integrations/nodejs/express/examples/embed.js b/examples/integrations/nodejs/express/embed.js similarity index 90% rename from integrations/nodejs/express/examples/embed.js rename to examples/integrations/nodejs/express/embed.js index f521f8e4..9b8e40a2 100644 --- a/integrations/nodejs/express/examples/embed.js +++ b/examples/integrations/nodejs/express/embed.js @@ -8,7 +8,7 @@ const port = process.env.PORT ?? 3000; // the docs inside a page of your own. The mount serves the collection and shell.js; the block // only has to know where the mount is. -app.use('/docs', apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', apiDocs({ collectionPath: './api-collection' })); app.get('/', (req, res) => { res.type('html').send(` diff --git a/integrations/nodejs/express/examples/quickstart.js b/examples/integrations/nodejs/express/quickstart.js similarity index 85% rename from integrations/nodejs/express/examples/quickstart.js rename to examples/integrations/nodejs/express/quickstart.js index 58f41f80..cd3f82fd 100644 --- a/integrations/nodejs/express/examples/quickstart.js +++ b/examples/integrations/nodejs/express/quickstart.js @@ -9,6 +9,6 @@ const port = process.env.PORT ?? 3000; app.get('/health', (req, res) => res.json({ ok: true })); // the path is relative to this file, not to where node was started -app.use('/docs', apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', apiDocs({ collectionPath: './api-collection' })); app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/`)); diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/folder.yml b/examples/integrations/nodejs/fastify/api-collection/catalog/folder.yml similarity index 100% rename from integrations/nodejs/fastify/examples/api-collection/catalog/folder.yml rename to examples/integrations/nodejs/fastify/api-collection/catalog/folder.yml diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/get product.yml b/examples/integrations/nodejs/fastify/api-collection/catalog/get product.yml similarity index 100% rename from integrations/nodejs/fastify/examples/api-collection/catalog/get product.yml rename to examples/integrations/nodejs/fastify/api-collection/catalog/get product.yml diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/list products.yml b/examples/integrations/nodejs/fastify/api-collection/catalog/list products.yml similarity index 100% rename from integrations/nodejs/fastify/examples/api-collection/catalog/list products.yml rename to examples/integrations/nodejs/fastify/api-collection/catalog/list products.yml diff --git a/integrations/nodejs/fastify/examples/api-collection/opencollection.yml b/examples/integrations/nodejs/fastify/api-collection/opencollection.yml similarity index 100% rename from integrations/nodejs/fastify/examples/api-collection/opencollection.yml rename to examples/integrations/nodejs/fastify/api-collection/opencollection.yml diff --git a/integrations/nodejs/fastify/examples/auth.js b/examples/integrations/nodejs/fastify/auth.js similarity index 91% rename from integrations/nodejs/fastify/examples/auth.js rename to examples/integrations/nodejs/fastify/auth.js index aec69de3..e4331656 100644 --- a/integrations/nodejs/fastify/examples/auth.js +++ b/examples/integrations/nodejs/fastify/auth.js @@ -19,7 +19,7 @@ app.register(async (guarded) => { } }); - await guarded.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); + await guarded.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); }); app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/ (Authorization: Bearer let-me-in)`)); diff --git a/integrations/nodejs/fastify/examples/csp.js b/examples/integrations/nodejs/fastify/csp.js similarity index 93% rename from integrations/nodejs/fastify/examples/csp.js rename to examples/integrations/nodejs/fastify/csp.js index 3f28a623..766bd293 100644 --- a/integrations/nodejs/fastify/examples/csp.js +++ b/examples/integrations/nodejs/fastify/csp.js @@ -24,6 +24,6 @@ app.addHook('onSend', async (request, reply) => { app.get('/health', async () => ({ ok: true })); -app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); +app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/`)); diff --git a/integrations/nodejs/fastify/examples/embed.js b/examples/integrations/nodejs/fastify/embed.js similarity index 89% rename from integrations/nodejs/fastify/examples/embed.js rename to examples/integrations/nodejs/fastify/embed.js index c9c72156..3d76a10d 100644 --- a/integrations/nodejs/fastify/examples/embed.js +++ b/examples/integrations/nodejs/fastify/embed.js @@ -8,7 +8,7 @@ const port = process.env.PORT ?? 3000; // the docs inside a page of your own. The mount serves the collection and shell.js; the block // only has to know where the mount is. -app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); +app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); app.get('/', async (request, reply) => { return reply.type('text/html').send(` diff --git a/integrations/nodejs/fastify/examples/quickstart.js b/examples/integrations/nodejs/fastify/quickstart.js similarity index 83% rename from integrations/nodejs/fastify/examples/quickstart.js rename to examples/integrations/nodejs/fastify/quickstart.js index f7f94a0e..e1077558 100644 --- a/integrations/nodejs/fastify/examples/quickstart.js +++ b/examples/integrations/nodejs/fastify/quickstart.js @@ -9,6 +9,6 @@ const port = process.env.PORT ?? 3000; app.get('/health', async () => ({ ok: true })); // prefix is the mount. The collection path is relative to this file, not to where node was started. -app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); +app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/`)); diff --git a/integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts b/examples/integrations/nodejs/nestjs/01-quickstart/app.module.ts similarity index 82% rename from integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts rename to examples/integrations/nodejs/nestjs/01-quickstart/app.module.ts index 57777936..9c4a09f7 100644 --- a/integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts +++ b/examples/integrations/nodejs/nestjs/01-quickstart/app.module.ts @@ -5,7 +5,7 @@ import { ApiDocsModule } from '@usebruno/api-docs-nestjs'; imports: [ // relative to the compiled entry file, dist/01-quickstart/main.js, not to this source file // and not to the cwd. From there, examples/api-collection is two levels up. - ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' }) + ApiDocsModule.forRoot({ collectionPath: '../../api-collection' }) ] }) export class AppModule {} diff --git a/integrations/nodejs/nestjs/examples/01-quickstart/main.ts b/examples/integrations/nodejs/nestjs/01-quickstart/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/01-quickstart/main.ts rename to examples/integrations/nodejs/nestjs/01-quickstart/main.ts diff --git a/integrations/nodejs/nestjs/examples/02-auth/app.module.ts b/examples/integrations/nodejs/nestjs/02-auth/app.module.ts similarity index 90% rename from integrations/nodejs/nestjs/examples/02-auth/app.module.ts rename to examples/integrations/nodejs/nestjs/02-auth/app.module.ts index 46171f0d..57ea8f5a 100644 --- a/integrations/nodejs/nestjs/examples/02-auth/app.module.ts +++ b/examples/integrations/nodejs/nestjs/02-auth/app.module.ts @@ -14,8 +14,8 @@ function requireApiKey(req: Request, res: Response, next: NextFunction): void { @Module({ imports: [ - ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' }), - ApiDocsModule.forRoot({ mountPath: '/internal/docs', collectionUrl: '../../api-collection' }) + ApiDocsModule.forRoot({ collectionPath: '../../api-collection' }), + ApiDocsModule.forRoot({ mountPath: '/internal/docs', collectionPath: '../../api-collection' }) ] }) export class AppModule implements NestModule { diff --git a/integrations/nodejs/nestjs/examples/02-auth/main.ts b/examples/integrations/nodejs/nestjs/02-auth/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/02-auth/main.ts rename to examples/integrations/nodejs/nestjs/02-auth/main.ts diff --git a/integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts b/examples/integrations/nodejs/nestjs/03-global-prefix/app.module.ts similarity index 63% rename from integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts rename to examples/integrations/nodejs/nestjs/03-global-prefix/app.module.ts index 1878b959..bb0f9295 100644 --- a/integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts +++ b/examples/integrations/nodejs/nestjs/03-global-prefix/app.module.ts @@ -2,6 +2,6 @@ import { Module } from '@nestjs/common'; import { ApiDocsModule } from '@usebruno/api-docs-nestjs'; @Module({ - imports: [ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' })] + imports: [ApiDocsModule.forRoot({ collectionPath: '../../api-collection' })] }) export class AppModule {} diff --git a/integrations/nodejs/nestjs/examples/03-global-prefix/main.ts b/examples/integrations/nodejs/nestjs/03-global-prefix/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/03-global-prefix/main.ts rename to examples/integrations/nodejs/nestjs/03-global-prefix/main.ts diff --git a/integrations/nodejs/nestjs/examples/05-csp/app.module.ts b/examples/integrations/nodejs/nestjs/04-fastify-adapter/app.module.ts similarity index 63% rename from integrations/nodejs/nestjs/examples/05-csp/app.module.ts rename to examples/integrations/nodejs/nestjs/04-fastify-adapter/app.module.ts index 1878b959..bb0f9295 100644 --- a/integrations/nodejs/nestjs/examples/05-csp/app.module.ts +++ b/examples/integrations/nodejs/nestjs/04-fastify-adapter/app.module.ts @@ -2,6 +2,6 @@ import { Module } from '@nestjs/common'; import { ApiDocsModule } from '@usebruno/api-docs-nestjs'; @Module({ - imports: [ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' })] + imports: [ApiDocsModule.forRoot({ collectionPath: '../../api-collection' })] }) export class AppModule {} diff --git a/integrations/nodejs/nestjs/examples/04-fastify-adapter/main.ts b/examples/integrations/nodejs/nestjs/04-fastify-adapter/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/04-fastify-adapter/main.ts rename to examples/integrations/nodejs/nestjs/04-fastify-adapter/main.ts diff --git a/integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts b/examples/integrations/nodejs/nestjs/05-csp/app.module.ts similarity index 63% rename from integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts rename to examples/integrations/nodejs/nestjs/05-csp/app.module.ts index 1878b959..bb0f9295 100644 --- a/integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts +++ b/examples/integrations/nodejs/nestjs/05-csp/app.module.ts @@ -2,6 +2,6 @@ import { Module } from '@nestjs/common'; import { ApiDocsModule } from '@usebruno/api-docs-nestjs'; @Module({ - imports: [ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' })] + imports: [ApiDocsModule.forRoot({ collectionPath: '../../api-collection' })] }) export class AppModule {} diff --git a/integrations/nodejs/nestjs/examples/05-csp/main.ts b/examples/integrations/nodejs/nestjs/05-csp/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/05-csp/main.ts rename to examples/integrations/nodejs/nestjs/05-csp/main.ts diff --git a/integrations/nodejs/nestjs/examples/06-embed/app.module.ts b/examples/integrations/nodejs/nestjs/06-embed/app.module.ts similarity index 74% rename from integrations/nodejs/nestjs/examples/06-embed/app.module.ts rename to examples/integrations/nodejs/nestjs/06-embed/app.module.ts index c48d0e8f..088a766b 100644 --- a/integrations/nodejs/nestjs/examples/06-embed/app.module.ts +++ b/examples/integrations/nodejs/nestjs/06-embed/app.module.ts @@ -4,6 +4,6 @@ import { PortalController } from './portal.controller'; @Module({ controllers: [PortalController], - imports: [ApiDocsModule.forRoot({ collectionUrl: '../../api-collection' })] + imports: [ApiDocsModule.forRoot({ collectionPath: '../../api-collection' })] }) export class AppModule {} diff --git a/integrations/nodejs/nestjs/examples/06-embed/main.ts b/examples/integrations/nodejs/nestjs/06-embed/main.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/06-embed/main.ts rename to examples/integrations/nodejs/nestjs/06-embed/main.ts diff --git a/integrations/nodejs/nestjs/examples/06-embed/portal.controller.ts b/examples/integrations/nodejs/nestjs/06-embed/portal.controller.ts similarity index 100% rename from integrations/nodejs/nestjs/examples/06-embed/portal.controller.ts rename to examples/integrations/nodejs/nestjs/06-embed/portal.controller.ts diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/folder.yml b/examples/integrations/nodejs/nestjs/api-collection/catalog/folder.yml similarity index 100% rename from integrations/nodejs/nestjs/examples/api-collection/catalog/folder.yml rename to examples/integrations/nodejs/nestjs/api-collection/catalog/folder.yml diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/get product.yml b/examples/integrations/nodejs/nestjs/api-collection/catalog/get product.yml similarity index 100% rename from integrations/nodejs/nestjs/examples/api-collection/catalog/get product.yml rename to examples/integrations/nodejs/nestjs/api-collection/catalog/get product.yml diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/list products.yml b/examples/integrations/nodejs/nestjs/api-collection/catalog/list products.yml similarity index 100% rename from integrations/nodejs/nestjs/examples/api-collection/catalog/list products.yml rename to examples/integrations/nodejs/nestjs/api-collection/catalog/list products.yml diff --git a/integrations/nodejs/nestjs/examples/api-collection/opencollection.yml b/examples/integrations/nodejs/nestjs/api-collection/opencollection.yml similarity index 100% rename from integrations/nodejs/nestjs/examples/api-collection/opencollection.yml rename to examples/integrations/nodejs/nestjs/api-collection/opencollection.yml diff --git a/examples/integrations/nodejs/nestjs/tsconfig.json b/examples/integrations/nodejs/nestjs/tsconfig.json new file mode 100644 index 00000000..2803a415 --- /dev/null +++ b/examples/integrations/nodejs/nestjs/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../integrations/tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "baseUrl": ".", + "paths": { "*": ["../../../../integrations/node_modules/*"] }, + "typeRoots": ["../../../../integrations/node_modules/@types"], + "declaration": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["0*/*.ts"] +} diff --git a/examples/react/index.html b/examples/react/index.html deleted file mode 100644 index 87d1bf83..00000000 --- a/examples/react/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - OpenCollection Playground - React Example - - -
- - - \ No newline at end of file diff --git a/examples/react/package-lock.json b/examples/react/package-lock.json deleted file mode 100644 index 591843c4..00000000 --- a/examples/react/package-lock.json +++ /dev/null @@ -1,1304 +0,0 @@ -{ - "name": "opencollection-react-example", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "opencollection-react-example", - "version": "1.0.0", - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.0.0", - "vite": "^4.4.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "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/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@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" - }, - "engines": { - "node": ">=6.9.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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@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.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz", - "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/examples/react/package.json b/examples/react/package.json deleted file mode 100644 index cce434ae..00000000 --- a/examples/react/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "opencollection-react-example", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.0.0", - "vite": "^4.4.0" - } -} \ No newline at end of file diff --git a/examples/react/src/App.jsx b/examples/react/src/App.jsx deleted file mode 100644 index e337e055..00000000 --- a/examples/react/src/App.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { OpenCollectionPlayground } from '../../../packages/oc-playground/dist/oc-playground.es.js'; -import '../../../packages/oc-playground/dist/playground.css'; - -const sampleCollection = { - name: "Sample API Collection", - description: "A comprehensive OpenCollection demonstration showcasing various API endpoints and configurations", - items: [ - { - type: "http", - name: "Get Users", - method: "GET", - url: "https://jsonplaceholder.typicode.com/users", - docs: "Retrieves a list of all users from the JSONPlaceholder API", - headers: [ - { - name: "Accept", - value: "application/json" - } - ] - }, - { - type: "http", - name: "Create User", - method: "POST", - url: "https://jsonplaceholder.typicode.com/users", - docs: "Creates a new user in the system", - headers: [ - { - name: "Content-Type", - value: "application/json" - } - ], - body: { - type: "json", - data: JSON.stringify({ - name: "John Doe", - email: "john@example.com" - }, null, 2) - } - }, - { - type: "http", - name: "Get All Posts", - method: "GET", - url: "https://jsonplaceholder.typicode.com/posts", - docs: "Retrieves all blog posts" - }, - { - type: "http", - name: "Get Post by ID", - method: "GET", - url: "https://jsonplaceholder.typicode.com/posts/1", - docs: "Retrieves a specific post by its ID" - }, - { - type: "script", - script: "// Utility functions for API testing\nfunction generateRandomId() {\n return Math.floor(Math.random() * 1000) + 1;\n}\n\nconsole.log('Utility functions loaded');" - } - ], - environments: [ - { - name: "Production", - description: "Production environment configuration", - variables: [ - { - name: "baseUrl", - value: "https://api.example.com" - } - ] - }, - { - name: "Development", - description: "Development environment for testing", - variables: [ - { - name: "baseUrl", - value: "http://localhost:3000" - } - ] - } - ] -}; - -function App() { - return ( - - 🚀 - - } - /> - ); -} - -export default App; \ No newline at end of file diff --git a/examples/react/src/main.jsx b/examples/react/src/main.jsx deleted file mode 100644 index 7c3fd8c2..00000000 --- a/examples/react/src/main.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.jsx'; - -const container = document.getElementById('root'); -const root = createRoot(container); - -root.render( - - - -); \ No newline at end of file diff --git a/examples/react/vite.config.js b/examples/react/vite.config.js deleted file mode 100644 index 4568ffba..00000000 --- a/examples/react/vite.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [react()], - server: { - port: 3001 - } -}); \ No newline at end of file diff --git a/examples/standalone-html/index.html b/examples/standalone-html/basic.html similarity index 100% rename from examples/standalone-html/index.html rename to examples/standalone-html/basic.html diff --git a/examples/standalone-html/bruno.json b/examples/standalone-html/bruno.json deleted file mode 100644 index 50a80eba..00000000 --- a/examples/standalone-html/bruno.json +++ /dev/null @@ -1,647 +0,0 @@ -{ - "name": "E-commerce API", - "version": "1", - "items": [ - { - "type": "folder", - "name": "Cart", - "filename": "Cart", - "seq": 4, - "root": { - "docs": "Shopping cart endpoints", - "meta": { - "name": "Cart", - "seq": 4 - } - }, - "items": [ - { - "type": "http", - "name": "Add Item to Cart", - "filename": "Add Item to Cart.bru", - "seq": 2, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/cart/add/item/:itemId", - "method": "POST", - "headers": [ - { - "name": "Content-Type", - "value": "application/json", - "enabled": true - }, - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [ - { - "name": "itemId", - "value": "cd3ad475-0784-4a0d-9fe4-a9a17e5391d9", - "type": "path", - "enabled": true - } - ], - "body": { - "mode": "json", - "json": "{ \n \"quantity\": 2\n}", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Add a product to the user's shopping cart", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Clear Cart", - "filename": "Clear Cart.bru", - "seq": 5, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/cart", - "method": "DELETE", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Remove all items from the user's cart", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Get User Cart", - "filename": "Get User Cart.bru", - "seq": 1, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/cart", - "method": "GET", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve the current user's shopping cart", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Remove Item from Cart", - "filename": "Remove Item from Cart.bru", - "seq": 4, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/cart/item/:itemId", - "method": "DELETE", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [ - { - "name": "itemId", - "value": "cd3ad475-0784-4a0d-9fe4-a9a17e5391d9", - "type": "path", - "enabled": true - } - ], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Remove a specific item from the cart", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Update Cart Item", - "filename": "Update Cart Item.bru", - "seq": 3, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/cart/item/:itemId", - "method": "PUT", - "headers": [ - { - "name": "Content-Type", - "value": "application/json", - "enabled": true - }, - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [ - { - "name": "itemId", - "value": "cd3ad475-0784-4a0d-9fe4-a9a17e5391d9", - "type": "path", - "enabled": true - } - ], - "body": { - "mode": "json", - "json": "{\n \"quantity\": 5\n}", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Update the quantity of an item in the cart", - "auth": { - "mode": "none" - } - } - } - ] - }, - { - "type": "folder", - "name": "Authentication", - "filename": "Authentication", - "seq": 2, - "root": { - "docs": "User authentication endpoints", - "meta": { - "name": "Authentication", - "seq": 2 - } - }, - "items": [ - { - "type": "http", - "name": "Register User", - "filename": "Register User.bru", - "seq": 1, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/auth/register", - "method": "POST", - "headers": [ - { - "name": "Content-Type", - "value": "application/json", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "json", - "json": "{\n \"name\": \"Anoop\",\n \"email\": \"test@example.com\",\n \"password\": \"password123\"\n}", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Register a new user account", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Whoami", - "filename": "Whoami.bru", - "seq": 3, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/auth/whoami", - "method": "GET", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve authenticated user's profile information", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Login", - "filename": "Login.bru", - "seq": 2, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/auth/login", - "method": "POST", - "headers": [], - "params": [], - "body": { - "mode": "json", - "json": "{\n \"email\": \"test@example.com\",\n \"password\": \"password123\"\n}", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": { - "res": "bru.setEnvVar(\"accessToken\", res.body.token)" - }, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Authenticate a user and receive a token", - "auth": { - "mode": "none" - } - } - } - ] - }, - { - "type": "folder", - "name": "Orders", - "filename": "Orders", - "seq": 5, - "root": { - "docs": "Order management endpoints", - "meta": { - "name": "Orders", - "seq": 5 - } - }, - "items": [ - { - "type": "http", - "name": "Create Order", - "filename": "Create Order.bru", - "seq": 1, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/orders", - "method": "POST", - "headers": [ - { - "name": "Content-Type", - "value": "application/json", - "enabled": true - }, - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "json", - "json": "{\n \"shippingAddress\": \"Mars\"\n}", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "// Check if the request was successful\nif (res.getStatus() === 200 || res.getStatus() === 201) {\n // Parse the response body\n var jsonData = res.getBody();\n \n // Check if the order has an ID\n if (jsonData && jsonData.id) {\n // Store the order ID in the environment\n bru.setEnvVar('order_id', jsonData.id);\n console.log('Order ID saved to environment: ' + jsonData.id);\n }\n}", - "docs": "Create a new order using the items in the user's cart", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Get User Orders", - "filename": "Get User Orders.bru", - "seq": 2, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/orders", - "method": "GET", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve a list of all orders placed by the current user", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Get Order by ID", - "filename": "Get Order by ID.bru", - "seq": 3, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/orders/:orderId", - "method": "GET", - "headers": [ - { - "name": "Authorization", - "value": "Bearer {{accessToken}}", - "enabled": true - } - ], - "params": [ - { - "name": "orderId", - "value": "9c4d1e4f-64dd-465a-b52a-5471caab828b", - "type": "path", - "enabled": true - } - ], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve detailed information about a specific order", - "auth": { - "mode": "none" - } - } - } - ] - }, - { - "type": "folder", - "name": "Products", - "filename": "Products", - "seq": 3, - "root": { - "docs": "Product-related endpoints. The server automatically seeds 5 sample products on startup if no products exist.", - "meta": { - "name": "Products", - "seq": 3 - } - }, - "items": [ - { - "type": "http", - "name": "Get Products by Category", - "filename": "Get Products by Category.bru", - "seq": 2, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/products?category=Electronics", - "method": "GET", - "headers": [], - "params": [ - { - "name": "category", - "value": "Electronics", - "type": "query", - "enabled": true - } - ], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve products filtered by category. Seeded categories include: Electronics, Sports, and Home.", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Get Product by ID", - "filename": "Get Product by ID.bru", - "seq": 3, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/products/:productId", - "method": "GET", - "headers": [], - "params": [ - { - "name": "productId", - "value": "cd3ad475-0784-4a0d-9fe4-a9a17e5391d9", - "type": "path", - "enabled": true - } - ], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve detailed information about a specific product", - "auth": { - "mode": "none" - } - } - }, - { - "type": "http", - "name": "Get All Products", - "filename": "Get All Products.bru", - "seq": 1, - "settings": {}, - "tags": [], - "request": { - "url": "{{baseUrl}}/products", - "method": "GET", - "headers": [], - "params": [ - { - "name": "category", - "value": "electronics", - "type": "query", - "enabled": false - } - ], - "body": { - "mode": "none", - "formUrlEncoded": [], - "multipartForm": [], - "file": [] - }, - "script": {}, - "vars": {}, - "assertions": [], - "tests": "", - "docs": "Retrieve a list of all products, optionally filtered by category. Default seeded categories include: Electronics, Sports, and Home.", - "auth": { - "mode": "none" - } - } - } - ] - } - ], - "activeEnvironmentUid": "RcGeNRbYHRYrqqtGIxs7V", - "environments": [ - { - "variables": [ - { - "name": "baseUrl", - "value": "http://localhost:3001/api", - "enabled": true, - "secret": false, - "type": "text" - }, - { - "name": "userToken", - "value": "ZjFhZDJhMDItNmQ3Yi00OTg2LWFhZjYtODJkZGE1M2UyYTJh", - "secret": false, - "enabled": true, - "type": "text" - }, - { - "name": "accessToken", - "value": "ZjFhZDJhMDItNmQ3Yi00OTg2LWFhZjYtODJkZGE1M2UyYTJh", - "secret": false, - "enabled": true, - "type": "text" - }, - { - "name": "order_id", - "value": "9c4d1e4f-64dd-465a-b52a-5471caab828b", - "secret": false, - "enabled": true, - "type": "text" - } - ], - "name": "dev" - } - ], - "root": { - "request": { - "auth": { - "mode": "none" - } - }, - "docs": "Collection for testing the E-commerce API endpoints. The server automatically seeds 5 sample products (Smartphone X, Running Shoes, Coffee Maker, Laptop Pro, Wireless Earbuds) on startup if no products exist in the database.\n\n[\"Fetch](https://fetch.usebruno.com?url=git%40bitbucket.org%3Ausebruno%2Fdemo-api-collection.git \"target=_blank rel=noopener noreferrer\")", - "meta": { - "name": "E-commerce API" - } - }, - "brunoConfig": { - "version": "1", - "name": "E-commerce API", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ], - "size": 0.005391120910644531, - "filesCount": 23 - } -} \ No newline at end of file diff --git a/examples/standalone-html/index2.html b/examples/standalone-html/nested-folders.html similarity index 100% rename from examples/standalone-html/index2.html rename to examples/standalone-html/nested-folders.html diff --git a/integrations/.dockerignore b/integrations/.dockerignore index b1b516e8..ebd0b95b 100644 --- a/integrations/.dockerignore +++ b/integrations/.dockerignore @@ -5,6 +5,5 @@ nodejs/*/dist shell/shell.js nodejs/*/shell test-results -nodejs/nestjs/examples/dist dist-tarballs shell/bru.js diff --git a/integrations/contract-tests/apps/express.js b/integrations/contract-tests/apps/express.js index e01d3f51..196c6e07 100644 --- a/integrations/contract-tests/apps/express.js +++ b/integrations/contract-tests/apps/express.js @@ -1,5 +1,7 @@ 'use strict'; +const fs = require('node:fs'); +const path = require('node:path'); const express = require('express'); const helmet = require('helmet'); const { apiDocs, embed } = require('@usebruno/api-docs-express'); @@ -10,6 +12,8 @@ const PORT = Number(process.env.PORT || 5456); const COLLECTION = '../fixtures/api-collection'; const BRU = '../fixtures/api-collection-bru'; const BUNDLED = '../fixtures/bundled.yml'; +const CONTENT = fs.readFileSync(path.join(__dirname, BUNDLED), 'utf8'); +const URL_FIXTURE = 'https://raw.githubusercontent.com/usebruno/bruno-api-docs/1aa76e1ad567b6e05e03e46ee53fb6bc1a66e7d5/integrations/contract-tests/fixtures/bundled.yml'; const app = express(); @@ -32,31 +36,39 @@ app.use( app.get('/control', (req, res) => res.json({ ok: true, from: 'the app itself' })); app.use('/docs', apiDocs({ - collectionUrl: COLLECTION, + collectionPath: COLLECTION, environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E', - gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection' + repositoryUrl: 'https://token:secret@github.com/acme/api-collection' })); -app.use('/api/v2/docs', apiDocs({ collectionUrl: COLLECTION, environments: { include: '*', exclude: ['Prod'] } })); +app.use('/api/v2/docs', apiDocs({ collectionPath: COLLECTION, environments: { include: '*', exclude: ['Prod'] } })); -app.use('/internal/docs', apiDocs({ collectionUrl: COLLECTION })); +app.use('/internal/docs', apiDocs({ collectionPath: COLLECTION })); -app.use('/bundled/docs', apiDocs({ collectionUrl: BUNDLED })); +app.use('/bundled/docs', apiDocs({ collectionPath: BUNDLED })); -app.use('/bru/docs', apiDocs({ collectionUrl: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } })); +app.use('/content/docs', apiDocs({ content: CONTENT })); -app.use('/broken/docs', apiDocs({ collectionUrl: './there-is-no-collection-here' })); +app.use('/priority/docs', apiDocs({ content: CONTENT, collectionPath: COLLECTION })); -app.use('/oversize/docs', apiDocs({ collectionUrl: '../fixtures/walk-oversize' })); +app.use('/url/docs', apiDocs({ url: URL_FIXTURE })); + +app.use('/url-missing/docs', apiDocs({ url: URL_FIXTURE.replace('bundled.yml', 'missing.yml') })); + +app.use('/bru/docs', apiDocs({ collectionPath: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } })); + +app.use('/broken/docs', apiDocs({ collectionPath: './there-is-no-collection-here' })); + +app.use('/oversize/docs', apiDocs({ collectionPath: '../fixtures/walk-oversize' })); // theme is what the renderer will take next; until it does, passing it is a mistake we report -app.use('/misconfigured/docs', apiDocs({ collectionUrl: COLLECTION, theme: 'dark' })); +app.use('/misconfigured/docs', apiDocs({ collectionPath: COLLECTION, theme: 'dark' })); // the docs inside the host's own page: the mount serves, the block only points at it -app.use('/portal/docs', apiDocs({ collectionUrl: COLLECTION, environments: { include: ['Local'] } })); +app.use('/portal/docs', apiDocs({ collectionPath: COLLECTION, environments: { include: ['Local'] } })); app.get('/portal', (req, res) => { res.type('html').send(` Acme Developer Portal diff --git a/integrations/contract-tests/apps/fastify.js b/integrations/contract-tests/apps/fastify.js index 62f2ca30..92dd0cba 100644 --- a/integrations/contract-tests/apps/fastify.js +++ b/integrations/contract-tests/apps/fastify.js @@ -1,5 +1,7 @@ 'use strict'; +const fs = require('node:fs'); +const path = require('node:path'); const Fastify = require('fastify'); const { apiDocs } = require('@usebruno/api-docs-fastify'); @@ -8,6 +10,8 @@ const PORT = Number(process.env.PORT || 5457); const COLLECTION = '../fixtures/api-collection'; const BRU = '../fixtures/api-collection-bru'; const BUNDLED = '../fixtures/bundled.yml'; +const CONTENT = fs.readFileSync(path.join(__dirname, BUNDLED), 'utf8'); +const URL_FIXTURE = 'https://raw.githubusercontent.com/usebruno/bruno-api-docs/1aa76e1ad567b6e05e03e46ee53fb6bc1a66e7d5/integrations/contract-tests/fixtures/bundled.yml'; const app = Fastify({ logger: false }); @@ -37,26 +41,30 @@ async function main() { await app.register(apiDocs, { prefix: '/docs', - collectionUrl: COLLECTION, + collectionPath: COLLECTION, environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E', - gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection' + repositoryUrl: 'https://token:secret@github.com/acme/api-collection' }); await app.register(apiDocs, { prefix: '/api/v2/docs', - collectionUrl: COLLECTION, + collectionPath: COLLECTION, environments: { include: '*', exclude: ['Prod'] } }); - await app.register(apiDocs, { prefix: '/internal/docs', collectionUrl: COLLECTION }); - await app.register(apiDocs, { prefix: '/bundled/docs', collectionUrl: BUNDLED }); - await app.register(apiDocs, { prefix: '/bru/docs', collectionUrl: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } }); - await app.register(apiDocs, { prefix: '/broken/docs', collectionUrl: './there-is-no-collection-here' }); - await app.register(apiDocs, { prefix: '/oversize/docs', collectionUrl: '../fixtures/walk-oversize' }); - await app.register(apiDocs, { prefix: '/misconfigured/docs', collectionUrl: COLLECTION, theme: 'dark' }); + await app.register(apiDocs, { prefix: '/internal/docs', collectionPath: COLLECTION }); + await app.register(apiDocs, { prefix: '/bundled/docs', collectionPath: BUNDLED }); + await app.register(apiDocs, { prefix: '/content/docs', content: CONTENT }); + await app.register(apiDocs, { prefix: '/priority/docs', content: CONTENT, collectionPath: COLLECTION }); + await app.register(apiDocs, { prefix: '/url/docs', url: URL_FIXTURE }); + await app.register(apiDocs, { prefix: '/url-missing/docs', url: URL_FIXTURE.replace('bundled.yml', 'missing.yml') }); + await app.register(apiDocs, { prefix: '/bru/docs', collectionPath: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } }); + await app.register(apiDocs, { prefix: '/broken/docs', collectionPath: './there-is-no-collection-here' }); + await app.register(apiDocs, { prefix: '/oversize/docs', collectionPath: '../fixtures/walk-oversize' }); + await app.register(apiDocs, { prefix: '/misconfigured/docs', collectionPath: COLLECTION, theme: 'dark' }); await app.listen({ port: PORT, host: '127.0.0.1' }); console.log(`fastify rig on http://localhost:${PORT}`); diff --git a/integrations/contract-tests/apps/nestjs.js b/integrations/contract-tests/apps/nestjs.js index 1a13a7a5..d8c660e3 100644 --- a/integrations/contract-tests/apps/nestjs.js +++ b/integrations/contract-tests/apps/nestjs.js @@ -1,6 +1,8 @@ 'use strict'; require('reflect-metadata'); +const fs = require('node:fs'); +const path = require('node:path'); const { Module } = require('@nestjs/common'); const { NestFactory } = require('@nestjs/core'); const helmet = require('helmet'); @@ -14,32 +16,38 @@ const ADAPTER = process.env.ADAPTER || 'express'; const COLLECTION = '../fixtures/api-collection'; const BRU = '../fixtures/api-collection-bru'; const BUNDLED = '../fixtures/bundled.yml'; +const CONTENT = fs.readFileSync(path.join(__dirname, BUNDLED), 'utf8'); +const URL_FIXTURE = 'https://raw.githubusercontent.com/usebruno/bruno-api-docs/1aa76e1ad567b6e05e03e46ee53fb6bc1a66e7d5/integrations/contract-tests/fixtures/bundled.yml'; // plain JS, so the decorator is applied as the function it is. The real Nest examples live in -// nodejs/nestjs/examples and are TypeScript. +// examples/integrations/nodejs/nestjs and are TypeScript. class AppModule {} Module({ imports: [ ApiDocsModule.forRoot({ mountPath: '/docs', - collectionUrl: COLLECTION, + collectionPath: COLLECTION, environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E', - gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection' + repositoryUrl: 'https://token:secret@github.com/acme/api-collection' }), ApiDocsModule.forRoot({ mountPath: '/api/v2/docs', - collectionUrl: COLLECTION, + collectionPath: COLLECTION, environments: { include: '*', exclude: ['Prod'] } }), - ApiDocsModule.forRoot({ mountPath: '/internal/docs', collectionUrl: COLLECTION }), - ApiDocsModule.forRoot({ mountPath: '/bundled/docs', collectionUrl: BUNDLED }), - ApiDocsModule.forRoot({ mountPath: '/bru/docs', collectionUrl: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } }), - ApiDocsModule.forRoot({ mountPath: '/broken/docs', collectionUrl: './there-is-no-collection-here' }), - ApiDocsModule.forRoot({ mountPath: '/oversize/docs', collectionUrl: '../fixtures/walk-oversize' }), - ApiDocsModule.forRoot({ mountPath: '/misconfigured/docs', collectionUrl: COLLECTION, theme: 'dark' }) + ApiDocsModule.forRoot({ mountPath: '/internal/docs', collectionPath: COLLECTION }), + ApiDocsModule.forRoot({ mountPath: '/bundled/docs', collectionPath: BUNDLED }), + ApiDocsModule.forRoot({ mountPath: '/content/docs', content: CONTENT }), + ApiDocsModule.forRoot({ mountPath: '/priority/docs', content: CONTENT, collectionPath: COLLECTION }), + ApiDocsModule.forRoot({ mountPath: '/url/docs', url: URL_FIXTURE }), + ApiDocsModule.forRoot({ mountPath: '/url-missing/docs', url: URL_FIXTURE.replace('bundled.yml', 'missing.yml') }), + ApiDocsModule.forRoot({ mountPath: '/bru/docs', collectionPath: BRU, environments: { include: ['Local'] }, tags: { exclude: ['internal'] } }), + ApiDocsModule.forRoot({ mountPath: '/broken/docs', collectionPath: './there-is-no-collection-here' }), + ApiDocsModule.forRoot({ mountPath: '/oversize/docs', collectionPath: '../fixtures/walk-oversize' }), + ApiDocsModule.forRoot({ mountPath: '/misconfigured/docs', collectionPath: COLLECTION, theme: 'dark' }) ] })(AppModule); diff --git a/integrations/contract-tests/browser/contract.spec.mjs b/integrations/contract-tests/browser/contract.spec.mjs index 6b4c8e13..cac86d92 100644 --- a/integrations/contract-tests/browser/contract.spec.mjs +++ b/integrations/contract-tests/browser/contract.spec.mjs @@ -37,6 +37,13 @@ test('the renderer booted, from the CDN, with our shell from our own origin', as expect(await page.evaluate(() => typeof window.OpenCollection), 'the entry the deployed bundle still carries').toBe('function'); }); +test('the repository link reaches the page, with its credentials stripped', async ({ page }) => { + await page.goto('/docs/', { waitUntil: 'networkidle' }); + const href = await page.locator('a[href^="https://fetch.usebruno.com"]').first().getAttribute('href'); + expect(href).toContain(encodeURIComponent('https://github.com/acme/api-collection')); + expect(href).not.toContain('secret'); +}); + test('the filters hold all the way to the page', async ({ page }) => { await page.goto('/docs/', { waitUntil: 'networkidle' }); const text = await page.evaluate(() => document.body.innerText); @@ -108,11 +115,13 @@ test('a .bru collection renders, and its filters hold', async ({ page }) => { expect(errors).toEqual([]); }); -test('a bundled single file renders the same way', async ({ page }) => { - await page.goto('/bundled/docs/', { waitUntil: 'networkidle' }); - await expect(page).toHaveTitle('API Documentation'); - await expect(page.getByText('Catalog', { exact: true }).first()).toBeVisible(); -}); +for (const mount of ['/bundled/docs/', '/content/docs/', '/url/docs/']) { + test(`${mount} a bundled document renders the same way`, async ({ page }) => { + await page.goto(mount, { waitUntil: 'networkidle' }); + await expect(page).toHaveTitle('API Documentation'); + await expect(page.getByText('Catalog', { exact: true }).first()).toBeVisible(); + }); +} test('a broken mount explains itself in the page, and the app is still up', async ({ page, request }) => { const response = await page.goto('/broken/docs/', { waitUntil: 'networkidle' }); diff --git a/integrations/contract-tests/check.sh b/integrations/contract-tests/check.sh index d00be945..14c3fb34 100755 --- a/integrations/contract-tests/check.sh +++ b/integrations/contract-tests/check.sh @@ -9,10 +9,14 @@ # Every rig serves the same five mounts over the same fixture. That convention is what makes one # script cover the whole matrix: a cell is an invocation, not an edit. # -# /docs environments include Local, tags exclude internal, pageTitle, gitCollectionUrl +# /docs environments include Local, tags exclude internal, pageTitle, repositoryUrl # /api/v2/docs environments all minus Prod # /internal/docs no options # /bundled/docs a single bundled yml +# /content/docs the same document given inline as `content` +# /priority/docs `content` and `collectionPath` both set: content wins +# /url/docs the bundled yml fetched from its public URL, this repo on GitHub at a pinned commit +# /url-missing/docs a URL that answers 404 # /bru/docs the same collection in Bruno's own format, same filters # /broken/docs a collection that is not there # /oversize/docs a collection over the caps @@ -126,6 +130,20 @@ check "/bundled/docs/collection.yml -> yaml" header_has Content-Type yaml "$B check "/bundled/docs/collection.yml -> ETag" revalidates "$BASE/bundled/docs/collection.yml" check "/bundled/docs -> served as written" body_has "$BASE/bundled/docs/collection.yml" 'Acme API (bundled)' +describe "content: the document given inline, served as a bundled file" +check "/content/docs/ -> the page" status_is 200 "$BASE/content/docs/" +check "/content/docs/collection.yml -> yaml" header_has Content-Type yaml "$BASE/content/docs/collection.yml" +check "/content/docs/collection.yml -> ETag" revalidates "$BASE/content/docs/collection.yml" +check "/content/docs -> the same bytes as the file" same_body "$BASE/content/docs/collection.yml" "$BASE/bundled/docs/collection.yml" +check "/priority/docs -> content wins over the path" same_body "$BASE/priority/docs/collection.yml" "$BASE/bundled/docs/collection.yml" + +describe "url: a bundled document fetched once at start" +check "/url/docs/ -> the page" status_is 200 "$BASE/url/docs/" +check "/url/docs/collection.yml -> yaml" header_has Content-Type yaml "$BASE/url/docs/collection.yml" +check "/url/docs -> the same bytes as the file" same_body "$BASE/url/docs/collection.yml" "$BASE/bundled/docs/collection.yml" +check "/url-missing/docs/ -> 404" status_is 404 "$BASE/url-missing/docs/" +check "naming the fetch" body_has "$BASE/url-missing/docs/" 'could not fetch' + describe "a broken setup does not stop the app" check "the app's own route still answers" body_has "$BASE/control" 'the app itself' check "/broken/docs/ -> 404, not a crash" status_is 404 "$BASE/broken/docs/" diff --git a/integrations/contract-tests/examples.sh b/integrations/contract-tests/examples.sh index ef92f969..1898cb52 100644 --- a/integrations/contract-tests/examples.sh +++ b/integrations/contract-tests/examples.sh @@ -1,14 +1,18 @@ #!/usr/bin/env bash -# Every example under nodejs/*/examples boots and does what its own file says it does. One at a -# time on one port, the way a reader runs them. The Nest ones run from their compiled dist, which -# is the point of writing them in TypeScript. +# Every example under examples/integrations/nodejs boots and does what its own file says it does. +# One at a time on one port, the way a reader runs them. The Nest ones run from their compiled +# dist, which is the point of writing them in TypeScript. # # bash contract-tests/examples.sh [--port 5480] set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" +EXAMPLES="$(cd "$ROOT/../examples/integrations/nodejs" && pwd)" source "$ROOT/contract-tests/lib.sh" +# the examples sit outside the workspace, so this is how they find the packages and the frameworks +export NODE_PATH="$ROOT/node_modules" + PORT=5480 while [ $# -gt 0 ]; do case "$1" in @@ -17,9 +21,10 @@ while [ $# -gt 0 ]; do esac done +( cd "$ROOT" && npm run build:examples --silent ) covered "$0" \ - $(cd "$ROOT" && ls nodejs/*/examples/*.js) \ - $(cd "$ROOT" && ls -d nodejs/nestjs/examples/0*/ | sed 's|examples/\(.*\)/$|examples/dist/\1/main.js|') + $(cd "$EXAMPLES" && ls */*.js) \ + $(cd "$EXAMPLES" && ls -d nestjs/0*/ | sed 's|nestjs/\(.*\)/$|nestjs/dist/\1/main.js|') BASE="http://localhost:$PORT" JUNIT_NAME="${JUNIT_NAME:-examples}" @@ -30,8 +35,8 @@ port_is_free "$PORT" # example FILE: stops the one before, boots this one example() { stop - describe "${1#nodejs/}" - boot "$ROOT/$1" "$PORT" + describe "$1" + boot "$EXAMPLES/$1" "$PORT" } CSP="Content-Security-Policy" @@ -40,14 +45,14 @@ API_KEY=(-H 'x-api-key: let-me-in') # ---- express ------------------------------------------------------------------- -example nodejs/express/examples/quickstart.js +example express/quickstart.js check "/docs -> 301 docs/" redirects_to "$BASE/docs" "docs/" check "/docs/ -> the page" header_has Content-Type text/html "$BASE/docs/" check "/docs/shell.js -> 200" status_is 200 "$BASE/docs/shell.js" check "the collection is the one beside it" body_has "$BASE/docs/collection.yml" 'Acme API' check "/health is the app's own" status_is 200 "$BASE/health" -example nodejs/express/examples/auth.js +example express/auth.js check "/health stays open" status_is 200 "$BASE/health" check "/docs/ -> 401" status_is 401 "$BASE/docs/" check "/docs/ says how to authenticate" header_has WWW-Authenticate Bearer "$BASE/docs/" @@ -57,13 +62,13 @@ check "/docs -> 401, the guard is before the redirect" status_is 401 "$BASE/docs check "/docs/ with the token -> 200" status_is 200 "${BEARER[@]}" "$BASE/docs/" check "/docs/collection.yml with the token -> 200" status_is 200 "${BEARER[@]}" "$BASE/docs/collection.yml" -example nodejs/express/examples/csp.js +example express/csp.js check "/docs/ -> 200" status_is 200 "$BASE/docs/" check "script-src allows the CDN, Monaco and wasm" header_has "$CSP" "script-src 'self' https://cdn.usebruno.com https://cdn.jsdelivr.net 'wasm-unsafe-eval'" "$BASE/docs/" check "connect-src allows data:" header_has "$CSP" "connect-src 'self' data:" "$BASE/docs/" check "style-src allows the fonts" header_has "$CSP" "fonts.googleapis.com" "$BASE/docs/" -example nodejs/express/examples/embed.js +example express/embed.js check "/ is the host's own page" body_has "$BASE/" 'Acme Developer Portal' check "with the docs block in it" body_has "$BASE/" 'id="bruno-docs"' check "loading from the mount" body_has "$BASE/" 'src="/docs/shell.js"' @@ -72,14 +77,14 @@ check "the mount serves the collection" body_has "$BASE/docs/collection # ---- fastify ------------------------------------------------------------------- -example nodejs/fastify/examples/quickstart.js +example fastify/quickstart.js check "/docs -> 301 docs/" redirects_to "$BASE/docs" "docs/" check "/docs/ -> the page" header_has Content-Type text/html "$BASE/docs/" check "/docs/shell.js -> 200" status_is 200 "$BASE/docs/shell.js" check "the collection is the one beside it" body_has "$BASE/docs/collection.yml" 'Acme API' check "/health is the app's own" status_is 200 "$BASE/health" -example nodejs/fastify/examples/auth.js +example fastify/auth.js check "/health stays open" status_is 200 "$BASE/health" check "/docs/ -> 401" status_is 401 "$BASE/docs/" check "/docs/ says how to authenticate" header_has WWW-Authenticate Bearer "$BASE/docs/" @@ -90,13 +95,13 @@ check "POST /docs/ -> 401, the guard is before the 405" status_is 401 -X POST "$ check "/docs/ with the token -> 200" status_is 200 "${BEARER[@]}" "$BASE/docs/" check "/docs/collection.yml with the token -> 200" status_is 200 "${BEARER[@]}" "$BASE/docs/collection.yml" -example nodejs/fastify/examples/csp.js +example fastify/csp.js check "/docs/ -> 200" status_is 200 "$BASE/docs/" check "script-src allows the CDN, Monaco and wasm" header_has "$CSP" "script-src 'self' https://cdn.usebruno.com https://cdn.jsdelivr.net 'wasm-unsafe-eval'" "$BASE/docs/" check "connect-src allows data:" header_has "$CSP" "connect-src 'self' data:" "$BASE/docs/" check "style-src allows the fonts" header_has "$CSP" "fonts.googleapis.com" "$BASE/docs/" -example nodejs/fastify/examples/embed.js +example fastify/embed.js check "/ is the host's own page" body_has "$BASE/" 'Acme Developer Portal' check "with the docs block in it" body_has "$BASE/" 'id="bruno-docs"' check "loading from the mount" body_has "$BASE/" 'src="/docs/shell.js"' @@ -105,13 +110,13 @@ check "the mount serves the collection" body_has "$BASE/docs/collection # ---- nestjs, from the compiled dist ------------------------------------------------- -example nodejs/nestjs/examples/dist/01-quickstart/main.js +example nestjs/dist/01-quickstart/main.js check "/docs -> 301 docs/" redirects_to "$BASE/docs" "docs/" check "/docs/ -> the page" header_has Content-Type text/html "$BASE/docs/" check "/docs/shell.js -> 200" status_is 200 "$BASE/docs/shell.js" check "the collection resolved from dist/" body_has "$BASE/docs/collection.yml" 'Acme API' -example nodejs/nestjs/examples/dist/02-auth/main.js +example nestjs/dist/02-auth/main.js check "/docs/ stays open" status_is 200 "$BASE/docs/" check "/internal/docs/ -> 401" status_is 401 "$BASE/internal/docs/" check "/internal/docs/collection.yml -> 401" status_is 401 "$BASE/internal/docs/collection.yml" @@ -120,27 +125,27 @@ check "/internal/docs -> 401, the guard is before the redirect" status_is 401 "$ check "/internal/docs/ with the key -> 200" status_is 200 "${API_KEY[@]}" "$BASE/internal/docs/" check "/internal/docs/collection.yml with the key -> 200" status_is 200 "${API_KEY[@]}" "$BASE/internal/docs/collection.yml" -example nodejs/nestjs/examples/dist/03-global-prefix/main.js +example nestjs/dist/03-global-prefix/main.js check "/docs/ -> 404, it moved" status_is 404 "$BASE/docs/" check "/api/docs -> 301 docs/" redirects_to "$BASE/api/docs" "docs/" check "/api/docs/ -> the page" header_has Content-Type text/html "$BASE/api/docs/" check "/api/docs/shell.js -> 200" status_is 200 "$BASE/api/docs/shell.js" check "/api/docs/collection.yml -> the collection" body_has "$BASE/api/docs/collection.yml" 'Acme API' -example nodejs/nestjs/examples/dist/04-fastify-adapter/main.js +example nestjs/dist/04-fastify-adapter/main.js check "/docs -> 301 docs/" redirects_to "$BASE/docs" "docs/" check "/docs/ -> the page" header_has Content-Type text/html "$BASE/docs/" check "/docs/shell.js -> 200, not the page" header_has Content-Type javascript "$BASE/docs/shell.js" check "/docs/collection.yml -> the collection" body_has "$BASE/docs/collection.yml" 'Acme API' check "/docs// -> the page" header_has Content-Type text/html "$BASE/docs/catalog/list-products/" -example nodejs/nestjs/examples/dist/05-csp/main.js +example nestjs/dist/05-csp/main.js check "/docs/ -> 200" status_is 200 "$BASE/docs/" check "script-src allows the CDN, Monaco and wasm" header_has "$CSP" "script-src 'self' https://cdn.usebruno.com https://cdn.jsdelivr.net 'wasm-unsafe-eval'" "$BASE/docs/" check "connect-src allows data:" header_has "$CSP" "connect-src 'self' data:" "$BASE/docs/" check "style-src allows the fonts" header_has "$CSP" "fonts.googleapis.com" "$BASE/docs/" -example nodejs/nestjs/examples/dist/06-embed/main.js +example nestjs/dist/06-embed/main.js check "/ is the host's own page" body_has "$BASE/" 'Acme Developer Portal' check "with the docs block in it" body_has "$BASE/" 'id="bruno-docs"' check "loading from the mount" body_has "$BASE/" 'src="/docs/shell.js"' diff --git a/integrations/contract-tests/lib.sh b/integrations/contract-tests/lib.sh index d167531b..a2ff4441 100644 --- a/integrations/contract-tests/lib.sh +++ b/integrations/contract-tests/lib.sh @@ -141,6 +141,7 @@ header_has() { local name=$1 pattern=$2; shift 2; want="$name containing '$patte header_absent() { local name=$1; shift; want="no $name header"; got="$name: $(header_of "$name" "$@")"; [ "$got" = "$name: " ]; } body_has() { local body; body="$(curl -s "$1")"; want="'$2' in the body"; got="not there, in ${#body} bytes"; grep -q "$2" <<<"$body"; } body_lacks() { want="no '$2' in the body"; got="it is there"; ! grep -Eq "$2" <<<"$(curl -s "$1")"; } +same_body() { want="the same bytes as $(basename "$2")"; got="they differ"; [ "$(curl -s "$1")" = "$(curl -s "$2")" ]; } file_has() { want="'$2' in $(basename "$1")"; got="not there"; grep -q "$2" "$1"; } file_lacks() { want="no '$2' in $(basename "$1")"; got="it is there"; ! grep -q "$2" "$1"; } revalidates() { status_is 304 -H "If-None-Match: $(header_of ETag "$1")" "$1"; } diff --git a/integrations/eslint.config.mjs b/integrations/eslint.config.mjs index 8ecb44a5..4097a255 100644 --- a/integrations/eslint.config.mjs +++ b/integrations/eslint.config.mjs @@ -5,71 +5,76 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; // The repo's stylistic rules, minus everything browser-shaped. -export default [ - { - ignores: ['**/node_modules/**', '**/dist/**', '**/shell/shell.js', '**/shell/bru.js', 'shell/vendor/**'] +export const ignores = { + ignores: ['**/node_modules/**', '**/dist/**', '**/shell/shell.js', '**/shell/bru.js', 'shell/vendor/**'] +}; + +// the rules for everything that runs in Node; the examples apply them from their own config +export const workspace = { + plugins: { + '@stylistic': stylistic, + '@typescript-eslint': tsPlugin }, - { - plugins: { - '@stylistic': stylistic, - '@typescript-eslint': tsPlugin - }, - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module' - }, - globals: { - ...globals.node - } + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' }, - files: ['eslint.config.mjs', '**/src/**/*.ts', '**/test/**/*.{ts,mjs}', 'contract-tests/**/*.{js,mjs}', '**/examples/**/*.{js,ts}'], - rules: { - ...js.configs.recommended.rules, - ...stylistic.configs.customize({ - indent: 2, - quotes: 'single', - semi: true, - jsx: false - }).rules, - '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], - '@stylistic/comma-dangle': ['error', 'never'], - '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }], - '@stylistic/arrow-parens': ['error', 'always'], - '@stylistic/array-bracket-spacing': ['error', 'never'], - '@stylistic/arrow-spacing': ['error', { before: true, after: true }], - '@stylistic/function-call-spacing': ['error', 'never'], - '@stylistic/semi-style': ['error', 'last'], - '@stylistic/max-len': ['error', { - code: 120, - ignoreUrls: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - ignoreRegExpLiterals: true - }], + globals: { + ...globals.node + } + }, + files: ['eslint.config.mjs', '**/src/**/*.ts', '**/test/**/*.{ts,mjs}', 'contract-tests/**/*.{js,mjs}'], + rules: { + ...js.configs.recommended.rules, + ...stylistic.configs.customize({ + indent: 2, + quotes: 'single', + semi: true, + jsx: false + }).rules, + '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], + '@stylistic/comma-dangle': ['error', 'never'], + '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }], + '@stylistic/arrow-parens': ['error', 'always'], + '@stylistic/array-bracket-spacing': ['error', 'never'], + '@stylistic/arrow-spacing': ['error', { before: true, after: true }], + '@stylistic/function-call-spacing': ['error', 'never'], + '@stylistic/semi-style': ['error', 'last'], + '@stylistic/max-len': ['error', { + code: 120, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + }], - 'curly': ['error', 'multi-line'], + 'curly': ['error', 'multi-line'], - 'no-undef': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['error', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_' - }], + 'no-undef': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], - 'eqeqeq': ['error', 'always', { null: 'ignore' }], - 'no-debugger': 'error', - 'no-var': 'error', - 'prefer-const': 'error', + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'no-debugger': 'error', + 'no-var': 'error', + 'prefer-const': 'error', - 'no-console': ['error', { allow: ['log', 'warn', 'error'] }] - } - }, + 'no-console': ['error', { allow: ['log', 'warn', 'error'] }] + } +}; + +export default [ + ignores, + workspace, { files: ['shell/*.mjs'], plugins: { '@stylistic': stylistic }, @@ -91,7 +96,7 @@ export default [ } }, { - files: ['contract-tests/apps/*.js', '**/examples/**/*.{js,ts}'], + files: ['contract-tests/apps/*.js'], rules: { 'no-console': 'off' } diff --git a/integrations/nodejs/core/README.md b/integrations/nodejs/core/README.md index 36bfaa8b..856ad101 100644 --- a/integrations/nodejs/core/README.md +++ b/integrations/nodejs/core/README.md @@ -20,7 +20,7 @@ npm install @usebruno/api-docs-core const http = require('node:http'); const { createDocs } = require('@usebruno/api-docs-core'); -const handler = createDocs({ collectionUrl: './api-collection' }).handler(); +const handler = createDocs({ collectionPath: './api-collection' }).handler(); http.createServer((req, res) => { if (req.url.startsWith('/docs')) { @@ -38,16 +38,20 @@ http.createServer((req, res) => { ```js createDocs({ - collectionUrl: './api-collection', + collectionPath: './api-collection', environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'https://acme.dev/logo.svg', - gitCollectionUrl: 'https://github.com/acme/api-collection' + repositoryUrl: 'https://github.com/acme/api-collection' }) ``` -`collectionUrl` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. +`collectionPath` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. + +`url` is the public address of a bundled collection file, `https://example.com/api/opencollection.yml`, fetched once at start. + +`content` is the document itself as a string. When more than one source is set the first of `content`, `url`, `collectionPath` wins. `environments` and `tags` take `{ include, exclude }`, where `include` is a list of names or `'*'`. Leave `environments` out and none are published. Excluded tags drop their requests from the served collection. diff --git a/integrations/nodejs/core/src/collection/index.ts b/integrations/nodejs/core/src/collection/index.ts index c7c8e520..86bcc680 100644 --- a/integrations/nodejs/core/src/collection/index.ts +++ b/integrations/nodejs/core/src/collection/index.ts @@ -1,23 +1,60 @@ // The collection layer's one entry point: where the document comes from is decided here, at startup. -import { ConfigError, type CollectionOptions } from '../options'; +import { ConfigError, SOURCE_KEYS, type SourceKey, type CollectionOptions, type CollectionFilters } from '../options'; import { log } from '../log'; import type { SkippedFile } from './walk'; import { resolveCollectionSource } from './source'; -import { fileProvider, dirProvider, type Provider, type BuiltProvider } from './providers'; +import { bundledProvider, fileProvider, dirProvider, type Provider, type BuiltProvider } from './providers'; +import { parseCollectionUrl, fetchBundled } from './remote'; + +interface Built { + provider: BuiltProvider; + bundled: boolean; + label: string; +} + +const BUILD: Record Built> = { + content: fromContent, + url: fromUrl, + collectionPath: fromPath +}; export function providerFor(options: CollectionOptions): Provider { - const source = resolveCollectionSource(options.collectionUrl); - const filters = { environments: options.environments, tags: options.tags }; + const set = SOURCE_KEYS.filter((key) => options[key]); + if (set.length === 0) { + throw new ConfigError(`apiDocs: one of ${SOURCE_KEYS.map((key) => `\`${key}\``).join(', ')} is required`); + } + const [chosen, ...ignored] = set; + for (const key of ignored) { + log.warn(`${chosen} is set, ignoring ${key}`); + } - // built first: a path that is not there is reported as missing, not as a filter mistake - const built = source.mode === 'file' ? fileProvider(source.path) : dirProvider(source.path, filters); - if (source.mode === 'file' && (filters.environments || filters.tags)) { + const filters = { environments: options.environments, tags: options.tags }; + const built = BUILD[chosen](options[chosen] as string, filters); + if (built.bundled && (filters.environments || filters.tags)) { throw new ConfigError('apiDocs: `environments` / `tags` filtering needs a collection directory'); } - report(source.path, built); + report(built.label, built.provider); + + return built.provider.serve; +} + +function fromContent(text: string): Built { + return { provider: bundledProvider(text, 'content'), bundled: true, label: 'content' }; +} + +function fromUrl(value: string): Built { + const url = parseCollectionUrl(value); + + return { provider: bundledProvider(fetchBundled(url), url), bundled: true, label: url }; +} + +function fromPath(value: string, filters: CollectionFilters): Built { + const source = resolveCollectionSource(value); + // built first: a path that is not there is reported as missing, not as a filter mistake + const provider = source.mode === 'file' ? fileProvider(source.path) : dirProvider(source.path, filters); - return built.serve; + return { provider, bundled: source.mode === 'file', label: source.path }; } /** diff --git a/integrations/nodejs/core/src/collection/providers.ts b/integrations/nodejs/core/src/collection/providers.ts index 5cdddabb..862e5a03 100644 --- a/integrations/nodejs/core/src/collection/providers.ts +++ b/integrations/nodejs/core/src/collection/providers.ts @@ -5,6 +5,7 @@ import { etagOf, type HttpResponse, type Conditional } from '../http'; import type { SkippedFile } from './walk'; import { walkCollection, CAPS, CapError, type CollectionFormat } from './walk'; import { applyFilters, isManifest } from './filter'; +import { RemoteError } from './remote'; export type Provider = (conditional: Conditional) => HttpResponse; @@ -41,15 +42,21 @@ function serveFromMemory(body: string | Buffer, contentType: string): Provider { }; } +export function bundledProvider(body: string | Buffer, label: string): BuiltProvider { + if (Buffer.byteLength(body) > CAPS.totalBytes) { + throw new CapError('total size cap (5MB)', label); + } + const contentType = /^\s*[[{]/.test(String(body)) ? 'application/json; charset=utf-8' : 'text/yaml; charset=utf-8'; + + return { serve: serveFromMemory(body, contentType), fileCount: 1, skipped: [], unknownEnvironments: [] }; +} + export function fileProvider(filePath: string): BuiltProvider { - const size = fs.statSync(filePath).size; - if (size > CAPS.totalBytes) { + if (fs.statSync(filePath).size > CAPS.totalBytes) { throw new CapError('total size cap (5MB)', filePath); } - const body = fs.readFileSync(filePath); - - return { serve: serveFromMemory(body, 'text/yaml; charset=utf-8'), fileCount: 1, skipped: [], unknownEnvironments: [] }; + return bundledProvider(fs.readFileSync(filePath), filePath); } const formatOf = (rootDir: string): CollectionFormat => @@ -81,7 +88,7 @@ export function errorProvider(err: unknown): Provider { if (err instanceof CapError) { return () => errorResponse(413, err.message); } - if (err instanceof ManifestError) { + if (err instanceof ManifestError || err instanceof RemoteError) { return () => errorResponse(404, err.message); } if (err instanceof ConfigError) { diff --git a/integrations/nodejs/core/src/collection/remote.ts b/integrations/nodejs/core/src/collection/remote.ts new file mode 100644 index 00000000..49c16d83 --- /dev/null +++ b/integrations/nodejs/core/src/collection/remote.ts @@ -0,0 +1,70 @@ +import { execFileSync } from 'node:child_process'; +import { ConfigError } from '../options'; +import { log } from '../log'; +import { stripGitCredentials } from '../url'; +import { CAPS, CapError } from './walk'; +import { safeLoad } from './yaml'; + +export class RemoteError extends Error {} + +const FETCH_TIMEOUT_MS = 30_000; + +export function parseCollectionUrl(value: string): string { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw new ConfigError(`apiDocs: \`url\` is not a URL: ${value}`); + } + if (parsed.protocol !== 'https:') { + throw new ConfigError('apiDocs: `url` must be https'); + } + if (parsed.username || parsed.password) { + log.warn('credentials in `url` were ignored, the file has to be public'); + } + if (!/\.(ya?ml|json)$/i.test(parsed.pathname)) { + throw new ConfigError('apiDocs: `url` takes the address of a bundled collection file, .yml, .yaml or .json'); + } + + return stripGitCredentials(value); +} + +// the boot is synchronous and fetch is not, so the one request runs in a child of this same node +const FETCH = ` +try { + const res = await fetch(process.argv[1], { redirect: 'follow', signal: AbortSignal.timeout(${FETCH_TIMEOUT_MS}) }); + if (!res.ok) { + throw new Error('HTTP ' + res.status); + } + process.stdout.write(await res.text()); +} catch (err) { + console.error(err.name === 'TimeoutError' ? 'no answer in ${FETCH_TIMEOUT_MS / 1000}s' : (err.cause?.code ?? err.cause?.errors?.[0]?.code ?? err.cause?.message ?? err.message)); + process.exit(2); +} +`; + +/** The document at a public address, fetched once at boot, under the same size cap as a file on disk. */ +export function fetchBundled(url: string): string { + let text: string; + try { + text = execFileSync(process.execPath, ['--input-type=module', '-e', FETCH, url], { + timeout: FETCH_TIMEOUT_MS + 5_000, + maxBuffer: CAPS.totalBytes, + stdio: ['ignore', 'pipe', 'pipe'] + }).toString(); + } catch (err) { + const failed = err as NodeJS.ErrnoException & { stderr?: Buffer }; + if (failed.code === 'ENOBUFS') { + throw new CapError('total size cap (5MB)', url); + } + const reason = failed.stderr?.toString().trim().split('\n').pop() || failed.message; + + throw new RemoteError(`could not fetch ${url}: ${reason}`); + } + + if (safeLoad(text)?.bundled === false) { + throw new ConfigError('apiDocs: the file at `url` is the manifest of a directory collection, `url` takes a bundled collection'); + } + + return text; +} diff --git a/integrations/nodejs/core/src/collection/source.ts b/integrations/nodejs/core/src/collection/source.ts index d117dc84..6174d56a 100644 --- a/integrations/nodejs/core/src/collection/source.ts +++ b/integrations/nodejs/core/src/collection/source.ts @@ -18,7 +18,7 @@ export function resolveCollectionPath(collectionOption: string): string { // guarded here rather than in validateOptions: an empty path resolves to the app's own // directory, and walking that would serve whatever yml the adopter happens to keep there if (!collectionOption) { - throw new ConfigError('apiDocs: `collectionUrl` is required'); + throw new ConfigError('apiDocs: `collectionPath` is required'); } const entryDir = require.main?.path diff --git a/integrations/nodejs/core/src/options.ts b/integrations/nodejs/core/src/options.ts index 0608ee82..1c110b44 100644 --- a/integrations/nodejs/core/src/options.ts +++ b/integrations/nodejs/core/src/options.ts @@ -8,13 +8,15 @@ export interface CollectionFilters { tags?: Filter; } -export interface CollectionOptions extends CollectionFilters { - collectionUrl: string; -} +/** Where the collection comes from, in priority order: the first one set is used. */ +export const SOURCE_KEYS = ['content', 'url', 'collectionPath'] as const; +export type SourceKey = typeof SOURCE_KEYS[number]; + +export interface CollectionOptions extends CollectionFilters, Partial> {} export interface RendererOptions { logo?: string; - gitCollectionUrl?: string; + repositoryUrl?: string; } export interface ApiDocsOptions extends CollectionOptions, RendererOptions { @@ -23,7 +25,7 @@ export interface ApiDocsOptions extends CollectionOptions, RendererOptions { export class ConfigError extends Error {} -const KNOWN_OPTIONS = new Set(['collectionUrl', 'environments', 'tags', 'logo', 'gitCollectionUrl', 'pageTitle']); +const KNOWN_OPTIONS = new Set([...SOURCE_KEYS, 'environments', 'tags', 'logo', 'repositoryUrl', 'pageTitle']); export function validateOptions(options: ApiDocsOptions): void { const unknown = Object.keys(options).filter((key) => !KNOWN_OPTIONS.has(key)); @@ -31,6 +33,12 @@ export function validateOptions(options: ApiDocsOptions): void { throw new ConfigError(`apiDocs: unknown option ${unknown.join(', ')}`); } + for (const key of SOURCE_KEYS) { + if (options[key] !== undefined && typeof options[key] !== 'string') { + throw new ConfigError(`apiDocs: \`${key}\` takes a string`); + } + } + for (const name of ['environments', 'tags'] as const) { const filter = options[name]; if (typeof filter?.include === 'string' && filter.include !== '*') { diff --git a/integrations/nodejs/core/src/routes/shell.ts b/integrations/nodejs/core/src/routes/shell.ts index d31628cf..2c89d262 100644 --- a/integrations/nodejs/core/src/routes/shell.ts +++ b/integrations/nodejs/core/src/routes/shell.ts @@ -1,6 +1,7 @@ // The browser half as the core serves it: shell.html stamped per mount, shell.js static, embed() body-only. import fs from 'node:fs'; +import { stripGitCredentials } from '../url'; import path from 'node:path'; import { ConfigError, type RendererOptions, type ApiDocsOptions } from '../options'; import { etagOf, type HttpResponse, type Conditional } from '../http'; @@ -16,19 +17,7 @@ const DEFAULT_TITLE = 'API Documentation'; const escapeHtml = (s: string): string => s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -export const stripGitCredentials = (url: string): string => { - try { - const parsed = new URL(url); - if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || (!parsed.username && !parsed.password)) { - return url; - } - parsed.username = ''; - parsed.password = ''; - return parsed.toString(); - } catch { - return url; - } -}; +export { stripGitCredentials }; /** What the browser is allowed to see. Built from named fields, never copied from the input. */ export function rendererConfig(options: RendererOptions): RendererOptions { @@ -36,8 +25,8 @@ export function rendererConfig(options: RendererOptions): RendererOptions { if (options.logo) { config.logo = options.logo; } - if (options.gitCollectionUrl) { - config.gitCollectionUrl = stripGitCredentials(options.gitCollectionUrl); + if (options.repositoryUrl) { + config.repositoryUrl = stripGitCredentials(options.repositoryUrl); } return config; diff --git a/integrations/nodejs/core/src/url.ts b/integrations/nodejs/core/src/url.ts new file mode 100644 index 00000000..74a1fa7b --- /dev/null +++ b/integrations/nodejs/core/src/url.ts @@ -0,0 +1,13 @@ +export const stripGitCredentials = (url: string): string => { + try { + const parsed = new URL(url); + if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || (!parsed.username && !parsed.password)) { + return url; + } + parsed.username = ''; + parsed.password = ''; + return parsed.toString(); + } catch { + return url; + } +}; diff --git a/integrations/nodejs/core/test/collection.test.mjs b/integrations/nodejs/core/test/collection.test.mjs index e5379d19..2b057b4f 100644 --- a/integrations/nodejs/core/test/collection.test.mjs +++ b/integrations/nodejs/core/test/collection.test.mjs @@ -105,16 +105,16 @@ const quiet = (fn) => { { const gone = path.join(tmp, 'not-here.yml'); assert.equal(resolveCollectionSource(gone).mode, 'file', 'an unstattable path falls through to file mode'); - assert.throws(() => quiet(() => providerFor({ collectionUrl: gone })), { code: 'ENOENT' }); - assert.equal(errorProvider(thrownBy(() => providerFor({ collectionUrl: gone })))({}).status, 404); - assert.throws(() => quiet(() => providerFor({ collectionUrl: gone, tags: { exclude: ['x'] } })), { code: 'ENOENT' }, + assert.throws(() => quiet(() => providerFor({ collectionPath: gone })), { code: 'ENOENT' }); + assert.equal(errorProvider(thrownBy(() => providerFor({ collectionPath: gone })))({}).status, 404); + assert.throws(() => quiet(() => providerFor({ collectionPath: gone, tags: { exclude: ['x'] } })), { code: 'ENOENT' }, 'with filters set, a missing path is still reported as missing, not as a filter mistake'); if (process.getuid?.() !== 0) { const locked = write('locked/opencollection.yml', 'opencollection: 1.0.0\nbundled: false\n'); fs.chmodSync(locked, 0o000); assert.equal(resolveCollectionSource(locked).mode, 'file', 'an unreadable manifest falls through to file mode'); - assert.equal(errorProvider(thrownBy(() => providerFor({ collectionUrl: locked })))({}).status, 500); + assert.equal(errorProvider(thrownBy(() => providerFor({ collectionPath: locked })))({}).status, 500); fs.chmodSync(locked, 0o644); } } @@ -122,9 +122,35 @@ const quiet = (fn) => { // --- filters need a directory, and that is a startup failure { const single = write('solo/opencollection.yml', 'opencollection: 1.0.0\nbundled: true\n'); - assert.throws(() => quiet(() => providerFor({ collectionUrl: single, tags: { exclude: ['internal'] } })), ConfigError); - assert.throws(() => quiet(() => providerFor({ collectionUrl: single, environments: { include: '*' } })), ConfigError); - assert.equal(quiet(() => providerFor({ collectionUrl: single }))({}).status, 200, 'without filters it serves'); + assert.throws(() => quiet(() => providerFor({ collectionPath: single, tags: { exclude: ['internal'] } })), ConfigError); + assert.throws(() => quiet(() => providerFor({ collectionPath: single, environments: { include: '*' } })), ConfigError); + assert.equal(quiet(() => providerFor({ collectionPath: single }))({}).status, 200, 'without filters it serves'); +} + +// --- content: the document itself, served as a bundled file, and it wins over collectionPath +{ + const text = 'opencollection: 1.0.0\ninfo:\n name: Inline\n'; + const served = quiet(() => providerFor({ content: text }))({}); + assert.equal(served.status, 200); + assert.equal(served.body, text, 'served as given, byte for byte'); + assert.match(served.headers['Content-Type'], /yaml/); + assert.match(quiet(() => providerFor({ content: '{"opencollection":"1.0.0"}' }))({}).headers['Content-Type'], /json/, 'a JSON document is served as JSON'); + + const dir = path.dirname(write('prio/opencollection.yml', 'opencollection: 1.0.0\nbundled: false\n')); + const warned = []; + const { log, warn } = console; + console.log = () => {}; + console.warn = (line) => warned.push(line); + try { + assert.equal(providerFor({ content: text, collectionPath: dir })({}).body, text, 'content wins'); + } finally { + Object.assign(console, { log, warn }); + } + assert.ok(warned.some((line) => line.includes('content is set, ignoring collectionPath')), 'and says what it ignored'); + + assert.throws(() => quiet(() => providerFor({ content: text, tags: { exclude: ['x'] } })), ConfigError, 'filters need a directory, as for a bundled file'); + assert.throws(() => quiet(() => providerFor({})), /one of `content`, `url`, `collectionPath` is required/); + assert.throws(() => quiet(() => providerFor({ content: '' })), /is required/, 'an empty document is not a source'); } // --- pointing at the manifest of an unbundled collection means the directory around it @@ -144,7 +170,7 @@ const quiet = (fn) => { // so any caller that skipped the validator would walk it and serve whatever yml lives there for (const empty of ['', undefined, null]) { assert.throws(() => resolveCollectionPath(empty), ConfigError, `${JSON.stringify(empty)} is refused`); - assert.throws(() => quiet(() => providerFor({ collectionUrl: empty })), ConfigError, 'and refused through providerFor'); + assert.throws(() => quiet(() => providerFor({ collectionPath: empty })), ConfigError, 'and refused through providerFor'); } } diff --git a/integrations/nodejs/core/test/docs.test.mjs b/integrations/nodejs/core/test/docs.test.mjs index 14ec2141..9215a121 100644 --- a/integrations/nodejs/core/test/docs.test.mjs +++ b/integrations/nodejs/core/test/docs.test.mjs @@ -42,7 +42,7 @@ const call = (handler, method, mount, sub, headers = {}) => { return res; }; -const docs = quiet(() => createDocs({ collectionUrl: collection, pageTitle: 'Fixture API' })); +const docs = quiet(() => createDocs({ collectionPath: collection, pageTitle: 'Fixture API' })); const handler = docs.handler(); // --- the routes, as the contract states them @@ -123,7 +123,7 @@ const handler = docs.handler(); const missing = path.join(os.tmpdir(), 'bruno-docs-absent-' + Date.now()); let broken; assert.doesNotThrow(() => { - broken = quiet(() => createDocs({ collectionUrl: missing })); + broken = quiet(() => createDocs({ collectionPath: missing })); }, 'createDocs never throws: a docs page cannot take down the API it documents'); const brokenHandler = broken.handler(); @@ -134,14 +134,14 @@ const handler = docs.handler(); } const overCap = quiet(() => createDocs({ - collectionUrl: path.join(here, '..', '..', '..', 'contract-tests', 'fixtures', 'walk-oversize') + collectionPath: path.join(here, '..', '..', '..', 'contract-tests', 'fixtures', 'walk-oversize') })); assert.equal(overCap.collection().status, 413, 'a collection over the caps is a 413 at the mount'); - const badOptions = quiet(() => createDocs({ collectionUrl: collection, environments: { exclude: ['Prod'] } })); + const badOptions = quiet(() => createDocs({ collectionPath: collection, environments: { exclude: ['Prod'] } })); assert.equal(badOptions.collection().status, 500, 'a config error reaches the mount too, not the boot'); - const typo = quiet(() => createDocs({ collectionUrl: collection, theme: 'dark' })); + const typo = quiet(() => createDocs({ collectionPath: collection, theme: 'dark' })); assert.equal(typo.collection().status, 500, 'an option we do not know is an error, not silently forwarded'); assert.match(String(typo.collection().body), /unknown option theme/); } diff --git a/integrations/nodejs/core/test/filter.test.mjs b/integrations/nodejs/core/test/filter.test.mjs index 3bbf314a..3dc1bf90 100644 --- a/integrations/nodejs/core/test/filter.test.mjs +++ b/integrations/nodejs/core/test/filter.test.mjs @@ -120,7 +120,10 @@ assert.throws(() => validateOptions({ environments: { include: 'Local' } }), /`e 'a string that is not the wildcard is named, not silently read as no names'); assert.throws(() => validateOptions({ tags: { include: 'internal' } }), /`tags.include` takes a list/); validateOptions({ environments: { include: '*' }, tags: { include: '*' } }); -assert.throws(() => validateOptions({ collectionUrl: './c', theme: 'dark', favicon: 'x' }), /unknown option theme, favicon/, 'every unknown key, named'); +assert.throws(() => validateOptions({ collectionPath: './c', theme: 'dark', favicon: 'x' }), /unknown option theme, favicon/, 'every unknown key, named'); +assert.throws(() => validateOptions({ content: { info: {} } }), /`content` takes a string/); +assert.throws(() => validateOptions({ url: 42 }), /`url` takes a string/); +assert.doesNotThrow(() => validateOptions({ content: 'opencollection: 1.0.0\n' })); validateOptions({ environments: { include: '*', exclude: ['Prod'] } }); validateOptions({}); diff --git a/integrations/nodejs/core/test/remote.test.mjs b/integrations/nodejs/core/test/remote.test.mjs new file mode 100644 index 00000000..0a79a963 --- /dev/null +++ b/integrations/nodejs/core/test/remote.test.mjs @@ -0,0 +1,68 @@ +// `url`: a bundled collection file at a public address, fetched once at boot. The fixture is this +// repo's own, on GitHub, pinned to a commit so it cannot drift. Needs the network, like the browser run. +// Run: npm run test:unit +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const dist = path.join(here, '..', 'dist'); +const { parseCollectionUrl, fetchBundled, RemoteError } = require(path.join(dist, 'collection', 'remote.js')); +const { errorProvider } = require(path.join(dist, 'collection', 'providers.js')); +const { ConfigError } = require(path.join(dist, 'options.js')); +const thrownBy = (fn) => { + try { + fn(); + return null; + } catch (err) { + return err; + } +}; + +const FIXTURES = 'https://raw.githubusercontent.com/usebruno/bruno-api-docs/1aa76e1ad567b6e05e03e46ee53fb6bc1a66e7d5/integrations/contract-tests/fixtures'; +const bundled = fs.readFileSync(path.join(here, '..', '..', '..', 'contract-tests', 'fixtures', 'bundled.yml'), 'utf8'); + +// --- what the option accepts +{ + assert.equal(parseCollectionUrl('https://example.com/api/opencollection.yml'), 'https://example.com/api/opencollection.yml'); + assert.equal(parseCollectionUrl('https://cdn.example.com/v1/collection.yaml?x=1'), 'https://cdn.example.com/v1/collection.yaml?x=1'); + assert.equal(parseCollectionUrl('https://example.com/api/collection.JSON'), 'https://example.com/api/collection.JSON'); + + const warned = []; + const { warn } = console; + console.warn = (line) => warned.push(line); + try { + assert.equal(parseCollectionUrl('https://user:tok@example.com/c.yml'), 'https://example.com/c.yml', 'credentials are dropped, the file is public by contract'); + } finally { + console.warn = warn; + } + assert.ok(warned.some((line) => line.includes('credentials in `url` were ignored')), 'and it says so'); + + assert.throws(() => parseCollectionUrl('http://example.com/c.yml'), /must be https/); + assert.throws(() => parseCollectionUrl('not a url'), /is not a URL/); + assert.throws(() => parseCollectionUrl('https://github.com/acme/api'), /bundled collection file/, 'a repository is not a file'); + assert.throws(() => parseCollectionUrl('https://github.com/acme/api/tree/main/collection'), /bundled collection file/, 'nor is a folder in it'); +} + +// --- the fetch +{ + assert.equal(fetchBundled(`${FIXTURES}/bundled.yml`), bundled, 'the document, byte for byte'); + assert.equal(fetchBundled('https://github.com/usebruno/bruno-api-docs/raw/1aa76e1ad567b6e05e03e46ee53fb6bc1a66e7d5/integrations/contract-tests/fixtures/bundled.yml'), bundled, 'a redirect is followed'); + + const manifest = thrownBy(() => fetchBundled(`${FIXTURES}/api-collection/opencollection.yml`)); + assert.ok(manifest instanceof ConfigError, 'the manifest of a directory collection is refused, not served empty'); + assert.match(manifest.message, /bundled collection/); + + const missing = thrownBy(() => fetchBundled(`${FIXTURES}/missing.yml`)); + assert.ok(missing instanceof RemoteError); + assert.match(missing.message, /HTTP 404/); + assert.equal(errorProvider(missing)({}).status, 404, 'a fetch that fails is a 404 with the reason'); + + const down = thrownBy(() => fetchBundled('https://127.0.0.1:9/c.yml')); + assert.ok(down instanceof RemoteError, 'a host that does not answer'); +} + +console.log('remote-test: all assertions passed'); diff --git a/integrations/nodejs/core/test/shell.test.mjs b/integrations/nodejs/core/test/shell.test.mjs index 0af9ce5e..3ffbd06c 100644 --- a/integrations/nodejs/core/test/shell.test.mjs +++ b/integrations/nodejs/core/test/shell.test.mjs @@ -13,7 +13,7 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p // --- stamping: every placeholder is filled, at any mount { - const shell = loadShell({ collectionUrl: './c', pageTitle: 'Acme API' }); + const shell = loadShell({ collectionPath: './c', pageTitle: 'Acme API' }); const html = shell.html('/docs/').body; assert.ok(!html.includes('{{'), 'no placeholder survives stamping'); @@ -23,13 +23,13 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p assert.ok(html.includes('src="/docs/shell.js"'), 'shell.js is loaded from the mount, same origin'); assert.ok(html.includes(`src="${CDN}/api-docs.js"`), 'the renderer comes from the CDN'); - assert.equal(loadShell({ collectionUrl: './c' }).html('/x/').body.match(/(.*)<\/title>/)[1], 'API Documentation'); - assert.ok(loadShell({ collectionUrl: './c' }).html('/a/b/c/').body.includes('data-base="/a/b/c/"'), 'any depth'); + assert.equal(loadShell({ collectionPath: './c' }).html('/x/').body.match(/<title>(.*)<\/title>/)[1], 'API Documentation'); + assert.ok(loadShell({ collectionPath: './c' }).html('/a/b/c/').body.includes('data-base="/a/b/c/"'), 'any depth'); } // --- the CSP promise: nothing inline { - const html = loadShell({ collectionUrl: './c' }).html('/docs/').body; + const html = loadShell({ collectionPath: './c' }).html('/docs/').body; assert.ok(!/<style/i.test(html), 'no inline style'); assert.ok(!/<script(?![^>]*\bsrc=)/i.test(html), 'every script tag has a src, none has a body'); } @@ -37,7 +37,7 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p // --- escaping, because pageTitle and the config are adopter input { const nasty = loadShell({ - collectionUrl: './c', + collectionPath: './c', pageTitle: '', logo: 'https://x.test/a"onerror="alert(1)' }).html('/docs/').body; @@ -50,21 +50,21 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p // --- only renderer options reach the browser, and credentials never do { const config = rendererConfig({ - collectionUrl: './secret-path', + collectionPath: './secret-path', environments: { include: '*' }, tags: { exclude: ['internal'] }, pageTitle: 'Docs', logo: 'https://x.test/l.svg', - gitCollectionUrl: 'https://user:tok@github.com/acme/api-collection' + repositoryUrl: 'https://user:tok@github.com/acme/api-collection' }); - assert.deepEqual(Object.keys(config).sort(), ['gitCollectionUrl', 'logo'], + assert.deepEqual(Object.keys(config).sort(), ['logo', 'repositoryUrl'], 'only what the renderer reads is forwarded'); - assert.equal(config.gitCollectionUrl, 'https://github.com/acme/api-collection', 'credentials stripped'); + assert.equal(config.repositoryUrl, 'https://github.com/acme/api-collection', 'credentials stripped'); assert.deepEqual(rendererConfig({}), {}, 'nothing set means nothing forwarded'); const html = loadShell({ - collectionUrl: './secret-path', + collectionPath: './secret-path', tags: { exclude: ['internal'] }, environments: { include: ['Prod'] }, pageTitle: 'Acme API' @@ -89,7 +89,7 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p // --- shell.js: static, cacheable, revalidates { - const shell = loadShell({ collectionUrl: './c' }); + const shell = loadShell({ collectionPath: './c' }); const res = shell.js({}); assert.equal(res.status, 200); assert.equal(res.headers['Content-Type'], 'application/javascript; charset=utf-8'); @@ -98,13 +98,13 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p assert.equal(shell.js({ ifNoneMatch: res.headers.ETag }).status, 304); assert.equal(shell.js({ ifNoneMatch: '"stale"' }).status, 200); - assert.equal(loadShell({ collectionUrl: './c', pageTitle: 'Other' }).js({}).headers.ETag, res.headers.ETag, + assert.equal(loadShell({ collectionPath: './c', pageTitle: 'Other' }).js({}).headers.ETag, res.headers.ETag, 'shell.js is identical whatever the options, so it caches across mounts'); } // --- the page itself must never be cached: it carries the per-mount base { - assert.equal(loadShell({ collectionUrl: './c' }).html('/docs/').headers['Cache-Control'], 'no-cache'); + assert.equal(loadShell({ collectionPath: './c' }).html('/docs/').headers['Cache-Control'], 'no-cache'); } // --- embed(): the body block only, for an adopter's own page. No collection involved. @@ -115,8 +115,8 @@ const { loadShell, embed, rendererConfig, stripGitCredentials, CDN } = require(p assert.ok(block.includes('id="bruno-docs"') && block.includes('src="/docs/shell.js"'), 'a base without a trailing slash still works'); assert.ok(block.startsWith('') && block.endsWith('')); assert.equal(embed({ base: '/docs/' }), block, 'with or without the slash, the same block'); - assert.ok(embed({ base: '/docs', gitCollectionUrl: 'https://u:p@github.com/acme/api' }).includes('github.com/acme/api'), 'renderer options reach the block'); - assert.ok(!embed({ base: '/docs', gitCollectionUrl: 'https://u:p@github.com/acme/api' }).includes('u:p@'), 'with credentials stripped, as on the page'); + assert.ok(embed({ base: '/docs', repositoryUrl: 'https://u:p@github.com/acme/api' }).includes('github.com/acme/api'), 'renderer options reach the block'); + assert.ok(!embed({ base: '/docs', repositoryUrl: 'https://u:p@github.com/acme/api' }).includes('u:p@'), 'with credentials stripped, as on the page'); assert.throws(() => embed({ base: '' }), /`base`/); } diff --git a/integrations/nodejs/express/README.md b/integrations/nodejs/express/README.md index d021e5e9..5ab2a073 100644 --- a/integrations/nodejs/express/README.md +++ b/integrations/nodejs/express/README.md @@ -11,7 +11,7 @@ npm install @usebruno/api-docs-express ```js const { apiDocs } = require('@usebruno/api-docs-express'); -app.use('/docs', apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', apiDocs({ collectionPath: './api-collection' })); ``` Open `http://localhost:/docs/`. @@ -20,16 +20,20 @@ Open `http://localhost:/docs/`. ```js apiDocs({ - collectionUrl: './api-collection', + collectionPath: './api-collection', environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'https://acme.dev/logo.svg', - gitCollectionUrl: 'https://github.com/acme/api-collection' + repositoryUrl: 'https://github.com/acme/api-collection' }) ``` -`collectionUrl` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. +`collectionPath` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. + +`url` is the public address of a bundled collection file, `https://example.com/api/opencollection.yml`, fetched once at start. + +`content` is the document itself as a string. When more than one source is set the first of `content`, `url`, `collectionPath` wins. `environments` and `tags` take `{ include, exclude }`, where `include` is a list of names or `'*'`. Leave `environments` out and none are published. Excluded tags drop their requests from the served collection. @@ -38,7 +42,7 @@ apiDocs({ Put your guard in front of the mount. It covers the collection file too. ```js -app.use('/docs', requireLogin, apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', requireLogin, apiDocs({ collectionPath: './api-collection' })); ``` ## Content Security Policy @@ -58,7 +62,7 @@ connect-src 'self' data: https://cdn.jsdelivr.net ```js const { apiDocs, embed } = require('@usebruno/api-docs-express'); -app.use('/docs', apiDocs({ collectionUrl: './api-collection' })); +app.use('/docs', apiDocs({ collectionPath: './api-collection' })); app.get('/', (req, res) => res.type('html').send(`

Acme

${embed({ base: '/docs' })}`)); ``` @@ -66,4 +70,4 @@ app.get('/', (req, res) => res.type('html').send(`

Acme

${embed({ base: ' Node 20 or later. Express 4 or 5. -Examples: [`examples/`](https://github.com/usebruno/bruno-api-docs/tree/main/integrations/nodejs/express/examples) +Examples: [`examples/`](https://github.com/usebruno/bruno-api-docs/tree/main/examples/integrations/nodejs/express) diff --git a/integrations/nodejs/express/src/index.ts b/integrations/nodejs/express/src/index.ts index 66227eec..d0a7a7f1 100644 --- a/integrations/nodejs/express/src/index.ts +++ b/integrations/nodejs/express/src/index.ts @@ -6,7 +6,7 @@ export type { ApiDocsOptions, CollectionOptions, CollectionFilters, Filter, Rend from '@usebruno/api-docs-core'; /** - * `app.use('/docs', apiDocs({ collectionUrl: './api-collection' }))`. Express decides the mount and + * `app.use('/docs', apiDocs({ collectionPath: './api-collection' }))`. Express decides the mount and * strips it before the handler sees the request, so there is no `mountPath` here. */ export function apiDocs(options: ApiDocsOptions): RequestHandler { diff --git a/integrations/nodejs/fastify/README.md b/integrations/nodejs/fastify/README.md index 33874593..87e5396c 100644 --- a/integrations/nodejs/fastify/README.md +++ b/integrations/nodejs/fastify/README.md @@ -11,7 +11,7 @@ npm install @usebruno/api-docs-fastify ```js const { apiDocs } = require('@usebruno/api-docs-fastify'); -app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); +app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); ``` Open `http://localhost:/docs/`. The plugin is encapsulated and its routes are hidden from `@fastify/swagger`. @@ -21,16 +21,20 @@ Open `http://localhost:/docs/`. The plugin is encapsulated and its routes ```js app.register(apiDocs, { prefix: '/docs', - collectionUrl: './api-collection', + collectionPath: './api-collection', environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'https://acme.dev/logo.svg', - gitCollectionUrl: 'https://github.com/acme/api-collection' + repositoryUrl: 'https://github.com/acme/api-collection' }); ``` -`collectionUrl` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. +`collectionPath` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. + +`url` is the public address of a bundled collection file, `https://example.com/api/opencollection.yml`, fetched once at start. + +`content` is the document itself as a string. When more than one source is set the first of `content`, `url`, `collectionPath` wins. `environments` and `tags` take `{ include, exclude }`, where `include` is a list of names or `'*'`. Leave `environments` out and none are published. Excluded tags drop their requests from the served collection. @@ -45,7 +49,7 @@ app.register(async (guarded) => { return reply.code(401).send(); } }); - await guarded.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); + await guarded.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); }); ``` @@ -66,7 +70,7 @@ connect-src 'self' data: https://cdn.jsdelivr.net ```js const { apiDocs, embed } = require('@usebruno/api-docs-fastify'); -app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' }); +app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' }); app.get('/', async (request, reply) => reply.type('text/html').send(`

Acme

${embed({ base: '/docs' })}`)); ``` @@ -74,4 +78,4 @@ app.get('/', async (request, reply) => reply.type('text/html').send(`

Acme { }; /** - * `app.register(apiDocs, { collectionUrl: './api-collection', prefix: '/docs' })`. The mount is + * `app.register(apiDocs, { collectionPath: './api-collection', prefix: '/docs' })`. The mount is * Fastify's own `prefix`, so there is no `mountPath`. Deliberately not wrapped in `fastify-plugin`: * staying encapsulated is what keeps the 405 off the host's routes and lets an adopter put auth in * front with `child.addHook('onRequest', ...)`. diff --git a/integrations/nodejs/nestjs/README.md b/integrations/nodejs/nestjs/README.md index 51023930..52d725d4 100644 --- a/integrations/nodejs/nestjs/README.md +++ b/integrations/nodejs/nestjs/README.md @@ -12,7 +12,7 @@ npm install @usebruno/api-docs-nestjs import { ApiDocsModule } from '@usebruno/api-docs-nestjs'; @Module({ - imports: [ApiDocsModule.forRoot({ collectionUrl: '../api-collection' })] + imports: [ApiDocsModule.forRoot({ collectionPath: '../api-collection' })] }) export class AppModule {} ``` @@ -25,17 +25,21 @@ Open `http://localhost:/docs/`. The path counts from `dist/main.js`, so a ```ts ApiDocsModule.forRoot({ - collectionUrl: '../api-collection', + collectionPath: '../api-collection', mountPath: '/docs', environments: { include: ['Local'] }, tags: { exclude: ['internal'] }, pageTitle: 'Acme API', logo: 'https://acme.dev/logo.svg', - gitCollectionUrl: 'https://github.com/acme/api-collection' + repositoryUrl: 'https://github.com/acme/api-collection' }) ``` -`collectionUrl` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. +`collectionPath` is the path to a Bruno folder (`bruno.json` and `.bru` files), an OpenCollection yml folder or one bundled `.yml` file. The path is relative to your app's entry file. + +`url` is the public address of a bundled collection file, `https://example.com/api/opencollection.yml`, fetched once at start. + +`content` is the document itself as a string. When more than one source is set the first of `content`, `url`, `collectionPath` wins. `environments` and `tags` take `{ include, exclude }`, where `include` is a list of names or `'*'`. Leave `environments` out and none are published. Excluded tags drop their requests from the served collection. @@ -81,4 +85,4 @@ portal(): string { Node 20 or later. NestJS 10, 11 or 12. -Examples: [`examples/`](https://github.com/usebruno/bruno-api-docs/tree/main/integrations/nodejs/nestjs/examples) +Examples: [`examples/`](https://github.com/usebruno/bruno-api-docs/tree/main/examples/integrations/nodejs/nestjs) diff --git a/integrations/nodejs/nestjs/examples/tsconfig.json b/integrations/nodejs/nestjs/examples/tsconfig.json deleted file mode 100644 index 5b45c3ad..00000000 --- a/integrations/nodejs/nestjs/examples/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "dist", - "declaration": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["0*/*.ts"] -} diff --git a/integrations/nodejs/nestjs/src/index.ts b/integrations/nodejs/nestjs/src/index.ts index dfc91c4f..e413c62a 100644 --- a/integrations/nodejs/nestjs/src/index.ts +++ b/integrations/nodejs/nestjs/src/index.ts @@ -41,7 +41,7 @@ export function subPathOf(fullUrl: string, mountPath: string): string { } /** - * `ApiDocsModule.forRoot({ collectionUrl: '../api-collection', mountPath: '/docs' })`. The path counts + * `ApiDocsModule.forRoot({ collectionPath: '../api-collection', mountPath: '/docs' })`. The path counts * from the built entry file, `dist/main.js`, so a collection beside `src/` is one level up. */ export class ApiDocsModule { diff --git a/integrations/nodejs/scripts/unit.sh b/integrations/nodejs/scripts/unit.sh index 279e80c2..ad761c74 100644 --- a/integrations/nodejs/scripts/unit.sh +++ b/integrations/nodejs/scripts/unit.sh @@ -7,12 +7,12 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)" source "$ROOT/contract-tests/lib.sh" JUNIT_NAME="${JUNIT_NAME:-unit}" -# runs CMD...: passes when the command exits clean; otherwise the tail of its output is what we saw +# runs CMD...: passes when the command exits clean; otherwise the error line and the tail are what we saw runs() { local out out="$("$@" 2>&1)" && return 0 want="a clean exit" - got="$(tail -4 <<<"$out")" + got="$(grep -m1 -E '^[A-Za-z]*Error' <<<"$out"; tail -4 <<<"$out")" return 1 } @@ -21,6 +21,7 @@ describe "core" check "walk: the safety rules and the caps" runs node "$ROOT/nodejs/core/test/walk.test.mjs" check "filter: environments and tags by file" runs node "$ROOT/nodejs/core/test/filter.test.mjs" check "collection: sources and providers" runs node "$ROOT/nodejs/core/test/collection.test.mjs" +check "remote: url parsing and the fetch" runs node "$ROOT/nodejs/core/test/remote.test.mjs" check "shell: the page, the bundle, embed()" runs node "$ROOT/nodejs/core/test/shell.test.mjs" check "docs: createDocs end to end" runs node "$ROOT/nodejs/core/test/docs.test.mjs" diff --git a/integrations/package.json b/integrations/package.json index 86352668..591e2b69 100644 --- a/integrations/package.json +++ b/integrations/package.json @@ -7,9 +7,9 @@ "nodejs/*" ], "scripts": { - "build": "npm run build:shell && npm run build:core && npm run build:express && npm run build:fastify && npm run build:nestjs && npm run build:examples", + "build": "npm run build:shell && npm run build:core && npm run build:express && npm run build:fastify && npm run build:nestjs", "build:core": "tsc -p nodejs/core/tsconfig.json", - "build:examples": "tsc -p nodejs/nestjs/examples/tsconfig.json", + "build:examples": "tsc -p ../examples/integrations/nodejs/nestjs/tsconfig.json", "build:express": "tsc -p nodejs/express/tsconfig.json", "build:fastify": "tsc -p nodejs/fastify/tsconfig.json", "build:nestjs": "tsc -p nodejs/nestjs/tsconfig.json", @@ -19,7 +19,7 @@ "check:fastify": "npm run build && bash contract-tests/check.sh --app contract-tests/apps/fastify.js --port 5457", "check:nestjs": "npm run build && bash contract-tests/check.sh --app contract-tests/apps/nestjs.js --port 5458", "check:nestjs-fastify": "npm run build && ADAPTER=fastify bash contract-tests/check.sh --app contract-tests/apps/nestjs.js --port 5459", - "lint": "eslint", + "lint": "eslint . && cd ../examples/integrations && ../../integrations/node_modules/.bin/eslint .", "lint:fix": "eslint --fix", "matrix": "npm run build && bash nodejs/scripts/matrix.sh", "matrix:docker": "npm run build && bash nodejs/scripts/matrix.sh --docker", diff --git a/integrations/scripts/try.sh b/integrations/scripts/try.sh index 79cee2a7..666a0f2f 100644 --- a/integrations/scripts/try.sh +++ b/integrations/scripts/try.sh @@ -51,15 +51,15 @@ echo case "$WHAT" in express) echo " const { apiDocs } = require('@usebruno/api-docs-express');" - echo " app.use('/docs', apiDocs({ collectionUrl: './api-collection' }));" + echo " app.use('/docs', apiDocs({ collectionPath: './api-collection' }));" ;; fastify) echo " const { apiDocs } = require('@usebruno/api-docs-fastify');" - echo " app.register(apiDocs, { prefix: '/docs', collectionUrl: './api-collection' });" + echo " app.register(apiDocs, { prefix: '/docs', collectionPath: './api-collection' });" ;; nestjs) echo " import { ApiDocsModule } from '@usebruno/api-docs-nestjs';" - echo " @Module({ imports: [ApiDocsModule.forRoot({ collectionUrl: '../api-collection' })] })" + echo " @Module({ imports: [ApiDocsModule.forRoot({ collectionPath: '../api-collection' })] })" ;; esac echo diff --git a/integrations/shell/shell.src.mjs b/integrations/shell/shell.src.mjs index 892d7214..9eb0357c 100644 --- a/integrations/shell/shell.src.mjs +++ b/integrations/shell/shell.src.mjs @@ -38,19 +38,19 @@ import { toOpenCollection } from './assemble.mjs'; ); } - const collectionUrl = new URL(base + 'collection.yml', location.origin).href; + const collectionPath = new URL(base + 'collection.yml', location.origin).href; let res; let text; try { - res = await fetch(collectionUrl); + res = await fetch(collectionPath); text = await res.text(); } catch (err) { - return fail('GET ' + collectionUrl + '\n -> ' + err.message); + return fail('GET ' + collectionPath + '\n -> ' + err.message); } if (!res.ok) { const detail = text ? '\n\n' + text : ''; - return fail('GET ' + collectionUrl + '\n -> HTTP ' + res.status + ' ' + res.statusText + detail); + return fail('GET ' + collectionPath + '\n -> HTTP ' + res.status + ' ' + res.statusText + detail); } let doc; @@ -67,7 +67,7 @@ import { toOpenCollection } from './assemble.mjs'; catch (err) { return fail('The renderer threw while mounting:\n ' + err.message); } - console.info('[bruno-docs] api-docs ' + version + ', collection from ' + collectionUrl); + console.info('[bruno-docs] api-docs ' + version + ', collection from ' + collectionPath); })(); function resolveBoot() { @@ -76,7 +76,8 @@ function resolveBoot() { } // until the CDN has deployed a bundle that carries the namespace if (typeof window.OpenCollection === 'function') { - return (target, config, doc) => new window.OpenCollection({ target, ...config, opencollection: doc }); + return (target, { repositoryUrl, ...config }, doc) => + new window.OpenCollection({ target, ...config, gitCollectionUrl: repositoryUrl, opencollection: doc }); } return null; } diff --git a/packages/bruno-api-docs/src/standalone.ts b/packages/bruno-api-docs/src/standalone.ts index 019504da..819ee546 100644 --- a/packages/bruno-api-docs/src/standalone.ts +++ b/packages/bruno-api-docs/src/standalone.ts @@ -22,6 +22,7 @@ export interface OpenCollectionOptions { export interface ApiDocsOptions extends Omit { content: OpenCollectionOptions['opencollection']; + repositoryUrl?: string; } export class OpenCollectionRenderer { @@ -120,7 +121,12 @@ if (typeof window !== 'undefined') { globals.OpenCollection = OpenCollectionRenderer; globals.Bruno = { ...globals.Bruno, - apiDocs: (target: HTMLElement, { content, ...options }: ApiDocsOptions) => - new OpenCollectionRenderer({ target, opencollection: content, ...options }) + apiDocs: (target: HTMLElement, { content, repositoryUrl, gitCollectionUrl, ...options }: ApiDocsOptions) => + new OpenCollectionRenderer({ + target, + opencollection: content, + gitCollectionUrl: repositoryUrl ?? gitCollectionUrl, + ...options + }) }; }