From bc50f2d983a3da22e2bd4843fb6d33ae2cbb6858 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:18:49 +0700 Subject: [PATCH 1/9] feat: add ProjectFramework enum and integrate it into project preparation and server service tests --- .../submission-project/project-framework.ts | 7 ++++ ...ct-preparation-service.integration.test.ts | 7 ++-- .../server/server-service.integration.test.ts | 32 +++++++++++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 src/entities/submission-project/project-framework.ts diff --git a/src/entities/submission-project/project-framework.ts b/src/entities/submission-project/project-framework.ts new file mode 100644 index 0000000..8630dbf --- /dev/null +++ b/src/entities/submission-project/project-framework.ts @@ -0,0 +1,7 @@ +enum ProjectFramework { + Hapi = 'HAPI', + Express = 'EXPRESS', + Unknown = 'UNKNOWN' +} + +export default ProjectFramework diff --git a/src/service/project-preparation/project-preparation-service.integration.test.ts b/src/service/project-preparation/project-preparation-service.integration.test.ts index ef4e7b9..089961a 100644 --- a/src/service/project-preparation/project-preparation-service.integration.test.ts +++ b/src/service/project-preparation/project-preparation-service.integration.test.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import ProjectPreparationService from "./project-preparation-service"; import SubmissionProject from "../../entities/submission-project/submission-project"; import ServerErrorException from "../../exception/server-error-exception"; +import ProjectFramework from "../../entities/submission-project/project-framework"; describe('project preparation test', () => { it('should install project properly', function () { @@ -22,7 +23,8 @@ describe('project preparation test', () => { "nanoid": "^4.0.0" } }, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const projectPreparation = new ProjectPreparationService() projectPreparation.install(submissionProject) @@ -41,7 +43,8 @@ describe('project preparation test', () => { "@hapi/hapi": "^21.1.2" } }, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const projectPreparation = new ProjectPreparationService() expect(()=> projectPreparation.install(submissionProject)).toThrow(new ServerErrorException('FAIL_INSTALLING_PACKAGE')) diff --git a/src/service/server/server-service.integration.test.ts b/src/service/server/server-service.integration.test.ts index bf33caa..258f9c9 100644 --- a/src/service/server/server-service.integration.test.ts +++ b/src/service/server/server-service.integration.test.ts @@ -6,11 +6,12 @@ import {spawn} from "child_process"; import PackageJson from "../../entities/submission-project/package-json"; import ProjectErrorException from "../../exception/project-error-exception"; import getSubmissionRequirement from "../../config/submission-requirement"; +import ProjectFramework from "../../entities/submission-project/project-framework"; jest.setTimeout(15000) describe('server service test', () => { const submissionRequirement = getSubmissionRequirement() - it.skip('should start & stop server', async function () { + it('should start & stop server', async function () { for (let i = 0; i < 10; i++) { const port = 9000 const serverPid = await startFakeServer(port) @@ -26,7 +27,8 @@ describe('server service test', () => { const submissionProject: SubmissionProject = { packageJsonPath: 'test/student-project/sample-project', packageJsonContent: {}, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const server = new ServerService() @@ -44,7 +46,8 @@ describe('server service test', () => { const submissionProject: SubmissionProject = { packageJsonPath: 'test/student-project/project-with-bad-port', packageJsonContent: {}, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const container = new ServerService() @@ -62,7 +65,8 @@ describe('server service test', () => { const submissionProject: SubmissionProject = { packageJsonPath: 'test/student-project/sample-project', packageJsonContent: {}, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const container = new ServerService() @@ -84,7 +88,8 @@ describe('server service test', () => { const submissionProject: SubmissionProject = { packageJsonPath: 'test/student-project/sample-project', packageJsonContent: {}, - runnerCommand: 'start' + runnerCommand: 'start', + framework: ProjectFramework.Unknown } const container = new ServerService() @@ -123,11 +128,26 @@ describe('server service test', () => { async function killServer(serverPid: number, port) { process.kill(-serverPid) try { - await tcpPortUsed.waitUntilFree(port, 100, 4000) + await waitUntilPortFree(port) } catch (e) { console.log(e) throw Error('Failed to kill server') } } + /** + * tcp-port-used can hit a transient ECONNRESET while the server socket is being torn down, + * which is the same race ServerService.stop() tolerates. Retrying keeps this test stable. + */ + async function waitUntilPortFree(port, remainingRetry = 1) { + try { + await tcpPortUsed.waitUntilFree(port, 100, 2000) + } catch (e) { + if (remainingRetry < 1 || !e.message?.includes('ECONNRESET')) { + throw e + } + await waitUntilPortFree(port, remainingRetry - 1) + } + } + }) From 92eff45d5b4984ee0e977c244d67305f40278db3 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:08 +0700 Subject: [PATCH 2/9] fix: update eslint checker to handle missing config and improve error reporting --- .../eslint-checker.integration.test.ts | 13 +++++---- src/service/eslint-checker/eslint-checker.ts | 29 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/service/eslint-checker/eslint-checker.integration.test.ts b/src/service/eslint-checker/eslint-checker.integration.test.ts index 77105a3..a841cc4 100644 --- a/src/service/eslint-checker/eslint-checker.integration.test.ts +++ b/src/service/eslint-checker/eslint-checker.integration.test.ts @@ -28,13 +28,16 @@ describe('eslint checker', () => { expect(checkResult.reason).toContain('problems') }); - //to run this test you need remove .eslintrc.cjs first - it.skip('should return error when project is installed eslint but config is not available', function () { + it('should return error when project is installed eslint but config is not available', function () { const project = createProject('./test/student-project/project-with-eslint-package-but-no-config') - const checkResult = eslintChecker.check(project) - expect(checkResult.isSuccess).toStrictEqual(false) - expect(checkResult.reason).toContain('ESLint couldn\'t find a configuration file') + expect(eslintChecker.check(project)).toStrictEqual({isSuccess: false, code: 'ESLINT_CONFIG_NOT_FOUND'}) + }); + + it('should recognise a flat eslint config', function () { + const project = createProject('./test/student/passed-sample-with-eslint') + + expect(eslintChecker.check(project).code).not.toStrictEqual('ESLINT_CONFIG_NOT_FOUND') }); it('should return success if no error happen', function () { diff --git a/src/service/eslint-checker/eslint-checker.ts b/src/service/eslint-checker/eslint-checker.ts index 80b850d..8aed159 100644 --- a/src/service/eslint-checker/eslint-checker.ts +++ b/src/service/eslint-checker/eslint-checker.ts @@ -1,7 +1,11 @@ import {execSync} from "child_process"; +import {readdirSync} from "fs"; +import {basename, sep} from "path"; import EslintCheckResult from "./eslint-check-result"; import SubmissionProject from "../../entities/submission-project/submission-project"; +const eslintConfigFilePattern = /^(\.eslintrc(\.(c|m)?js|\.json|\.ya?ml)?|eslint\.config\.(c|m)?[jt]s)$/ + class EslintChecker { check(submissionProject: SubmissionProject): EslintCheckResult { const packageJSONContent = submissionProject.packageJsonContent @@ -9,11 +13,11 @@ class EslintChecker { return {isSuccess: false, code: 'ESLINT_NOT_INSTALLED'} } + if (!this.isEslintConfigAvailable(submissionProject)) { + return {isSuccess: false, code: 'ESLINT_CONFIG_NOT_FOUND'} + } + try { - //check eslint config available or not - execSync('npx eslint ./ --ignore-pattern \'eslint.config.*\'', { - cwd: submissionProject.packageJsonPath, stdio: "pipe" - }) const result = execSync('npx eslint ./ --rule \'linebreak-style:off\' --ignore-pattern \'eslint.config.*\'', { cwd: submissionProject.packageJsonPath, stdio: "pipe" @@ -32,6 +36,21 @@ class EslintChecker { throw new Error('Error when check eslint' + e.message) } } + + /** + * eslint stops reporting a missing config as an error as soon as any cli option is given, + * so the config has to be looked for on the project itself instead of asking eslint about it. + */ + private isEslintConfigAvailable(submissionProject: SubmissionProject): boolean { + if (submissionProject.packageJsonContent.eslintConfig) { + return true + } + + return readdirSync(submissionProject.packageJsonPath, {recursive: true}) + .map(entry => entry.toString()) + .filter(entry => !entry.split(sep).includes('node_modules')) + .some(entry => eslintConfigFilePattern.test(basename(entry))) + } } -export default EslintChecker \ No newline at end of file +export default EslintChecker From 66532c7156410bcdd734a332a16e974bccf2f0f3 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:17 +0700 Subject: [PATCH 3/9] feat: implement framework detection in SubmissionProjectFactory and add corresponding unit tests --- .../submission-project-factory.ts | 23 ++++++++++- .../submission-project-factory.unit.test.ts | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/factories/submission-project/submission-project-factory.ts b/src/factories/submission-project/submission-project-factory.ts index 9f14e56..cbc6272 100644 --- a/src/factories/submission-project/submission-project-factory.ts +++ b/src/factories/submission-project/submission-project-factory.ts @@ -5,6 +5,7 @@ import PackageJson from "../../entities/submission-project/package-json"; import SubmissionProject from "../../entities/submission-project/submission-project"; import {SubmissionRequirement} from "../../config/submission-requirement"; import domainEvent from "../../common/domain-event"; +import ProjectFramework from "../../entities/submission-project/project-framework"; export default class SubmissionProjectFactory { @@ -25,10 +26,30 @@ export default class SubmissionProjectFactory { return { packageJsonContent: this.packageJsonContent, packageJsonPath: projectPath, - runnerCommand + runnerCommand, + framework: this.getFramework() } } + private getFramework(): ProjectFramework { + const dependencies = { + ...this.packageJsonContent.dependencies, + ...this.packageJsonContent.devDependencies + } + + const detectedFrameworks = [ + {framework: ProjectFramework.Hapi, packages: ['@hapi/hapi', 'hapi']}, + {framework: ProjectFramework.Express, packages: ['express']} + ].filter(candidate => candidate.packages.some(packageName => packageName in dependencies)) + + // a project using both frameworks is ambiguous, so it gets the generic advice + if (detectedFrameworks.length !== 1) { + return ProjectFramework.Unknown + } + + return detectedFrameworks[0].framework + } + private validate(projectPath: string) { if (projectPath === null) { throw new ProjectErrorException('PATH_NOT_CONTAIN_PACKAGE_JSON') diff --git a/src/factories/submission-project/submission-project-factory.unit.test.ts b/src/factories/submission-project/submission-project-factory.unit.test.ts index 3c7ef1b..ec6b1f7 100644 --- a/src/factories/submission-project/submission-project-factory.unit.test.ts +++ b/src/factories/submission-project/submission-project-factory.unit.test.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import ProjectErrorException from "../../exception/project-error-exception"; import SubmissionProjectFactory from "./submission-project-factory"; import getSubmissionRequirement from "../../config/submission-requirement"; +import ProjectFramework from "../../entities/submission-project/project-framework"; /** * @review need to remove mock @@ -87,4 +88,43 @@ describe('create submission project test', () => { expect(submissionProject.packageJsonPath).toStrictEqual('/home/app/1234') expect(submissionRequirement.project_have_correct_runner_script.status).toBeTruthy() }); + + describe('framework detection', () => { + const createWithDependencies = (dependencies: string) => { + mockFS.readFileSync.mockReturnValue(`{ "scripts": { "start": "node src/index.js" }, ${dependencies} }`) + mockFS.existsSync.mockReturnValue(false) + + return submissionProjectFactory.create(submissionRequirement, '/home/app/1234') + } + + it('should detect hapi framework', function () { + expect(createWithDependencies('"dependencies": { "@hapi/hapi": "^21.4.3" }').framework) + .toStrictEqual(ProjectFramework.Hapi) + }); + + it('should detect hapi framework from its legacy package name', function () { + expect(createWithDependencies('"dependencies": { "hapi": "^18.1.0" }').framework) + .toStrictEqual(ProjectFramework.Hapi) + }); + + it('should detect express framework', function () { + expect(createWithDependencies('"dependencies": { "express": "^5.1.0" }').framework) + .toStrictEqual(ProjectFramework.Express) + }); + + it('should detect framework listed on dev dependencies', function () { + expect(createWithDependencies('"devDependencies": { "express": "^5.1.0" }').framework) + .toStrictEqual(ProjectFramework.Express) + }); + + it('should not detect any framework when project use both of them', function () { + expect(createWithDependencies('"dependencies": { "express": "^5.1.0", "@hapi/hapi": "^21.4.3" }').framework) + .toStrictEqual(ProjectFramework.Unknown) + }); + + it('should not detect any framework when project has no dependencies', function () { + expect(createWithDependencies('"dependencies": { "nanoid": "^5.1.6" }').framework) + .toStrictEqual(ProjectFramework.Unknown) + }); + }); }) From b025a574a1383dddca62d0d94e35858d6b9e4eb5 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:27 +0700 Subject: [PATCH 4/9] feat: integrate project framework into CourseSubmissionAcception and CourseSubmissionReview for best practice messaging --- .../course-submission-acception.test.ts | 63 +++++++++++++++++++ .../course-submission-acception.ts | 23 ++++++- .../course-submission-review.ts | 8 ++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/entities/review-result/course-submission-acception/course-submission-acception.test.ts b/src/entities/review-result/course-submission-acception/course-submission-acception.test.ts index 8416813..d115179 100644 --- a/src/entities/review-result/course-submission-acception/course-submission-acception.test.ts +++ b/src/entities/review-result/course-submission-acception/course-submission-acception.test.ts @@ -3,6 +3,7 @@ import EslintCheckResult from "../../../service/eslint-checker/eslint-check-resu import SubmissionRatingFactory from "../../../factories/submission-rating/submission-rating-factory"; import SubmissionCriteriaCheck from "../submission-criteria-check/submission-criteria-check"; import getSubmissionRequirement from "../../../config/submission-requirement"; +import ProjectFramework from "../../submission-project/project-framework"; describe('course submission acception test', () => { it('should accept submission properly', function () { @@ -115,4 +116,66 @@ describe('course submission acception test', () => { expect(courseSubmissionAcception.rating).toStrictEqual(4) expect(courseSubmissionAcception.messages).toContain('...') }); + + describe('best practice reference on advice message', () => { + const acceptFlawlessSubmission = (projectFramework?: ProjectFramework) => { + const submissionRatingGenerator = { + get rating(): number { + return 5 + }, + get eslintCheckResult(): EslintCheckResult { + return { + get isSuccess(): boolean { + return true + }, + } + } + } + + const courseSubmissionAcception = new CourseSubmissionAcception( + {}, submissionRatingGenerator, projectFramework + ) + courseSubmissionAcception.accept() + + return courseSubmissionAcception.messages + } + + it('should only suggest hapi documentation when project use hapi', function () { + const messages = acceptFlawlessSubmission(ProjectFramework.Hapi) + + expect(messages).toContain('Hapi Framework') + expect(messages).toContain('Joi') + expect(messages).not.toContain('Express Framework') + expect(messages).not.toContain('express-validator') + }); + + it('should only suggest express documentation when project use express', function () { + const messages = acceptFlawlessSubmission(ProjectFramework.Express) + + expect(messages).toContain('Express Framework') + expect(messages).toContain('express-validator') + expect(messages).not.toContain('Hapi Framework') + expect(messages).not.toContain('Joi') + }); + + it('should suggest both framework documentation when framework is unknown', function () { + const messages = acceptFlawlessSubmission(ProjectFramework.Unknown) + + expect(messages).toContain('Hapi Framework') + expect(messages).toContain('Express Framework') + }); + + it('should fall back to unknown framework when framework is not given', function () { + expect(acceptFlawlessSubmission()).toStrictEqual(acceptFlawlessSubmission(ProjectFramework.Unknown)) + }); + + it('should always suggest database documentation', function () { + [ProjectFramework.Hapi, ProjectFramework.Express, ProjectFramework.Unknown].forEach(projectFramework => { + const messages = acceptFlawlessSubmission(projectFramework) + + expect(messages).toContain('Postgres') + expect(messages).toContain('node-postgres') + }) + }); + }); }) diff --git a/src/entities/review-result/course-submission-acception/course-submission-acception.ts b/src/entities/review-result/course-submission-acception/course-submission-acception.ts index cb0ca2f..8ef449f 100644 --- a/src/entities/review-result/course-submission-acception/course-submission-acception.ts +++ b/src/entities/review-result/course-submission-acception/course-submission-acception.ts @@ -2,6 +2,8 @@ import exceptionToReviewMessage from "../../../exception/exception-to-review-mes import SubmissionRatingFactory from "../../../factories/submission-rating/submission-rating-factory"; import SubmissionCriteriaCheck from "../submission-criteria-check/submission-criteria-check"; import {SubmissionRequirement} from "../../../config/submission-requirement"; +import ProjectFramework from "../../submission-project/project-framework"; +import bestPracticeReference from "../../../config/best-practice-reference"; class CourseSubmissionAcception { @@ -10,11 +12,17 @@ class CourseSubmissionAcception { private submissionRatingGenerator: SubmissionRatingFactory; private readonly _reviewChecklistResults: SubmissionRequirement; private submissionCriteriaCheck: SubmissionCriteriaCheck; + private readonly projectFramework: ProjectFramework; - constructor(submissionCriteriaCheck: SubmissionCriteriaCheck, submissionRatingGenerator: SubmissionRatingFactory) { + constructor( + submissionCriteriaCheck: SubmissionCriteriaCheck, + submissionRatingGenerator: SubmissionRatingFactory, + projectFramework: ProjectFramework = ProjectFramework.Unknown + ) { this.submissionRatingGenerator = submissionRatingGenerator; this._reviewChecklistResults = submissionCriteriaCheck.reviewChecklistResult; this.submissionCriteriaCheck = submissionCriteriaCheck + this.projectFramework = projectFramework } accept() { @@ -28,7 +36,18 @@ class CourseSubmissionAcception { if (messageFromEslint || messageFromOptionalTest) { return messageFromEslint + messageFromOptionalTest } - return '
  • Untuk mengetahui best practices yang ada, terutama penggunaan framework atau tools, cara terbaiknya adalah dengan bereksplorasi kepada dokumentasi resmi yang diberikan. Semakin kamu mengenal frameworknya tentu semakin paham best practice penggunaannya. Silakan eksplorasi dokumentasi beberapa stack framework dan tools yang dapat digunakan pada proyekmu.
  • ' + return this.getMessageFromBestPracticeReference() + } + + private getMessageFromBestPracticeReference(): string { + const references = bestPracticeReference[this.projectFramework] + .map(reference => { + const link = `${reference.url}` + return `
  • ${reference.name}: ${link}
    ${reference.description}
  • ` + }) + .join('') + + return `
  • Untuk mengetahui best practices yang ada, terutama penggunaan framework atau tools, cara terbaiknya adalah dengan bereksplorasi kepada dokumentasi resmi yang diberikan. Semakin kamu mengenal frameworknya tentu semakin paham best practice penggunaannya. Silakan eksplorasi dokumentasi beberapa stack framework dan tools yang dapat digunakan pada proyekmu.
      ${references}
  • ` } diff --git a/src/entities/review-result/course-submission-review/course-submission-review.ts b/src/entities/review-result/course-submission-review/course-submission-review.ts index 11bfa60..ed092a3 100644 --- a/src/entities/review-result/course-submission-review/course-submission-review.ts +++ b/src/entities/review-result/course-submission-review/course-submission-review.ts @@ -5,20 +5,24 @@ import EslintCheckResult from "../../../service/eslint-checker/eslint-check-resu import SubmissionCriteriaCheck from "../submission-criteria-check/submission-criteria-check"; import ReviewResult, {ReviewResultStatus} from "./review-result"; import SubmissionRatingFactory from "../../../factories/submission-rating/submission-rating-factory"; +import ProjectFramework from "../../submission-project/project-framework"; class CourseSubmissionReview { private readonly submissionCriteriaCheck: SubmissionCriteriaCheck; private readonly rejectException?: SubmissionErrorException; private readonly eslintCheckResult?: EslintCheckResult; + private readonly projectFramework: ProjectFramework; constructor( submissionCriteriaCheck: SubmissionCriteriaCheck, eslintCheckResult?: EslintCheckResult, - rejectException?: SubmissionErrorException + rejectException?: SubmissionErrorException, + projectFramework: ProjectFramework = ProjectFramework.Unknown ) { this.rejectException = rejectException; this.submissionCriteriaCheck = submissionCriteriaCheck; this.eslintCheckResult = eslintCheckResult; + this.projectFramework = projectFramework; } @@ -32,7 +36,7 @@ class CourseSubmissionReview { private generateApproval(): ReviewResult { const submissionRatingGenerator = new SubmissionRatingFactory(this.submissionCriteriaCheck.failurePostmanTest, this.eslintCheckResult) - const courseSubmissionAcception = new CourseSubmissionAcception(this.submissionCriteriaCheck, submissionRatingGenerator) + const courseSubmissionAcception = new CourseSubmissionAcception(this.submissionCriteriaCheck, submissionRatingGenerator, this.projectFramework) courseSubmissionAcception.accept() return { From 6350cebb52604a363f596161270638c2dffaff5e Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:43 +0700 Subject: [PATCH 5/9] feat: add best practice references for Hapi, Express, Joi, and database libraries --- src/config/best-practice-reference.ts | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/config/best-practice-reference.ts diff --git a/src/config/best-practice-reference.ts b/src/config/best-practice-reference.ts new file mode 100644 index 0000000..147849f --- /dev/null +++ b/src/config/best-practice-reference.ts @@ -0,0 +1,52 @@ +import ProjectFramework from "../entities/submission-project/project-framework"; + +export interface BestPracticeReference { + name: string + url: string + description: string +} + +const hapiReference: BestPracticeReference = { + name: 'Hapi Framework', + url: 'https://hapi.dev/tutorials/?lang=en_US', + description: 'Kamu bisa eksplor tentang apa saja yang bisa digunakan pada framework Hapi.' +} + +const expressReference: BestPracticeReference = { + name: 'Express Framework', + url: 'https://expressjs.com/en/guide/routing.html', + description: 'Kamu bisa eksplor tentang apa saja yang bisa digunakan pada framework Express, mulai dari routing sampai middleware.' +} + +const joiReference: BestPracticeReference = { + name: 'Joi', + url: 'https://joi.dev/', + description: 'Jika kamu ingin membuat validasi data dengan mudah kamu juga bisa menggunakan library Joi.' +} + +const expressValidatorReference: BestPracticeReference = { + name: 'express-validator', + url: 'https://express-validator.github.io/docs/', + description: 'Jika kamu ingin membuat validasi data dengan mudah kamu juga bisa menggunakan library express-validator.' +} + +const databaseReferences: BestPracticeReference[] = [ + { + name: 'Postgres', + url: 'https://www.postgresql.org/docs/current/index.html', + description: 'Agar aplikasi yang kamu buat datanya bisa bertahan ketika server direstart, kamu bisa mempelajari PostgreSQL sebagai penyimpanan data.' + }, + { + name: 'node-postgres', + url: 'https://node-postgres.com/', + description: 'Untuk menghubungkan aplikasi nodejs dengan postgresql kamu bisa menggunakan library node-postgres.' + } +] + +const bestPracticeReference: Record = { + [ProjectFramework.Hapi]: [hapiReference, joiReference, ...databaseReferences], + [ProjectFramework.Express]: [expressReference, expressValidatorReference, ...databaseReferences], + [ProjectFramework.Unknown]: [hapiReference, expressReference, joiReference, ...databaseReferences] +} + +export default bestPracticeReference From 466e617572bcc98f261f476dae77eed574983683 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:48 +0700 Subject: [PATCH 6/9] feat: add eslintConfig to PackageJson interface and enhance Dependencies type --- src/entities/submission-project/package-json.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/entities/submission-project/package-json.ts b/src/entities/submission-project/package-json.ts index 335bf39..02284e9 100644 --- a/src/entities/submission-project/package-json.ts +++ b/src/entities/submission-project/package-json.ts @@ -2,10 +2,12 @@ interface PackageJson { scripts?: object dependencies?: Dependencies devDependencies?: Dependencies + eslintConfig?: object } interface Dependencies{ eslint?: string + [packageName: string]: string | undefined } -export default PackageJson \ No newline at end of file +export default PackageJson From 02a6879c8f8b5cc35aca1eecb06cea8a954f2f31 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:19:55 +0700 Subject: [PATCH 7/9] feat: add framework property to SubmissionProject interface --- src/entities/submission-project/submission-project.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/entities/submission-project/submission-project.ts b/src/entities/submission-project/submission-project.ts index c0ac6d2..a18d7f6 100644 --- a/src/entities/submission-project/submission-project.ts +++ b/src/entities/submission-project/submission-project.ts @@ -1,7 +1,9 @@ import PackageJson from "./package-json"; +import ProjectFramework from "./project-framework"; export default interface SubmissionProject { packageJsonContent: PackageJson, packageJsonPath: string, - runnerCommand: string -} \ No newline at end of file + runnerCommand: string, + framework: ProjectFramework +} From f92b2cc9b1be58b8f11e81afc0793e520f362312 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:20:07 +0700 Subject: [PATCH 8/9] feat: add ESLINT_CONFIG_NOT_FOUND message to exception handling --- src/exception/exception-to-review-message.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/exception/exception-to-review-message.ts b/src/exception/exception-to-review-message.ts index c66110a..cbc4675 100644 --- a/src/exception/exception-to-review-message.ts +++ b/src/exception/exception-to-review-message.ts @@ -4,6 +4,7 @@ const exceptionToReviewMessage = { 'RUNNER_SCRIPT_NOT_FOUND': 'sepertinya kamu lupa memasukkan command untuk menjalankan project ke dalam properti "script" pada file package.json. Silakan ditambahkan terlebih dahulu yaa.', 'CANNOT_PARSE_PACKAGE_JSON': 'sepertinya file package.json yang kamu buat belum bisa terformat dengan baik. Silakan diperbaiki yaa.', 'ESLINT_NOT_INSTALLED': 'Aplikasi yang kamu buat sudah berjalan dengan baik. Tetapi kode yang dituliskan masih ada yang bisa ditingkatkan. Sebaiknya gunakan salah satu style guide yang ada pada eslint agar kode yang kamu buat bisa semakin konsisten. Tingkatkan aplikasinya untuk latihan yaa. Semangattt!!', + 'ESLINT_CONFIG_NOT_FOUND': 'Aplikasi yang kamu buat sudah berjalan dengan baik. Kamu juga sudah menambahkan eslint pada file package.json, tetapi belum ada file konfigurasi eslint pada project yang kamu kirimkan sehingga eslint belum benar-benar berjalan. Silakan tambahkan file konfigurasinya, contohnya .eslintrc.json atau eslint.config.js, lalu gunakan salah satu style guide yang ada agar kode yang kamu buat bisa semakin konsisten. Tingkatkan aplikasinya untuk latihan yaa. Semangattt!!', 'ESLINT_ERROR' : 'Masih terdapat error ketika eslint dijalankan dengan perintah npx eslint ./src. Sebaiknya diperbaiki yaa, pastikan tidak ada warning atau error pada kode yang kamu buat.', 'SEVER_FAILED_TO_START': '', 'PORT_NOT_MEET_REQUIREMENT': 'port yang kamu gunakan untuk menjalankan aplikasi masih belum sesuai dengan kriteria submission, silakan dicek kembali kriterianya yaa.', From 91c2131d7451c08e37396f25258f87e035b05d20 Mon Sep 17 00:00:00 2001 From: Fauzi Kurniawan Date: Tue, 8 Sep 2026 15:20:15 +0700 Subject: [PATCH 9/9] feat: add projectFramework to generateReviewResult and update tests for server error handling --- src/index.ts | 7 ++++--- src/service/server/server-error-handler.test.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8d74c4c..48888c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import ReviewResult from "./entities/review-result/course-submission-review/revi import SubmissionProjectFactory from "./factories/submission-project/submission-project-factory"; import getSubmissionRequirement, {SubmissionRequirement} from "./config/submission-requirement"; import raiseDomainEvent from "./common/domain-event"; +import ProjectFramework from "./entities/submission-project/project-framework"; class Main { private postmanRunner: PostmanRunner; @@ -51,7 +52,7 @@ class Main { const eslintCheckResult = this.eslintChecker.check(submissionProject) raiseDomainEvent('eslint check completed') - return this.generateReviewResult(submissionCriteriaCheck, eslintCheckResult) + return this.generateReviewResult(submissionCriteriaCheck, eslintCheckResult, null, submissionProject.framework) } catch (e) { if (e instanceof SubmissionErrorException) { submissionCriteriaCheck = submissionCriteriaCheck ?? this.submissionCriteriaCheckFactory.check(this.submissionRequirements) @@ -68,8 +69,8 @@ class Main { } } - private generateReviewResult(submissionCriteriaCheck, eslintCheckResult?, submissionErrorException?) { - const courseSubmissionReview = new CourseSubmissionReview(submissionCriteriaCheck, eslintCheckResult, submissionErrorException) + private generateReviewResult(submissionCriteriaCheck, eslintCheckResult?, submissionErrorException?, projectFramework?: ProjectFramework) { + const courseSubmissionReview = new CourseSubmissionReview(submissionCriteriaCheck, eslintCheckResult, submissionErrorException, projectFramework) return courseSubmissionReview.review() } diff --git a/src/service/server/server-error-handler.test.ts b/src/service/server/server-error-handler.test.ts index e034012..043eb77 100644 --- a/src/service/server/server-error-handler.test.ts +++ b/src/service/server/server-error-handler.test.ts @@ -3,6 +3,7 @@ import ProjectErrorException from "../../exception/project-error-exception"; import ServerErrorException from "../../exception/server-error-exception"; import SubmissionProject from "../../entities/submission-project/submission-project"; import PackageJson from "../../entities/submission-project/package-json"; +import ProjectFramework from "../../entities/submission-project/project-framework"; describe('test server utils', () => { @@ -38,7 +39,8 @@ describe('test server utils', () => { const submissionProject :SubmissionProject = { packageJsonPath: '', packageJsonContent: {}, - runnerCommand: '' + runnerCommand: '', + framework: ProjectFramework.Unknown } return new ServerErrorHandler(logErrors, submissionProject) }