diff --git a/apps/hello-nest/package-lock.json b/apps/hello-nest/package-lock.json new file mode 100644 index 0000000..f4cb032 --- /dev/null +++ b/apps/hello-nest/package-lock.json @@ -0,0 +1,1132 @@ +{ + "name": "hello-nest", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hello-nest", + "dependencies": { + "@nestjs/common": "file:../../vendor/nestjs-common-12.1.3.tgz", + "@nestjs/core": "file:../../vendor/nestjs-core-12.1.3.tgz", + "@nestjs/platform-node": "file:../../vendor/nestjs-platform-node-12.1.3.tgz", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2" + }, + "devDependencies": { + "@types/node": "24.19.2", + "typescript": "5.9.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "12.1.3", + "resolved": "file:../../vendor/nestjs-common-12.1.3.tgz", + "integrity": "sha512-8w4tI5o1iJkdxRXSGpS+5+2eKyxm1uQIWMZ4ciWIxIeK2gxUg0iMy3HR4CnL+7rys7tidGs64TkfaLALSeRK1A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0", + "file-type": "22.1.1", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "12.1.3", + "resolved": "file:../../vendor/nestjs-core-12.1.3.tgz", + "integrity": "sha512-aYwIezZwFVTZLc71dX5ABRI85+9r2y8ojUM5z0XMS54KtPDNro6e2YdypE56FnojyqKeIBzQWzFlW/nTs9EVIA==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/microservices": "^12.0.0", + "@nestjs/platform-express": "^12.0.0", + "@nestjs/websockets": "^12.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-node": { + "version": "12.1.3", + "resolved": "file:../../vendor/nestjs-platform-node-12.1.3.tgz", + "integrity": "sha512-pQA9kVun/H/pWiZr+wq9OUePrZBdCAVUVLNJTFELpTwK3E7g/RAX40BWAaXkjRlDivdXAFL2/8hhhJqO8njrEA==", + "license": "MIT", + "dependencies": { + "content-type": "2.1.0", + "cookie": "0.7.2", + "cookie-signature": "1.2.2", + "cors": "2.8.6", + "encodeurl": "2.0.0", + "fast-querystring": "1.1.2", + "find-my-way": "9.9.0", + "mime-types": "3.0.2", + "multer": "2.4.0", + "on-finished": "2.4.1", + "path-to-regexp": "8.4.2", + "qs": "6.15.3", + "secure-json-parse": "4.1.0", + "send": "1.2.1", + "serve-static": "2.2.1", + "tslib": "2.8.1", + "type-is": "2.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^12.0.0", + "@nestjs/core": "^12.0.0" + } + }, + "node_modules/@nestjs/platform-node/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@nestjs/platform-node/node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "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/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.1.1.tgz", + "integrity": "sha512-sEB0oStmeLGtpmSAD88RDNuLsvhv4iC3u0lE25CUvfCULvj49xoQ3l3K525Vn+ZzsZ6DOLajQCV6RmhwwhqiJg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/find-my-way": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz", + "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "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": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/multer": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/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/multer/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/multer/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/multer/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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz", + "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strtok3": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.4.1.tgz", + "integrity": "sha512-MeSp4vHXiZRXM9K0rSWqTvXMU+YzmGICc5+briMVK+RY8qQ1GRTffFYVpK6cSgV/ueImFIEh3uCJJD3xBh1TaQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "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/apps/hello-nest/package.json b/apps/hello-nest/package.json new file mode 100644 index 0000000..c0b157a --- /dev/null +++ b/apps/hello-nest/package.json @@ -0,0 +1,20 @@ +{ + "name": "hello-nest", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js" + }, + "dependencies": { + "@nestjs/common": "file:../../vendor/nestjs-common-12.1.3.tgz", + "@nestjs/core": "file:../../vendor/nestjs-core-12.1.3.tgz", + "@nestjs/platform-node": "file:../../vendor/nestjs-platform-node-12.1.3.tgz", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2" + }, + "devDependencies": { + "@types/node": "24.19.2", + "typescript": "5.9.3" + } +} diff --git a/apps/hello-nest/server.ts b/apps/hello-nest/server.ts new file mode 100644 index 0000000..65b5048 --- /dev/null +++ b/apps/hello-nest/server.ts @@ -0,0 +1,23 @@ +import { Controller, Get, Module } from '@nestjs/common' +import { NestFactory } from '@nestjs/core' +import { NodeAdapter } from '@nestjs/platform-node' + +@Controller() +class AppController { + @Get() + hello(): string { + return 'Hello Nest!' + } + + @Get('json') + json(): { hello: string } { + return { hello: 'world' } + } +} + +@Module({ controllers: [AppController] }) +class AppModule {} + +const app = await NestFactory.create(AppModule, new NodeAdapter(), { logger: false }) +await app.listen(3900) +console.log('nest-native listening on http://127.0.0.1:3900') diff --git a/apps/hello-nest/tsconfig.json b/apps/hello-nest/tsconfig.json new file mode 100644 index 0000000..cfbc0dc --- /dev/null +++ b/apps/hello-nest/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "ES2023", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "skipLibCheck": true, + "outDir": "./dist" + }, + "files": ["server.ts"] +} diff --git a/runtime/node/nest/shims/@nestjs/common/index.ts b/runtime/node/nest/shims/@nestjs/common/index.ts new file mode 100644 index 0000000..459ad0c --- /dev/null +++ b/runtime/node/nest/shims/@nestjs/common/index.ts @@ -0,0 +1,373 @@ + + +export type ClassDecoratorLike = (target: unknown) => void +export type MethodDecoratorLike = (target: unknown, propertyKey: string, descriptor: unknown) => void +export type ParameterDecoratorLike = (target: unknown, propertyKey: string | undefined, parameterIndex: number) => void + +export interface ControllerOptions { + path?: string | string[] + host?: string | string[] +} + +export interface ModuleMetadata { + imports?: unknown[] + controllers?: unknown[] + providers?: unknown[] + exports?: unknown[] +} + +export interface InjectableOptions { + scope?: number +} + +const classDecorator: ClassDecoratorLike = () => {} +const methodDecorator: MethodDecoratorLike = () => {} +const parameterDecorator: ParameterDecoratorLike = () => {} + +export function Controller(prefixOrOptions?: string | string[] | ControllerOptions): ClassDecoratorLike { + return classDecorator +} +export function Module(metadata: ModuleMetadata): ClassDecoratorLike { + return classDecorator +} +export function Injectable(options?: InjectableOptions): ClassDecoratorLike { + return classDecorator +} + +export function Get(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Post(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Put(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Delete(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Patch(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Options(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Head(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function All(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Search(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Propfind(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Proppatch(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Mkcol(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Copy(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Move(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Lock(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function Unlock(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function QueryMethod(path?: string | string[]): MethodDecoratorLike { + return methodDecorator +} +export function HttpCode(statusCode: number): MethodDecoratorLike { + return methodDecorator +} +export function Header(name: string, value: string): MethodDecoratorLike { + return methodDecorator +} + +export function Param(property?: string): ParameterDecoratorLike { + return parameterDecorator +} +export function Query(property?: string): ParameterDecoratorLike { + return parameterDecorator +} +export function Body(property?: string): ParameterDecoratorLike { + return parameterDecorator +} +export function Headers(property?: string): ParameterDecoratorLike { + return parameterDecorator +} +export function Req(): ParameterDecoratorLike { + return parameterDecorator +} +export function Request(): ParameterDecoratorLike { + return parameterDecorator +} +export function Res(): ParameterDecoratorLike { + return parameterDecorator +} +export function Response(): ParameterDecoratorLike { + return parameterDecorator +} + +export enum HttpStatus { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLYHINTS = 103, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + CONTENT_DIFFERENT = 210, + AMBIGUOUS = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + REQUESTED_RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + I_AM_A_TEAPOT = 418, + MISDIRECTED = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + UNRECOVERABLE_ERROR = 456, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508 +} + +export interface HttpExceptionOptions { + cause?: unknown + description?: string + errorCode?: string +} + +export class HttpExceptionBody { + message: string + error: string | undefined + statusCode: number + errorCode: string | undefined + + constructor(message: string, error: string | undefined, statusCode: number, errorCode: string | undefined) { + this.message = message + this.error = error + this.statusCode = statusCode + this.errorCode = errorCode + } + + toJson(): string { + let json = '{"message":' + JSON.stringify(this.message) + if (this.error !== undefined) json += ',"error":' + JSON.stringify(this.error) + json += ',"statusCode":' + String(this.statusCode) + if (this.errorCode !== undefined) json += ',"errorCode":' + JSON.stringify(this.errorCode) + return json + '}' + } +} + +export type HttpExceptionResponse = string | HttpExceptionBody + +export class IntrinsicException extends Error {} + +export class HttpException extends IntrinsicException { + private readonly response: HttpExceptionResponse + private readonly status: number + errorCode: string | undefined + + constructor(response: HttpExceptionResponse, status: number, options?: HttpExceptionOptions) { + super(typeof response === 'string' ? response : response.message, options?.cause === undefined ? undefined : { cause: options.cause }) + this.response = response + this.status = status + this.errorCode = options?.errorCode + this.name = 'HttpException' + } + + getResponse(): HttpExceptionResponse { + return this.response + } + + getStatus(): number { + return this.status + } + + toJson(): string { + const response = this.response + if (typeof response === 'string') { + let json = '{"statusCode":' + String(this.status) + ',"message":' + JSON.stringify(response) + if (this.errorCode !== undefined) json += ',"errorCode":' + JSON.stringify(this.errorCode) + return json + '}' + } + return response.toJson() + } + + static createBody(objectOrError: HttpExceptionResponse | undefined, description: string, statusCode: number, errorCode: string | undefined): HttpExceptionBody { + if (objectOrError === undefined || objectOrError === '') return new HttpExceptionBody(description, undefined, statusCode, errorCode) + if (typeof objectOrError === 'string') return new HttpExceptionBody(objectOrError, description, statusCode, errorCode) + return objectOrError + } + + static getDescriptionFrom(descriptionOrOptions: string | HttpExceptionOptions, fallback: string): string { + if (typeof descriptionOrOptions === 'string') return descriptionOrOptions + return descriptionOrOptions.description ?? fallback + } + + static getHttpExceptionOptionsFrom(descriptionOrOptions: string | HttpExceptionOptions): HttpExceptionOptions { + return typeof descriptionOrOptions === 'string' ? {} : descriptionOrOptions + } +} + +function defaultException(objectOrError: HttpExceptionResponse | undefined, descriptionOrOptions: string | HttpExceptionOptions, fallback: string, status: number): [HttpExceptionBody, HttpExceptionOptions] { + const description = HttpException.getDescriptionFrom(descriptionOrOptions, fallback) + const options = HttpException.getHttpExceptionOptionsFrom(descriptionOrOptions) + return [HttpException.createBody(objectOrError, description, status, options.errorCode), options] +} + +export class BadRequestException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Bad Request') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Bad Request', HttpStatus.BAD_REQUEST) + super(body, HttpStatus.BAD_REQUEST, options) + this.name = 'BadRequestException' + } +} + +export class UnauthorizedException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Unauthorized') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Unauthorized', HttpStatus.UNAUTHORIZED) + super(body, HttpStatus.UNAUTHORIZED, options) + this.name = 'UnauthorizedException' + } +} + +export class ForbiddenException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Forbidden') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Forbidden', HttpStatus.FORBIDDEN) + super(body, HttpStatus.FORBIDDEN, options) + this.name = 'ForbiddenException' + } +} + +export class NotFoundException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Not Found') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Not Found', HttpStatus.NOT_FOUND) + super(body, HttpStatus.NOT_FOUND, options) + this.name = 'NotFoundException' + } +} + +export class MethodNotAllowedException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Method Not Allowed') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Method Not Allowed', HttpStatus.METHOD_NOT_ALLOWED) + super(body, HttpStatus.METHOD_NOT_ALLOWED, options) + this.name = 'MethodNotAllowedException' + } +} + +export class ConflictException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Conflict') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Conflict', HttpStatus.CONFLICT) + super(body, HttpStatus.CONFLICT, options) + this.name = 'ConflictException' + } +} + +export class PayloadTooLargeException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Payload Too Large') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Payload Too Large', HttpStatus.PAYLOAD_TOO_LARGE) + super(body, HttpStatus.PAYLOAD_TOO_LARGE, options) + this.name = 'PayloadTooLargeException' + } +} + +export class UnsupportedMediaTypeException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Unsupported Media Type') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Unsupported Media Type', HttpStatus.UNSUPPORTED_MEDIA_TYPE) + super(body, HttpStatus.UNSUPPORTED_MEDIA_TYPE, options) + this.name = 'UnsupportedMediaTypeException' + } +} + +export class UnprocessableEntityException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Unprocessable Entity') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Unprocessable Entity', HttpStatus.UNPROCESSABLE_ENTITY) + super(body, HttpStatus.UNPROCESSABLE_ENTITY, options) + this.name = 'UnprocessableEntityException' + } +} + +export class InternalServerErrorException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Internal Server Error') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR) + super(body, HttpStatus.INTERNAL_SERVER_ERROR, options) + this.name = 'InternalServerErrorException' + } +} + +export class NotImplementedException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Not Implemented') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Not Implemented', HttpStatus.NOT_IMPLEMENTED) + super(body, HttpStatus.NOT_IMPLEMENTED, options) + this.name = 'NotImplementedException' + } +} + +export class BadGatewayException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Bad Gateway') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Bad Gateway', HttpStatus.BAD_GATEWAY) + super(body, HttpStatus.BAD_GATEWAY, options) + this.name = 'BadGatewayException' + } +} + +export class ServiceUnavailableException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Service Unavailable') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Service Unavailable', HttpStatus.SERVICE_UNAVAILABLE) + super(body, HttpStatus.SERVICE_UNAVAILABLE, options) + this.name = 'ServiceUnavailableException' + } +} + +export class GatewayTimeoutException extends HttpException { + constructor(objectOrError?: HttpExceptionResponse, descriptionOrOptions: string | HttpExceptionOptions = 'Gateway Timeout') { + const [body, options] = defaultException(objectOrError, descriptionOrOptions, 'Gateway Timeout', HttpStatus.GATEWAY_TIMEOUT) + super(body, HttpStatus.GATEWAY_TIMEOUT, options) + this.name = 'GatewayTimeoutException' + } +} diff --git a/runtime/node/nest/shims/@nestjs/core/index.ts b/runtime/node/nest/shims/@nestjs/core/index.ts new file mode 100644 index 0000000..412e7c2 --- /dev/null +++ b/runtime/node/nest/shims/@nestjs/core/index.ts @@ -0,0 +1,135 @@ +import { BadRequestException, HttpException, NotFoundException } from '../common/index' +import { NodeAdapter, type NodeRequest, type NodeResponse, UriTooLongError } from '../platform-node/index' +import { BodyParserError } from '../../../platform-node/body-parser/body-parser' +import type { ParsedQuery } from '../../fast-querystring/index' +import { HttpError } from '../../http-errors/index' +import type { Server } from 'node:http' + +export interface NestApplicationOptions { + logger?: boolean + bodyParser?: boolean +} + +export type NestModuleDefinition = (app: NestApplication) => void + +export type NestRouteHandler = (req: NodeRequest, res: NodeResponse) => void + +const UNKNOWN_EXCEPTION_BODY = '{"statusCode":500,"message":"Internal server error"}' + +export class NestApplication { + private readonly adapter: NodeAdapter + private readonly logging: boolean + private readonly bodyParser: boolean + + constructor(adapter: NodeAdapter, options: NestApplicationOptions) { + this.adapter = adapter + this.logging = options.logger !== false + this.bodyParser = options.bodyParser !== false + } + + init(definition: NestModuleDefinition): void { + this.adapter.init( + (req: NodeRequest, res: NodeResponse) => this.fail(res, new NotFoundException(`Cannot ${req.method} ${req.url}`)), + (error: unknown, req: NodeRequest, res: NodeResponse) => this.fail(res, error), + this.bodyParser + ) + definition(this) + } + + route(method: string, path: string, handler: NestRouteHandler): void { + this.adapter.route(method, path, handler) + } + + header(res: NodeResponse, name: string, value: string): void { + res.setHeader(name, value) + } + + text(res: NodeResponse, body: string, statusCode: number): void { + this.adapter.replyText(res, body, statusCode) + } + + json(res: NodeResponse, json: string, statusCode: number): void { + this.adapter.replyJson(res, json, statusCode) + } + + bytes(res: NodeResponse, body: Uint8Array, statusCode: number): void { + this.adapter.replyBytes(res, body, statusCode) + } + + empty(res: NodeResponse, statusCode: number): void { + this.adapter.replyEmpty(res, statusCode) + } + + param(req: NodeRequest, key: string): string { + const value = req.params[key] + if (typeof value === 'string') return value + throw new Error(`route parameter ${key} is a wildcard`) + } + + query(req: NodeRequest, key: string): string | string[] | undefined { + const query: ParsedQuery = req.query + return key in query ? query[key] : undefined + } + + fail(res: NodeResponse, raw: unknown): void { + const error = mapException(raw) + let statusCode: number + let body: string + if (error instanceof HttpException) { + statusCode = error.getStatus() + body = error.toJson() + } else if (error instanceof BodyParserError) { + statusCode = error.statusCode + body = httpErrorBody(error.statusCode, error.message) + } else if (error instanceof HttpError) { + statusCode = error.statusCode + body = httpErrorBody(error.statusCode, error.message) + } else if (error instanceof UriTooLongError) { + statusCode = error.statusCode + body = httpErrorBody(error.statusCode, error.message) + } else { + statusCode = 500 + body = UNKNOWN_EXCEPTION_BODY + if (this.logging) console.error(error) + } + if (res.headersSent) res.end() + else this.adapter.replyJson(res, body, statusCode) + } + + listen(port: number, hostname?: string): Promise { + return new Promise((resolve) => { + this.adapter.listen(port, hostname, () => resolve()) + }) + } + + getHttpServer(): Server | undefined { + return this.adapter.getHttpServer() + } + + close(): Promise { + return new Promise((resolve) => { + this.adapter.close(() => resolve()) + }) + } +} + +function httpErrorBody(statusCode: number, message: string): string { + return '{"statusCode":' + String(statusCode) + ',"message":' + JSON.stringify(message) + '}' +} + +function mapException(error: unknown): unknown { + if (error instanceof HttpException) return error + if (error instanceof SyntaxError || error instanceof URIError) return new BadRequestException(error.message) + if (error instanceof BodyParserError && error.name === 'SyntaxError') return new BadRequestException(error.message) + return error +} + +export class NestFactoryStatic { + async create(definition: NestModuleDefinition, adapter: NodeAdapter, options?: NestApplicationOptions): Promise { + const app = new NestApplication(adapter, options === undefined ? {} : options) + app.init(definition) + return app + } +} + +export const NestFactory = new NestFactoryStatic() diff --git a/runtime/node/nest/shims/@nestjs/platform-node/index.ts b/runtime/node/nest/shims/@nestjs/platform-node/index.ts new file mode 100644 index 0000000..dedf880 --- /dev/null +++ b/runtime/node/nest/shims/@nestjs/platform-node/index.ts @@ -0,0 +1,135 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { endWithoutBody, NodeRequest, NodeResponse } from '../../../platform-node/adapters/node-request' +import { json, urlencoded } from '../../../platform-node/body-parser/body-parser' +import { PARAM_TOO_LONG, type RouteMatch, RouteTable } from '../../../platform-node/router/route-table' +import { getRoutingPath } from '../../../platform-node/router/utils' + +export { NodeRequest, NodeResponse } + +export interface NodeAdapterOptions { + maxParamLength?: number +} + +export type NodeRouteHandler = (req: NodeRequest, res: NodeResponse) => void +export type NodeErrorHandler = (error: unknown, req: NodeRequest, res: NodeResponse) => void + +type BodyParser = (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void + +export class UriTooLongError extends Error { + status = 414 + statusCode = 414 + expose = true + constructor() { + super('Route parameter too long') + this.name = 'Error' + } +} + +function rejectUnset(req: NodeRequest, res: NodeResponse): void { + res.statusCode = 404 + res.end() +} +function failUnset(error: unknown, req: NodeRequest, res: NodeResponse): void { + res.statusCode = 500 + res.end() +} + +export class NodeAdapter { + private readonly routes: RouteTable + private readonly parseJson: BodyParser + private readonly parseUrlencoded: BodyParser + private parsers = true + private notFound: NodeRouteHandler = rejectUnset + private onError: NodeErrorHandler = failUnset + private httpServer: Server | undefined = undefined + + constructor(options?: NodeAdapterOptions) { + this.routes = new RouteTable(options === undefined ? {} : { maxParamLength: options.maxParamLength }) + this.parseJson = json() + this.parseUrlencoded = urlencoded({ extended: true }) + } + + init(notFound: NodeRouteHandler, onError: NodeErrorHandler, bodyParser: boolean): void { + this.notFound = notFound + this.onError = onError + this.parsers = bodyParser + } + + route(method: string, path: string, handler: NodeRouteHandler): void { + this.routes.add(method === 'ALL' ? null : method, path, handler) + } + + handle(req: NodeRequest, res: NodeResponse): void { + if (!this.parsers) return this.dispatch(req, res) + this.parseJson(req, res, (err?: unknown) => { + if (err !== undefined) return this.onError(err, req, res) + this.parseUrlencoded(req, res, (err2?: unknown) => { + if (err2 !== undefined) return this.onError(err2, req, res) + this.dispatch(req, res) + }) + }) + } + + private dispatch(req: NodeRequest, res: NodeResponse): void { + let matched: RouteMatch | null + try { + matched = this.routes.lookup(req.method ?? 'GET', getRoutingPath(req.url ?? '/')) + } catch (err) { + return this.onError(err, req, res) + } + if (matched === PARAM_TOO_LONG) return this.onError(new UriTooLongError(), req, res) + if (matched === null) return this.notFound(req, res) + req.params = matched.params + const handler = matched.handlers[0] + try { + handler(req, res, (err?: unknown) => (err === undefined ? this.notFound(req, res) : this.onError(err, req, res))) + } catch (err) { + this.onError(err, req, res) + } + } + + private writeHead(res: NodeResponse, contentLength: number, defaultContentType: string, statusCode: number): boolean { + res.statusCode = statusCode + if (endWithoutBody(res)) return false + if (res.hasHeader('Content-Type')) res.writeHead(statusCode, { 'Content-Length': contentLength }) + else res.writeHead(statusCode, { 'Content-Type': defaultContentType, 'Content-Length': contentLength }) + return true + } + + replyText(res: NodeResponse, text: string, statusCode: number): void { + if (this.writeHead(res, Buffer.byteLength(text), 'text/plain; charset=utf-8', statusCode)) res.end(text) + } + + replyJson(res: NodeResponse, json: string, statusCode: number): void { + if (this.writeHead(res, Buffer.byteLength(json), 'application/json; charset=utf-8', statusCode)) res.end(json) + } + + replyBytes(res: NodeResponse, bytes: Uint8Array, statusCode: number): void { + if (this.writeHead(res, bytes.byteLength, 'application/octet-stream', statusCode)) res.end(bytes) + } + + replyEmpty(res: NodeResponse, statusCode: number): void { + res.statusCode = statusCode + if (!endWithoutBody(res)) res.end() + } + + listen(port: number, hostname: string | undefined, callback: () => void): Server { + const server = createServer({ IncomingMessage: NodeRequest, ServerResponse: NodeResponse }, (incoming: IncomingMessage, outgoing: ServerResponse) => { + this.handle(incoming as NodeRequest, outgoing as NodeResponse) + }) + this.httpServer = server + if (hostname === undefined) server.listen(port, callback) + else server.listen(port, hostname, callback) + return server + } + + getHttpServer(): Server | undefined { + return this.httpServer + } + + close(callback: () => void): void { + const server = this.httpServer + if (server === undefined) return callback() + server.close(() => callback()) + } +} diff --git a/runtime/node/nest/shims/reflect-metadata/index.ts b/runtime/node/nest/shims/reflect-metadata/index.ts new file mode 100644 index 0000000..336ce12 --- /dev/null +++ b/runtime/node/nest/shims/reflect-metadata/index.ts @@ -0,0 +1 @@ +export {} diff --git a/scripts/build.mjs b/scripts/build.mjs index 395b5d4..0fad9b2 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -57,6 +57,7 @@ import { compilerInputs, assertCompilerInputs } from './compiler-input.mjs' const compilerRoot = resolveCompilerRoot() const { compile } = await import(compilerModuleUrl('.', compilerRoot)) const { preparePackageSources } = await import(compilerModuleUrl('./preparation', compilerRoot)) +const { noPluginCapabilities } = await import(compilerModuleUrl('./plugin', compilerRoot)) // The compile runs in this process and allocates hard: on a database driver // the default young generation made V8 GC 28% of the emission (24.8k @@ -597,6 +598,36 @@ const typesOnlyPackages = (() => { })() const { projectFile, unconditionalAmbientRoots } = writeProject() +const nestAotPlugin = await (async () => { + const imported = ts.preProcessFile(fs.readFileSync(entry, 'utf8'), true, true).importedFiles.map((file) => file.fileName) + if (!imported.includes('@nestjs/core')) return null + const { generateNestApplication } = await import('../tools/nest/aot/generate.mjs') + const { NestAnalysisError } = await import('../tools/nest/aot/analyze.mjs') + let rewrites + try { + ;({ rewrites } = generateNestApplication({ ts, entry })) + } catch (error) { + if (!(error instanceof NestAnalysisError)) throw error + console.error(`[build] nest-aot: ${error.diagnostics.length} construct(s) the AOT resolver does not read:`) + for (const diagnostic of error.diagnostics) console.error(` ${displayPath(diagnostic.file)}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.message}`) + writeReport({ threw: `nest-aot: ${error.message}` }) + process.exit(1) + } + if (process.env.GEA_NEST_AOT_DIR) { + fs.mkdirSync(process.env.GEA_NEST_AOT_DIR, { recursive: true }) + for (const [fileName, text] of rewrites) fs.writeFileSync(path.join(process.env.GEA_NEST_AOT_DIR, path.basename(fileName)), text) + } + console.error(`[build] nest-aot: ${rewrites.size} file(s) rewritten (${[...rewrites.keys()].map(displayPath).join(', ')})`) + return { + name: 'nest-aot', + instantiate: () => ({ + producers: () => [], + lower: () => false, + transformSource: (input) => rewrites.get(input.fileName) ?? null, + capabilities: { ...noPluginCapabilities } + }) + } +})() writeReport({ packageSources }) console.error(`[build] compiling ${displayPath(entry)} -> ${displayPath(outDir)}`) @@ -646,7 +677,7 @@ const compileSources = (sources, javaScriptSources) => dynamicFallback, javaScriptSources, projectFileName: projectFile, - plugins: [geatscNodePlugin()], + plugins: nestAotPlugin ? [geatscNodePlugin(), nestAotPlugin] : [geatscNodePlugin()], // `entry` plus its resolved dependency graph (npm packages included via // `packageSources`) IS the whole application -- the same // `compile-module-graph` shape `cli-emit.ts`'s `runModuleGraph` states for diff --git a/scripts/test-hello-nest.mjs b/scripts/test-hello-nest.mjs new file mode 100644 index 0000000..a307557 --- /dev/null +++ b/scripts/test-hello-nest.mjs @@ -0,0 +1,114 @@ +import assert from 'node:assert/strict' +import { execFileSync, spawn } from 'node:child_process' +import { once } from 'node:events' +import fs from 'node:fs' +import http from 'node:http' +import net from 'node:net' +import { resolve } from 'node:path' +import { setTimeout as delay } from 'node:timers/promises' + +const root = resolve(import.meta.dirname, '..') +const app = resolve(root, 'apps/hello-nest') +const entry = resolve(app, 'server.ts') +const oracle = resolve(app, 'dist/server.js') +const output = resolve(app, 'dist-gea') +const executable = resolve(output, process.platform === 'win32' ? 'server.exe' : 'server') +const port = 3900 +const recordDir = process.argv.includes('--record') ? process.argv[process.argv.indexOf('--record') + 1] : null + +if (!process.argv.includes('--no-build')) { + execFileSync('npm', ['run', 'build'], { cwd: app, stdio: 'inherit' }) + execFileSync(process.execPath, [resolve(root, 'scripts/build.mjs'), entry, '--out', output, '--exe', executable], { cwd: root, stdio: 'inherit' }) +} + +const requests = [ + { name: 'get-root', method: 'GET', path: '/' }, + { name: 'get-json', method: 'GET', path: '/json' }, + { name: 'get-json-slash', method: 'GET', path: '/json/' }, + { name: 'get-json-upper', method: 'GET', path: '/JSON' }, + { name: 'get-missing', method: 'GET', path: '/missing?x=1' }, + { name: 'head-json', method: 'HEAD', path: '/json' }, + { name: 'post-root', method: 'POST', path: '/' }, + { name: 'post-json-body', method: 'POST', path: '/json', headers: { 'content-type': 'application/json' }, body: '{"a":1}' }, + { name: 'post-bad-json', method: 'POST', path: '/json', headers: { 'content-type': 'application/json' }, body: '{bad' }, + { name: 'post-form', method: 'POST', path: '/missing', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: 'a=1&b[c]=2' }, + { name: 'get-bad-escape', method: 'GET', path: '/%FF' } +] + +const assertFreePort = async () => { + const probe = net.createServer() + await new Promise((accept, reject) => { + probe.once('error', reject) + probe.listen(port, '127.0.0.1', accept) + }) + await new Promise((accept) => probe.close(accept)) +} + +const request = ({ method, path, headers, body }) => + new Promise((accept, reject) => { + const req = http.request({ hostname: '127.0.0.1', port, path, method, headers: headers ?? {} }, (res) => { + const chunks = [] + res.on('data', (chunk) => chunks.push(chunk)) + res.on('end', () => { + const rawHeaders = [] + for (let i = 0; i < res.rawHeaders.length; i += 2) if (res.rawHeaders[i].toLowerCase() !== 'date') rawHeaders.push(`${res.rawHeaders[i]}: ${res.rawHeaders[i + 1]}`) + accept({ status: `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`, headers: rawHeaders, body: Buffer.concat(chunks).toString('utf8') }) + }) + res.on('error', reject) + }) + req.on('error', reject) + req.setTimeout(5000, () => req.destroy(new Error('HTTP response timed out'))) + if (body !== undefined) req.write(body) + req.end() + }) + +const run = async (label, command, args) => { + await assertFreePort() + const server = spawn(command, args, { cwd: app, stdio: ['ignore', 'pipe', 'pipe'] }) + const stopped = once(server, 'exit') + let log = '' + server.stdout.on('data', (chunk) => (log += chunk)) + server.stderr.on('data', (chunk) => (log += chunk)) + try { + let ready = false + for (let attempt = 0; attempt < 200; attempt++) { + if (server.exitCode !== null || server.signalCode !== null) throw new Error(`${label} exited: ${log}`) + try { + await request({ method: 'GET', path: '/' }) + ready = true + break + } catch { + await delay(50) + } + } + assert.ok(ready, `${label} did not start: ${log}`) + const responses = {} + for (const r of requests) responses[r.name] = await request(r) + return { responses, log } + } finally { + server.kill('SIGTERM') + await Promise.race([stopped, delay(3000).then(() => server.kill('SIGKILL'))]) + } +} + +const render = (r) => `${r.status}\n${r.headers.join('\n')}\n\n${r.body}` +const expected = await run('node oracle', process.execPath, [oracle]) +const actual = await run('compiled', executable, []) +if (recordDir) { + for (const [side, result] of [['node', expected], ['compiled', actual]]) { + fs.mkdirSync(resolve(recordDir, side), { recursive: true }) + for (const [name, response] of Object.entries(result.responses)) fs.writeFileSync(resolve(recordDir, side, `${name}.txt`), render(response)) + } +} +let failed = 0 +for (const r of requests) { + const e = render(expected.responses[r.name]) + const a = render(actual.responses[r.name]) + if (e === a) console.log(`PASS ${r.method} ${r.path} (${r.name}): ${expected.responses[r.name].status}, identical headers (minus Date) and body`) + else { + failed++ + console.log(`FAIL ${r.method} ${r.path} (${r.name})\n--- node\n${e}\n--- compiled\n${a}`) + } +} +console.log(`HELLO_NEST_PARITY: ${requests.length - failed} of ${requests.length} identical`) +if (failed > 0) process.exit(1) diff --git a/tools/nest/aot/analyze.mjs b/tools/nest/aot/analyze.mjs new file mode 100644 index 0000000..6f115ca --- /dev/null +++ b/tools/nest/aot/analyze.mjs @@ -0,0 +1,410 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +export const REQUEST_MAPPINGS = new Map([ + ['Get', 'GET'], + ['Post', 'POST'], + ['Put', 'PUT'], + ['Delete', 'DELETE'], + ['Patch', 'PATCH'], + ['Options', 'OPTIONS'], + ['Head', 'HEAD'], + ['All', 'ALL'], + ['Search', 'SEARCH'], + ['Propfind', 'PROPFIND'], + ['Proppatch', 'PROPPATCH'], + ['Mkcol', 'MKCOL'], + ['Copy', 'COPY'], + ['Move', 'MOVE'], + ['Lock', 'LOCK'], + ['Unlock', 'UNLOCK'], + ['QueryMethod', 'QUERY'] +]) + +const PARAM_SOURCES = new Map([ + ['Param', 'param'], + ['Query', 'query'], + ['Body', 'body'], + ['Headers', 'headers'], + ['Req', 'request'], + ['Request', 'request'], + ['Res', 'response'], + ['Response', 'response'] +]) + +export class NestAnalysisError extends Error { + constructor(diagnostics) { + super(diagnostics.map((d) => `${d.file}:${d.line}:${d.column}: ${d.message}`).join('\n')) + this.name = 'NestAnalysisError' + this.diagnostics = diagnostics + } +} + +export const sourceProjectOptions = (ts, entry) => { + const configFile = ts.findConfigFile(path.dirname(entry), ts.sys.fileExists) + if (!configFile) return { options: {}, configFile: null } + const config = ts.readConfigFile(configFile, ts.sys.readFile) + if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n')) + const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configFile)) + return { options: { ...parsed.options, noEmit: true }, configFile } +} + +export const analyzeNestApplication = ({ ts, entry, options }) => { + const program = ts.createProgram({ rootNames: [entry], options: options ?? sourceProjectOptions(ts, entry).options }) + const checker = program.getTypeChecker() + const diagnostics = [] + const location = (node) => { + const file = node.getSourceFile() + const { line, character } = file.getLineAndCharacterOfPosition(node.getStart()) + return { file: file.fileName, line: line + 1, column: character + 1 } + } + const refuse = (node, message) => diagnostics.push({ ...location(node), message }) + + const declaringPackage = (symbol) => { + const resolved = symbol && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol + const declaration = resolved?.declarations?.[0] + if (!declaration) return null + const fileName = declaration.getSourceFile().fileName + const match = fileName.match(/node_modules\/(@[^/]+\/[^/]+|[^@][^/]*)\//) + return { name: match ? match[1] : null, resolved, fileName } + } + const classOfType = (type) => { + const symbol = type.getSymbol() + const declaration = symbol?.declarations?.find((d) => ts.isClassDeclaration(d)) + return declaration ?? null + } + const classOfExpression = (node) => { + if (!ts.isIdentifier(node)) return null + const symbol = checker.getSymbolAtLocation(node) + const resolved = symbol && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol + return resolved?.declarations?.find((d) => ts.isClassDeclaration(d)) ?? null + } + + const nestDecorators = (node) => { + const found = [] + for (const decorator of ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []) { + const expression = decorator.expression + const callee = ts.isCallExpression(expression) ? expression.expression : expression + const origin = declaringPackage(checker.getSymbolAtLocation(callee)) + if (origin?.name !== '@nestjs/common') { + refuse(decorator, `decorator ${callee.getText()} is not from @nestjs/common; phase 1 reads only Nest's own decorators`) + continue + } + found.push({ name: origin.resolved.name, args: ts.isCallExpression(expression) ? [...expression.arguments] : [], node: decorator }) + } + return found + } + + const stringLiteral = (node) => (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null) + const stringOrStrings = (node, what) => { + const single = stringLiteral(node) + if (single !== null) return [single] + if (ts.isArrayLiteralExpression(node)) { + const items = node.elements.map(stringLiteral) + if (items.every((item) => item !== null)) return items + } + refuse(node, `${what} must be a string literal or an array of string literals; the AOT resolver reads it at build time`) + return null + } + + const classModels = new Map() + const className = (declaration) => declaration.name?.text ?? '' + const constructorDependencies = (declaration) => { + const constructor = declaration.members.find((m) => ts.isConstructorDeclaration(m) && m.body) + if (!constructor) return [] + return constructor.parameters.map((parameter) => { + for (const decorator of nestDecorators(parameter)) { + refuse(decorator.node, `@${decorator.name} on a constructor parameter is not in phase 1; dependencies are resolved from the parameter's class type`) + } + const type = checker.getTypeAtLocation(parameter) + const target = classOfType(type) + if (!target) { + refuse(parameter, `constructor parameter ${parameter.name.getText()} of ${className(declaration)} is typed ${checker.typeToString(type)}, not a class the module provides`) + return null + } + return { name: parameter.name.getText(), target } + }) + } + + const controllerModel = (declaration, decorator) => { + let prefixes = ['/'] + const [argument] = decorator.args + if (argument !== undefined) { + if (ts.isObjectLiteralExpression(argument)) { + for (const property of argument.properties) { + const key = property.name?.getText() + if (key === 'path' && ts.isPropertyAssignment(property)) prefixes = stringOrStrings(property.initializer, '@Controller path') ?? prefixes + else refuse(property, `@Controller option ${key ?? property.getText()} is not in phase 1 (only path)`) + } + } else prefixes = stringOrStrings(argument, '@Controller prefix') ?? prefixes + } + const routes = [] + for (const member of declaration.members) { + if (!ts.isMethodDeclaration(member)) { + for (const d of nestDecorators(member)) refuse(d.node, `@${d.name} on a ${ts.SyntaxKind[member.kind]} is not in phase 1`) + continue + } + const decorators = nestDecorators(member) + const mappings = decorators.filter((d) => REQUEST_MAPPINGS.has(d.name)) + if (mappings.length === 0) { + for (const d of decorators) refuse(d.node, `@${d.name} on ${className(declaration)}.${member.name.getText()} has no request-mapping decorator`) + continue + } + if (mappings.length > 1) refuse(mappings[1].node, `${className(declaration)}.${member.name.getText()} has more than one request-mapping decorator; Nest keeps one`) + const mapping = mappings[0] + const paths = mapping.args[0] === undefined ? ['/'] : (stringOrStrings(mapping.args[0], `@${mapping.name} path`) ?? ['/']) + let httpCode = null + const headers = [] + for (const d of decorators) { + if (REQUEST_MAPPINGS.has(d.name)) continue + if (d.name === 'HttpCode') { + if (d.args.length === 1 && ts.isNumericLiteral(d.args[0])) httpCode = Number(d.args[0].text) + else refuse(d.node, '@HttpCode takes a numeric literal in phase 1') + } else if (d.name === 'Header') { + const name = d.args[0] && stringLiteral(d.args[0]) + const value = d.args[1] && stringLiteral(d.args[1]) + if (name !== null && name !== undefined && value !== null && value !== undefined) headers.push({ name, value }) + else refuse(d.node, '@Header takes two string literals in phase 1') + } else refuse(d.node, `@${d.name} on a route handler is not in phase 1`) + } + const params = member.parameters.map((parameter, index) => { + const pds = nestDecorators(parameter) + if (pds.length !== 1) { + refuse(parameter, pds.length === 0 + ? `handler parameter ${parameter.name.getText()} has no Nest parameter decorator; Nest would pass undefined` + : `handler parameter ${parameter.name.getText()} has ${pds.length} decorators`) + return null + } + const [pd] = pds + const source = PARAM_SOURCES.get(pd.name) + if (source === undefined) { + refuse(pd.node, `@${pd.name} is not in phase 1 (Param, Query, Body, Headers, Req, Res)`) + return null + } + let key = null + if (pd.args.length > 0) { + const first = stringLiteral(pd.args[0]) + if (first === null || pd.args.length > 1 || source === 'request' || source === 'response' || (source === 'body' && first !== null)) { + refuse(pd.node, `@${pd.name}(${pd.args.map((a) => a.getText()).join(', ')}) is not in phase 1: ${source === 'body' ? 'only the whole body' : source === 'request' || source === 'response' ? 'no options' : 'a string literal key, no pipes'}`) + return null + } + key = first + } + const type = checker.getTypeAtLocation(parameter) + return { index, name: parameter.name.getText(), source, key, type: checker.typeToString(type), typeNode: parameter.type ?? null, parameter, location: location(parameter) } + }) + const returnType = checker.getTypeAtLocation(member) + const signature = checker.getSignaturesOfType(returnType, ts.SignatureKind.Call)[0] + const resultType = signature ? checker.getReturnTypeOfSignature(signature) : null + routes.push({ + method: member.name.getText(), + verb: REQUEST_MAPPINGS.get(mapping.name), + paths, + httpCode, + headers, + params, + async: resultType !== null && checker.typeToString(resultType).startsWith('Promise<'), + resultType: resultType ? checker.typeToString(resultType) : 'unknown', + location: location(member) + }) + } + return { kind: 'controller', name: className(declaration), declaration, prefixes, routes, dependencies: constructorDependencies(declaration), location: location(declaration) } + } + + const classReferences = (node, what) => { + if (!ts.isArrayLiteralExpression(node)) { + refuse(node, `@Module ${what} must be an array literal of class references`) + return [] + } + const targets = [] + for (const element of node.elements) { + const target = classOfExpression(element) + if (target) targets.push(target) + else refuse(element, `@Module ${what} entry ${element.getText()} is not a class reference (dynamic modules and custom providers are not in phase 1)`) + } + return targets + } + + const moduleModel = (declaration, decorator) => { + const model = { kind: 'module', name: className(declaration), declaration, imports: [], controllers: [], providers: [], exports: [], location: location(declaration) } + const [argument] = decorator.args + if (argument === undefined || !ts.isObjectLiteralExpression(argument)) { + refuse(decorator.node, '@Module takes an object literal') + return model + } + for (const property of argument.properties) { + const key = property.name?.getText() + if (!ts.isPropertyAssignment(property) || !['imports', 'controllers', 'providers', 'exports'].includes(key)) { + refuse(property, `@Module option ${key ?? property.getText()} is not in phase 1`) + continue + } + model[key] = classReferences(property.initializer, key) + } + if (declaration.members.some((m) => ts.isConstructorDeclaration(m))) refuse(declaration, `module ${model.name} has a constructor; module injection is not in phase 1`) + return model + } + + const modelOf = (declaration) => { + if (classModels.has(declaration)) return classModels.get(declaration) + const decorators = nestDecorators(declaration) + const kinds = decorators.filter((d) => ['Module', 'Controller', 'Injectable'].includes(d.name)) + let model + if (kinds.length !== 1) { + refuse(declaration, `class ${className(declaration)} is referenced by a module but carries ${kinds.length === 0 ? 'no' : 'more than one of'} @Module/@Controller/@Injectable`) + model = { kind: 'unknown', name: className(declaration), declaration, location: location(declaration) } + } else if (kinds[0].name === 'Module') model = moduleModel(declaration, kinds[0]) + else if (kinds[0].name === 'Controller') model = controllerModel(declaration, kinds[0]) + else model = { kind: 'provider', name: className(declaration), declaration, dependencies: constructorDependencies(declaration), location: location(declaration) } + for (const d of decorators) if (!kinds.includes(d)) refuse(d.node, `@${d.name} on class ${className(declaration)} is not in phase 1`) + classModels.set(declaration, model) + return model + } + + const entryFile = program.getSourceFile(entry) + if (!entryFile) throw new Error(`entry ${entry} is not in the program`) + const bootstraps = [] + const visit = (node) => { + if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'create') { + const origin = declaringPackage(checker.getSymbolAtLocation(node.expression.expression)) + if (origin?.name === '@nestjs/core' && origin.resolved.name === 'NestFactory') bootstraps.push(node) + } + ts.forEachChild(node, visit) + } + for (const file of program.getSourceFiles()) if (!file.fileName.includes('/node_modules/')) visit(file) + if (bootstraps.length !== 1) { + const message = `found ${bootstraps.length} NestFactory.create(...) calls; phase 1 generates one application` + if (bootstraps.length === 0) diagnostics.push({ file: entry, line: 1, column: 1, message }) + else bootstraps.slice(1).forEach((call) => refuse(call, message)) + } + let root = null + const bootstrap = bootstraps[0] + if (bootstrap) { + const [moduleArg, adapterArg, optionsArg, ...rest] = bootstrap.arguments + const rootDeclaration = moduleArg && classOfExpression(moduleArg) + if (!rootDeclaration) refuse(moduleArg ?? bootstrap, 'NestFactory.create: the first argument must be a reference to a @Module class') + else root = modelOf(rootDeclaration) + const adapterClass = adapterArg && ts.isNewExpression(adapterArg) ? declaringPackage(checker.getSymbolAtLocation(adapterArg.expression)) : null + if (adapterClass?.name !== '@nestjs/platform-node' || adapterClass.resolved.name !== 'NodeAdapter') { + refuse(adapterArg ?? bootstrap, 'NestFactory.create: the second argument must be `new NodeAdapter(...)` from @nestjs/platform-node') + } else if ((adapterArg.arguments?.length ?? 0) > 0) { + const [adapterOptions] = adapterArg.arguments + if (!ts.isObjectLiteralExpression(adapterOptions)) refuse(adapterOptions, 'new NodeAdapter(...) takes an object literal in phase 1') + else for (const property of adapterOptions.properties) { + if (ts.isPropertyAssignment(property) && property.name.getText() === 'maxParamLength' && ts.isNumericLiteral(property.initializer)) continue + refuse(property, `NodeAdapter option ${property.name?.getText() ?? property.getText()} is not in phase 1 (maxParamLength as a numeric literal)`) + } + } + if (optionsArg !== undefined) { + if (!ts.isObjectLiteralExpression(optionsArg)) refuse(optionsArg, 'NestFactory.create options must be an object literal in phase 1') + else for (const property of optionsArg.properties) { + const key = property.name?.getText() + if (key === 'logger' && ts.isPropertyAssignment(property) && (property.initializer.kind === ts.SyntaxKind.FalseKeyword || property.initializer.kind === ts.SyntaxKind.TrueKeyword)) continue + refuse(property, `NestFactory.create option ${key ?? property.getText()} is not in phase 1 (logger: true | false)`) + } + } + for (const extra of rest) refuse(extra, 'NestFactory.create takes at most three arguments') + } + + const modules = [] + const seen = new Set() + const walkModule = (model) => { + if (model.kind !== 'module') { + refuse(model.declaration, `${model.name} is imported as a module but is not a @Module class`) + return + } + if (seen.has(model)) return + seen.add(model) + model.importModels = model.imports.map(modelOf) + model.controllerModels = model.controllers.map(modelOf) + model.providerModels = model.providers.map(modelOf) + model.exportModels = model.exports.map(modelOf) + for (const c of model.controllerModels) if (c.kind !== 'controller') refuse(c.declaration, `${c.name} is listed under controllers but is not a @Controller class`) + for (const p of model.providerModels) if (p.kind !== 'provider') refuse(p.declaration, `${p.name} is listed under providers but is not an @Injectable class`) + for (const e of model.exportModels) if (e.kind !== 'provider' && e.kind !== 'module') refuse(e.declaration, `${e.name} is exported but is neither a provider nor a module`) + for (const imported of model.importModels) walkModule(imported) + modules.push(model) + } + if (root) walkModule(root) + + const exportedProviders = (model, trail = new Set()) => { + if (trail.has(model)) return [] + trail.add(model) + const out = [] + for (const e of model.exportModels ?? []) { + if (e.kind === 'provider') out.push(e) + else if (e.kind === 'module') out.push(...exportedProviders(e, trail)) + } + return out + } + for (const model of modules) { + model.scope = new Map() + for (const p of model.providerModels) if (p.kind === 'provider') model.scope.set(p.declaration, p) + for (const imported of model.importModels) for (const p of exportedProviders(imported)) if (!model.scope.has(p.declaration)) model.scope.set(p.declaration, p) + for (const consumer of [...model.providerModels, ...model.controllerModels]) { + if (!consumer.dependencies) continue + consumer.resolved = consumer.dependencies.map((dependency) => { + if (dependency === null) return null + const provider = model.scope.get(dependency.target) + if (!provider) { + refuse(consumer.declaration, `Nest can't resolve dependencies of ${consumer.name}: ${className(dependency.target)} is not provided in module ${model.name} (phase 1 resolves providers by class)`) + return null + } + return provider + }) + } + } + const order = [] + const state = new Map() + const place = (provider, trail) => { + const s = state.get(provider) + if (s === 'done') return + if (s === 'active') { + refuse(provider.declaration, `circular dependency: ${[...trail, provider].map((p) => p.name).join(' -> ')} (forwardRef is not in phase 1)`) + return + } + state.set(provider, 'active') + for (const dependency of provider.resolved ?? []) if (dependency) place(dependency, [...trail, provider]) + state.set(provider, 'done') + order.push(provider) + } + for (const model of modules) for (const p of model.providerModels) if (p.kind === 'provider') place(p, []) + const controllers = modules.flatMap((m) => m.controllerModels.filter((c) => c.kind === 'controller')) + for (const c of controllers) for (const dependency of c.resolved ?? []) if (dependency) place(dependency, []) + + if (diagnostics.length > 0) throw new NestAnalysisError(diagnostics) + return { entry, program, checker, bootstrap, root, modules, providers: order, controllers } +} + +export const describeModel = (model) => ({ + entry: model.entry, + root: model.root.name, + modules: model.modules.map((m) => ({ + name: m.name, + imports: m.importModels.map((x) => x.name), + controllers: m.controllerModels.map((x) => x.name), + providers: m.providerModels.map((x) => x.name), + exports: m.exportModels.map((x) => x.name) + })), + providers: model.providers.map((p) => ({ name: p.name, dependencies: p.resolved.map((d) => d.name), location: p.location })), + controllers: model.controllers.map((c) => ({ + name: c.name, + prefixes: c.prefixes, + dependencies: c.resolved.map((d) => d.name), + routes: c.routes.map((r) => ({ ...r, params: r.params.map(({ parameter, typeNode, ...rest }) => rest) })) + })) +}) + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const ts = (await import('typescript')).default + const entry = path.resolve(process.argv[2]) + try { + console.log(JSON.stringify(describeModel(analyzeNestApplication({ ts, entry })), null, 2)) + } catch (error) { + if (error instanceof NestAnalysisError) { + console.error(error.message) + process.exit(1) + } + throw error + } +} diff --git a/tools/nest/aot/generate.mjs b/tools/nest/aot/generate.mjs new file mode 100644 index 0000000..d3bcde3 --- /dev/null +++ b/tools/nest/aot/generate.mjs @@ -0,0 +1,350 @@ +import { analyzeNestApplication, NestAnalysisError } from './analyze.mjs' + +const BLANK_PATTERN = /[^\n]/g + +const blankRanges = (text, ranges) => { + let out = text + for (const [start, end] of ranges) out = out.slice(0, start) + out.slice(start, end).replace(BLANK_PATTERN, ' ') + out.slice(end) + return out +} + +const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1) + +const joinRoute = (prefix, methodPath) => { + const stripEnd = (p) => (p && p[p.length - 1] === '/' ? p.slice(0, -1) : p) + const addLeading = (p) => (p && p[0] !== '/' ? `/${p}` : p) + let path = '' + if (prefix) path = stripEnd(path) + addLeading(prefix) + if (methodPath) path = stripEnd(path) + addLeading(methodPath) + path = addLeading(path || '/') + return path !== '/' ? stripEnd(path) : path +} + +const routeParameters = (path) => ({ + names: [...path.matchAll(/:([A-Za-z_$][\w$]*)/g)].map((m) => m[1]), + wildcard: path.includes('*') +}) + +const typeSet = (text) => new Set(text.split(' | ').map((s) => s.trim())) +const sameTypeSet = (text, expected) => { + const actual = typeSet(text) + return actual.size === expected.length && expected.every((t) => actual.has(t)) +} + +const resultReply = ({ ts, checker, type, refuse, node }) => { + let inner = type + let async = false + const symbolName = type.getSymbol()?.getName() + if (symbolName === 'Promise' && checker.getTypeArguments(type).length === 1) { + async = true + inner = checker.getTypeArguments(type)[0] + } + const parts = inner.isUnion() ? inner.types : [inner] + const nullish = (t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) !== 0 + const present = parts.filter((t) => !nullish(t)) + const optional = present.length !== parts.length + if (present.length === 0) return { kind: 'empty', async, optional: false } + const kinds = new Set( + present.map((t) => { + if (t.flags & (ts.TypeFlags.StringLike)) return 'text' + if (t.flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike)) return 'string-text' + if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'any' + if (t.flags & ts.TypeFlags.Object) { + const name = t.getSymbol()?.getName() + if (name === 'Buffer' || name === 'Uint8Array' || name === 'Uint8ClampedArray' || name === 'Int8Array' || name === 'DataView') return 'bytes' + if (name === 'StreamableFile' || name === 'Observable') return 'unsupported' + return 'json' + } + return 'unsupported' + }) + ) + if (kinds.size !== 1) { + refuse(node, `result type ${checker.typeToString(type)} mixes reply kinds (${[...kinds].join(', ')}); phase 1 replies one kind per route`) + return null + } + const [kind] = kinds + if (kind === 'any') { + refuse(node, `result type ${checker.typeToString(type)} is not declared; the reply is chosen from the declared type at build time`) + return null + } + if (kind === 'unsupported') { + refuse(node, `result type ${checker.typeToString(type)} is not in phase 1 (string, number, boolean, void, an object, a Buffer, or a Promise of one)`) + return null + } + return { kind, async, optional } +} + +export const generateNestApplication = ({ ts, entry, options }) => { + const model = analyzeNestApplication({ ts, entry, options }) + const { checker, program } = model + const diagnostics = [] + const location = (node) => { + const file = node.getSourceFile() + const { line, character } = file.getLineAndCharacterOfPosition(node.getStart()) + return { file: file.fileName, line: line + 1, column: character + 1 } + } + const refuse = (node, message) => diagnostics.push({ ...location(node), message }) + + const blanks = new Map() + const blank = (node) => { + const fileName = node.getSourceFile().fileName + if (!blanks.has(fileName)) blanks.set(fileName, []) + blanks.get(fileName).push([node.getStart(), node.getEnd()]) + } + const appends = new Map() + const append = (fileName, text) => { + if (!appends.has(fileName)) appends.set(fileName, []) + appends.get(fileName).push(text) + } + const replacements = new Map() + + const consumed = new Set() + const consume = (node) => { + for (const decorator of ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []) { + consumed.add(decorator) + blank(decorator) + } + } + const consumeClass = (declaration) => { + consume(declaration) + for (const member of declaration.members) { + consume(member) + if (ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member)) for (const parameter of member.parameters) consume(parameter) + } + } + for (const module of model.modules) consumeClass(module.declaration) + for (const provider of model.providers) consumeClass(provider.declaration) + for (const controller of model.controllers) consumeClass(controller.declaration) + for (const file of program.getSourceFiles()) { + if (file.fileName.includes('/node_modules/') || file.isDeclarationFile) continue + const visit = (node) => { + if (ts.isDecorator(node) && !consumed.has(node)) refuse(node, `decorator ${node.expression.getText()} is outside the module graph NestFactory.create reaches; phase 1 strips only the decorators it resolved`) + ts.forEachChild(node, visit) + } + visit(file) + } + + const importSpecifierOf = (file, name) => { + for (const statement of file.statements) { + if (!ts.isImportDeclaration(statement) || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) continue + for (const element of statement.importClause.namedBindings.elements) { + if (element.name.text === name) return { specifier: statement.moduleSpecifier.text, exported: (element.propertyName ?? element.name).text } + } + } + return null + } + const functionName = (module) => `__nestAot_${module.name}` + const functionReference = (file, module, referencingNode) => { + const declaringFile = module.declaration.getSourceFile() + if (declaringFile === file) return functionName(module) + const imported = importSpecifierOf(file, module.name) + if (!imported) { + refuse(referencingNode, `${module.name} is not imported into ${file.fileName} by name; phase 1 finds the generated module function through the same import`) + return functionName(module) + } + const resolved = ts.resolveModuleName(imported.specifier, file.fileName, program.getCompilerOptions(), ts.sys).resolvedModule + if (!resolved || resolved.resolvedFileName !== declaringFile.fileName) { + refuse(referencingNode, `${module.name} is imported from ${imported.specifier}, which is not the file declaring it (${declaringFile.fileName}); phase 1 does not follow re-exports`) + } + append(file.fileName, `import { ${functionName(module)} } from ${JSON.stringify(imported.specifier)}`) + return functionName(module) + } + + const importedCount = new Map() + for (const module of model.modules) for (const imported of module.importModels) importedCount.set(imported, (importedCount.get(imported) ?? 0) + 1) + for (const [module, count] of importedCount) if (count > 1) refuse(module.declaration, `${module.name} is imported by ${count} modules; phase 1 instantiates a module once per importer, Nest once`) + + for (const module of model.modules) { + const file = module.declaration.getSourceFile() + const lines = [] + const names = new Set() + const uniqueName = (base) => { + let name = base + let n = 2 + while (names.has(name)) name = `${base}${n++}` + names.add(name) + return name + } + const providerExpression = new Map() + for (const imported of module.importModels) { + const variable = uniqueName(lowerFirst(imported.name)) + lines.push(` const ${variable} = ${functionReference(file, imported, module.declaration)}(app)`) + const exported = exportedProvidersOf(imported) + for (const provider of exported) providerExpression.set(provider, `${variable}.${provider.name}`) + } + const own = model.providers.filter((p) => module.providerModels.includes(p)) + for (const provider of own) { + const variable = uniqueName(lowerFirst(provider.name)) + const args = provider.resolved.map((d) => providerExpression.get(d) ?? `/* unresolved ${d.name} */ undefined`) + lines.push(` const ${variable} = new ${provider.name}(${args.join(', ')})`) + providerExpression.set(provider, variable) + } + for (const controller of module.controllerModels) { + if (controller.kind !== 'controller') continue + const variable = uniqueName(lowerFirst(controller.name)) + const args = controller.resolved.map((d) => providerExpression.get(d) ?? `/* unresolved ${d.name} */ undefined`) + lines.push(` const ${variable} = new ${controller.name}(${args.join(', ')})`) + for (const route of controller.routes) lines.push(...routeLines({ controller, variable, route })) + } + const returned = module.exportModels.filter((e) => e.kind === 'provider').map((p) => `${p.name}: ${providerExpression.get(p)}`) + lines.push(` return { ${returned.join(', ')} }`) + const returnType = `{ ${module.exportModels.filter((e) => e.kind === 'provider').map((p) => `${p.name}: ${p.name}`).join('; ')} }` + append( + file.fileName, + [ + `// Generated by tools/nest/aot (geastack) from this file's Nest decorators: what @nestjs/core resolves at bootstrap, resolved at build time.`, + `import type { NestApplication as __NestAotApplication } from '@nestjs/core'`, + `import type { NodeRequest as __NestAotRequest, NodeResponse as __NestAotResponse } from '@nestjs/platform-node'`, + `export function ${functionName(module)}(app: __NestAotApplication): ${returnType} {`, + ...lines, + `}` + ].join('\n') + ) + } + + function exportedProvidersOf(module, trail = new Set()) { + if (trail.has(module)) return [] + trail.add(module) + const out = [] + for (const e of module.exportModels) { + if (e.kind === 'provider') out.push(e) + else if (e.kind === 'module') out.push(...exportedProvidersOf(e, trail)) + } + return out + } + + function routeLines({ controller, variable, route }) { + const out = [] + const paths = controller.prefixes.flatMap((prefix) => route.paths.map((methodPath) => joinRoute(prefix, methodPath))) + const methodNode = controller.declaration.members.find((m) => ts.isMethodDeclaration(m) && m.name.getText() === route.method) + const reply = resultReply({ ts, checker, type: checker.getTypeAtLocation(methodNode).getCallSignatures()[0].getReturnType(), refuse, node: methodNode }) + if (!reply) return out + let handled = false + const args = route.params.map((param) => { + if (param === null) return 'undefined' + const expected = (types, what) => { + if (!sameTypeSet(param.type, types)) refuse(param.parameter, `@${param.source === 'param' ? 'Param' : param.source === 'query' ? 'Query' : param.source === 'body' ? 'Body' : param.source === 'headers' ? 'Headers' : param.source === 'request' ? 'Req' : 'Res'}${param.key === null ? '()' : `('${param.key}')`} parameter ${param.name} is typed ${param.type}; ${what}`) + } + switch (param.source) { + case 'param': { + if (param.key === null) { + refuse(param.parameter, '@Param() without a key is not in phase 1') + return 'undefined' + } + for (const path of paths) { + const { names, wildcard } = routeParameters(path) + if (wildcard) refuse(param.parameter, `route ${path} has a wildcard; @Param on a wildcard route is not in phase 1`) + if (!names.includes(param.key)) refuse(param.parameter, `route ${path} has no :${param.key} parameter; Nest would pass undefined`) + } + expected(['string'], 'a route parameter is a string') + return `app.param(req, ${JSON.stringify(param.key)})` + } + case 'query': + if (param.key === null) { + if (!sameTypeSet(param.type, ['ParsedQuery']) && !sameTypeSet(param.type, ['Record'])) expected(['Record'], 'the parsed query is a Record') + return 'req.query' + } + expected(['string', 'string[]', 'undefined'], 'a query value is string | string[] | undefined (absent, one, or repeated)') + return `app.query(req, ${JSON.stringify(param.key)})` + case 'body': + expected(['unknown'], 'the parsed body is unknown until the handler narrows it') + return 'req.body' + case 'headers': + if (param.key === null) { + if (!sameTypeSet(param.type, ['IncomingHttpHeaders']) && !sameTypeSet(param.type, ['Record'])) expected(['Record'], 'the headers are a Record') + return 'req.headers' + } + expected(['string', 'undefined'], 'a header is string | undefined') + return `req.headers[${JSON.stringify(param.key.toLowerCase())}]` + case 'request': + expected(['NodeRequest'], 'the request is a NodeRequest (import it from @nestjs/platform-node)') + return 'req' + case 'response': + handled = true + expected(['NodeResponse'], 'the response is a NodeResponse (import it from @nestjs/platform-node)') + return 'res' + default: + refuse(param.parameter, `parameter source ${param.source} is not in phase 1`) + return 'undefined' + } + }) + const status = route.httpCode ?? (route.verb === 'POST' ? 201 : 200) + const call = `${variable}.${route.method}(${args.join(', ')})` + const replyLines = (resultExpression) => { + if (handled) return [` ${resultExpression}`] + const send = (expression) => { + switch (reply.kind) { + case 'text': + return `app.text(res, ${expression}, ${status})` + case 'string-text': + return `app.text(res, String(${expression}), ${status})` + case 'json': + return `app.json(res, JSON.stringify(${expression}), ${status})` + case 'bytes': + return `app.bytes(res, ${expression}, ${status})` + default: + return `app.empty(res, ${status})` + } + } + if (reply.kind === 'empty') return [` ${resultExpression}`, ` ${send('undefined')}`] + if (!reply.optional) return [` ${send(resultExpression)}`] + return [` const result = ${resultExpression}`, ` if (result === undefined || result === null) app.empty(res, ${status})`, ` else ${send('result')}`] + } + for (const path of paths) { + out.push(` app.route(${JSON.stringify(route.verb)}, ${JSON.stringify(path)}, (req: __NestAotRequest, res: __NestAotResponse) => {`) + for (const header of route.headers) out.push(` app.header(res, ${JSON.stringify(header.name)}, ${JSON.stringify(header.value)})`) + if (reply.async) { + out.push(` ${call}.then(`) + out.push(` (${handled || reply.kind === 'empty' ? '' : 'value'}) => {`) + out.push(...(handled ? [] : reply.kind === 'empty' ? [` app.empty(res, ${status})`] : replyLines('value').map((l) => l.replace(/^ const result = value$/, ' const result = value')))) + out.push(` },`) + out.push(` (error: unknown) => app.fail(res, error)`) + out.push(` )`) + } else { + out.push(` try {`) + out.push(...replyLines(call)) + out.push(` } catch (error) {`) + out.push(` app.fail(res, error)`) + out.push(` }`) + } + out.push(` })`) + } + return out + } + + const entryFile = model.bootstrap.getSourceFile() + const moduleArgument = model.bootstrap.arguments[0] + const reference = functionReference(entryFile, model.root, moduleArgument) + if (!replacements.has(entryFile.fileName)) replacements.set(entryFile.fileName, []) + replacements.get(entryFile.fileName).push([moduleArgument.getStart(), moduleArgument.getEnd(), reference]) + + if (diagnostics.length > 0) throw new NestAnalysisError(diagnostics) + + const rewrites = new Map() + const files = new Set([...blanks.keys(), ...appends.keys(), ...replacements.keys()]) + for (const fileName of files) { + const source = program.getSourceFile(fileName) + let text = blankRanges(source.text, blanks.get(fileName) ?? []) + for (const [start, end, replacement] of (replacements.get(fileName) ?? []).sort((a, b) => b[0] - a[0])) text = text.slice(0, start) + replacement + text.slice(end) + const extra = appends.get(fileName) ?? [] + if (extra.length > 0) text = `${text}\n${[...new Set(extra)].join('\n')}\n` + rewrites.set(fileName, text) + } + return { rewrites, model } +} + +if (process.argv[1] && (await import('node:path')).resolve(process.argv[1]) === (await import('node:url')).fileURLToPath(import.meta.url)) { + const ts = (await import('typescript')).default + const path = await import('node:path') + const entry = path.resolve(process.argv[2]) + try { + const { rewrites } = generateNestApplication({ ts, entry }) + for (const [fileName, text] of rewrites) console.log(`==== ${fileName}\n${text}`) + } catch (error) { + if (error instanceof NestAnalysisError) { + console.error(error.message) + process.exit(1) + } + throw error + } +} diff --git a/tools/nest/tsconfig.shims.json b/tools/nest/tsconfig.shims.json index 72c4893..1306b71 100644 --- a/tools/nest/tsconfig.shims.json +++ b/tools/nest/tsconfig.shims.json @@ -13,5 +13,8 @@ }, "include": [ "../../runtime/node/nest/shims/**/*.ts" + ], + "exclude": [ + "../../runtime/node/nest/shims/@nestjs/**" ] }