From c31010dd4164f65ff447e49d69338f6f38ad4c32 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Thu, 5 Feb 2026 13:54:24 -0500 Subject: [PATCH 1/6] Switch to workspace configuration --- frontend/package.json | 29 ++ frontend/src/service.ts | 30 +- package-lock.json | 317 ++++++------------ package.json | 54 +-- server/package.json | 25 ++ {src => server/src}/api.spec.ts | 0 {src => server/src}/app.ts | 2 +- {src => server/src}/auth.service.ts | 0 {src => server/src}/server.ts | 4 +- .../src}/transcript.service.spec.ts | 0 {src => server/src}/transcript.service.ts | 7 +- tsconfig.json => server/tsconfig.json | 4 +- shared/package.json | 28 ++ shared/shared.spec.ts | 37 ++ shared/shared.ts | 41 +++ shared/tsconfig.json | 22 ++ src/types.ts | 8 - 17 files changed, 324 insertions(+), 284 deletions(-) create mode 100644 frontend/package.json create mode 100644 server/package.json rename {src => server/src}/api.spec.ts (100%) rename {src => server/src}/app.ts (97%) rename {src => server/src}/auth.service.ts (100%) rename {src => server/src}/server.ts (87%) rename {src => server/src}/transcript.service.spec.ts (100%) rename {src => server/src}/transcript.service.ts (95%) rename tsconfig.json => server/tsconfig.json (86%) create mode 100644 shared/package.json create mode 100644 shared/shared.spec.ts create mode 100644 shared/shared.ts create mode 100644 shared/tsconfig.json delete mode 100644 src/types.ts diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..22ede50 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "@cs4530-workspaces/frontend", + "version": "1.0.0", + "description": "CS4530 Template", + "type": "module", + "scripts": { + "check": "tsc", + "lint": "eslint .", + "prettier": "prettier --ignore-path ../.gitignore --check .", + "prettier:fix": "prettier --ignore-path ../.gitignore --write .", + "test": "vitest run", + "dev": "vite", + "build": "tsc -b && vite build" + }, + "author": "Rob Simmons", + "dependencies": { + "@cs4530-workspaces/shared": "^1.0.0", + "express": "^5.2.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.3", + "vitest": "^4.0.18" + } +} diff --git a/frontend/src/service.ts b/frontend/src/service.ts index 822a736..c935b37 100644 --- a/frontend/src/service.ts +++ b/frontend/src/service.ts @@ -1,4 +1,13 @@ import { z } from "zod"; +import { + zAddGradeResponse, + zAddStudentResponse, + zError, + zGetTranscriptResponse, + type AddGradeResponse, + type AddStudentResponse, + type GetTranscriptResponse, +} from "@cs4530-workspaces/shared"; export class ServiceError extends Error { constructor(message: string) { @@ -6,9 +15,6 @@ export class ServiceError extends Error { } } -const zError = z.object({ error: z.string() }); - -const zAddStudentResponse = z.object({ studentID: z.int() }); /** * Validate inputs and call the `addStudent` api * @@ -20,7 +26,7 @@ const zAddStudentResponse = z.object({ studentID: z.int() }); export async function addStudent( password: string, studentName: string, -): Promise> { +): Promise { if (studentName === "") throw new ServiceError("Student name must be non-empty"); const response = await fetch("/api/addStudent", { @@ -36,7 +42,6 @@ export async function addStudent( return data; } -const zAddGradeResponse = z.object({ success: z.literal(true) }); /** * Validate inputs and call the `addGrade` api * @@ -52,7 +57,7 @@ export async function addGrade( studentIDStr: string, courseName: string, courseGradeStr: string, -): Promise> { +): Promise { const studentID = parseInt(studentIDStr); if (isNaN(studentID) || `${studentID}` !== studentIDStr || studentID < 0) { throw new ServiceError("Student ID is invalid"); @@ -87,17 +92,6 @@ export async function addGrade( return data; } -const zGetTranscriptResponse = z.union([ - z.object({ success: z.literal(false) }), - z.object({ - success: z.literal(true), - transcript: z.object({ - student: z.object({ studentID: z.int(), studentName: z.string() }), - grades: z.array(z.object({ course: z.string(), grade: z.number() })), - }), - }), -]); - /** * Validate inputs and call the `getTranscript` API * @@ -109,7 +103,7 @@ const zGetTranscriptResponse = z.union([ export async function getTranscript( password: string, studentIDStr: string, -): Promise> { +): Promise { const studentID = parseInt(studentIDStr); if (isNaN(studentID) || `${studentID}` !== studentIDStr || studentID < 0) { throw new ServiceError("Student ID is invalid"); diff --git a/package-lock.json b/package-lock.json index 49400b7..c663a49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,40 +1,33 @@ { - "name": "cs4530-vite", + "name": "cs4530-workspaces", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cs4530-vite", + "name": "cs4530-workspaces", + "version": "1.0.0", + "workspaces": [ + "frontend", + "server", + "shared" + ] + }, + "frontend": { + "name": "@cs4530-workspaces/frontend", "version": "1.0.0", "dependencies": { + "@cs4530-workspaces/shared": "^1.0.0", "express": "^5.2.1", "react": "^19.2.3", "react-dom": "^19.2.3", "zod": "^4.3.6" }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/express": "^5.0.6", - "@types/react": "^19.2.9", + "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", - "@types/supertest": "^6.0.3", - "@vitejs/plugin-react": "^5.1.2", - "@vitest/coverage-v8": "^4.0.18", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.0", - "npm-run-all2": "^8.0.4", - "prettier": "^3.7.4", - "supertest": "^7.2.2", - "typescript": "^5.9.3", - "typescript-eslint": "^8.50.0", - "vite": "^7.3.1", - "vitest": "^4.0.17" + "@vitejs/plugin-react": "^5.1.3", + "vitest": "^4.0.18" } }, "node_modules/@babel/code-frame": { @@ -52,6 +45,13 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/compat-data": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", @@ -94,6 +94,16 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", @@ -128,6 +138,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -330,6 +350,18 @@ "node": ">=18" } }, + "node_modules/@cs4530-workspaces/frontend": { + "resolved": "frontend", + "link": true + }, + "node_modules/@cs4530-workspaces/server": { + "resolved": "server", + "link": true + }, + "node_modules/@cs4530-workspaces/shared": { + "resolved": "shared", + "link": true + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -1939,19 +1971,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", @@ -2654,13 +2673,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3672,6 +3684,16 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.5.5", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", @@ -4614,19 +4636,6 @@ "semver": "^7.7.1" } }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -4998,9 +5007,9 @@ } }, "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -5037,16 +5046,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5169,19 +5168,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5200,15 +5186,6 @@ "node": ">= 0.8" } }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -5356,82 +5333,6 @@ "dev": true, "license": "MIT" }, - "node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-run-all2": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz", - "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "cross-spawn": "^7.0.6", - "memorystream": "^0.3.1", - "picomatch": "^4.0.2", - "pidtree": "^0.6.0", - "read-package-json-fast": "^4.0.0", - "shell-quote": "^1.7.3", - "which": "^5.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "npm-run-all2": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": "^20.5.0 || >=22.0.0", - "npm": ">= 10" - } - }, - "node_modules/npm-run-all2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm-run-all2/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm-run-all2/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5707,6 +5608,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5714,19 +5616,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5900,20 +5789,6 @@ "node": ">=0.10.0" } }, - "node_modules/read-package-json-fast": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", - "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6128,13 +6003,16 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -6260,19 +6138,6 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -7269,6 +7134,40 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "server": { + "name": "@cs4530-workspaces/server", + "version": "1.0.0", + "dependencies": { + "express": "^5.2.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/supertest": "^6.0.3", + "@vitest/coverage-v8": "^4.0.18", + "supertest": "^7.2.2", + "vitest": "^4.0.18" + } + }, + "shared": { + "name": "@cs4530-workspaces/shared", + "version": "1.0.0", + "dependencies": { + "zod": "^4.3.6" + }, + "devDependencies": { + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.0", + "prettier": "^3.7.4", + "typescript-eslint": "^8.50.0", + "vitest": "^4.0.17" + } } } } diff --git a/package.json b/package.json index e1a52a9..0a17194 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,21 @@ { - "name": "cs4530-fullstack", + "name": "cs4530-workspaces", "version": "1.0.0", "description": "CS4530 Template", - "type": "module", + "author": "Rob Simmons", + "workspaces": [ + "frontend", + "server", + "shared" + ], "scripts": { - "check": "npm-run-all check:*", - "check:frontend": "tsc -p frontend/tsconfig.json", - "check:server": "tsc", + "check": "npm run check --workspaces", "lint": "eslint .", - "lint:fix": "eslint . --fix", "prettier": "prettier --check .", "prettier:fix": "prettier --write .", - "test": "vitest run --coverage", - "dev": "echo \"Run \\`npm run dev:server\\` and \\`npm run dev:frontend\\` separately\\n\"", - "dev:frontend": "vite frontend", - "dev:server": "node --watch ./src/server.ts", - "build": "tsc -p frontend/tsconfig.json --noEmit && vite build frontend", - "start": "MODE=production node ./src/server.ts" - }, - "author": "Rob Simmons", - "devDependencies": { - "@eslint/js": "^9.39.2", - "@types/express": "^5.0.6", - "@types/react": "^19.2.9", - "@types/react-dom": "^19.2.3", - "@types/supertest": "^6.0.3", - "@vitejs/plugin-react": "^5.1.2", - "@vitest/coverage-v8": "^4.0.18", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.0", - "npm-run-all2": "^8.0.4", - "prettier": "^3.7.4", - "supertest": "^7.2.2", - "typescript": "^5.9.3", - "typescript-eslint": "^8.50.0", - "vite": "^7.3.1", - "vitest": "^4.0.17" - }, - "dependencies": { - "express": "^5.2.1", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "zod": "^4.3.6" + "test": "npm run test --workspaces", + "dev": "echo \"Run \\`npm run dev -w=server\\` and \\`npm run dev -w=frontend\\` separately\\n\"", + "build": "npm run build -w=frontend", + "start": "npm start -w=server" } } diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..6264410 --- /dev/null +++ b/server/package.json @@ -0,0 +1,25 @@ +{ + "name": "@cs4530-workspaces/server", + "version": "1.0.0", + "type": "module", + "scripts": { + "check": "tsc", + "lint": "eslint .", + "prettier": "prettier --ignore-path ../.gitignore --check . ", + "prettier:fix": "prettier --ignore-path ../.gitignore --write .", + "test": "vitest run --coverage", + "dev": "node --watch ./src/server.ts", + "start": "MODE=production node ./src/server.ts" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/supertest": "^6.0.3", + "@vitest/coverage-v8": "^4.0.18", + "supertest": "^7.2.2", + "vitest": "^4.0.18" + }, + "dependencies": { + "express": "^5.2.1", + "zod": "^4.3.6" + } +} diff --git a/src/api.spec.ts b/server/src/api.spec.ts similarity index 100% rename from src/api.spec.ts rename to server/src/api.spec.ts diff --git a/src/app.ts b/server/src/app.ts similarity index 97% rename from src/app.ts rename to server/src/app.ts index 4c610ad..5057f91 100644 --- a/src/app.ts +++ b/server/src/app.ts @@ -2,7 +2,7 @@ import express from "express"; import { z } from "zod"; import { checkPassword } from "./auth.service.ts"; import { TranscriptDB } from "./transcript.service.ts"; -import type { Transcript } from "./types.ts"; +import type { Transcript } from "@cs4530-workspaces/shared"; export const app = express(); app.use(express.json()); diff --git a/src/auth.service.ts b/server/src/auth.service.ts similarity index 100% rename from src/auth.service.ts rename to server/src/auth.service.ts diff --git a/src/server.ts b/server/src/server.ts similarity index 87% rename from src/server.ts rename to server/src/server.ts index e8c5df6..411027c 100644 --- a/src/server.ts +++ b/server/src/server.ts @@ -16,9 +16,9 @@ import express from "express"; // matter what you do. if (process.env.MODE === "production") { // In production mode, we want to serve the frontend code from Express - app.use(express.static(path.join(import.meta.dirname, "../frontend/dist"))); + app.use(express.static(path.join(import.meta.dirname, "../../frontend/dist"))); app.get(/(.*)/, (req, res) => - res.sendFile(path.join(import.meta.dirname, "../frontend/dist/index.html")), + res.sendFile(path.join(import.meta.dirname, "../../frontend/dist/index.html")), ); } else { app.get("/", (req, res) => { diff --git a/src/transcript.service.spec.ts b/server/src/transcript.service.spec.ts similarity index 100% rename from src/transcript.service.spec.ts rename to server/src/transcript.service.spec.ts diff --git a/src/transcript.service.ts b/server/src/transcript.service.ts similarity index 95% rename from src/transcript.service.ts rename to server/src/transcript.service.ts index 55ec386..da0d5d2 100644 --- a/src/transcript.service.ts +++ b/server/src/transcript.service.ts @@ -1,4 +1,9 @@ -import { type StudentID, type Student, type Course, type Transcript } from "./types.ts"; +import { + type StudentID, + type Student, + type Course, + type Transcript, +} from "@cs4530-workspaces/shared"; export class TranscriptDB { /** diff --git a/tsconfig.json b/server/tsconfig.json similarity index 86% rename from tsconfig.json rename to server/tsconfig.json index 02ea886..84742b5 100644 --- a/tsconfig.json +++ b/server/tsconfig.json @@ -18,7 +18,5 @@ "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUncheckedSideEffectImports": true - }, - "include": ["./**/*.ts"], - "exclude": ["./node_modules", "./client", "./frontend"] + } } diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..a689fd1 --- /dev/null +++ b/shared/package.json @@ -0,0 +1,28 @@ +{ + "name": "@cs4530-workspaces/shared", + "version": "1.0.0", + "type": "module", + "main": "shared.ts", + "scripts": { + "check": "tsc", + "lint": "eslint .", + "prettier": "prettier --ignore-path ../.gitignore --check .", + "prettier:fix": "prettier --ignore-path ../.gitignore --write .", + "test": "vitest run" + }, + "devDependencies": { + "eslint": "^9.39.2", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.0", + "prettier": "^3.7.4", + "typescript-eslint": "^8.50.0", + "vitest": "^4.0.17" + }, + "dependencies": { + "zod": "^4.3.6" + } +} diff --git a/shared/shared.spec.ts b/shared/shared.spec.ts new file mode 100644 index 0000000..50afa7e --- /dev/null +++ b/shared/shared.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { zTranscript } from "./shared.ts"; + +describe("the Transcript validator", () => { + it("forbids omission of the grades element", () => { + const response = zTranscript.safeParse({ + student: { studentID: 4, studentName: "Naomi" }, + }); + expect(response.success).toBe(false); + }); + + it("forbids omission of the student element", () => { + const response = zTranscript.safeParse({ + grades: [], + }); + expect(response.success).toBe(false); + }); + + it("accepts transcripts with no grades", () => { + const response = zTranscript.safeParse({ + student: { studentID: 4, studentName: "Naomi" }, + grades: [], + }); + expect(response.success).toBe(true); + }); + + it("accepts transcripts with duplicate grades", () => { + const response = zTranscript.safeParse({ + student: { studentID: 4, studentName: "Naomi" }, + grades: [ + { course: "Math", grade: 4 }, + { course: "Math", grade: 3 }, + ], + }); + expect(response.success).toBe(true); + }); +}); diff --git a/shared/shared.ts b/shared/shared.ts new file mode 100644 index 0000000..a717c57 --- /dev/null +++ b/shared/shared.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +/** Validator for error responses from the API*/ +export const zError = z.object({ error: z.string() }); + +/** Validator for non-error POST `/api/addStudent` responses */ +export const zAddStudentResponse = z.object({ studentID: z.int() }); +/** Response type for POST /api/addStudent */ +export type AddStudentResponse = z.infer; + +/** Validator for POST `/api/addGrade` responses */ +export const zAddGradeResponse = z.object({ success: z.literal(true) }); +/** Response type for POST `/api/addGrade` */ +export type AddGradeResponse = z.infer; + +/** Validator for student records */ +export const zStudent = z.object({ studentID: z.int(), studentName: z.string() }); +export type Student = z.infer; + +/** Validator for individual course grade records */ +export const zCourseGrade = z.object({ course: z.string(), grade: z.number() }); +export type CourseGrade = z.infer; + +/** Validator for transcripts */ +export const zTranscript = z.object({ student: zStudent, grades: z.array(zCourseGrade) }); +/** Type of student transcripts */ +export type Transcript = z.infer; + +/** Validator for POST `/api/getTranscript` responses */ +export const zGetTranscriptResponse = z.union([ + z.object({ success: z.literal(false) }), + z.object({ success: z.literal(true), transcript: zTranscript }), +]); +/** Response type for POST `/api/zGetTranscript` */ +export type GetTranscriptResponse = z.infer; + +// Aliases for simple types + +export type Course = string; +export type StudentID = number; +export type StudentName = string; diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..84742b5 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,22 @@ +/* Visit https://www.typescriptlang.org/tsconfig/ to learn about this file */ +{ + "compilerOptions": { + /* Basic configuration */ + "lib": ["ES2024"], + "module": "node18", + "skipLibCheck": true, + + /* Support the use of type stripping in Node */ + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "noEmit": true, + "verbatimModuleSyntax": true, + + /* Linting */ + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUncheckedSideEffectImports": true + } +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index b2b9f26..0000000 --- a/src/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// types.ts - types for the transcript service - -export type Course = string; -export type StudentID = number; -export type StudentName = string; -export type Student = { studentID: StudentID; studentName: StudentName }; -export type CourseGrade = { course: Course; grade: number }; -export type Transcript = { student: Student; grades: CourseGrade[] }; From 9e637ef0ad2b43bae1406c07d9ed9caa7accb2cb Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Thu, 5 Feb 2026 13:57:25 -0500 Subject: [PATCH 2/6] Update README --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9008fb6..057024f 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,14 @@ This is a template project for CS4530, Software Engineering at Northeastern. ## Vite+Express Full-stack React Application -This project has two parts: - -1. A minimal Express transcript API for a very simple transcript server -2. A Vite frontend with code that calls that server (this lives in the - `./frontend` directory) +This project has three parts, which together form an +[npm workspaces](https://docs.npmjs.com/cli/v8/using-npm/workspaces) project. + +1. A minimal Express transcript API for a very simple transcript server (in + the `./server` directory) +2. A Vite frontend with code that calls that server (in the `./frontend` + directory) +3. Shared Zod validation and type definitions (in the `./shared` directory) The way this project runs in "production mode" versus "development mode" is very different. From 6e7cb18b3143c4f4338e81659dac1761d18b9105 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Thu, 5 Feb 2026 17:19:24 -0500 Subject: [PATCH 3/6] Re-merge tests --- .github/workflows/main.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7095250..9547831 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,11 +31,8 @@ jobs: - name: Linting run: npm run lint - - name: Run server tests - run: npm run test:server - - name: Install Playwright dependencies run: npx playwright install --with-deps chromium - - name: Run Playwright end-to-end tests - run: npm run test:frontend + - name: Run tests + run: npm run test From e4355704db85af2a6d7a3ac12093b462aec398cf Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Thu, 5 Feb 2026 20:36:58 -0500 Subject: [PATCH 4/6] feels better to have typescript in devDependencies here --- server/package.json | 1 + shared/package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 6264410..037ba49 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "start": "MODE=production node ./src/server.ts" }, "devDependencies": { + "@cs4530-workspaces/shared": "^1.0.0", "@types/express": "^5.0.6", "@types/supertest": "^6.0.3", "@vitest/coverage-v8": "^4.0.18", diff --git a/shared/package.json b/shared/package.json index a689fd1..13910f0 100644 --- a/shared/package.json +++ b/shared/package.json @@ -12,13 +12,14 @@ }, "devDependencies": { "eslint": "^9.39.2", - "eslint-import-resolver-typescript": "^4.4.4", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.0", "prettier": "^3.7.4", + "typescript": "^5.9.3", "typescript-eslint": "^8.50.0", "vitest": "^4.0.17" }, From c45bdbfa79a80d5a95b9cc72ed6e5f5c4f335301 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Sat, 14 Mar 2026 17:50:37 -0400 Subject: [PATCH 5/6] Add comments to vite.config --- frontend/package.json | 3 ++- frontend/vite.config.mjs | 8 +++++++- server/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c8d3c06..7a25cef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,8 @@ "@playwright/test": "^1.58.1", "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.0", "vitest": "^4.0.18" } } diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs index 8e407dc..bd95efd 100644 --- a/frontend/vite.config.mjs +++ b/frontend/vite.config.mjs @@ -1,9 +1,15 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; +// Must match the port the server creates +const DEV_BACKEND_PORT = 3000; + +// https://vite.dev/config/ export default defineConfig({ plugins: [react()], server: { - proxy: { "/api": `http://localhost:3000` }, + proxy: { + "/api": `http://localhost:${DEV_BACKEND_PORT}`, + }, }, }); diff --git a/server/package.json b/server/package.json index 037ba49..b101452 100644 --- a/server/package.json +++ b/server/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@cs4530-workspaces/shared": "^1.0.0", "@types/express": "^5.0.6", - "@types/supertest": "^6.0.3", + "@types/supertest": "^7.2.0", "@vitest/coverage-v8": "^4.0.18", "supertest": "^7.2.2", "vitest": "^4.0.18" From 26eeb5a20aaccc2f9fc248013a9ff3cb8ee3f859 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Mon, 30 Mar 2026 19:23:43 -0400 Subject: [PATCH 6/6] prettier --- server/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index 5c0e8cb..2b9db53 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,6 +14,6 @@ /* Linting */ "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, + "noImplicitReturns": true } }