From 86e5609e60d86a92fd6060679641105beec715e5 Mon Sep 17 00:00:00 2001 From: Jayden Seric Date: Tue, 24 May 2022 00:06:08 +1000 Subject: [PATCH] Update supported Node.js versions and dependencies, implement TypeScript JSDoc types. --- .vscode/settings.json | 5 + api/config/UPLOAD_DIRECTORY_URL.mjs | 2 + api/jsconfig.json | 11 + api/package-lock.json | 325 ++++++++++++++++++---------- api/package.json | 12 +- api/schema/FileType.mjs | 2 + api/schema/MutationType.mjs | 28 ++- api/schema/QueryType.mjs | 2 + api/schema/index.mjs | 2 + api/server.mjs | 8 +- api/storeUpload.mjs | 6 +- app/components/Header.mjs | 12 +- app/components/Page.mjs | 26 ++- app/components/Section.mjs | 12 +- app/components/UploadBlob.mjs | 43 +++- app/components/UploadFile.mjs | 26 ++- app/components/UploadFileList.mjs | 38 +++- app/components/Uploads.mjs | 25 ++- app/jsconfig.json | 11 + app/next.config.mjs | 2 + app/package-lock.json | 116 +++++++++- app/package.json | 14 +- app/pages/_app.mjs | 127 ++++++----- app/pages/index.mjs | 16 +- app/typings.d.ts | 4 + 25 files changed, 626 insertions(+), 249 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 api/jsconfig.json create mode 100644 app/jsconfig.json create mode 100644 app/typings.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b523c66 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.tsdk": "api/node_modules/typescript/lib" +} diff --git a/api/config/UPLOAD_DIRECTORY_URL.mjs b/api/config/UPLOAD_DIRECTORY_URL.mjs index 5ce4487..1fc0f29 100644 --- a/api/config/UPLOAD_DIRECTORY_URL.mjs +++ b/api/config/UPLOAD_DIRECTORY_URL.mjs @@ -1 +1,3 @@ +// @ts-check + export default new URL("../uploads/", import.meta.url); diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 0000000..7935e73 --- /dev/null +++ b/api/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "maxNodeModuleJsDepth": 10, + "module": "nodenext", + "noEmit": true, + "strict": true + }, + "typeAcquisition": { + "enable": false + } +} diff --git a/api/package-lock.json b/api/package-lock.json index c16d5c8..073c9a1 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,19 +10,22 @@ "apollo-server-koa": "^3.7.0", "dotenv": "^16.0.1", "graphql": "^16.5.0", - "graphql-upload": "^13.0.0", + "graphql-upload": "^14.0.0", "koa": "^2.13.4", "make-dir": "^3.1.0", "shortid": "^2.2.16" }, "devDependencies": { + "@types/koa": "^2.13.4", + "@types/node": "^17.0.35", "eslint": "^8.16.0", "eslint-plugin-simple-import-sort": "^7.0.0", "nodemon": "^2.0.16", - "prettier": "^2.6.2" + "prettier": "^2.6.2", + "typescript": "^4.7.1-rc" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >= 16.0.0", + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0", "npm": ">= 7" }, "funding": { @@ -283,6 +286,14 @@ "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.3.2.tgz", + "integrity": "sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -397,6 +408,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==" }, + "node_modules/@types/object-path": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.1.tgz", + "integrity": "sha512-219LSCO9HPcoXcRTC6DbCs0FRhZgBnEMzf16RRqkT40WbkKx3mOeQuz3e2XqbfhOz/AHfbru0kzB1n1RCAsIIg==" + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -1018,14 +1034,6 @@ "node": ">= 0.8" } }, - "node_modules/cookies/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==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/copy-to": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", @@ -1119,11 +1127,11 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/destroy": { @@ -1649,23 +1657,36 @@ } }, "node_modules/graphql-upload": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-13.0.0.tgz", - "integrity": "sha512-YKhx8m/uOtKu4Y1UzBFJhbBGJTlk7k4CydlUUiNrtxnwZv0WigbRHP+DVhRNKt7u7DXOtcKZeYJlGtnMXvreXA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-14.0.0.tgz", + "integrity": "sha512-l3utaN0p/VLUFFrUAijwEfmsSxEolVri5uuD91yhbOkxRD6q5l0rsy1F4Naq4TKWFSKsGciCjyrc1ZhCT2Ew7A==", "dependencies": { + "@types/busboy": "^0.3.2", + "@types/node": "*", + "@types/object-path": "^0.11.1", "busboy": "^0.3.1", "fs-capacitor": "^6.2.0", - "http-errors": "^1.8.1", + "http-errors": "^2.0.0", "object-path": "^0.11.8" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >= 16.0.0" + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/jaydenseric" }, "peerDependencies": { - "graphql": "0.13.1 - 16" + "@types/express": "^4.0.29", + "@types/koa": "^2.11.4", + "graphql": "^16.3.0" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/koa": { + "optional": true + } } }, "node_modules/has": { @@ -1734,13 +1755,15 @@ "node": ">= 0.8" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/http-errors": { + "node_modules/http-assert/node_modules/http-errors": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", @@ -1755,6 +1778,35 @@ "node": ">= 0.6" } }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2100,12 +2152,35 @@ "node": ">= 10" } }, - "node_modules/koa/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==", + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" } }, "node_modules/latest-version": { @@ -2623,37 +2698,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/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==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -2883,11 +2927,11 @@ "dev": true }, "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/streamsearch": { @@ -3058,6 +3102,19 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.7.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.1-rc.tgz", + "integrity": "sha512-EQd2NVelDe6ZVc2sO1CSpuSs+RHzY8c2n/kTNQAHw4um/eAXY+ZY4IKoUpNK0wO6C5hN+XcUXR7yqT8VbwwNIQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3528,6 +3585,14 @@ "@types/node": "*" } }, + "@types/busboy": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.3.2.tgz", + "integrity": "sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==", + "requires": { + "@types/node": "*" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -3642,6 +3707,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==" }, + "@types/object-path": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.1.tgz", + "integrity": "sha512-219LSCO9HPcoXcRTC6DbCs0FRhZgBnEMzf16RRqkT40WbkKx3mOeQuz3e2XqbfhOz/AHfbru0kzB1n1RCAsIIg==" + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -4109,13 +4179,6 @@ "requires": { "depd": "~2.0.0", "keygrip": "~1.1.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } } }, "copy-to": { @@ -4191,9 +4254,9 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { "version": "1.2.0", @@ -4587,13 +4650,16 @@ } }, "graphql-upload": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-13.0.0.tgz", - "integrity": "sha512-YKhx8m/uOtKu4Y1UzBFJhbBGJTlk7k4CydlUUiNrtxnwZv0WigbRHP+DVhRNKt7u7DXOtcKZeYJlGtnMXvreXA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-14.0.0.tgz", + "integrity": "sha512-l3utaN0p/VLUFFrUAijwEfmsSxEolVri5uuD91yhbOkxRD6q5l0rsy1F4Naq4TKWFSKsGciCjyrc1ZhCT2Ew7A==", "requires": { + "@types/busboy": "^0.3.2", + "@types/node": "*", + "@types/object-path": "^0.11.1", "busboy": "^0.3.1", "fs-capacitor": "^6.2.0", - "http-errors": "^1.8.1", + "http-errors": "^2.0.0", "object-path": "^0.11.8" } }, @@ -4637,6 +4703,30 @@ "requires": { "deep-equal": "~1.0.1", "http-errors": "~1.8.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } } }, "http-cache-semantics": { @@ -4646,14 +4736,14 @@ "dev": true }, "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" } }, @@ -4898,10 +4988,29 @@ "vary": "^1.1.2" }, "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" } } }, @@ -5291,30 +5400,6 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } } }, "rc": { @@ -5483,9 +5568,9 @@ "dev": true }, "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, "streamsearch": { "version": "0.1.2", @@ -5610,6 +5695,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.7.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.1-rc.tgz", + "integrity": "sha512-EQd2NVelDe6ZVc2sO1CSpuSs+RHzY8c2n/kTNQAHw4um/eAXY+ZY4IKoUpNK0wO6C5hN+XcUXR7yqT8VbwwNIQ==", + "dev": true + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/api/package.json b/api/package.json index aaeea2a..8e936c7 100644 --- a/api/package.json +++ b/api/package.json @@ -16,29 +16,33 @@ "bugs": "https://github.com/jaydenseric/apollo-upload-examples/issues", "funding": "https://github.com/sponsors/jaydenseric", "engines": { - "node": "^12.22.0 || ^14.17.0 || >= 16.0.0", + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0", "npm": ">= 7" }, "dependencies": { "apollo-server-koa": "^3.7.0", "dotenv": "^16.0.1", "graphql": "^16.5.0", - "graphql-upload": "^13.0.0", + "graphql-upload": "^14.0.0", "koa": "^2.13.4", "make-dir": "^3.1.0", "shortid": "^2.2.16" }, "devDependencies": { + "@types/koa": "^2.13.4", + "@types/node": "^17.0.35", "eslint": "^8.16.0", "eslint-plugin-simple-import-sort": "^7.0.0", "nodemon": "^2.0.16", - "prettier": "^2.6.2" + "prettier": "^2.6.2", + "typescript": "^4.7.1-rc" }, "scripts": { "dev": "nodemon", "start": "node -r dotenv/config server.mjs", "eslint": "eslint .", "prettier": "prettier -c .", - "test": "npm run eslint && npm run prettier" + "types": "tsc -p jsconfig.json", + "test": "npm run eslint && npm run prettier && npm run types" } } diff --git a/api/schema/FileType.mjs b/api/schema/FileType.mjs index 7be414e..e655900 100644 --- a/api/schema/FileType.mjs +++ b/api/schema/FileType.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; import UPLOAD_DIRECTORY_URL from "../config/UPLOAD_DIRECTORY_URL.mjs"; diff --git a/api/schema/MutationType.mjs b/api/schema/MutationType.mjs index 63b0863..063c738 100644 --- a/api/schema/MutationType.mjs +++ b/api/schema/MutationType.mjs @@ -1,5 +1,7 @@ +// @ts-check + import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; -import GraphQLUpload from "graphql-upload/public/GraphQLUpload.js"; +import GraphQLUpload from "graphql-upload/GraphQLUpload.js"; import storeUpload from "../storeUpload.mjs"; import FileType from "./FileType.mjs"; @@ -29,15 +31,25 @@ export default new GraphQLObjectType({ ), }, }, - async resolve(parent, { files }) { + async resolve( + parent, + /** + * @type {{ files: Array< + * Promise + * >}} + */ + { files } + ) { + /** @type {Array} */ + const storedFileNames = []; + // Ensure an error storing one upload doesn’t prevent storing the rest. - const results = await Promise.allSettled(files.map(storeUpload)); - return results.reduce((storedFiles, { value, reason }) => { - if (value) storedFiles.push(value); + for (const result of await Promise.allSettled(files.map(storeUpload))) + if ("value" in result) storedFileNames.push(result.value); // Realistically you would do more than just log an error. - else console.error(`Failed to store upload: ${reason}`); - return storedFiles; - }, []); + else console.error(`Failed to store upload: ${result.reason}`); + + return storedFileNames; }, }, }), diff --git a/api/schema/QueryType.mjs b/api/schema/QueryType.mjs index eb102a8..b770896 100644 --- a/api/schema/QueryType.mjs +++ b/api/schema/QueryType.mjs @@ -1,3 +1,5 @@ +// @ts-check + import fs from "fs"; import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; diff --git a/api/schema/index.mjs b/api/schema/index.mjs index ac807b5..2c3432e 100644 --- a/api/schema/index.mjs +++ b/api/schema/index.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { GraphQLSchema } from "graphql"; import MutationType from "./MutationType.mjs"; diff --git a/api/server.mjs b/api/server.mjs index e44fb8f..cd9a098 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -1,5 +1,7 @@ +// @ts-check + import { ApolloServer } from "apollo-server-koa"; -import graphqlUploadKoa from "graphql-upload/public/graphqlUploadKoa.js"; +import graphqlUploadKoa from "graphql-upload/graphqlUploadKoa.js"; import Koa from "koa"; import makeDir from "make-dir"; import { fileURLToPath } from "url"; @@ -7,9 +9,7 @@ import { fileURLToPath } from "url"; import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs"; import schema from "./schema/index.mjs"; -/** - * Starts the API server. - */ +/** Starts the API server. */ async function startServer() { // Ensure the upload directory exists. await makeDir(fileURLToPath(UPLOAD_DIRECTORY_URL)); diff --git a/api/storeUpload.mjs b/api/storeUpload.mjs index fabec45..6d12fdb 100644 --- a/api/storeUpload.mjs +++ b/api/storeUpload.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { createWriteStream, unlink } from "fs"; import shortId from "shortid"; @@ -5,7 +7,9 @@ import UPLOAD_DIRECTORY_URL from "./config/UPLOAD_DIRECTORY_URL.mjs"; /** * Stores a GraphQL file upload in the filesystem. - * @param {Promise} upload GraphQL file upload. + * @param {Promise< + * import("graphql-upload/processRequest.js").FileUpload + * >} upload GraphQL file upload. * @returns {Promise} Resolves the stored file name. */ export default async function storeUpload(upload) { diff --git a/app/components/Header.mjs b/app/components/Header.mjs index 93b43d5..967ed92 100644 --- a/app/components/Header.mjs +++ b/app/components/Header.mjs @@ -1,6 +1,14 @@ +// @ts-check + import { createElement as h } from "react"; import styles from "./Header.module.css"; -export const Header = (props) => - h("header", { ...props, className: styles.header }); +/** + * React component for a header. + * @param {object} props Props. + * @param {import("react").ReactNode} [props.children] Children. + */ +export default function Header({ children }) { + return h("header", { className: styles.header }, children); +} diff --git a/app/components/Page.mjs b/app/components/Page.mjs index 8a1a59e..e235cdf 100644 --- a/app/components/Page.mjs +++ b/app/components/Page.mjs @@ -1,11 +1,19 @@ -import Head from "next/head"; -import PropTypes from "prop-types"; +// @ts-check + +import nextHead from "next/head.js"; import { createElement as h, Fragment } from "react"; -export const Page = ({ title, children }) => - h(Fragment, null, h(Head, null, h("title", null, title)), children); - -Page.propTypes = { - title: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -}; +/** + * React component for a page. + * @param {object} props Props. + * @param {string} props.title Title. + * @param {import("react").ReactNode} [props.children] Children. + */ +export default function Page({ title, children }) { + return h( + Fragment, + null, + h(nextHead.default, null, h("title", null, title)), + children + ); +} diff --git a/app/components/Section.mjs b/app/components/Section.mjs index c29f363..2f27311 100644 --- a/app/components/Section.mjs +++ b/app/components/Section.mjs @@ -1,6 +1,14 @@ +// @ts-check + import { createElement as h } from "react"; import styles from "./Section.module.css"; -export const Section = (props) => - h("section", { ...props, className: styles.section }); +/** + * React component for a section. + * @param {object} props Props. + * @param {import("react").ReactNode} [props.children] Children. + */ +export default function Section({ children }) { + return h("section", { className: styles.section }, children); +} diff --git a/app/components/UploadBlob.mjs b/app/components/UploadBlob.mjs index ccc993b..57963e0 100644 --- a/app/components/UploadBlob.mjs +++ b/app/components/UploadBlob.mjs @@ -1,4 +1,8 @@ -import { gql, useApolloClient, useMutation } from "@apollo/client"; +// @ts-check + +import { gql } from "@apollo/client/core"; +import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js"; +import { useMutation } from "@apollo/client/react/hooks/useMutation.js"; import ButtonSubmit from "device-agnostic-ui/ButtonSubmit.mjs"; import Code from "device-agnostic-ui/Code.mjs"; import Fieldset from "device-agnostic-ui/Fieldset.mjs"; @@ -13,7 +17,8 @@ const SINGLE_UPLOAD_MUTATION = gql` } `; -export function UploadBlob() { +/** React component for a uploading a blob. */ +export default function UploadBlob() { const [name, setName] = useState(""); const [content, setContent] = useState(""); const [singleUploadMutation, { loading }] = useMutation( @@ -21,18 +26,36 @@ export function UploadBlob() { ); const apolloClient = useApolloClient(); - const onNameChange = ({ target: { value } }) => setName(value); - const onContentChange = ({ target: { value } }) => setContent(value); - const onSubmit = (event) => { + /** + * @type {import("react").ChangeEventHandler< + * HTMLInputElement | HTMLTextAreaElement + * >} + */ + function onNameChange({ target: { value } }) { + setName(value); + } + + /** + * @type {import("react").ChangeEventHandler< + * HTMLInputElement | HTMLTextAreaElement + * >} + */ + function onContentChange({ target: { value } }) { + setContent(value); + } + + /** @type {import("react").FormEventHandler} */ + function onSubmit(event) { event.preventDefault(); - const file = new Blob([content], { type: "text/plain" }); - file.name = `${name}.txt`; - - singleUploadMutation({ variables: { file } }).then(() => { + singleUploadMutation({ + variables: { + file: new File([content], `${name}.txt`, { type: "text/plain" }), + }, + }).then(() => { apolloClient.resetStore(); }); - }; + } return h( "form", diff --git a/app/components/UploadFile.mjs b/app/components/UploadFile.mjs index 81836ab..1ab7ab7 100644 --- a/app/components/UploadFile.mjs +++ b/app/components/UploadFile.mjs @@ -1,4 +1,8 @@ -import { gql, useApolloClient, useMutation } from "@apollo/client"; +// @ts-check + +import { gql } from "@apollo/client/core"; +import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js"; +import { useMutation } from "@apollo/client/react/hooks/useMutation.js"; import { createElement as h } from "react"; const SINGLE_UPLOAD_MUTATION = gql` @@ -9,20 +13,18 @@ const SINGLE_UPLOAD_MUTATION = gql` } `; -export function UploadFile() { +/** React component for a uploading a single file. */ +export default function UploadFile() { const [uploadFileMutation] = useMutation(SINGLE_UPLOAD_MUTATION); const apolloClient = useApolloClient(); - const onChange = ({ - target: { - validity, - files: [file], - }, - }) => - validity.valid && - uploadFileMutation({ variables: { file } }).then(() => { - apolloClient.resetStore(); - }); + /** @type {import("react").ChangeEventHandler} */ + function onChange({ target: { validity, files } }) { + if (validity.valid && files && files[0]) + uploadFileMutation({ variables: { file: files[0] } }).then(() => { + apolloClient.resetStore(); + }); + } return h("input", { type: "file", required: true, onChange }); } diff --git a/app/components/UploadFileList.mjs b/app/components/UploadFileList.mjs index 7202d82..ce10337 100644 --- a/app/components/UploadFileList.mjs +++ b/app/components/UploadFileList.mjs @@ -1,4 +1,8 @@ -import { gql, useApolloClient, useMutation } from "@apollo/client"; +// @ts-check + +import { gql } from "@apollo/client/core"; +import { useApolloClient } from "@apollo/client/react/hooks/useApolloClient.js"; +import { useMutation } from "@apollo/client/react/hooks/useMutation.js"; import { createElement as h } from "react"; const MULTIPLE_UPLOAD_MUTATION = gql` @@ -9,15 +13,33 @@ const MULTIPLE_UPLOAD_MUTATION = gql` } `; -export function UploadFileList() { - const [multipleUploadMutation] = useMutation(MULTIPLE_UPLOAD_MUTATION); +/** + * @typedef {{ + * multipleUpload: { + * id: string, + * }, + * }} MultipleUploadMutationData + */ + +/** React component for a uploading a file list. */ +export default function UploadFileList() { + const [multipleUploadMutation] = + /** + * @type {import("@apollo/client/react/types/types.js").MutationTuple< + * MultipleUploadMutationData, + * { files: FileList } + * >} + */ + (useMutation(MULTIPLE_UPLOAD_MUTATION)); const apolloClient = useApolloClient(); - const onChange = ({ target: { validity, files } }) => - validity.valid && - multipleUploadMutation({ variables: { files } }).then(() => { - apolloClient.resetStore(); - }); + /** @type {import("react").ChangeEventHandler} */ + function onChange({ target: { validity, files } }) { + if (validity.valid && files && files[0]) + multipleUploadMutation({ variables: { files } }).then(() => { + apolloClient.resetStore(); + }); + } return h("input", { type: "file", multiple: true, required: true, onChange }); } diff --git a/app/components/Uploads.mjs b/app/components/Uploads.mjs index 87f72c0..041f98f 100644 --- a/app/components/Uploads.mjs +++ b/app/components/Uploads.mjs @@ -1,4 +1,7 @@ -import { gql, useQuery } from "@apollo/client"; +// @ts-check + +import { gql } from "@apollo/client/core"; +import { useQuery } from "@apollo/client/react/hooks/useQuery.js"; import Scroll from "device-agnostic-ui/Scroll.mjs"; import Table from "device-agnostic-ui/Table.mjs"; import { createElement as h } from "react"; @@ -12,8 +15,24 @@ const UPLOADS_QUERY = gql` } `; -export function Uploads() { - const { data: { uploads = [] } = {} } = useQuery(UPLOADS_QUERY); +/** + * @typedef {{ + * uploads: Array<{ + * id: string, + * url: string + * }>, + * }} UploadsQueryData + */ + +/** React component for displaying uploads. */ +export default function Uploads() { + const { data: { uploads = [] } = {} } = + /** + * @type {import("@apollo/client/react/types/types.js").QueryResult< + * UploadsQueryData + * >} + */ + (useQuery(UPLOADS_QUERY)); return h( Scroll, diff --git a/app/jsconfig.json b/app/jsconfig.json new file mode 100644 index 0000000..7935e73 --- /dev/null +++ b/app/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "maxNodeModuleJsDepth": 10, + "module": "nodenext", + "noEmit": true, + "strict": true + }, + "typeAcquisition": { + "enable": false + } +} diff --git a/app/next.config.mjs b/app/next.config.mjs index 93655ef..e5882cf 100644 --- a/app/next.config.mjs +++ b/app/next.config.mjs @@ -1,3 +1,5 @@ +// @ts-check + export default { env: { API_URI: process.env.API_URI, diff --git a/app/package-lock.json b/app/package-lock.json index 5ead567..bb7a3ff 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -12,12 +12,14 @@ "device-agnostic-ui": "~10.0.0", "graphql": "^16.5.0", "next": "^12.1.6", - "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "@babel/eslint-parser": "^7.17.0", + "@types/node": "^17.0.35", + "@types/react": "^17.0.45", + "@types/react-dom": "^17.0.17", "babel-plugin-graphql-tag": "^3.3.0", "eslint": "^8.16.0", "eslint-plugin-react-hooks": "^4.5.0", @@ -25,10 +27,11 @@ "prettier": "^2.6.2", "stylelint": "^14.8.3", "stylelint-config-recommended": "^7.0.0", - "stylelint-prettier": "^2.0.0" + "stylelint-prettier": "^2.0.0", + "typescript": "^4.7.1-rc" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >= 16.0.0", + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0", "npm": ">= 7" }, "funding": { @@ -764,6 +767,12 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "node_modules/@types/node": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", + "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==", + "dev": true + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -776,6 +785,38 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.45.tgz", + "integrity": "sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz", + "integrity": "sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==", + "dev": true, + "dependencies": { + "@types/react": "^17" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "node_modules/@wry/context": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", @@ -1181,6 +1222,12 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3637,6 +3684,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "4.7.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.1-rc.tgz", + "integrity": "sha512-EQd2NVelDe6ZVc2sO1CSpuSs+RHzY8c2n/kTNQAHw4um/eAXY+ZY4IKoUpNK0wO6C5hN+XcUXR7yqT8VbwwNIQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4234,6 +4294,12 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "@types/node": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", + "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==", + "dev": true + }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -4246,6 +4312,38 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.45.tgz", + "integrity": "sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz", + "integrity": "sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==", + "dev": true, + "requires": { + "@types/react": "^17" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@wry/context": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", @@ -4530,6 +4628,12 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csstype": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6316,6 +6420,12 @@ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", "dev": true }, + "typescript": { + "version": "4.7.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.1-rc.tgz", + "integrity": "sha512-EQd2NVelDe6ZVc2sO1CSpuSs+RHzY8c2n/kTNQAHw4um/eAXY+ZY4IKoUpNK0wO6C5hN+XcUXR7yqT8VbwwNIQ==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/app/package.json b/app/package.json index 93918d2..8537440 100644 --- a/app/package.json +++ b/app/package.json @@ -16,22 +16,24 @@ "bugs": "https://github.com/jaydenseric/apollo-upload-examples/issues", "funding": "https://github.com/sponsors/jaydenseric", "engines": { - "node": "^12.22.0 || ^14.17.0 || >= 16.0.0", + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0", "npm": ">= 7" }, - "browserslist": "Node 12.22 - 13 and Node < 13, Node 14.17 - 15 and Node < 15, Node >= 16, > 0.5%, not OperaMini all, not IE > 0, not dead", + "browserslist": "Node 14.17 - 15 and Node < 15, Node 16 - 17 and Node < 17, Node >= 18, > 0.5%, not OperaMini all, not IE > 0, not dead", "dependencies": { "@apollo/client": "^3.6.4", "apollo-upload-client": "^17.0.0", "device-agnostic-ui": "~10.0.0", "graphql": "^16.5.0", "next": "^12.1.6", - "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "@babel/eslint-parser": "^7.17.0", + "@types/node": "^17.0.35", + "@types/react": "^17.0.45", + "@types/react-dom": "^17.0.17", "babel-plugin-graphql-tag": "^3.3.0", "eslint": "^8.16.0", "eslint-plugin-react-hooks": "^4.5.0", @@ -39,7 +41,8 @@ "prettier": "^2.6.2", "stylelint": "^14.8.3", "stylelint-config-recommended": "^7.0.0", - "stylelint-prettier": "^2.0.0" + "stylelint-prettier": "^2.0.0", + "typescript": "^4.7.1-rc" }, "scripts": { "dev": "next dev", @@ -48,6 +51,7 @@ "eslint": "eslint .", "stylelint": "stylelint '**/*.css'", "prettier": "prettier -c .", - "test": "npm run eslint && npm run stylelint && npm run prettier" + "types": "tsc -p jsconfig.json", + "test": "npm run eslint && npm run stylelint && npm run prettier && npm run types" } } diff --git a/app/pages/_app.mjs b/app/pages/_app.mjs index 34ea463..3f861cc 100644 --- a/app/pages/_app.mjs +++ b/app/pages/_app.mjs @@ -1,3 +1,5 @@ +// @ts-check + import "device-agnostic-ui/theme.css"; import "device-agnostic-ui/global.css"; import "device-agnostic-ui/Button.css"; @@ -11,12 +13,18 @@ import "device-agnostic-ui/Scroll.css"; import "device-agnostic-ui/Table.css"; import "device-agnostic-ui/Textbox.css"; -import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache/inmemory/inMemoryCache.js"; +import { ApolloClient } from "@apollo/client/core/ApolloClient.js"; +import { ApolloProvider } from "@apollo/client/react/context/ApolloProvider.js"; import { createUploadLink } from "apollo-upload-client"; -import Head from "next/head"; -import PropTypes from "prop-types"; -import { createElement as h } from "react"; +import nextApp from "next/app.js"; +import nextHead from "next/head.js"; +import { createElement as h, Fragment } from "react"; +/** + * Creates an Apollo Client instance. + * @param {{ [key: string]: unknown }} [cache] Apollo Client initial cache. + */ const createApolloClient = (cache = {}) => new ApolloClient({ ssrMode: typeof window === "undefined", @@ -24,64 +32,75 @@ const createApolloClient = (cache = {}) => link: createUploadLink({ uri: process.env.API_URI }), }); -const App = ({ +/** + * React component for the Next.js app. + * @param {import("next/app.js").AppProps & AppCustomProps} props Props. + */ +function App({ Component, pageProps, apolloCache, apolloClient = createApolloClient(apolloCache), -}) => - h( - ApolloProvider, - { client: apolloClient }, - h( - Head, +}) { + return h(ApolloProvider, { + client: apolloClient, + children: h( + Fragment, null, - h("meta", { - name: "viewport", - content: "width=device-width, initial-scale=1", - }), - h("meta", { name: "color-scheme", content: "light dark" }), - h("meta", { name: "theme-color", content: "white" }), - h("link", { rel: "manifest", href: "/manifest.webmanifest" }) + h( + nextHead.default, + null, + h("meta", { + name: "viewport", + content: "width=device-width, initial-scale=1", + }), + h("meta", { name: "color-scheme", content: "light dark" }), + h("meta", { name: "theme-color", content: "white" }), + h("link", { rel: "manifest", href: "/manifest.webmanifest" }) + ), + h(Component, pageProps) ), - h(Component, pageProps) - ); + }); +} -App.getInitialProps = async (context) => { - const props = { - pageProps: context.Component.getInitialProps - ? await context.Component.getInitialProps(context) - : {}, - }; +if (typeof window === "undefined") + App.getInitialProps = + /** @param {import("next/app.js").AppContext} context */ + async function getInitialProps(context) { + const apolloClient = createApolloClient(); + const [props, { default: ReactDOMServer }, { getMarkupFromTree }] = + await Promise.all([ + nextApp.default.getInitialProps(context), + import("react-dom/server.js"), + import("@apollo/client/react/ssr/getDataFromTree.js"), + ]); - if (context.ctx.req) { - const apolloClient = createApolloClient(); - try { - const { getDataFromTree } = await import("@apollo/client/react/ssr"); - await getDataFromTree( - h(App, { - ...props, - apolloClient, - router: context.router, - Component: context.Component, - }) - ); - } catch (error) { - // Prevent crash from GraphQL errors. - console.error(error); - } + try { + await getMarkupFromTree({ + tree: h(App, { + ...props, + apolloClient, + router: context.router, + Component: context.Component, + }), + renderFunction: ReactDOMServer.renderToStaticMarkup, + }); + } catch (error) { + // Prevent crash from GraphQL errors. + console.error(error); + } - props.apolloCache = apolloClient.cache.extract(); - } - - return props; -}; - -App.propTypes = { - Component: PropTypes.elementType.isRequired, - pageProps: PropTypes.object, - apolloCache: PropTypes.object, - apolloClient: PropTypes.instanceOf(ApolloClient), -}; + return { + ...props, + apolloCache: apolloClient.cache.extract(), + }; + }; export default App; + +/** + * Next.js app custom props. + * @typedef {object} AppCustomProps + * @prop {{ [key: string]: unknown }} [apolloCache] Apollo Client initial cache. + * @prop {ApolloClient} apolloClient Apollo Client. + */ diff --git a/app/pages/index.mjs b/app/pages/index.mjs index 3e9ebe3..e28b379 100644 --- a/app/pages/index.mjs +++ b/app/pages/index.mjs @@ -1,15 +1,17 @@ +// @ts-check + import Code from "device-agnostic-ui/Code.mjs"; import Heading from "device-agnostic-ui/Heading.mjs"; import Margin from "device-agnostic-ui/Margin.mjs"; import { createElement as h } from "react"; -import { Header } from "../components/Header.mjs"; -import { Page } from "../components/Page.mjs"; -import { Section } from "../components/Section.mjs"; -import { UploadBlob } from "../components/UploadBlob.mjs"; -import { UploadFile } from "../components/UploadFile.mjs"; -import { UploadFileList } from "../components/UploadFileList.mjs"; -import { Uploads } from "../components/Uploads.mjs"; +import Header from "../components/Header.mjs"; +import Page from "../components/Page.mjs"; +import Section from "../components/Section.mjs"; +import UploadBlob from "../components/UploadBlob.mjs"; +import UploadFile from "../components/UploadFile.mjs"; +import UploadFileList from "../components/UploadFileList.mjs"; +import Uploads from "../components/Uploads.mjs"; export default function IndexPage() { return h( diff --git a/app/typings.d.ts b/app/typings.d.ts new file mode 100644 index 0000000..f2d12bb --- /dev/null +++ b/app/typings.d.ts @@ -0,0 +1,4 @@ +declare module "*.module.css" { + const classes: { [key: string]: string }; + export default classes; +}