diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4b20242869..f8e70af8d7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,6 +2,8 @@ "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/typescript-node:24", "features": { - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "moby": false + } } } diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index b55e981ccf..53f2654388 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: run: npm run docs - name: Deploy docs - uses: JamesIves/github-pages-deploy-action@v4.7.3 + uses: JamesIves/github-pages-deploy-action@v4.7.4 with: branch: gh-pages # The branch the action should deploy to. folder: docs # The folder the action should deploy. diff --git a/examples/typescript/watch/watch-example.ts b/examples/typescript/watch/watch-example.ts index ecc8cc2bb3..b33d71f653 100644 --- a/examples/typescript/watch/watch-example.ts +++ b/examples/typescript/watch/watch-example.ts @@ -26,9 +26,13 @@ const req = await watch } console.log(apiObj); }, - // done callback is called if the watch terminates normally + // done callback is called if the watch terminates either normally or with an error (err) => { - console.log(err); + if (err) { + console.error(err); + } else { + console.log('watch finished normally') + } }, ) // watch returns a request object which you can use to abort the watch. diff --git a/package-lock.json b/package-lock.json index 54d1ded563..2c6b13d195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@types/mock-fs": "^4.13.1", "c8": "^10.0.0", "eslint": "^9.18.0", - "eslint-plugin-erasable-syntax-only": "^0.3.0", + "eslint-plugin-erasable-syntax-only": "^0.4.0", "husky": "^9.0.6", "mock-fs": "^5.2.0", "nock": "^14.0.5", @@ -522,12 +522,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -540,6 +541,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -550,6 +552,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -558,22 +561,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -632,9 +635,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -645,22 +648,23 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1026,12 +1030,12 @@ } }, "node_modules/@types/node": { - "version": "24.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", - "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/node-fetch": { @@ -1045,9 +1049,9 @@ } }, "node_modules/@types/stream-buffers": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.7.tgz", - "integrity": "sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.8.tgz", + "integrity": "sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1061,17 +1065,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", - "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/type-utils": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1085,7 +1089,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", + "@typescript-eslint/parser": "^8.46.4", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1101,16 +1105,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", - "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4" }, "engines": { @@ -1126,14 +1130,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", - "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.1", - "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", "debug": "^4.3.4" }, "engines": { @@ -1148,14 +1152,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1166,9 +1170,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", - "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", "dev": true, "license": "MIT", "engines": { @@ -1183,15 +1187,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", - "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1208,9 +1212,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "dev": true, "license": "MIT", "engines": { @@ -1222,16 +1226,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1251,16 +1255,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", - "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1" + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1275,13 +1279,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/types": "8.46.4", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1905,25 +1909,24 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -1966,9 +1969,9 @@ } }, "node_modules/eslint-plugin-erasable-syntax-only": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-erasable-syntax-only/-/eslint-plugin-erasable-syntax-only-0.3.1.tgz", - "integrity": "sha512-f4Eo1LEZJfUP7/2PwNdFUcIUJlYAoCoie9BSoThQ4y4eO8bW+Sq38qC4IskYTu4+FW887mPW0wYuI9HU0nQnlg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-erasable-syntax-only/-/eslint-plugin-erasable-syntax-only-0.4.0.tgz", + "integrity": "sha512-7dx2gpfVn0FFEGukLqsF/izW4hGvSaySNOkcuSCO4q6Khh0ecOGYlRhW9hhDUtNryuKc+cXd1WFiQdpae3xzaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2683,9 +2686,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3817,16 +3820,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", - "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.1", - "@typescript-eslint/parser": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1" + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3848,9 +3851,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/uri-js": { diff --git a/package.json b/package.json index 6d15d2e893..c801a5ad52 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@types/mock-fs": "^4.13.1", "c8": "^10.0.0", "eslint": "^9.18.0", - "eslint-plugin-erasable-syntax-only": "^0.3.0", + "eslint-plugin-erasable-syntax-only": "^0.4.0", "husky": "^9.0.6", "mock-fs": "^5.2.0", "nock": "^14.0.5", diff --git a/src/cp.ts b/src/cp.ts index 7e676bfb0b..f8920e908d 100644 --- a/src/cp.ts +++ b/src/cp.ts @@ -26,7 +26,7 @@ export class Cp { tgtPath: string, cwd?: string, ): Promise { - const command = ['tar', 'zcf', '-']; + const command = ['tar', 'cf', '-']; if (cwd) { command.push('-C', cwd); } diff --git a/src/cp_test.ts b/src/cp_test.ts index 0782106cba..fb38247df3 100644 --- a/src/cp_test.ts +++ b/src/cp_test.ts @@ -22,7 +22,7 @@ describe('Cp', () => { const container = 'container'; const srcPath = '/'; const tgtPath = '/'; - const cmdArray = ['tar', 'zcf', '-', srcPath]; + const cmdArray = ['tar', 'cf', '-', srcPath]; const path = `/api/v1/namespaces/${namespace}/pods/${pod}/exec`; const query = { @@ -51,7 +51,7 @@ describe('Cp', () => { const srcPath = '/'; const tgtPath = '/'; const cwd = '/abc'; - const cmdArray = ['tar', 'zcf', '-', '-C', cwd, srcPath]; + const cmdArray = ['tar', 'cf', '-', '-C', cwd, srcPath]; const path = `/api/v1/namespaces/${namespace}/pods/${pod}/exec`; const query = { diff --git a/src/exec_auth.ts b/src/exec_auth.ts index c9d3d23d85..4698f83980 100644 --- a/src/exec_auth.ts +++ b/src/exec_auth.ts @@ -10,7 +10,7 @@ export interface CredentialStatus { readonly token: string; readonly clientCertificateData: string; readonly clientKeyData: string; - readonly expirationTimestamp: string; + readonly expirationTimestamp?: string; } export interface Credential { @@ -74,6 +74,9 @@ export class ExecAuth implements Authenticator { // TODO: Add a unit test for token caching. const cachedToken = this.tokenCache[user.name]; if (cachedToken) { + if (!cachedToken.status.expirationTimestamp) { + return cachedToken; + } const date = Date.parse(cachedToken.status.expirationTimestamp); if (date > Date.now()) { return cachedToken; diff --git a/src/exec_auth_test.ts b/src/exec_auth_test.ts index 7577d87cd8..320d93f860 100644 --- a/src/exec_auth_test.ts +++ b/src/exec_auth_test.ts @@ -225,6 +225,77 @@ describe('ExecAuth', () => { strictEqual(execCount, 2); }); + it('should cache tokens without expirationTimestamp (non-expiring tokens)', async () => { + // TODO: fix this test for Windows + if (process.platform === 'win32') { + return; + } + const auth = new ExecAuth(); + let execCount = 0; + const tokenValue = 'non-expiring-token'; + + (auth as any).execFn = ( + command: string, + args?: readonly string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): child_process.ChildProcessWithoutNullStreams => { + execCount++; + return { + stdout: { + setEncoding: () => {}, + on: (_data: string, f: (data: Buffer | string) => void) => { + // Note: No expirationTimestamp field - token should never expire + f( + Buffer.from( + JSON.stringify({ + status: { token: tokenValue }, + }), + ), + ); + }, + }, + stderr: { + setEncoding: () => {}, + on: () => {}, + }, + on: (op: string, f: any) => { + if (op === 'close') { + f(0); + } + }, + } as unknown as child_process.ChildProcessWithoutNullStreams; + }; + + const user = { + name: 'user', + authProvider: { + config: { + exec: { + command: 'echo', + }, + }, + }, + }; + + const opts = {} as https.RequestOptions; + opts.headers = {} as OutgoingHttpHeaders; + + // First call - should execute the command + await auth.applyAuthentication(user, opts); + strictEqual(opts.headers.Authorization, `Bearer ${tokenValue}`); + strictEqual(execCount, 1); + + // Second call - should use cached token (no expiration means never expires) + await auth.applyAuthentication(user, opts); + strictEqual(opts.headers.Authorization, `Bearer ${tokenValue}`); + strictEqual(execCount, 1, 'exec should not be called again for non-expiring token'); + + // Third call - still should use cached token + await auth.applyAuthentication(user, opts); + strictEqual(opts.headers.Authorization, `Bearer ${tokenValue}`); + strictEqual(execCount, 1, 'exec should still not be called again'); + }); + it('should return null on no exec info', async () => { const auth = new ExecAuth(); const opts = {} as https.RequestOptions; diff --git a/src/test/integration/cpFromPod.ts b/src/test/integration/cpFromPod.ts new file mode 100644 index 0000000000..001368dfdf --- /dev/null +++ b/src/test/integration/cpFromPod.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { setTimeout } from 'node:timers/promises'; +import { CoreV1Api, KubeConfig, V1Pod } from '../../index.js'; +import { Cp } from '../../cp.js'; +import { generateName } from './name.js'; + +export default async function cpFromPod() { + const kc = new KubeConfig(); + kc.loadFromDefault(); + + const coreV1Client = kc.makeApiClient(CoreV1Api); + const cp = new Cp(kc); + + const testPodName = generateName('cp-test-pod'); + const namespace = 'default'; + + const pod = new V1Pod(); + pod.metadata = { name: testPodName }; + pod.spec = { + containers: [ + { + name: 'test-container', + image: 'busybox', + command: ['sh', '-c', 'echo "test content" > /tmp/test.txt && sleep 3600'], + }, + ], + restartPolicy: 'Never', + }; + + console.log(`Creating pod ${testPodName}`); + await coreV1Client.createNamespacedPod({ namespace, body: pod }); + + console.log('Waiting for pod to be ready...'); + let podReady = false; + for (let i = 0; i < 30; i++) { + const currentPod = await coreV1Client.readNamespacedPod({ name: testPodName, namespace }); + if (currentPod.status?.phase === 'Running') { + podReady = true; + break; + } + await setTimeout(1000); + } + + assert.strictEqual(podReady, true, 'Pod did not become ready in time'); + console.log('Pod is ready'); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8s-cp-test-')); + + try { + console.log('Copying file from pod...'); + + cp.cpFromPod(namespace, testPodName, 'test-container', 'test.txt', tempDir, '/tmp'); + + // Wait for file to appear + const copiedFilePath = path.join(tempDir, 'test.txt'); + let fileExists = false; + for (let i = 0; i < 20; i++) { + if (fs.existsSync(copiedFilePath)) { + fileExists = true; + break; + } + await setTimeout(500); + } + + assert.strictEqual(fileExists, true, 'File was not copied'); + + const content = fs.readFileSync(copiedFilePath, 'utf-8'); + assert.strictEqual(content.trim(), 'test content', 'File content does not match'); + + console.log('cpFromPod test passed!'); + } finally { + console.log('Cleaning up...'); + await coreV1Client.deleteNamespacedPod({ name: testPodName, namespace }); + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} diff --git a/src/test/integration/index.ts b/src/test/integration/index.ts index 5065dcd429..ee26fa03df 100644 --- a/src/test/integration/index.ts +++ b/src/test/integration/index.ts @@ -1,5 +1,7 @@ import patchNamespace from './patchNamespace.js'; +import cpFromPod from './cpFromPod.js'; console.log('Integration testing'); await patchNamespace(); +await cpFromPod(); diff --git a/src/util.ts b/src/util.ts index e502e600d9..fe6a0a3558 100644 --- a/src/util.ts +++ b/src/util.ts @@ -165,6 +165,45 @@ export function normalizeResponseHeaders(response: Response): { [key: string]: s return normalizedHeaders; } +/** + * Built-in Kubernetes API groups that have generated TypeScript models. + * Custom resources and third-party API groups (like Knative) are not included. + */ +const BUILT_IN_API_GROUPS = new Set([ + 'core', // maps to "" (empty string) for core resources like Pod, Service, etc. + 'admissionregistration.k8s.io', + 'apiextensions.k8s.io', + 'apiregistration.k8s.io', + 'apps', + 'authentication.k8s.io', + 'authorization.k8s.io', + 'autoscaling', + 'batch', + 'certificates.k8s.io', + 'coordination.k8s.io', + 'discovery.k8s.io', + 'events.k8s.io', + 'flowcontrol.apiserver.k8s.io', + 'internal.apiserver.k8s.io', + 'networking.k8s.io', + 'node.k8s.io', + 'policy', + 'rbac.authorization.k8s.io', + 'resource.k8s.io', + 'scheduling.k8s.io', + 'storage.k8s.io', + 'storagemigration.k8s.io', +]); + +/** + * Check if the given API group is a built-in Kubernetes API group. + * @param group - The API group to check (e.g., "apps", "serving.knative.dev", "core") + * @returns true if the group is a built-in Kubernetes API group, false otherwise + */ +function isBuiltInApiGroup(group: string): boolean { + return BUILT_IN_API_GROUPS.has(group); +} + export function getSerializationType(apiVersion?: string, kind?: string): string { if (apiVersion === undefined || kind === undefined) { return 'KubernetesObject'; @@ -172,6 +211,12 @@ export function getSerializationType(apiVersion?: string, kind?: string): string // Types are defined in src/gen/api/models with the format "". // Version and Kind are in PascalCase. const gv = groupVersion(apiVersion); + + // Only return a type name if this is a built-in Kubernetes API group + if (!isBuiltInApiGroup(gv.group)) { + return 'KubernetesObject'; + } + const version = gv.version.charAt(0).toUpperCase() + gv.version.slice(1); return `${version}${kind}`; } diff --git a/src/util_test.ts b/src/util_test.ts index 74a323a866..a932b7bbd0 100644 --- a/src/util_test.ts +++ b/src/util_test.ts @@ -147,6 +147,18 @@ describe('Utils', () => { it('should get the serialization type correctly', () => { strictEqual(getSerializationType('v1', 'Pod'), 'V1Pod'); strictEqual(getSerializationType('apps/v1', 'Deployment'), 'V1Deployment'); + // Built-in Kubernetes resources should return a type + strictEqual(getSerializationType('v1', 'Pod'), 'V1Pod'); + strictEqual(getSerializationType('apps/v1', 'Deployment'), 'V1Deployment'); + strictEqual(getSerializationType('v1', 'Service'), 'V1Service'); + strictEqual(getSerializationType('batch/v1', 'Job'), 'V1Job'); + + // Non-built-in resources should return 'KubernetesObject' + strictEqual(getSerializationType('serving.knative.dev/v1', 'Service'), 'KubernetesObject'); + strictEqual(getSerializationType('example.com/v1', 'MyCustomResource'), 'KubernetesObject'); + strictEqual(getSerializationType('custom.io/v1alpha1', 'CustomThing'), 'KubernetesObject'); + + // Undefined inputs should return 'KubernetesObject' strictEqual(getSerializationType(undefined, undefined), 'KubernetesObject'); }); }); diff --git a/src/watch.ts b/src/watch.ts index 4c51399a62..7664b07291 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -6,6 +6,7 @@ import { KubeConfig } from './config.js'; export class Watch { public static SERVER_SIDE_CLOSE: object = { error: 'Connection closed on server' }; public config: KubeConfig; + private requestTimeoutMs: number = 30000; public constructor(config: KubeConfig) { this.config = config; @@ -39,9 +40,8 @@ export class Watch { const requestInit = await this.config.applyToFetchOptions({}); const controller = new AbortController(); - const timeoutSignal = AbortSignal.timeout(30000); + const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs); requestInit.signal = AbortSignal.any([controller.signal, timeoutSignal]); - requestInit.signal = controller.signal as AbortSignal; requestInit.method = 'GET'; let doneCalled: boolean = false; diff --git a/src/watch_test.ts b/src/watch_test.ts index 5bf842565d..7be5c1f6c0 100644 --- a/src/watch_test.ts +++ b/src/watch_test.ts @@ -452,6 +452,39 @@ describe('Watch', () => { s.done(); }); + it('should timeout when server takes too long to respond', async (t) => { + const kc = await setupMockSystem(t, (_req: any, _res: any) => { + // Don't respond - simulate a hanging server + }); + const watch = new Watch(kc); + + // NOTE: Hack around the type system to make the timeout shorter + (watch as any).requestTimeoutMs = 10; + + let doneErr: any; + + let doneResolve: () => void; + const donePromise = new Promise((resolve) => { + doneResolve = resolve; + }); + + await watch.watch( + '/some/path/to/object', + {}, + () => { + throw new Error('Unexpected data received - timeout should have occurred before any data'); + }, + (err: any) => { + doneErr = err; + doneResolve(); + }, + ); + + await donePromise; + + strictEqual(doneErr.name, 'AbortError'); + }); + it('should throw on empty config', async () => { const kc = new KubeConfig(); const watch = new Watch(kc); diff --git a/src/yaml_test.ts b/src/yaml_test.ts index 958f7ba45b..0174393a94 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -2,6 +2,7 @@ import { describe, it } from 'node:test'; import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert'; import { V1CustomResourceDefinition, V1Namespace } from './api.js'; import { dumpYaml, loadAllYaml, loadYaml } from './yaml.js'; +import { KubernetesObject } from './types.js'; describe('yaml', () => { it('should load safely', () => { @@ -154,4 +155,54 @@ spec: // not using strict equality as types are not matching deepEqual(actual, expected); }); + + it('should load Knative Service correctly preserving spec', () => { + const yaml = `apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: hello-world +spec: + template: + spec: + containers: + - image: ghcr.io/knative/helloworld-go:latest + ports: + - containerPort: 8080 + env: + - name: TARGET + value: "World"`; + const knativeService = loadYaml(yaml) as KubernetesObject; + + strictEqual(knativeService.apiVersion, 'serving.knative.dev/v1'); + strictEqual(knativeService.kind, 'Service'); + strictEqual((knativeService as any).metadata.name, 'hello-world'); + // Verify that the spec is preserved + strictEqual( + (knativeService as any).spec.template.spec.containers[0].image, + 'ghcr.io/knative/helloworld-go:latest', + ); + strictEqual((knativeService as any).spec.template.spec.containers[0].ports[0].containerPort, 8080); + strictEqual((knativeService as any).spec.template.spec.containers[0].env[0].name, 'TARGET'); + strictEqual((knativeService as any).spec.template.spec.containers[0].env[0].value, 'World'); + }); + + it('should load custom resources correctly', () => { + const yaml = `apiVersion: example.com/v1 +kind: MyCustomResource +metadata: + name: my-resource +spec: + customField: customValue + nestedObject: + key1: value1 + key2: value2`; + const customResource = loadYaml(yaml) as KubernetesObject; + + strictEqual((customResource as any).apiVersion, 'example.com/v1'); + strictEqual((customResource as any).kind, 'MyCustomResource'); + strictEqual((customResource as any).metadata.name, 'my-resource'); + strictEqual((customResource as any).spec.customField, 'customValue'); + strictEqual((customResource as any).spec.nestedObject.key1, 'value1'); + strictEqual((customResource as any).spec.nestedObject.key2, 'value2'); + }); });