From a6dd72ce8e52367d6807642e15163a3f05bb64bf Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 12 Dec 2024 16:50:05 -0300 Subject: [PATCH 01/24] add test to work --- .vscode/launch.json | 7 +++ .../existing-folder/package.json | 2 +- examples/user-natives | 1 + package-lock.json | 4 +- package.json | 1 + test/userNatives.test.ts | 56 +++++++++++++++++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 160000 examples/user-natives create mode 100644 test/userNatives.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index cfc2114..fe25f3d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,13 @@ "name": "Run Wollok CLI tests", "request": "launch", "type": "node-terminal" + }, + { + "command": "npm run start -- run 'mainValidaciones.Validaciones' --skipValidations -p '/home/leo/workspaces/obj1unq/validaciones' ", + "name": "Run Wollok CLI ?", + "request": "launch", + "type": "node-terminal" } ] } + diff --git a/examples/init-examples/existing-folder/package.json b/examples/init-examples/existing-folder/package.json index ef8cb6a..155a6ec 100644 --- a/examples/init-examples/existing-folder/package.json +++ b/examples/init-examples/existing-folder/package.json @@ -2,6 +2,6 @@ "name": "existing-folder", "version": "1.0.0", "wollokVersion": "4.0.0", - "author": "palumbon", + "author": "leo", "license": "ISC" } diff --git a/examples/user-natives b/examples/user-natives new file mode 160000 index 0000000..d889fe0 --- /dev/null +++ b/examples/user-natives @@ -0,0 +1 @@ +Subproject commit d889fe0ec227de6c862a5ebc72322b5dba746324 diff --git a/package-lock.json b/package-lock.json index 7f6a7bf..a991964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wollok-ts-cli", - "version": "0.2.11", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wollok-ts-cli", - "version": "0.2.11", + "version": "0.3.0", "license": "MIT", "dependencies": { "@badisi/latest-version": "^7.0.10", diff --git a/package.json b/package.json index 0ded75c..d4dc296 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint": "eslint . ", "lint:fix": "eslint . --fix", "test:unit": "mocha --parallel -r ts-node/register/transpile-only test/**/*.test.ts --timeout 5000", + "test:file": "mocha -r ts-node/register/transpile-only", "build:tools": "shx cp ./node_modules/wollok-web-tools/dist/web/game-index.js ./public/game/lib && shx cp ./node_modules/wollok-web-tools/dist/dynamicDiagram/diagram-index.js ./public/diagram", "build": "shx rm -rf build && shx mkdir ./build && shx cp -r ./public ./build/public && tsc -p ./tsconfig.build.json", "watch": "npm run build -- -w", diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts new file mode 100644 index 0000000..786bf50 --- /dev/null +++ b/test/userNatives.test.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai' +import logger from 'loglevel' +import { join } from 'path' +import sinon from 'sinon' +import { Environment } from 'wollok-ts' +import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' +import { logger as fileLogger } from '../src/logger' +import { buildEnvironmentForProject } from '../src/utils' +import { spyCalledWithSubstring } from './assertions' + +describe('UserNatives', () => { + + describe('smoke test for test default function', () => { + + let fileLoggerInfoSpy: sinon.SinonStub + let loggerInfoSpy: sinon.SinonStub + let loggerErrorSpy: sinon.SinonStub + let processExitSpy: sinon.SinonStub + + const projectPath = join('examples', 'user-natives') + + const emptyOptions = { + project: projectPath, + skipValidations: true, + file: undefined, + describe: undefined, + test: undefined, + } + + beforeEach(() => { + loggerInfoSpy = sinon.stub(logger, 'info') + fileLoggerInfoSpy = sinon.stub(fileLogger, 'info') + processExitSpy = sinon.stub(process, 'exit') + loggerErrorSpy = sinon.stub(logger, 'error') + }) + + afterEach(() => { + sinon.restore() + }) + + it('passes all the tests successfully and exits normally', async () => { + await test(undefined, { + ...emptyOptions, + file: 'userNatives.wtest', + }) + + expect(processExitSpy.callCount).to.equal(0) + expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true + expect(spyCalledWithSubstring(loggerInfoSpy, '1 passed')).to.be.true + expect(spyCalledWithSubstring(loggerInfoSpy, '0 failed')).to.be.false + expect(spyCalledWithSubstring(loggerInfoSpy, '0 errored')).to.be.false + expect(fileLoggerInfoSpy.calledOnce).to.be.true + expect(fileLoggerInfoSpy.firstCall.firstArg.result).to.deep.equal({ ok: 1, failed: 0, errored: 0 }) + }) + }) +}) \ No newline at end of file From 45ffb98a1fcf6038b261802d043f082a7b00b1d7 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 12 Dec 2024 17:45:12 -0300 Subject: [PATCH 02/24] Removed submodule reference --- .vscode/launch.json | 4 ++-- tsconfig.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index fe25f3d..49df0f3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,8 +8,8 @@ "type": "node-terminal" }, { - "command": "npm run start -- run 'mainValidaciones.Validaciones' --skipValidations -p '/home/leo/workspaces/obj1unq/validaciones' ", - "name": "Run Wollok CLI ?", + "command": "npm run test:file ${file}", + "name": "Run a single test file", "request": "launch", "type": "node-terminal" } diff --git a/tsconfig.json b/tsconfig.json index fa748bb..8376fc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "esModuleInterop": true, "experimentalDecorators": true }, - "include": ["test/**/*.ts", "src/**/*.ts"], + "include": ["test/**/*.ts", "src/**/*.ts", "examples/**/*.ts"], "exclude": ["**/*.js", "node_modules"] } From 5de08e5bc47852d8b3fa00e6de0a14f3c44c39ba Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 12 Dec 2024 17:45:23 -0300 Subject: [PATCH 03/24] Removed submodule reference --- examples/user-natives | 1 - 1 file changed, 1 deletion(-) delete mode 160000 examples/user-natives diff --git a/examples/user-natives b/examples/user-natives deleted file mode 160000 index d889fe0..0000000 --- a/examples/user-natives +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d889fe0ec227de6c862a5ebc72322b5dba746324 From d82c6ffb4e5cdfc0532601131ee6286097626fff Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 12 Dec 2024 17:45:54 -0300 Subject: [PATCH 04/24] partial commit --- examples/user-natives/model.wlk | 5 +++++ examples/user-natives/natives/model.ts | 9 +++++++++ examples/user-natives/package.json | 8 ++++++++ examples/user-natives/userNatives.wtest | 5 +++++ test/userNatives.test.ts | 2 ++ 5 files changed, 29 insertions(+) create mode 100644 examples/user-natives/model.wlk create mode 100644 examples/user-natives/natives/model.ts create mode 100644 examples/user-natives/package.json create mode 100644 examples/user-natives/userNatives.wtest diff --git a/examples/user-natives/model.wlk b/examples/user-natives/model.wlk new file mode 100644 index 0000000..bd5f288 --- /dev/null +++ b/examples/user-natives/model.wlk @@ -0,0 +1,5 @@ + +object myModel { + + method nativeOne() native +} diff --git a/examples/user-natives/natives/model.ts b/examples/user-natives/natives/model.ts new file mode 100644 index 0000000..81106e7 --- /dev/null +++ b/examples/user-natives/natives/model.ts @@ -0,0 +1,9 @@ +const model = { + + myModel: { + *nativeOne(self: any) : any { + return 1 + }, + }, +} +export default model \ No newline at end of file diff --git a/examples/user-natives/package.json b/examples/user-natives/package.json new file mode 100644 index 0000000..aa8704d --- /dev/null +++ b/examples/user-natives/package.json @@ -0,0 +1,8 @@ +{ + "name": "user-natives", + "version": "1.0.0", + "resourceFolder": "assets", + "wollokVersion": "4.0.0", + "author": "leo", + "license": "ISC" +} diff --git a/examples/user-natives/userNatives.wtest b/examples/user-natives/userNatives.wtest new file mode 100644 index 0000000..833b051 --- /dev/null +++ b/examples/user-natives/userNatives.wtest @@ -0,0 +1,5 @@ +import model.myModel + +test "call a native method used by user" { + assert.equals(1, myModel.nativemodelOne()) +} \ No newline at end of file diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index 786bf50..222c7ad 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -35,9 +35,11 @@ describe('UserNatives', () => { }) afterEach(() => { + console.log('Error logs:', loggerErrorSpy.getCalls()) //No commitear, quitar cuando ya ande!!! sinon.restore() }) + it('passes all the tests successfully and exits normally', async () => { await test(undefined, { ...emptyOptions, From 973aeb43af01a57f45938c2595776acda6b3bde6 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 15 Dec 2024 00:42:38 -0300 Subject: [PATCH 05/24] read natives in tests --- .vscode/launch.json | 1 - .../myNativesFolder/myPackage/myInnerFile.js | 7 ++ .../user-natives/myNativesFolder/rootFile.ts | 7 ++ .../user-natives/myPackage/myInnerFile.wlk | 3 + examples/user-natives/natives/model.ts | 9 -- .../user-natives/{model.wlk => rootFile.wlk} | 2 - examples/user-natives/userNatives.wtest | 6 +- src/commands/test.ts | 7 +- src/utils.ts | 26 +++++- test/userNatives.test.ts | 86 +++++++++---------- 10 files changed, 93 insertions(+), 61 deletions(-) create mode 100644 examples/user-natives/myNativesFolder/myPackage/myInnerFile.js create mode 100644 examples/user-natives/myNativesFolder/rootFile.ts create mode 100644 examples/user-natives/myPackage/myInnerFile.wlk delete mode 100644 examples/user-natives/natives/model.ts rename examples/user-natives/{model.wlk => rootFile.wlk} (96%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 49df0f3..3cc2dc0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,4 +15,3 @@ } ] } - diff --git a/examples/user-natives/myNativesFolder/myPackage/myInnerFile.js b/examples/user-natives/myNativesFolder/myPackage/myInnerFile.js new file mode 100644 index 0000000..c4f52e9 --- /dev/null +++ b/examples/user-natives/myNativesFolder/myPackage/myInnerFile.js @@ -0,0 +1,7 @@ +const packageModel = { + *nativeTwo(self) { + return yield* this.reify(2) + }, +} + +module.exports = { packageModel }; diff --git a/examples/user-natives/myNativesFolder/rootFile.ts b/examples/user-natives/myNativesFolder/rootFile.ts new file mode 100644 index 0000000..0e506fb --- /dev/null +++ b/examples/user-natives/myNativesFolder/rootFile.ts @@ -0,0 +1,7 @@ +const myModel = { + *nativeOne(self: any) : any { + return yield* this.reify(1) + }, +} + +export { myModel } \ No newline at end of file diff --git a/examples/user-natives/myPackage/myInnerFile.wlk b/examples/user-natives/myPackage/myInnerFile.wlk new file mode 100644 index 0000000..c24fd16 --- /dev/null +++ b/examples/user-natives/myPackage/myInnerFile.wlk @@ -0,0 +1,3 @@ +object packageModel { + method nativeTwo() native +} \ No newline at end of file diff --git a/examples/user-natives/natives/model.ts b/examples/user-natives/natives/model.ts deleted file mode 100644 index 81106e7..0000000 --- a/examples/user-natives/natives/model.ts +++ /dev/null @@ -1,9 +0,0 @@ -const model = { - - myModel: { - *nativeOne(self: any) : any { - return 1 - }, - }, -} -export default model \ No newline at end of file diff --git a/examples/user-natives/model.wlk b/examples/user-natives/rootFile.wlk similarity index 96% rename from examples/user-natives/model.wlk rename to examples/user-natives/rootFile.wlk index bd5f288..1ec23d5 100644 --- a/examples/user-natives/model.wlk +++ b/examples/user-natives/rootFile.wlk @@ -1,5 +1,3 @@ - object myModel { - method nativeOne() native } diff --git a/examples/user-natives/userNatives.wtest b/examples/user-natives/userNatives.wtest index 833b051..3868a6c 100644 --- a/examples/user-natives/userNatives.wtest +++ b/examples/user-natives/userNatives.wtest @@ -1,5 +1,7 @@ -import model.myModel +import rootFile.myModel +import myPackage.myInnerFile.packageModel test "call a native method used by user" { - assert.equals(1, myModel.nativemodelOne()) + assert.equals(1, myModel.nativeOne()) + assert.equals(2, packageModel.nativeTwo()) } \ No newline at end of file diff --git a/src/commands/test.ts b/src/commands/test.ts index 9032069..d8fa7f7 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -2,10 +2,12 @@ import { bold, red } from 'chalk' import { time, timeEnd } from 'console' import logger from 'loglevel' import { Entity, Environment, Node, Test, is, match, when, WRENatives as natives, interpret, Describe, count } from 'wollok-ts' -import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription } from '../utils' +import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives } from '../utils' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' import { Package } from 'wollok-ts' +import path, { join } from 'path' + const { log } = console @@ -15,6 +17,7 @@ export type Options = { test: string | undefined, project: string skipValidations: boolean + natives: string | undefined } class TestSearchMissError extends Error{} @@ -112,7 +115,7 @@ export default async function (filter: string | undefined, options: Options): Pr const debug = logger.getLevel() <= logger.levels.DEBUG if (debug) time('Run finished') - const interpreter = interpret(environment, natives) + const interpreter = interpret(environment, await readNatives(options.natives ? join(options.project, options.natives) : options.project)) const testsFailed: TestExecutionError[] = [] let successes = 0 diff --git a/src/utils.ts b/src/utils.ts index c3dea6d..615e67d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import globby from 'globby' import logger from 'loglevel' import path, { join } from 'path' import { getDataDiagram, VALID_IMAGE_EXTENSIONS, VALID_SOUND_EXTENSIONS } from 'wollok-web-tools' -import { buildEnvironment, Environment, getDynamicDiagramData, Interpreter, Package, Problem, validate, WOLLOK_EXTRA_STACK_TRACE_HEADER, WollokException } from 'wollok-ts' +import { buildEnvironment, Environment, getDynamicDiagramData, Interpreter, Natives, Package, Problem, validate, WOLLOK_EXTRA_STACK_TRACE_HEADER, WollokException, natives, List } from 'wollok-ts' import { ElementDefinition } from 'cytoscape' const { time, timeEnd } = console @@ -87,6 +87,30 @@ export const handleError = (error: any): void => { logger.debug(failureDescription('ℹ️ Stack trace:', error)) } + + + +export async function readNatives(nativeFolder: string): Promise { + const paths = await globby('**/*.@(ts|js)', { cwd: nativeFolder }) + const debug = logger.getLevel() <= logger.levels.DEBUG + + if (debug) time('Reading natives files') + + const nativesObjects: List = await Promise.all( + paths.map(async (filePath) => { + const fullPath = path.resolve(nativeFolder, filePath) + const importedModule = await import(fullPath) + const segments = filePath.replace(/\.(ts|js)$/, '').split(path.sep) + + return segments.reduceRight((acc, segment) => { return { [segment]: acc }}, importedModule.default || importedModule) + }) + ) + + return natives(nativesObjects) // Ahora `natives` recibe un array de objetos +} + + + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PRINTING // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index 222c7ad..0451f50 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -2,57 +2,55 @@ import { expect } from 'chai' import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' -import { Environment } from 'wollok-ts' -import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' +import test from '../src/commands/test' import { logger as fileLogger } from '../src/logger' -import { buildEnvironmentForProject } from '../src/utils' import { spyCalledWithSubstring } from './assertions' describe('UserNatives', () => { + let fileLoggerInfoSpy: sinon.SinonStub + let loggerInfoSpy: sinon.SinonStub + let loggerErrorSpy: sinon.SinonStub + let processExitSpy: sinon.SinonStub + + const projectPath = join('examples', 'user-natives') + + const emptyOptions = { + project: projectPath, + skipValidations: true, + file: undefined, + describe: undefined, + test: undefined, + } + + beforeEach(() => { + loggerInfoSpy = sinon.stub(logger, 'info') + fileLoggerInfoSpy = sinon.stub(fileLogger, 'info') + processExitSpy = sinon.stub(process, 'exit') + loggerErrorSpy = sinon.stub(logger, 'error') + }) - describe('smoke test for test default function', () => { - - let fileLoggerInfoSpy: sinon.SinonStub - let loggerInfoSpy: sinon.SinonStub - let loggerErrorSpy: sinon.SinonStub - let processExitSpy: sinon.SinonStub - - const projectPath = join('examples', 'user-natives') - - const emptyOptions = { - project: projectPath, - skipValidations: true, - file: undefined, - describe: undefined, - test: undefined, - } - - beforeEach(() => { - loggerInfoSpy = sinon.stub(logger, 'info') - fileLoggerInfoSpy = sinon.stub(fileLogger, 'info') - processExitSpy = sinon.stub(process, 'exit') - loggerErrorSpy = sinon.stub(logger, 'error') - }) + afterEach(() => { + console.log('Error logs:', loggerErrorSpy.getCalls()) //No commitear, quitar cuando ya ande!!! + sinon.restore() + }); - afterEach(() => { - console.log('Error logs:', loggerErrorSpy.getCalls()) //No commitear, quitar cuando ya ande!!! - sinon.restore() + it('passes all the tests successfully and exits normally', async () => { + await test(undefined, { + ...emptyOptions, + file: 'userNatives.wtest', + natives: 'myNativesFolder', }) - - it('passes all the tests successfully and exits normally', async () => { - await test(undefined, { - ...emptyOptions, - file: 'userNatives.wtest', - }) - - expect(processExitSpy.callCount).to.equal(0) - expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true - expect(spyCalledWithSubstring(loggerInfoSpy, '1 passed')).to.be.true - expect(spyCalledWithSubstring(loggerInfoSpy, '0 failed')).to.be.false - expect(spyCalledWithSubstring(loggerInfoSpy, '0 errored')).to.be.false - expect(fileLoggerInfoSpy.calledOnce).to.be.true - expect(fileLoggerInfoSpy.firstCall.firstArg.result).to.deep.equal({ ok: 1, failed: 0, errored: 0 }) + expect(processExitSpy.callCount).to.equal(0) + expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true + expect(spyCalledWithSubstring(loggerInfoSpy, '1 passed')).to.be.true + expect(spyCalledWithSubstring(loggerInfoSpy, '0 failed')).to.be.false + expect(spyCalledWithSubstring(loggerInfoSpy, '0 errored')).to.be.false + expect(fileLoggerInfoSpy.calledOnce).to.be.true + expect(fileLoggerInfoSpy.firstCall.firstArg.result).to.deep.equal({ + ok: 1, + failed: 0, + errored: 0, }) }) -}) \ No newline at end of file +}) From d1ffb70ba95bb8d0530648c3df1a100d1d2e05c5 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 15 Dec 2024 13:30:55 -0300 Subject: [PATCH 06/24] Options refactor --- .../user-natives/myNativesFolder/rootFile.ts | 6 +- package-lock.json | 116 ++++++----------- package.json | 2 +- src/commands/init.ts | 16 +-- src/commands/test.ts | 19 ++- src/utils.ts | 33 ++++- test/init.test.ts | 34 ++--- test/test.test.ts | 121 +++++++----------- test/userNatives.test.ts | 27 ++-- tsconfig.json | 5 +- 10 files changed, 152 insertions(+), 227 deletions(-) diff --git a/examples/user-natives/myNativesFolder/rootFile.ts b/examples/user-natives/myNativesFolder/rootFile.ts index 0e506fb..5d4c50a 100644 --- a/examples/user-natives/myNativesFolder/rootFile.ts +++ b/examples/user-natives/myNativesFolder/rootFile.ts @@ -1,7 +1,7 @@ const myModel = { - *nativeOne(self: any) : any { - return yield* this.reify(1) - }, + *nativeOne(this: any, _self: any) : any { + return yield* this.reify(1) + }, } export { myModel } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a991964..8c5001d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "print-message": "^3.0.1", "socket.io": "^4.5.1", "winston": "^3.11.0", - "wollok-ts": "^4.1.9", + "wollok-ts": "file:../wollok-ts", "wollok-web-tools": "^1.1.7" }, "bin": { @@ -56,6 +56,41 @@ "typescript": "~5.5.1" } }, + "../wollok-ts": { + "version": "4.1.10", + "license": "MIT", + "dependencies": { + "@types/parsimmon": "^1.10.8", + "parsimmon": "^1.18.1", + "prettier-printer": "^1.1.4", + "unraw": "^3.0.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/chai": "^4.3.9", + "@types/mocha": "^10.0.3", + "@types/node": "^18.14.1", + "@types/sinon": "^10.0.20", + "@types/sinon-chai": "^3.2.11", + "@types/uuid": "^9.0.6", + "@types/yargs": "^17.0.22", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@typescript-eslint/parser": "^5.53.0", + "chai": "^4.3.7", + "chalk": "^5.2.0", + "dedent": "^1.5.1", + "eslint": "^8.35.0", + "globby": "^11.1.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "simple-git": "^3.20.0", + "sinon": "17.0.0", + "sinon-chai": "^3.7.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "yargs": "^17.7.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1176,12 +1211,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/parsimmon": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.9.tgz", - "integrity": "sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A==", - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", @@ -3866,12 +3895,6 @@ "node": ">=8" } }, - "node_modules/infestines": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/infestines/-/infestines-0.4.11.tgz", - "integrity": "sha512-09nHagZLOYUaXKHqdV+nxEaYaD0hRlKyhQMhgTMwfbvWpMkowXf4XLZzAkLq6Y90wZ7Wqm6aMoL2trBsNNKGeg==", - "license": "MIT" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5411,31 +5434,6 @@ "node": ">= 0.8" } }, - "node_modules/parsimmon": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", - "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==", - "license": "MIT" - }, - "node_modules/partial.lenses": { - "version": "14.17.0", - "resolved": "https://registry.npmjs.org/partial.lenses/-/partial.lenses-14.17.0.tgz", - "integrity": "sha512-Iq+wDw5b5iwmn7b9MO+//buOqF0YoAC2Hsj+HoeG88BGeT3NkL/YwHNGDrySyX7xZSqj0LFEWwfgGSj4cSFmXQ==", - "license": "MIT", - "dependencies": { - "infestines": "^0.4.11" - } - }, - "node_modules/partial.lenses.validation": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/partial.lenses.validation/-/partial.lenses.validation-2.0.0.tgz", - "integrity": "sha512-NBZhmrunQWc4Ih5pJGYPHCEJfjIITPEkYn0Z6YB8qmW0iN4ZeqN0eZTWAPAH8MxjsEGx3G+8xnTMsEHbzy0k/A==", - "license": "MIT", - "dependencies": { - "infestines": "^0.4.10", - "partial.lenses": "^14.0.0" - } - }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -5735,16 +5733,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier-printer": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/prettier-printer/-/prettier-printer-1.1.4.tgz", - "integrity": "sha512-gQIWF7PKVknRcMoZ4Lsqj2icc97W+Q9JTa7xQgNem07yZFzOimUPq50N5oRfKkSRBZT8QqrVW1IBEMBPWqjBgQ==", - "license": "MIT", - "dependencies": { - "infestines": "^0.4.11", - "partial.lenses.validation": "^2.0.0" - } - }, "node_modules/print-message": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/print-message/-/print-message-3.0.1.tgz", @@ -7151,12 +7139,6 @@ "node": ">= 0.8" } }, - "node_modules/unraw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", - "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==", - "license": "MIT" - }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -7393,30 +7375,8 @@ } }, "node_modules/wollok-ts": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/wollok-ts/-/wollok-ts-4.1.9.tgz", - "integrity": "sha512-mgbaqdebQrM+RQMhtgTAtHP9m/8u6JPRAUYV53xqjBaMZh1wQ4xrQb8e6wmJJoXs8XRWdUgTZXLnJLvN1HyA2Q==", - "license": "MIT", - "dependencies": { - "@types/parsimmon": "^1.10.8", - "parsimmon": "^1.18.1", - "prettier-printer": "^1.1.4", - "unraw": "^3.0.0", - "uuid": "^9.0.1" - } - }, - "node_modules/wollok-ts/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } + "resolved": "../wollok-ts", + "link": true }, "node_modules/wollok-web-tools": { "version": "1.1.7", diff --git a/package.json b/package.json index d4dc296..a5ccb95 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "print-message": "^3.0.1", "socket.io": "^4.5.1", "winston": "^3.11.0", - "wollok-ts": "^4.1.9", + "wollok-ts": "file:../wollok-ts", "wollok-web-tools": "^1.1.7" }, "devDependencies": { diff --git a/src/commands/init.ts b/src/commands/init.ts index b8afe6a..b43566b 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -3,19 +3,19 @@ import logger from 'loglevel' import { existsSync, writeFileSync } from 'node:fs' import { basename, join } from 'node:path' import { userInfo } from 'os' -import { ENTER, createFolderIfNotExists } from '../utils' +import { ENTER, createFolderIfNotExists , BaseOptions} from '../utils' import { PROGRAM_FILE_EXTENSION, TEST_FILE_EXTENSION, WOLLOK_FILE_EXTENSION } from 'wollok-ts' import { execSync } from 'node:child_process' -export type Options = { - project: string, - name?: string | undefined, - noTest: boolean, - noCI: boolean, - game: boolean, - noGit: boolean +export class Options extends BaseOptions { + name?: string + noTest!: boolean + noCI!: boolean + game!: boolean + noGit!: boolean } + export default function (folder: string | undefined, { project: _project, name, noTest = false, noCI = false, game = false, noGit = false }: Options): void { const project = join(_project, folder ?? '') diff --git a/src/commands/test.ts b/src/commands/test.ts index d8fa7f7..5aa2304 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,23 +1,20 @@ import { bold, red } from 'chalk' import { time, timeEnd } from 'console' import logger from 'loglevel' -import { Entity, Environment, Node, Test, is, match, when, WRENatives as natives, interpret, Describe, count } from 'wollok-ts' -import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives } from '../utils' +import { Entity, Environment, Node, Test, is, match, when, interpret, Describe, count } from 'wollok-ts' +import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives, BaseOptions } from '../utils' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' import { Package } from 'wollok-ts' -import path, { join } from 'path' const { log } = console -export type Options = { - file: string | undefined, - describe: string | undefined, - test: string | undefined, - project: string - skipValidations: boolean - natives: string | undefined +export class Options extends BaseOptions { + file?: string + describe?: string + test?: string + skipValidations?: boolean } class TestSearchMissError extends Error{} @@ -115,7 +112,7 @@ export default async function (filter: string | undefined, options: Options): Pr const debug = logger.getLevel() <= logger.levels.DEBUG if (debug) time('Run finished') - const interpreter = interpret(environment, await readNatives(options.natives ? join(options.project, options.natives) : options.project)) + const interpreter = interpret(environment, await readNatives(options.nativesFolder)) const testsFailed: TestExecutionError[] = [] let successes = 0 diff --git a/src/utils.ts b/src/utils.ts index 615e67d..a5f8615 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,6 +25,29 @@ export const folderIcon = '🗂️' // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // FILE / PATH HANDLING // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +export class BaseOptions { + project!: string + natives?: string + + static new(this: new () => T, config: Partial = {}): T { + const instance = new this() + Object.assign(instance, config) + return instance + } + + new(this: T, config: Partial): T { + const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this) + Object.assign(clone, config) + return clone + } + + get nativesFolder(): string { + return this.natives ? join(this.project, this.natives) : this.project + } + +} + + export function relativeFilePath(project: string, filePath: string): string { return path.relative(project, filePath).split('.')[0] } @@ -87,14 +110,11 @@ export const handleError = (error: any): void => { logger.debug(failureDescription('ℹ️ Stack trace:', error)) } - - - export async function readNatives(nativeFolder: string): Promise { const paths = await globby('**/*.@(ts|js)', { cwd: nativeFolder }) const debug = logger.getLevel() <= logger.levels.DEBUG - if (debug) time('Reading natives files') + if (debug) time('Loading natives files') const nativesObjects: List = await Promise.all( paths.map(async (filePath) => { @@ -105,12 +125,11 @@ export async function readNatives(nativeFolder: string): Promise { return segments.reduceRight((acc, segment) => { return { [segment]: acc }}, importedModule.default || importedModule) }) ) + if (debug) timeEnd('Loading natives files') - return natives(nativesObjects) // Ahora `natives` recibe un array de objetos + return natives(nativesObjects) } - - // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PRINTING // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/test/init.test.ts b/test/init.test.ts index a919d2d..45a7460 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { readFileSync, rmSync } from 'fs' import sinon from 'sinon' import init, { Options } from '../src/commands/init' -import test from '../src/commands/test' +import test, {Options as TestOptions }from '../src/commands/test' import { pathAssertions } from './assertions' chai.should() @@ -16,13 +16,13 @@ const customFolderName = 'custom-folder' const customFolderProject = join(project, customFolderName) const GITHUB_FOLDER = join('.github', 'workflows') -const baseOptions: Options = { - project, +const baseOptions = Options.new({ + project: project, noCI: false, noTest: false, game: false, noGit: false, -} +}) describe('testing init', () => { @@ -52,22 +52,18 @@ describe('testing init', () => { expect(join(project, '.git/HEAD')).to.pathExists expect(getResourceFolder()).to.be.undefined - await test(undefined, { - project, + await test(undefined, TestOptions.new({ + project: project, skipValidations: false, - file: undefined, - describe: undefined, - test: undefined, - }) + })) expect(processExitSpy.callCount).to.equal(0) }) it('should create files successfully for game project with ci & custom example name', () => { - init(undefined, { - ...baseOptions, + init(undefined, baseOptions.new({ game: true, name: 'pepita', - }) + })) expect(join(project, 'pepita.wlk')).to.pathExists expect(join(project, 'testPepita.wtest')).to.pathExists @@ -80,12 +76,11 @@ describe('testing init', () => { }) it('should create files successfully for game project with no ci & no test custom example name', async () => { - init(undefined, { - ...baseOptions, + init(undefined, baseOptions.new({ noCI: true, noTest: true, name: 'pepita', - }) + })) expect(join(project, 'pepita.wlk')).to.pathExists expect(join(project, 'testPepita.wtest')).to.pathExists @@ -109,7 +104,7 @@ describe('testing init', () => { }) it('should skip the initialization of a git repository if notGit flag es enabled', async () => { - init(undefined, { ...baseOptions, noGit: true }) + init(undefined, baseOptions.new({ noGit: true })) expect(join(project, '.git')).not.to.pathExists expect(join(project, '.git/HEAD')).not.to.pathExists @@ -124,10 +119,7 @@ describe('testing init', () => { }) it('should exit with code 1 if folder already exists', () => { - init(undefined, { - ...baseOptions, - project: join('examples', 'init-examples', 'existing-folder'), - }) + init(undefined, baseOptions.new({ project: join('examples', 'init-examples', 'existing-folder') })) expect(processExitSpy.calledWith(1)).to.be.true }) diff --git a/test/test.test.ts b/test/test.test.ts index 65caf89..8177105 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -3,7 +3,7 @@ import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' import { Environment } from 'wollok-ts' -import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' +import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters, Options } from '../src/commands/test' import { logger as fileLogger } from '../src/logger' import { buildEnvironmentForProject } from '../src/utils' import { spyCalledWithSubstring } from './assertions' @@ -18,13 +18,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'normal-case') - const emptyOptions = { + const emptyOptions = Options.new({ project: projectPath, skipValidations: false, file: undefined, describe: undefined, test: undefined, - } + }) beforeEach(async () => { environment = await buildEnvironmentForProject(projectPath) @@ -83,47 +83,34 @@ describe('Test', () => { describe('with file/describe/test options', () => { it('should filter by test using test option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - test: 'another test', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ test: 'another test' })) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another test"') }) it('should filter by test using test option - case sensitive', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - test: 'aNother Test', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ test: 'aNother Test' })) expect(tests.length).to.equal(0) }) it('should filter by describe using describe option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - describe: 'second describe', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ describe: 'second describe' })) expect(tests.length).to.equal(2) expect(tests[0].name).to.equal('"second test"') expect(tests[1].name).to.equal('"another second test"') }) it('should filter by describe & test using describe & test option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, + const tests = getTarget(environment, undefined, emptyOptions.new({ describe: 'second describe', test: 'another second test', - }) + })) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another second test"') }) it('should filter by file using file option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - file: 'test-one.wtest', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'test-one.wtest' })) expect(tests.length).to.equal(3) expect(tests[0].name).to.equal('"a test"') expect(tests[1].name).to.equal('"another test"') @@ -131,11 +118,10 @@ describe('Test', () => { }) it('should filter by file & describe using file & describe option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, + const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'test-one.wtest', describe: 'this describe', - }) + })) expect(tests.length).to.equal(3) expect(tests[0].name).to.equal('"a test"') expect(tests[1].name).to.equal('"another test"') @@ -143,12 +129,11 @@ describe('Test', () => { }) it('should filter by file & describe & test using file & describe & test option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, + const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'test-one.wtest', describe: 'this describe', test: 'another test', - }) + })) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another test"') }) @@ -160,13 +145,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'only-case') - const emptyOptions = { + const emptyOptions = Options.new({ project: projectPath, skipValidations: false, file: undefined, describe: undefined, test: undefined, - } + }) beforeEach(async () => { environment = await buildEnvironmentForProject(projectPath) @@ -179,10 +164,7 @@ describe('Test', () => { }) it('should execute single test when running a describe using file option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - describe: 'only describe', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ describe: 'only describe' })) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"this is the one"') }) @@ -194,10 +176,7 @@ describe('Test', () => { }) it('should execute single test when running a file using file option', () => { - const tests = getTarget(environment, undefined, { - ...emptyOptions, - file: 'only-file.wtest', - }) + const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'only-file.wtest' })) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"this is the one"') }) @@ -219,13 +198,13 @@ describe('Test', () => { describe('validateParameters', () => { - const emptyOptions = { + const emptyOptions = Options.new({ skipValidations: false, project: '', file: undefined, describe: undefined, test: undefined, - } + }) it('should pass if no filter and no options passed', () => { expect(() => { validateParameters(undefined, emptyOptions) }).not.to.throw() @@ -236,29 +215,27 @@ describe('Test', () => { }) it('should pass if options is passed and no filter is passed', () => { - expect(() => { validateParameters(undefined, { - ...emptyOptions, - test: 'some test', - }) }).not.to.throw() + expect(() => { validateParameters(undefined, + emptyOptions.new({ test: 'some test' })) + }).not.to.throw() }) it('should fail if filter and options are passed', () => { - expect(() => { validateParameters('some describe', { - ...emptyOptions, - test: 'some test', - }) }).to.throw(/You should either use filter by full name or file/) + expect(() => { validateParameters('some describe', + emptyOptions.new({ test: 'some test' })) + }).to.throw(/You should either use filter by full name or file/) }) }) describe('matching test description', () => { - const emptyOptions = { + const emptyOptions = Options.new({ project: '', skipValidations: false, file: undefined, describe: undefined, test: undefined, - } + }) it('should return empty string if no filter or options are passed', () => { @@ -270,21 +247,19 @@ describe('Test', () => { }) it('should return options descriptions if options are passed', () => { - expect(matchingTestDescription(undefined, { - ...emptyOptions, + expect(matchingTestDescription(undefined, emptyOptions.new({ file: 'test-one.wtest', describe: 'this describe', test: 'another test', - })).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') + }))).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') }) it('should return options descriptions with wildcards if options are missing', () => { - expect(matchingTestDescription(undefined, { - ...emptyOptions, + expect(matchingTestDescription(undefined, emptyOptions.new({ file: undefined, describe: 'this discribe', test: undefined, - })).to.include('*.\'this discribe\'.*') + }))).to.include('*.\'this discribe\'.*') }) }) @@ -308,13 +283,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'normal-case') - const emptyOptions = { + const emptyOptions = Options.new({ project: projectPath, skipValidations: true, file: undefined, describe: undefined, test: undefined, - } + }) beforeEach(() => { loggerInfoSpy = sinon.stub(logger, 'info') @@ -328,10 +303,7 @@ describe('Test', () => { }) it('passes all the tests successfully and exits normally', async () => { - await test(undefined, { - ...emptyOptions, - file: 'test-one.wtest', - }) + await test(undefined, emptyOptions.new({ file: 'test-one.wtest' })) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 3 tests')).to.be.true @@ -343,10 +315,7 @@ describe('Test', () => { }) it('passing a wrong filename runs no tests and logs a warning', async () => { - await test(undefined, { - ...emptyOptions, - file: 'non-existing-file.wtest', - }) + await test(undefined, emptyOptions.new({ file: 'non-existing-file.wtest' })) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true @@ -354,11 +323,10 @@ describe('Test', () => { }) it('passing a wrong describe runs no tests and logs a warning', async () => { - await test(undefined, { - ...emptyOptions, + await test(undefined, emptyOptions.new({ file: 'test-one.wtest', describe: 'non-existing-describe', - }) + })) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true @@ -380,12 +348,11 @@ describe('Test', () => { }) it('returns exit code 2 if one or more tests fail', async () => { - await test(undefined, { - ...emptyOptions, + await test(undefined, emptyOptions.new({ file: 'test-two.wtest', describe: 'third describe', test: 'just a test', - }) + })) expect(processExitSpy.calledWith(2)).to.be.true expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 test')).to.be.true @@ -399,12 +366,11 @@ describe('Test', () => { }) it('returns exit code 2 if one or more tests have errors', async () => { - await test(undefined, { - ...emptyOptions, + await test(undefined, emptyOptions.new({ file: 'test-two.wtest', describe: 'second describe', test: 'second test', - }) + })) expect(processExitSpy.calledWith(2)).to.be.true expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 test')).to.be.true @@ -418,11 +384,10 @@ describe('Test', () => { }) it('returns exit code 1 if tests has parse errors', async () => { - await test(undefined, { - ...emptyOptions, + await test(undefined, emptyOptions.new({ skipValidations: false, project: join('examples', 'test-examples', 'failing-case'), - }) + })) expect(processExitSpy.calledWith(1)).to.be.true }) diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index 0451f50..aa37020 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -2,44 +2,33 @@ import { expect } from 'chai' import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' -import test from '../src/commands/test' +import test, { Options } from '../src/commands/test' import { logger as fileLogger } from '../src/logger' import { spyCalledWithSubstring } from './assertions' describe('UserNatives', () => { let fileLoggerInfoSpy: sinon.SinonStub let loggerInfoSpy: sinon.SinonStub - let loggerErrorSpy: sinon.SinonStub let processExitSpy: sinon.SinonStub - const projectPath = join('examples', 'user-natives') - const emptyOptions = { - project: projectPath, - skipValidations: true, - file: undefined, - describe: undefined, - test: undefined, - } + const options = Options.new({ + project: join('examples', 'user-natives'), + natives: 'myNativesFolder', + }) beforeEach(() => { loggerInfoSpy = sinon.stub(logger, 'info') fileLoggerInfoSpy = sinon.stub(fileLogger, 'info') processExitSpy = sinon.stub(process, 'exit') - loggerErrorSpy = sinon.stub(logger, 'error') }) afterEach(() => { - console.log('Error logs:', loggerErrorSpy.getCalls()) //No commitear, quitar cuando ya ande!!! sinon.restore() - }); + }) it('passes all the tests successfully and exits normally', async () => { - await test(undefined, { - ...emptyOptions, - file: 'userNatives.wtest', - natives: 'myNativesFolder', - }) + await test(undefined, options) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true @@ -53,4 +42,4 @@ describe('UserNatives', () => { errored: 0, }) }) -}) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8376fc8..85c3163 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,8 @@ "experimentalDecorators": true }, "include": ["test/**/*.ts", "src/**/*.ts", "examples/**/*.ts"], - "exclude": ["**/*.js", "node_modules"] + "exclude": ["**/*.js", "node_modules"], + "references": [ + { "path": "../wollok-ts" } + ] } From 7d0a59d92320053644c5ce22418e4a995c41f53e Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 15 Dec 2024 18:23:45 -0300 Subject: [PATCH 07/24] fix path assertions --- src/commands/init.ts | 5 +++-- src/utils.ts | 6 +++++- test/assertions.ts | 14 +++++++++----- test/init.test.ts | 42 ++++++++++++++++++++++++++++++++++-------- tsconfig.json | 5 +---- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index b43566b..60fb8db 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -3,7 +3,7 @@ import logger from 'loglevel' import { existsSync, writeFileSync } from 'node:fs' import { basename, join } from 'node:path' import { userInfo } from 'os' -import { ENTER, createFolderIfNotExists , BaseOptions} from '../utils' +import { ENTER, createFolderIfNotExists, BaseOptions } from '../utils' import { PROGRAM_FILE_EXTENSION, TEST_FILE_EXTENSION, WOLLOK_FILE_EXTENSION } from 'wollok-ts' import { execSync } from 'node:child_process' @@ -16,7 +16,7 @@ export class Options extends BaseOptions { } -export default function (folder: string | undefined, { project: _project, name, noTest = false, noCI = false, game = false, noGit = false }: Options): void { +export default function (folder: string | undefined, { project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder }: Options): void { const project = join(_project, folder ?? '') // Initialization @@ -28,6 +28,7 @@ export default function (folder: string | undefined, { project: _project, name, // Creating folders createFolderIfNotExists(project) + createFolderIfNotExists(nativesFolder) createFolderIfNotExists(join(project, '.github')) createFolderIfNotExists(join(project, '.github', 'workflows')) if (game) { diff --git a/src/utils.ts b/src/utils.ts index a5f8615..e43d67a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -42,7 +42,11 @@ export class BaseOptions { } get nativesFolder(): string { - return this.natives ? join(this.project, this.natives) : this.project + return this.natives + ? path.isAbsolute(this.natives) + ? this.natives + : join(this.project, this.natives) + : this.project } } diff --git a/test/assertions.ts b/test/assertions.ts index 331d762..c4dc231 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -7,7 +7,7 @@ declare global { export namespace Chai { interface Assertion { // TODO: split into the separate modules connect: (label: string, sourceLabel: string, targetLabel: string, width?: number, style?: string) => Assertion - pathExists(path: string): Assertion + pathExists: Assertion } interface Include { @@ -19,17 +19,21 @@ declare global { export const pathAssertions: Chai.ChaiPlugin = (chai) => { const { Assertion } = chai - Assertion.addMethod('pathExists', function (path) { + Assertion.addProperty('pathExists', function () { + const path:string = this._obj const exists = existsSync(path) this.assert( exists, - 'expected path #{this} to exist', - 'expected path #{this} to not exist', - path + `expected this path to exist: '${path}'`, + `expected this path to not exist: '${path}'`, + true, + exists, + false ) }) } + export const diagramAssertions: Chai.ChaiPlugin = (chai) => { const { Assertion } = chai diff --git a/test/init.test.ts b/test/init.test.ts index 45a7460..61ae6bb 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -3,8 +3,9 @@ import { join } from 'path' import { readFileSync, rmSync } from 'fs' import sinon from 'sinon' import init, { Options } from '../src/commands/init' -import test, {Options as TestOptions }from '../src/commands/test' +import test, {Options as TestOptions } from '../src/commands/test' import { pathAssertions } from './assertions' +import { homedir } from 'os' chai.should() @@ -14,6 +15,7 @@ use(pathAssertions) const project = join('examples', 'init-examples', 'basic-example') const customFolderName = 'custom-folder' const customFolderProject = join(project, customFolderName) +const absoluteFolder = join(homedir(), '_____folder_for_wollok_unit_test_please_remove_it______') const GITHUB_FOLDER = join('.github', 'workflows') const baseOptions = Options.new({ @@ -35,6 +37,8 @@ describe('testing init', () => { afterEach(() => { rmSync(project, { recursive: true, force: true }) rmSync(customFolderProject, { recursive: true, force: true }) + rmSync(absoluteFolder, { recursive: true, force: true }) + sinon.restore() }) @@ -47,7 +51,7 @@ describe('testing init', () => { expect(join(project, GITHUB_FOLDER, 'ci.yml')).to.pathExists expect(join(project, 'README.md')).to.pathExists expect(join(project, '.gitignore')).to.pathExists - expect(join(project, 'mainExample.wpgm')).to.pathExists + expect(join(project, 'mainExample.wpgm')).to.not.pathExists expect(join(project, '.git')).to.pathExists expect(join(project, '.git/HEAD')).to.pathExists expect(getResourceFolder()).to.be.undefined @@ -83,20 +87,21 @@ describe('testing init', () => { })) expect(join(project, 'pepita.wlk')).to.pathExists - expect(join(project, 'testPepita.wtest')).to.pathExists + expect(join(project, 'testPepita.wtest')).to.not.pathExists expect(join(project, 'package.json')).to.pathExists - expect(join(project, 'mainPepita.wpgm')).to.pathExists - expect(join(project, GITHUB_FOLDER, 'ci.yml')).to.pathExists + expect(join(project, 'mainPepita.wpgm')).to.not.pathExists + expect(join(project, GITHUB_FOLDER, 'ci.yml')).to.not.pathExists expect(join(project, '.gitignore')).to.pathExists expect(join(project, 'README.md')).to.pathExists }) it('should create files successfully with an argument for the folder name working in combination with project option', async () => { - init(customFolderName, baseOptions) + init(customFolderName, baseOptions.new({ name: 'pepita' })) + expect(join(customFolderProject, 'pepita.wlk')).to.pathExists expect(join(customFolderProject, 'testPepita.wtest')).to.pathExists - expect(join(customFolderProject, 'mainPepita.wpgm')).to.pathExists + expect(join(customFolderProject, 'mainPepita.wpgm')).to.not.pathExists expect(join(customFolderProject, 'package.json')).to.pathExists expect(join(customFolderProject, GITHUB_FOLDER, 'ci.yml')).to.pathExists expect(join(customFolderProject, 'README.md')).to.pathExists @@ -114,7 +119,7 @@ describe('testing init', () => { expect(join(project, GITHUB_FOLDER, 'ci.yml')).to.pathExists expect(join(project, 'README.md')).to.pathExists expect(join(project, '.gitignore')).to.pathExists - expect(join(project, 'mainExample.wpgm')).to.pathExists + expect(join(project, 'mainExample.wpgm')).to.not.pathExists expect(getResourceFolder()).to.be.undefined }) @@ -123,6 +128,27 @@ describe('testing init', () => { expect(processExitSpy.calledWith(1)).to.be.true }) + + it('should create a natives folder when it is required', () => { + init(undefined, baseOptions.new({ natives: 'myNatives' })) + expect(join(project, 'myNatives')).to.pathExists + + }) + + it('should create a natives nested folders when it is required', () => { + const nativesFolder =join('myNatives', 'myReallyNatives') + init(undefined, baseOptions.new({ natives: nativesFolder })) + expect(join(project, nativesFolder)).to.pathExists + + }) + + it('should create a native folders event it is an absolute path', () => { + init(undefined, baseOptions.new({ natives: absoluteFolder })) + expect(absoluteFolder).to.pathExists + + }) + + }) const getResourceFolder = () => { diff --git a/tsconfig.json b/tsconfig.json index 85c3163..8376fc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,5 @@ "experimentalDecorators": true }, "include": ["test/**/*.ts", "src/**/*.ts", "examples/**/*.ts"], - "exclude": ["**/*.js", "node_modules"], - "references": [ - { "path": "../wollok-ts" } - ] + "exclude": ["**/*.js", "node_modules"] } From 55106f064702bbd6b8dfb65dc8a549db9cbffe7c Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 15 Dec 2024 19:45:51 -0300 Subject: [PATCH 08/24] set folder as part of options --- src/commands/init.ts | 6 ++---- src/index.ts | 8 ++++++-- src/utils.ts | 7 ++++++- test/init.test.ts | 20 ++++++++++---------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 60fb8db..3c438ed 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -15,10 +15,8 @@ export class Options extends BaseOptions { noGit!: boolean } - -export default function (folder: string | undefined, { project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder }: Options): void { - const project = join(_project, folder ?? '') - +export default function ({ project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder, sourceFolder }: Options): void { + const project = sourceFolder // Initialization if (existsSync(join(project, 'package.json'))) { logger.info(yellow(bold(`🚨 There is already a project inside ${project} folder`))) diff --git a/src/index.ts b/src/index.ts index b2b6878..8e23743 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Command } from 'commander' import repl from './commands/repl' import run from './commands/run' import test from './commands/test' -import init from './commands/init' +import init, { Options as InitOptions } from './commands/init' import logger from 'loglevel' import pkg from '../package.json' import { cyan } from 'chalk' @@ -65,7 +65,11 @@ updateNotifier().finally(() => { .option('-c, --noCI', 'avoids creating a file for CI', false) .option('-ng, --noGit', 'avoids initializing a git repository', false) .allowUnknownOption() - .action(init) + .action((folder, options) => { + const customOptions = InitOptions.new(options) + if (folder) customOptions.folder = folder + init(folder, customOptions) + }) program.parseAsync() }) \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index e43d67a..6191ef0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,6 +28,7 @@ export const folderIcon = '🗂️' export class BaseOptions { project!: string natives?: string + folder?: string static new(this: new () => T, config: Partial = {}): T { const instance = new this() @@ -41,11 +42,15 @@ export class BaseOptions { return clone } + get sourceFolder() : string { + return this.folder ? join(this.project, this.folder) : this.project + } + get nativesFolder(): string { return this.natives ? path.isAbsolute(this.natives) ? this.natives - : join(this.project, this.natives) + : join(this.sourceFolder, this.natives) : this.project } diff --git a/test/init.test.ts b/test/init.test.ts index 61ae6bb..8bb6875 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { readFileSync, rmSync } from 'fs' import sinon from 'sinon' import init, { Options } from '../src/commands/init' -import test, {Options as TestOptions } from '../src/commands/test' +import test, { Options as TestOptions } from '../src/commands/test' import { pathAssertions } from './assertions' import { homedir } from 'os' @@ -43,7 +43,7 @@ describe('testing init', () => { }) it('should create files successfully for default values: ci, no game, example name & git', async () => { - init(undefined, baseOptions) + init(baseOptions) expect(join(project, 'example.wlk')).to.pathExists expect(join(project, 'testExample.wtest')).to.pathExists @@ -64,7 +64,7 @@ describe('testing init', () => { }) it('should create files successfully for game project with ci & custom example name', () => { - init(undefined, baseOptions.new({ + init(baseOptions.new({ game: true, name: 'pepita', })) @@ -80,7 +80,7 @@ describe('testing init', () => { }) it('should create files successfully for game project with no ci & no test custom example name', async () => { - init(undefined, baseOptions.new({ + init(baseOptions.new({ noCI: true, noTest: true, name: 'pepita', @@ -96,7 +96,7 @@ describe('testing init', () => { }) it('should create files successfully with an argument for the folder name working in combination with project option', async () => { - init(customFolderName, baseOptions.new({ name: 'pepita' })) + init(baseOptions.new({ name: 'pepita' , folder: customFolderName})) expect(join(customFolderProject, 'pepita.wlk')).to.pathExists @@ -109,7 +109,7 @@ describe('testing init', () => { }) it('should skip the initialization of a git repository if notGit flag es enabled', async () => { - init(undefined, baseOptions.new({ noGit: true })) + init(baseOptions.new({ noGit: true })) expect(join(project, '.git')).not.to.pathExists expect(join(project, '.git/HEAD')).not.to.pathExists @@ -124,26 +124,26 @@ describe('testing init', () => { }) it('should exit with code 1 if folder already exists', () => { - init(undefined, baseOptions.new({ project: join('examples', 'init-examples', 'existing-folder') })) + init(baseOptions.new({ project: join('examples', 'init-examples', 'existing-folder') })) expect(processExitSpy.calledWith(1)).to.be.true }) it('should create a natives folder when it is required', () => { - init(undefined, baseOptions.new({ natives: 'myNatives' })) + init(baseOptions.new({ natives: 'myNatives' })) expect(join(project, 'myNatives')).to.pathExists }) it('should create a natives nested folders when it is required', () => { const nativesFolder =join('myNatives', 'myReallyNatives') - init(undefined, baseOptions.new({ natives: nativesFolder })) + init(baseOptions.new({ natives: nativesFolder })) expect(join(project, nativesFolder)).to.pathExists }) it('should create a native folders event it is an absolute path', () => { - init(undefined, baseOptions.new({ natives: absoluteFolder })) + init(baseOptions.new({ natives: absoluteFolder })) expect(absoluteFolder).to.pathExists }) From b3dcb4b0e901f8106a41fe4a113b838108dbad9e Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 15 Dec 2024 19:46:58 -0300 Subject: [PATCH 09/24] fix init call --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8e23743..ed69148 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ updateNotifier().finally(() => { .action((folder, options) => { const customOptions = InitOptions.new(options) if (folder) customOptions.folder = folder - init(folder, customOptions) + init(customOptions) }) program.parseAsync() From e3ffc1e3667a00be95b01a1730af4a39f1b8814e Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Mon, 16 Dec 2024 11:06:20 -0300 Subject: [PATCH 10/24] add natives to package.json --- src/commands/init.ts | 10 +++---- src/index.ts | 10 +++---- src/utils.ts | 7 +---- test/assertions.ts | 71 +++++++++++++++++++++++++++++++++++++++++++- test/init.test.ts | 17 +++++------ 5 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 3c438ed..01fbfc4 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -15,7 +15,7 @@ export class Options extends BaseOptions { noGit!: boolean } -export default function ({ project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder, sourceFolder }: Options): void { +export default function ({ project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder, sourceFolder, natives }: Options): void { const project = sourceFolder // Initialization if (existsSync(join(project, 'package.json'))) { @@ -51,7 +51,7 @@ export default function ({ project: _project, name, noTest = false, noCI = fals } logger.info('Creating package.json') - writeFileSync(join(project, 'package.json'), packageJsonDefinition(project, game)) + writeFileSync(join(project, 'package.json'), packageJsonDefinition(project, game, natives )) if (!noCI) { logger.info('Creating CI files') @@ -129,16 +129,16 @@ program PepitaGame { } ` -const packageJsonDefinition = (projectName: string, game: boolean) => `{ +const packageJsonDefinition = (projectName: string, game: boolean, natives: string) => `{ "name": "${basename(projectName)}", "version": "1.0.0", ${game ? assetsConfiguration() : ''}"wollokVersion": "4.0.0", - "author": "${userInfo().username}", + "author": "${userInfo().username}",${nativesConfiguration(natives)} "license": "ISC" } ` - const assetsConfiguration = () => `"resourceFolder": "assets",${ENTER} ` +const nativesConfiguration = (natives: string) => natives ? `${ENTER}"natives": "${natives}",` : '' const ymlForCI = `name: build diff --git a/src/index.ts b/src/index.ts index ed69148..a73c49b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { Command } from 'commander' import repl from './commands/repl' import run from './commands/run' -import test from './commands/test' +import test, { Options as TestOptions } from './commands/test' import init, { Options as InitOptions } from './commands/init' import logger from 'loglevel' import pkg from '../package.json' @@ -40,7 +40,7 @@ updateNotifier().finally(() => { .option('-t, --test [test]', 'test to run', '') .option('--skipValidations', 'skip code validation', false) .option('-v, --verbose', 'print debugging information', false) - .action(test) + .action((filter, options) => {test(filter, TestOptions.new(options))}) program.command('repl') .description('Start Wollok interactive console') @@ -66,9 +66,9 @@ updateNotifier().finally(() => { .option('-ng, --noGit', 'avoids initializing a git repository', false) .allowUnknownOption() .action((folder, options) => { - const customOptions = InitOptions.new(options) - if (folder) customOptions.folder = folder - init(customOptions) + const initOptions = InitOptions.new(options) + if (folder) initOptions.folder = folder + init(initOptions) }) program.parseAsync() diff --git a/src/utils.ts b/src/utils.ts index 6191ef0..e64e67a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -47,16 +47,11 @@ export class BaseOptions { } get nativesFolder(): string { - return this.natives - ? path.isAbsolute(this.natives) - ? this.natives - : join(this.sourceFolder, this.natives) - : this.project + return this.natives ? join(this.sourceFolder, this.natives) : this.sourceFolder } } - export function relativeFilePath(project: string, filePath: string): string { return path.relative(project, filePath).split('.')[0] } diff --git a/test/assertions.ts b/test/assertions.ts index c4dc231..0575895 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -1,5 +1,5 @@ import { ElementDefinition } from 'cytoscape' -import { existsSync } from 'fs' +import { existsSync, readFileSync } from 'fs' type ElementDefinitionQuery = Partial @@ -8,6 +8,8 @@ declare global { interface Assertion { // TODO: split into the separate modules connect: (label: string, sourceLabel: string, targetLabel: string, width?: number, style?: string) => Assertion pathExists: Assertion + jsonKeys(expectedKeys: string[]): Assertion; + jsonMatch(expected: Record): Assertion; } interface Include { @@ -81,4 +83,71 @@ export const spyCalledWithSubstring = (spy: sinon.SinonStub, value: string, debu } } return false +} + +export const jsonAssertions: Chai.ChaiPlugin = (chai) => { + const { Assertion } = chai + + const getNestedValue = (obj: Record, path: string): any => + path.split('.').reduce((acc, key) => (acc ? acc[key] : undefined), obj) + + const matchPartial = ( + expected: Record, + actual: Record, + prefix: string = '' + ): boolean => + Object.keys(expected).every((key) => { + const fullPath = prefix ? `${prefix}.${key}` : key + if (typeof expected[key] === 'object' && expected[key] !== null) { + if (typeof actual[key] !== 'object' || actual[key] === null) { + return false + } + return matchPartial(expected[key], actual[key], fullPath) + } + return actual[key] === expected[key] + }) + + const getJsonContent = (filePath: string): any => { + if (!existsSync(filePath)) { + throw new chai.AssertionError(`Expected file "${filePath}" to exist`) + } + + try { + return JSON.parse(readFileSync(filePath, 'utf8')) + } catch (error) { + throw new chai.AssertionError( + `Failed to parse JSON from file "${filePath}": ${String(error)}` + ) + } + } + + Assertion.addMethod('jsonKeys', function (expectedKeys: string[]) { + const filePath = this._obj as string + + const jsonContent = getJsonContent(filePath) + + expectedKeys.forEach((key) => + this.assert( + getNestedValue(jsonContent, key) !== undefined, + `Expected JSON to have key "${key}"`, + `Expected JSON not to have key "${key}"`, + key + ) + ) + }) + + Assertion.addMethod('jsonMatch', function (expected: Record) { + const filePath = this._obj as string + + const jsonContent = getJsonContent(filePath) + + this.assert( + matchPartial(expected, jsonContent), + `Expected JSON to match: ${JSON.stringify(expected)}, but got: ${JSON.stringify(jsonContent)}`, + `Expected JSON not to match: ${JSON.stringify(expected)}`, + expected, + jsonContent, + true + ) + }) } \ No newline at end of file diff --git a/test/init.test.ts b/test/init.test.ts index 8bb6875..878a383 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -4,18 +4,17 @@ import { readFileSync, rmSync } from 'fs' import sinon from 'sinon' import init, { Options } from '../src/commands/init' import test, { Options as TestOptions } from '../src/commands/test' -import { pathAssertions } from './assertions' -import { homedir } from 'os' +import { pathAssertions, jsonAssertions } from './assertions' chai.should() const expect = chai.expect use(pathAssertions) +use(jsonAssertions) const project = join('examples', 'init-examples', 'basic-example') const customFolderName = 'custom-folder' const customFolderProject = join(project, customFolderName) -const absoluteFolder = join(homedir(), '_____folder_for_wollok_unit_test_please_remove_it______') const GITHUB_FOLDER = join('.github', 'workflows') const baseOptions = Options.new({ @@ -37,7 +36,6 @@ describe('testing init', () => { afterEach(() => { rmSync(project, { recursive: true, force: true }) rmSync(customFolderProject, { recursive: true, force: true }) - rmSync(absoluteFolder, { recursive: true, force: true }) sinon.restore() }) @@ -132,20 +130,21 @@ describe('testing init', () => { it('should create a natives folder when it is required', () => { init(baseOptions.new({ natives: 'myNatives' })) expect(join(project, 'myNatives')).to.pathExists + expect('package.json') + expect(join(project, 'package.json')).jsonMatch({ natives: 'myNatives' }) }) it('should create a natives nested folders when it is required', () => { const nativesFolder =join('myNatives', 'myReallyNatives') init(baseOptions.new({ natives: nativesFolder })) - expect(join(project, nativesFolder)).to.pathExists + expect(join(project, 'package.json')).jsonMatch({ natives: nativesFolder }) }) - it('should create a native folders event it is an absolute path', () => { - init(baseOptions.new({ natives: absoluteFolder })) - expect(absoluteFolder).to.pathExists - + it('should not create a natives folders when it is not specified', () => { + init(baseOptions) + expect(join(project, 'package.json')).not.jsonKeys(['natives']) }) From 2f9c7ad780d98f222b9b05a85b6696e71ae48985 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Mon, 16 Dec 2024 21:36:54 -0300 Subject: [PATCH 11/24] add Options class to REPL --- .vscode/launch.json | 6 ++++++ src/commands/init.ts | 4 ++-- src/commands/repl.ts | 21 ++++++++++----------- test/assertions.ts | 2 +- test/fullRepl.test.ts | 19 ++++++++++--------- test/init.test.ts | 2 +- test/repl.test.ts | 26 +++++++++++++++++++++++--- 7 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3cc2dc0..5c12d88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,12 @@ "name": "Run a single test file", "request": "launch", "type": "node-terminal" + }, + { + "command": "npm start init -- -p pirulitoProject", + "name": "wollok init", + "request": "launch", + "type": "node-terminal" } ] } diff --git a/src/commands/init.ts b/src/commands/init.ts index 01fbfc4..313d614 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -129,7 +129,7 @@ program PepitaGame { } ` -const packageJsonDefinition = (projectName: string, game: boolean, natives: string) => `{ +const packageJsonDefinition = (projectName: string, game: boolean, natives?: string) => `{ "name": "${basename(projectName)}", "version": "1.0.0", ${game ? assetsConfiguration() : ''}"wollokVersion": "4.0.0", @@ -138,7 +138,7 @@ const packageJsonDefinition = (projectName: string, game: boolean, natives: stri } ` const assetsConfiguration = () => `"resourceFolder": "assets",${ENTER} ` -const nativesConfiguration = (natives: string) => natives ? `${ENTER}"natives": "${natives}",` : '' +const nativesConfiguration = (natives?: string) => natives ? `${ENTER}"natives": "${natives}",` : '' const ymlForCI = `name: build diff --git a/src/commands/repl.ts b/src/commands/repl.ts index 6dff32e..d84ac08 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -7,21 +7,20 @@ import http from 'http' import logger from 'loglevel' import { CompleterResult, Interface, createInterface as Repl } from 'readline' import { Server, Socket } from 'socket.io' -import { Entity, Environment, Evaluation, Interpreter, Package, REPL, interprete, link, WRENatives as natives } from 'wollok-ts' +import { Entity, Environment, Evaluation, Interpreter, Package, REPL, interprete, link } from 'wollok-ts' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' -import { ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' +import { BaseOptions, ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, readNatives } from '../utils' // TODO: // - autocomplete piola -export type Options = { - project: string - skipValidations: boolean, - darkMode: boolean, - host: string, - port: string, - skipDiagram: boolean, +export class Options extends BaseOptions { + skipValidations!: boolean + darkMode!: boolean + host!: string + port!: string + skipDiagram!: boolean } type DynamicDiagramClient = { @@ -105,7 +104,7 @@ export function interpreteLine(interpreter: Interpreter, line: string): string { return errored ? failureDescription(result, error) : successDescription(result) } -export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations }: Options): Promise { +export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations, nativesFolder }: Options): Promise { let environment: Environment const timeMeasurer = new TimeMeasurer() @@ -128,7 +127,7 @@ export async function initializeInterpreter(autoImportPath: string | undefined, const replPackage = new Package({ name: REPL }) environment = link([replPackage], environment) } - return new Interpreter(Evaluation.build(environment, natives)) + return new Interpreter(Evaluation.build(environment, await readNatives(nativesFolder))) } catch (error: any) { handleError(error) fileLogger.info({ message: `${replIcon} REPL execution - build failed for ${project}`, timeElapsed: timeMeasurer.elapsedTime(), ok: false, error: sanitizeStackTrace(error) }) diff --git a/test/assertions.ts b/test/assertions.ts index 0575895..ce81bec 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -89,7 +89,7 @@ export const jsonAssertions: Chai.ChaiPlugin = (chai) => { const { Assertion } = chai const getNestedValue = (obj: Record, path: string): any => - path.split('.').reduce((acc, key) => (acc ? acc[key] : undefined), obj) + path.split('.').reduce((acc, key) => acc ? acc[key] : undefined, obj) const matchPartial = ( expected: Record, diff --git a/test/fullRepl.test.ts b/test/fullRepl.test.ts index 068b49f..7117164 100644 --- a/test/fullRepl.test.ts +++ b/test/fullRepl.test.ts @@ -14,33 +14,34 @@ chai.use(chaiHttp) chai.use(chaiAsPromised) const expect = chai.expect -const baseOptions = { +const baseOptions = Options.new({ darkMode: true, port: '8080', host: 'localhost', skipDiagram: true, -} - -const buildOptionsFor = (path: string, skipValidations = false) => ({ - ...baseOptions, - project: join('examples', 'bad-files-examples', path), - skipValidations, }) +const buildOptionsFor = (path: string, skipValidations = false) => + baseOptions.new({ + project: join('examples', 'bad-files-examples', path), + skipValidations: skipValidations, + }) + + const callRepl = (autoImportPath: string, options: Options) => replFn(join(options.project, autoImportPath), options) describe('REPL integration test for valid project', () => { const projectPath = join('examples', 'repl-examples') - const options = { + const options = Options.new({ project: projectPath, skipValidations: false, darkMode: true, host: 'localhost', port: '8080', skipDiagram: true, - } + }) let processExitSpy: sinon.SinonStub let consoleLogSpy: sinon.SinonStub let repl: Interface diff --git a/test/init.test.ts b/test/init.test.ts index 878a383..a6a2cb0 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -94,7 +94,7 @@ describe('testing init', () => { }) it('should create files successfully with an argument for the folder name working in combination with project option', async () => { - init(baseOptions.new({ name: 'pepita' , folder: customFolderName})) + init(baseOptions.new({ name: 'pepita', folder: customFolderName })) expect(join(customFolderProject, 'pepita.wlk')).to.pathExists diff --git a/test/repl.test.ts b/test/repl.test.ts index f6c3b66..81b6111 100644 --- a/test/repl.test.ts +++ b/test/repl.test.ts @@ -1,7 +1,7 @@ import { should } from 'chai' import { join } from 'path' import { Interpreter, REPL } from 'wollok-ts' -import { initializeInterpreter, interpreteLine } from '../src/commands/repl' +import { initializeInterpreter, interpreteLine, Options } from '../src/commands/repl' import { failureDescription, successDescription } from '../src/utils' should() @@ -10,14 +10,14 @@ const projectPath = join('examples', 'repl-examples') describe('REPL', () => { - const options = { + const options = Options.new({ project: projectPath, skipValidations: false, darkMode: true, port: '8080', host: 'localhost', skipDiagram: true, - } + }) let interpreter: Interpreter @@ -258,6 +258,26 @@ describe('REPL', () => { }) + describe('User Natives', () => { + + const project = join('examples', 'user-natives' ) + const userNativesOptions = options.new({ project: project, natives: 'myNativesFolder' }) + + it('should execute user natives', async () => { + interpreter = await initializeInterpreter(join(project, 'rootFile.wlk'), userNativesOptions) + + const result = interpreteLine(interpreter, 'myModel.nativeOne()') + result.should.be.equal(successDescription('1')) + }) + + it('should execute user natives in package', async () => { + interpreter = await initializeInterpreter(join(project, 'myPackage', 'myInnerFile.wlk'), userNativesOptions) + + const result = interpreteLine(interpreter, 'packageModel.nativeTwo()') + result.should.be.equal(successDescription('2')) + }) + + }) }) From 429df79d709ba3307d62440e9cae56b51084df68 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Wed, 18 Dec 2024 10:34:21 -0300 Subject: [PATCH 12/24] use base options on repl --- src/index.ts | 14 ++++++++----- src/utils.ts | 34 +++++++++++++++++++++++++++++++ test/diagram.test.ts | 6 +++--- test/dynamicDiagramClient.test.ts | 11 ++++------ 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index a73c49b..b0e2232 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { Command } from 'commander' -import repl from './commands/repl' +import repl, { Options as ReplOptions } from './commands/repl' import run from './commands/run' import test, { Options as TestOptions } from './commands/test' import init, { Options as InitOptions } from './commands/init' @@ -40,7 +40,7 @@ updateNotifier().finally(() => { .option('-t, --test [test]', 'test to run', '') .option('--skipValidations', 'skip code validation', false) .option('-v, --verbose', 'print debugging information', false) - .action((filter, options) => {test(filter, TestOptions.new(options))}) + .action((filter, options) => {test(filter, TestOptions.load(options))}) program.command('repl') .description('Start Wollok interactive console') @@ -53,6 +53,7 @@ updateNotifier().finally(() => { .option('--port [port]', 'port to run the server', '3000') .option('-v, --verbose', 'print debugging information', false) .action(repl) + .action((file, options) => {repl(file, ReplOptions.load(options))}) program.command('init') @@ -66,10 +67,13 @@ updateNotifier().finally(() => { .option('-ng, --noGit', 'avoids initializing a git repository', false) .allowUnknownOption() .action((folder, options) => { - const initOptions = InitOptions.new(options) - if (folder) initOptions.folder = folder - init(initOptions) + //init(InitOptions.new({ ...options, folder })) + const actionOptions = InitOptions.new( options ) + if ( folder ) { actionOptions.folder = folder } + init(actionOptions) + }) + program.parseAsync() }) \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index e64e67a..bfc24f1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,6 +36,40 @@ export class BaseOptions { return instance } + private safeLoadJson(this: T, path: string) { + try { + const rawData = fs.readFileSync(path, 'utf-8') + const jsonData = JSON.parse(rawData) + + Object.entries(jsonData) + .filter(([key]) => key in this) + .forEach(([key, value]) => { + try { + this[key as keyof T] = value as T[keyof T] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_error) { + // This is not a real error, if package.json has a value of different type then it is a non important value + // Silence here or log? + } + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_error) { + // No package.json or it is invalid. This is not a real problem. + // Silence here or log? + } + } + + static load(this: new () => T, partial: Partial): T { + const instance = new this() + + if ('project' in partial) { + const packageJsonPath = path.join(partial.project as string, 'package.json') + instance.safeLoadJson(packageJsonPath) + } + Object.assign(instance, partial) + return instance // Devolver la instancia creada + } + new(this: T, config: Partial): T { const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this) Object.assign(clone, config) diff --git a/test/diagram.test.ts b/test/diagram.test.ts index 24ba8b9..e8340a3 100644 --- a/test/diagram.test.ts +++ b/test/diagram.test.ts @@ -1,7 +1,7 @@ import { should, use } from 'chai' import { join } from 'path' import { interprete, Interpreter } from 'wollok-ts' -import { initializeInterpreter } from '../src/commands/repl' +import { initializeInterpreter, Options } from '../src/commands/repl' import { getDynamicDiagram } from '../src/utils' import { diagramAssertions } from './assertions' @@ -13,14 +13,14 @@ const simpleFile = join(projectPath, 'fish.wlk') const fileWithImports = join(projectPath, 'using-imports', 'base.wlk') describe('Dynamic diagram', () => { - const options = { + const options = Options.new({ project: projectPath, skipValidations: true, port: '8080', host: 'localhost', darkMode: true, skipDiagram: true, // we don't want to open a socket - } + }) let interpreter: Interpreter diff --git a/test/dynamicDiagramClient.test.ts b/test/dynamicDiagramClient.test.ts index b8a254b..25970c3 100644 --- a/test/dynamicDiagramClient.test.ts +++ b/test/dynamicDiagramClient.test.ts @@ -4,7 +4,7 @@ import chaiHttp from 'chai-http' import { join } from 'path' import { Interface, createInterface as Repl } from 'readline' import { Interpreter } from 'wollok-ts' -import { initializeClient, initializeInterpreter } from '../src/commands/repl' +import { initializeClient, initializeInterpreter, Options } from '../src/commands/repl' chai.should() chai.use(chaiHttp) @@ -14,14 +14,14 @@ const expect = chai.expect describe('dynamic diagram client', () => { const projectPath = join('examples', 'repl-examples') - const options = { + const options = Options.new({ project: projectPath, skipValidations: false, darkMode: true, port: '8080', host: 'localhost', skipDiagram: false, - } + }) let interpreter: Interpreter let repl: Interface @@ -46,10 +46,7 @@ describe('dynamic diagram client', () => { }) it('should return a fake client if skipDiagram is set', async () => { - const skipDiagramOptions = { - ...options, - skipDiagram: true, - } + const skipDiagramOptions = options.new({ skipDiagram: true }) const { enabled, app, server } = await initializeClient(skipDiagramOptions, repl, interpreter) expect(enabled).to.be.false expect(app).to.be.undefined From f6fb2b23a5d21dd027eecde00a4cfc92ca08dc00 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Wed, 18 Dec 2024 10:57:07 -0300 Subject: [PATCH 13/24] run options --- src/commands/run.ts | 17 ++++++++--------- src/index.ts | 4 ++-- test/run.test.ts | 21 +++++++++------------ 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/commands/run.ts b/src/commands/run.ts index 677644c..85dcf60 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -9,19 +9,18 @@ import { Server, Socket } from 'socket.io' import { Asset, boardState, buildKeyPressEvent, queueEvent, SoundState, soundState, VisualState, visualState } from 'wollok-web-tools' import { Environment, GAME_MODULE, interpret, Interpreter, Name, Package, RuntimeObject, WollokException, WRENatives as natives } from 'wollok-ts' import { logger as fileLogger } from '../logger' -import { buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' +import { BaseOptions, buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' import { DummyProfiler, EventProfiler, TimeMeasurer } from './../time-measurer' const { time, timeEnd } = console -export type Options = { - project: string - assets: string - skipValidations: boolean - host: string, - port: string - game: boolean, - startDiagram: boolean, +export class Options extends BaseOptions { + assets!: string //TODO: move to base options optional and remove logic to read package json? + skipValidations!: boolean + host!: string + port!: string + game!: boolean + startDiagram!: boolean } const DEFAULT_PORT = '4200' diff --git a/src/index.ts b/src/index.ts index b0e2232..b2f2b39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from 'commander' import repl, { Options as ReplOptions } from './commands/repl' -import run from './commands/run' +import run, { Options as RunOptions } from './commands/run' import test, { Options as TestOptions } from './commands/test' import init, { Options as InitOptions } from './commands/init' import logger from 'loglevel' @@ -29,7 +29,7 @@ updateNotifier().finally(() => { .option('-g, --game', 'sets the program as a game', false) .option('-v, --verbose', 'print debugging information', false) .option('-d, --startDiagram', 'activate the dynamic diagram (only for games)', false) - .action((programFQN, options) => { run(programFQN, options) }) + .action((programFQN, options) => { run(programFQN, RunOptions.load(options)) }) program.command('test') .description('Run Wollok tests') diff --git a/test/run.test.ts b/test/run.test.ts index 7193cd3..682cde4 100644 --- a/test/run.test.ts +++ b/test/run.test.ts @@ -19,10 +19,10 @@ const assets = 'assets' describe('testing run', () => { - const buildOptions = (game: boolean, assets: string): Options => ({ - game, - project, - assets, + const buildOptions = (game: boolean, assets: string) => Options.new({ + game: game, + project: project, + assets: assets, skipValidations: false, startDiagram: false, host: 'localhost', @@ -78,10 +78,7 @@ describe('testing run', () => { it('should return all visuals for a simple project', async () => { const imageProject = join('examples', 'run-examples', 'asset-example') - const options = { - ...buildOptions(true, 'assets'), - project: imageProject, - } + const options = buildOptions(true, 'assets').new({ project: imageProject }) const environment = await buildEnvironmentForProgram(options) const interpreter = getGameInterpreter(environment)! @@ -178,7 +175,7 @@ describe('testing run', () => { it('should work if program has no errors', async () => { - await run('mainExample.PepitaProgram', { + await run('mainExample.PepitaProgram', Options.new({ project: join('examples', 'run-examples', 'basic-example'), skipValidations: false, game: false, @@ -186,7 +183,7 @@ describe('testing run', () => { assets, host: 'localhost', port: '3000', - }) + })) expect(spyCalledWithSubstring(consoleLogSpy, 'Pepita empieza con 70')).to.be.true expect(spyCalledWithSubstring(consoleLogSpy, 'Vuela')).to.be.true expect(spyCalledWithSubstring(consoleLogSpy, '40')).to.be.true @@ -200,7 +197,7 @@ describe('testing run', () => { }) it('should exit if program has errors', async () => { - await run('mainExample.PepitaProgram', { + await run('mainExample.PepitaProgram', Options.new({ project: join('examples', 'run-examples', 'bad-example'), skipValidations: false, game: false, @@ -208,7 +205,7 @@ describe('testing run', () => { assets, host: 'localhost', port: '3000', - }) + })) expect(processExitSpy.calledWith(21)).to.be.true expect(fileLoggerInfoSpy.calledOnce).to.be.true const fileLoggerArg = fileLoggerInfoSpy.firstCall.firstArg From 2cb3d0279ad02775e86ec705bafdba4164d2297f Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Wed, 18 Dec 2024 11:02:23 -0300 Subject: [PATCH 14/24] add natives options to init --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index b2f2b39..63ac729 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,7 @@ updateNotifier().finally(() => { .option('-t, --noTest', 'avoids creating a test file', false) .option('-c, --noCI', 'avoids creating a file for CI', false) .option('-ng, --noGit', 'avoids initializing a git repository', false) + .option('-N, --natives', 'folder name for natives files', undefined) .allowUnknownOption() .action((folder, options) => { //init(InitOptions.new({ ...options, folder })) From b213aa8683a39ce4c82b15a9ad2057c090c8d846 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Wed, 18 Dec 2024 15:02:08 -0300 Subject: [PATCH 15/24] fix run --- examples/user-natives/myNativesFolder/rootFile.ts | 2 +- examples/user-natives/myProgram.wpgm | 8 ++++++++ examples/user-natives/package.json | 3 ++- src/commands/run.ts | 12 ++++++------ src/index.ts | 2 +- src/utils.ts | 6 ++++-- test/run.test.ts | 14 +++++--------- test/userNatives.test.ts | 3 +-- 8 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 examples/user-natives/myProgram.wpgm diff --git a/examples/user-natives/myNativesFolder/rootFile.ts b/examples/user-natives/myNativesFolder/rootFile.ts index 5d4c50a..d079f68 100644 --- a/examples/user-natives/myNativesFolder/rootFile.ts +++ b/examples/user-natives/myNativesFolder/rootFile.ts @@ -1,5 +1,5 @@ const myModel = { - *nativeOne(this: any, _self: any) : any { + *nativeOne(this: any, _self: any): any { return yield* this.reify(1) }, } diff --git a/examples/user-natives/myProgram.wpgm b/examples/user-natives/myProgram.wpgm new file mode 100644 index 0000000..df11c19 --- /dev/null +++ b/examples/user-natives/myProgram.wpgm @@ -0,0 +1,8 @@ +import myPackage.myInnerFile.packageModel +import rootFile.myModel + +program myProgram { + + console.println(myModel.nativeOne()) + console.println(packageModel.nativeTwo()) +} diff --git a/examples/user-natives/package.json b/examples/user-natives/package.json index aa8704d..ced106f 100644 --- a/examples/user-natives/package.json +++ b/examples/user-natives/package.json @@ -4,5 +4,6 @@ "resourceFolder": "assets", "wollokVersion": "4.0.0", "author": "leo", - "license": "ISC" + "license": "ISC", + "natives": "myNativesFolder" } diff --git a/src/commands/run.ts b/src/commands/run.ts index 85dcf60..90825b4 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -7,15 +7,15 @@ import logger from 'loglevel' import { join, relative } from 'path' import { Server, Socket } from 'socket.io' import { Asset, boardState, buildKeyPressEvent, queueEvent, SoundState, soundState, VisualState, visualState } from 'wollok-web-tools' -import { Environment, GAME_MODULE, interpret, Interpreter, Name, Package, RuntimeObject, WollokException, WRENatives as natives } from 'wollok-ts' +import { Environment, GAME_MODULE, interpret, Interpreter, Name, Natives, Package, RuntimeObject, WollokException } from 'wollok-ts' import { logger as fileLogger } from '../logger' -import { BaseOptions, buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' +import { BaseOptions, buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readNatives, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' import { DummyProfiler, EventProfiler, TimeMeasurer } from './../time-measurer' const { time, timeEnd } = console export class Options extends BaseOptions { - assets!: string //TODO: move to base options optional and remove logic to read package json? + assets!: string //TODO: move to base options optional and remove logic to read package j? skipValidations!: boolean host!: string port!: string @@ -52,8 +52,8 @@ export default async function (programFQN: Name, options: Options): Promise(programFQN).parent as Package const dynamicDiagramClient = await initializeDynamicDiagram(programPackage, options, interpreter) const ioGame: Server | undefined = initializeGameClient(options) @@ -79,7 +79,7 @@ export default async function (programFQN: Name, options: Options): Promise { +export const getGameInterpreter = (environment: Environment, natives:Natives): Interpreter => { return interpret(environment, natives) } diff --git a/src/index.ts b/src/index.ts index 63ac729..937ae78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,7 @@ updateNotifier().finally(() => { .option('-t, --noTest', 'avoids creating a test file', false) .option('-c, --noCI', 'avoids creating a file for CI', false) .option('-ng, --noGit', 'avoids initializing a git repository', false) - .option('-N, --natives', 'folder name for natives files', undefined) + .option('-N, --natives [natives]', 'folder name for natives files', undefined) .allowUnknownOption() .action((folder, options) => { //init(InitOptions.new({ ...options, folder })) diff --git a/src/utils.ts b/src/utils.ts index bfc24f1..e159d74 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,9 @@ import path, { join } from 'path' import { getDataDiagram, VALID_IMAGE_EXTENSIONS, VALID_SOUND_EXTENSIONS } from 'wollok-web-tools' import { buildEnvironment, Environment, getDynamicDiagramData, Interpreter, Natives, Package, Problem, validate, WOLLOK_EXTRA_STACK_TRACE_HEADER, WollokException, natives, List } from 'wollok-ts' import { ElementDefinition } from 'cytoscape' +import { register } from 'ts-node' + +register({ transpileOnly: true }) const { time, timeEnd } = console @@ -42,8 +45,7 @@ export class BaseOptions { const jsonData = JSON.parse(rawData) Object.entries(jsonData) - .filter(([key]) => key in this) - .forEach(([key, value]) => { + .forEach(([key, value]) => { //TODO! I want filter just for this properties, but 'in'operator doesn't work. et all properties work fine try { this[key as keyof T] = value as T[keyof T] // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/test/run.test.ts b/test/run.test.ts index 682cde4..164e285 100644 --- a/test/run.test.ts +++ b/test/run.test.ts @@ -81,7 +81,7 @@ describe('testing run', () => { const options = buildOptions(true, 'assets').new({ project: imageProject }) const environment = await buildEnvironmentForProgram(options) - const interpreter = getGameInterpreter(environment)! + const interpreter = getGameInterpreter(environment, await utils.readNatives(options.nativesFolder))! const game = interpreter.object('wollok.game.game') interpreter.send('addVisual', game, interpreter.object('mainGame.elementoVisual')) @@ -234,30 +234,26 @@ describe('testing run', () => { }) it('smoke test - should work if program has no errors', async () => { - const ioGame = await run('mainGame.PepitaGame', { + const ioGame = await run('mainGame.PepitaGame', buildOptions(true, 'specialAssets').new({ project: join('examples', 'run-examples', 'basic-game'), skipValidations: false, - game: true, startDiagram: false, - assets: 'specialAssets', port: '3000', host: 'localhost', - }) + })) ioGame?.close() expect(processExitSpy.calledWith(0)).to.be.false expect(errorReturned).to.be.undefined }) it('smoke test - should not work if program has errors', async () => { - const ioGame = await run('mainGame.PepitaGame', { + const ioGame = await run('mainGame.PepitaGame', buildOptions(true, 'specialAssets').new({ project: join('examples', 'run-examples', 'basic-example'), skipValidations: false, - game: true, startDiagram: false, - assets: 'specialAssets', port: '3000', host: 'localhost', - }) + })) ioGame?.close() expect(processExitSpy.calledWith(21)).to.be.false expect(errorReturned).to.equal('Folder image examples/run-examples/basic-example/specialAssets does not exist') diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index aa37020..585986e 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -12,9 +12,8 @@ describe('UserNatives', () => { let processExitSpy: sinon.SinonStub - const options = Options.new({ + const options = Options.load({ //load package.json project: join('examples', 'user-natives'), - natives: 'myNativesFolder', }) beforeEach(() => { From 2529174d8e3cc63b4d0fe4b33a65b4cfc2a468ef Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Wed, 18 Dec 2024 15:38:20 -0300 Subject: [PATCH 16/24] use typescript in runtime --- package.json | 6 +++--- src/commands/init.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a5ccb95..1290c86 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "socket.io": "^4.5.1", "winston": "^3.11.0", "wollok-ts": "file:../wollok-ts", - "wollok-web-tools": "^1.1.7" + "wollok-web-tools": "^1.1.7", + "typescript": "~5.5.1" }, "devDependencies": { "@stylistic/eslint-plugin-ts": "^2.8.0", @@ -92,7 +93,6 @@ "shx": "^0.3.4", "sinon": "^17.0.1", "socket.io-client": "^4.7.5", - "ts-node": "10.9.1", - "typescript": "~5.5.1" + "ts-node": "10.9.1" } } diff --git a/src/commands/init.ts b/src/commands/init.ts index 313d614..6982c24 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -138,7 +138,7 @@ const packageJsonDefinition = (projectName: string, game: boolean, natives?: str } ` const assetsConfiguration = () => `"resourceFolder": "assets",${ENTER} ` -const nativesConfiguration = (natives?: string) => natives ? `${ENTER}"natives": "${natives}",` : '' +const nativesConfiguration = (natives?: string) => natives ? `${ENTER} "natives": "${natives}",` : '' const ymlForCI = `name: build From d611fd844395031ca16eb2f89ef52dc149c3047e Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 26 Dec 2024 21:48:14 -0300 Subject: [PATCH 17/24] revert BaseOptions to use type --- .vscode/launch.json | 4 +- package-lock.json | 5 +- package.json | 2 +- src/commands/dependencies.ts | 65 +++++++++++++++ src/commands/init.ts | 22 +++--- src/commands/repl.ts | 21 ++--- src/commands/run.ts | 24 +++--- src/commands/test.ts | 18 +++-- src/index.ts | 55 +++++++++---- src/utils.ts | 76 +++++++++--------- test/assertions.ts | 6 +- test/diagram.test.ts | 6 +- test/dynamicDiagramClient.test.ts | 11 ++- test/fullRepl.test.ts | 19 +++-- test/init.test.ts | 45 ++++++----- test/repl.test.ts | 8 +- test/run.test.ts | 35 +++++---- test/test.test.ts | 126 +++++++++++++++++++----------- test/userNatives.test.ts | 13 ++- 19 files changed, 361 insertions(+), 200 deletions(-) create mode 100644 src/commands/dependencies.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c12d88..618731f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,8 +14,8 @@ "type": "node-terminal" }, { - "command": "npm start init -- -p pirulitoProject", - "name": "wollok init", + "command": " npm start -- test -p /home/leo/workspaces/wollok-dev/wollok-ts-cli/examples/user-natives -f userNatives.wtest -v", + "name": "wollok prueba", "request": "launch", "type": "node-terminal" } diff --git a/package-lock.json b/package-lock.json index 8c5001d..77f6da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "pkg": "^5.8.1", "print-message": "^3.0.1", "socket.io": "^4.5.1", + "typescript": "~5.5.1", "winston": "^3.11.0", "wollok-ts": "file:../wollok-ts", "wollok-web-tools": "^1.1.7" @@ -52,8 +53,7 @@ "shx": "^0.3.4", "sinon": "^17.0.1", "socket.io-client": "^4.7.5", - "ts-node": "10.9.1", - "typescript": "~5.5.1" + "ts-node": "10.9.1" } }, "../wollok-ts": { @@ -7086,7 +7086,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 1290c86..cb24e21 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lint": "eslint . ", "lint:fix": "eslint . --fix", "test:unit": "mocha --parallel -r ts-node/register/transpile-only test/**/*.test.ts --timeout 5000", - "test:file": "mocha -r ts-node/register/transpile-only", + "test:file": "mocha --parallel -r ts-node/register/transpile-only", "build:tools": "shx cp ./node_modules/wollok-web-tools/dist/web/game-index.js ./public/game/lib && shx cp ./node_modules/wollok-web-tools/dist/dynamicDiagram/diagram-index.js ./public/diagram", "build": "shx rm -rf build && shx mkdir ./build && shx cp -r ./public ./build/public && tsc -p ./tsconfig.build.json", "watch": "npm run build -- -w", diff --git a/src/commands/dependencies.ts b/src/commands/dependencies.ts new file mode 100644 index 0000000..a47f42a --- /dev/null +++ b/src/commands/dependencies.ts @@ -0,0 +1,65 @@ +import { execSync } from 'child_process' +import path from 'path' +import { existsSync } from 'fs' // Importar existsSync +import console from 'console' +import { Project } from '../utils' + +export type Options = { //TODO revisar lo que se necesita! + project: string, + update: boolean, +} + +const installDependencies = (projectPath: string): void => { + if (!existsSync(path.join(projectPath, 'node_modules'))) { + console.log('node_modules folder not found. Installing dependencies...') + } else { + console.log('Synchronizing dependencies...') + } + + execSync('npm install', { cwd: projectPath, stdio: 'inherit' }) +} + +const add = ( { project, update=false } : Options, packages: string[]): void => { + const proj = new Project(project) + proj.properties.dependencies = proj.properties.dependencies || {} + + packages.forEach(pkg => { + const [name, version] = pkg.split('@') + if (proj.properties.dependencies[name]) { + console.log(`Updating ${name} to version ${version || 'latest'}`) + } else { + console.log(`Adding ${name} version ${version || 'latest'}`) + } + proj.properties.dependencies[name] = version || 'latest' + }) + proj.save() + if (update) { + installDependencies(project) + } +} + +const remove = ( { project, update }: Options, packages: string[] ): void => { + const proj = new Project(project) + + proj.properties.dependencies = proj.properties.dependencies || {} + + packages.forEach(pkg => { + if (proj.properties.dependencies[pkg]) { + console.log(`Removing ${pkg}`) + delete proj.properties.dependencies[pkg] + } else { + console.log(`${pkg} is not a dependency`) + } + }) + + proj.save() + if (update) { + installDependencies(project) + } +} + +const download = (options: Options): void => { + installDependencies(options.project) +} + +export default { add, remove, download } \ No newline at end of file diff --git a/src/commands/init.ts b/src/commands/init.ts index 6982c24..ceff8f2 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -3,20 +3,24 @@ import logger from 'loglevel' import { existsSync, writeFileSync } from 'node:fs' import { basename, join } from 'node:path' import { userInfo } from 'os' -import { ENTER, createFolderIfNotExists, BaseOptions } from '../utils' +import { ENTER, createFolderIfNotExists } from '../utils' import { PROGRAM_FILE_EXTENSION, TEST_FILE_EXTENSION, WOLLOK_FILE_EXTENSION } from 'wollok-ts' import { execSync } from 'node:child_process' -export class Options extends BaseOptions { - name?: string - noTest!: boolean - noCI!: boolean - game!: boolean - noGit!: boolean +export type Options = { + project: string, + name?: string | undefined, + noTest: boolean, + noCI: boolean, + game: boolean, + noGit: boolean, + natives?: string } -export default function ({ project: _project, name, noTest = false, noCI = false, game = false, noGit = false, nativesFolder, sourceFolder, natives }: Options): void { - const project = sourceFolder +export default function (folder: string | undefined, { project: _project, name, noTest = false, noCI = false, game = false, noGit = false, natives = undefined }: Options): void { + const project = join(_project, folder ?? '') + const nativesFolder = join(project, natives ?? '') + // Initialization if (existsSync(join(project, 'package.json'))) { logger.info(yellow(bold(`🚨 There is already a project inside ${project} folder`))) diff --git a/src/commands/repl.ts b/src/commands/repl.ts index d84ac08..030df89 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -10,17 +10,19 @@ import { Server, Socket } from 'socket.io' import { Entity, Environment, Evaluation, Interpreter, Package, REPL, interprete, link } from 'wollok-ts' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' -import { BaseOptions, ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, readNatives } from '../utils' - +import { ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, readNatives } from '../utils' +import { join } from 'path' // TODO: // - autocomplete piola -export class Options extends BaseOptions { - skipValidations!: boolean - darkMode!: boolean - host!: string - port!: string - skipDiagram!: boolean +export type Options = { + project: string + skipValidations: boolean, + darkMode: boolean, + host: string, + port: string, + skipDiagram: boolean, + natives?: string } type DynamicDiagramClient = { @@ -104,9 +106,10 @@ export function interpreteLine(interpreter: Interpreter, line: string): string { return errored ? failureDescription(result, error) : successDescription(result) } -export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations, nativesFolder }: Options): Promise { +export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations, natives }: Options): Promise { let environment: Environment const timeMeasurer = new TimeMeasurer() + const nativesFolder = join(project, natives || '') try { environment = await buildEnvironmentForProject(project) diff --git a/src/commands/run.ts b/src/commands/run.ts index 90825b4..74a2a99 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -9,18 +9,20 @@ import { Server, Socket } from 'socket.io' import { Asset, boardState, buildKeyPressEvent, queueEvent, SoundState, soundState, VisualState, visualState } from 'wollok-web-tools' import { Environment, GAME_MODULE, interpret, Interpreter, Name, Natives, Package, RuntimeObject, WollokException } from 'wollok-ts' import { logger as fileLogger } from '../logger' -import { BaseOptions, buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readNatives, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' +import { buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readNatives, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' import { DummyProfiler, EventProfiler, TimeMeasurer } from './../time-measurer' const { time, timeEnd } = console -export class Options extends BaseOptions { - assets!: string //TODO: move to base options optional and remove logic to read package j? - skipValidations!: boolean - host!: string - port!: string - game!: boolean - startDiagram!: boolean +export type Options = { + project: string + assets: string + skipValidations: boolean + host: string, + port: string + game: boolean, + startDiagram: boolean, + natives?: string, } const DEFAULT_PORT = '4200' @@ -31,8 +33,10 @@ type DynamicDiagramClient = { } export default async function (programFQN: Name, options: Options): Promise { - const { game, project, assets } = options + const { game, project, assets, natives } = options const timeMeasurer = new TimeMeasurer() + const nativesFolder = join(project, natives || '') + try { logger.info(`${game ? gameIcon : programIcon} Running program ${valueDescription(programFQN)} ${runner(game)} on ${valueDescription(project)}`) options.assets = game ? getAssetsFolder(options) : '' @@ -52,7 +56,7 @@ export default async function (programFQN: Name, options: Options): Promise(programFQN).parent as Package const dynamicDiagramClient = await initializeDynamicDiagram(programPackage, options, interpreter) diff --git a/src/commands/test.ts b/src/commands/test.ts index 5aa2304..78ffcf9 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -2,19 +2,22 @@ import { bold, red } from 'chalk' import { time, timeEnd } from 'console' import logger from 'loglevel' import { Entity, Environment, Node, Test, is, match, when, interpret, Describe, count } from 'wollok-ts' -import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives, BaseOptions } from '../utils' +import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives } from '../utils' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' import { Package } from 'wollok-ts' +import { join } from 'path' const { log } = console -export class Options extends BaseOptions { - file?: string - describe?: string - test?: string - skipValidations?: boolean +export type Options = { + file: string | undefined, + describe: string | undefined, + test: string | undefined, + project: string + skipValidations: boolean, + natives?: string } class TestSearchMissError extends Error{} @@ -93,6 +96,7 @@ type TestExecutionError = { export default async function (filter: string | undefined, options: Options): Promise { try { validateParameters(filter, options) + const nativesFolder = join(options.project, options.natives || '') const timeMeasurer = new TimeMeasurer() const { project, skipValidations } = options @@ -112,7 +116,7 @@ export default async function (filter: string | undefined, options: Options): Pr const debug = logger.getLevel() <= logger.levels.DEBUG if (debug) time('Run finished') - const interpreter = interpret(environment, await readNatives(options.nativesFolder)) + const interpreter = interpret(environment, await readNatives(nativesFolder)) const testsFailed: TestExecutionError[] = [] let successes = 0 diff --git a/src/index.ts b/src/index.ts index 937ae78..b9e5b35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node import { Command } from 'commander' -import repl, { Options as ReplOptions } from './commands/repl' -import run, { Options as RunOptions } from './commands/run' -import test, { Options as TestOptions } from './commands/test' -import init, { Options as InitOptions } from './commands/init' +import repl from './commands/repl' +import run from './commands/run' +import test from './commands/test' +import init from './commands/init' +//import dependencies from './commands/dependencies' import logger from 'loglevel' import pkg from '../package.json' import { cyan } from 'chalk' @@ -29,7 +30,7 @@ updateNotifier().finally(() => { .option('-g, --game', 'sets the program as a game', false) .option('-v, --verbose', 'print debugging information', false) .option('-d, --startDiagram', 'activate the dynamic diagram (only for games)', false) - .action((programFQN, options) => { run(programFQN, RunOptions.load(options)) }) + .action((programFQN, options) => { run(programFQN, options) }) program.command('test') .description('Run Wollok tests') @@ -40,7 +41,7 @@ updateNotifier().finally(() => { .option('-t, --test [test]', 'test to run', '') .option('--skipValidations', 'skip code validation', false) .option('-v, --verbose', 'print debugging information', false) - .action((filter, options) => {test(filter, TestOptions.load(options))}) + .action(test) program.command('repl') .description('Start Wollok interactive console') @@ -53,8 +54,6 @@ updateNotifier().finally(() => { .option('--port [port]', 'port to run the server', '3000') .option('-v, --verbose', 'print debugging information', false) .action(repl) - .action((file, options) => {repl(file, ReplOptions.load(options))}) - program.command('init') .description('Create a new Wollok project') @@ -67,14 +66,42 @@ updateNotifier().finally(() => { .option('-ng, --noGit', 'avoids initializing a git repository', false) .option('-N, --natives [natives]', 'folder name for natives files', undefined) .allowUnknownOption() - .action((folder, options) => { - //init(InitOptions.new({ ...options, folder })) - const actionOptions = InitOptions.new( options ) - if ( folder ) { actionOptions.folder = folder } - init(actionOptions) + .action(init) - }) + // const dependencyCommand = new Command('dependency') + // .description('Manage dependencies for a Wollok project') + + // dependencyCommand + // .command('add') + // .description('Add one or more dependencies to the project') + // .argument('', 'Names of the packages to add (e.g., lodash@latest)') + // .option('-p, --project [path]', 'Path to project', process.cwd()) + // .option('-v, --verbose', 'Print debugging information', false) + // .option('-u, --update', 'Update dependencies after they are added', false) + // .action((packages, options) => { + // dependencies.add(options, packages ) + // }) + + // dependencyCommand + // .command('remove') + // .description('Remove one or more dependencies from the project') + // .argument('', 'Names of the packages to remove (e.g., lodash)') + // .option('-p, --project [path]', 'Path to project', process.cwd()) + // .option('-v, --verbose', 'Print debugging information', false) + // .option('-u, --update', 'Update dependencies after they are removed', false) + // .action((packages, options) => { + // dependencies.remove(DependenciesOptions.load({ ...options, packages })) + // }) + // dependencyCommand + // .command('download') + // .description('Download and synchronize all dependencies') + // .option('-p, --project [path]', 'Path to project', process.cwd()) + // .option('-v, --verbose', 'Print debugging information', false) + // .action((packages, options) => { + // dependencies.add(DependenciesOptions.load({ ...options, packages })) + // }) + // program.addCommand(dependencyCommand) program.parseAsync() }) \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index e159d74..553d681 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,13 @@ import { buildEnvironment, Environment, getDynamicDiagramData, Interpreter, Nati import { ElementDefinition } from 'cytoscape' import { register } from 'ts-node' -register({ transpileOnly: true }) +register({ + transpileOnly: true, + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + }, +}) const { time, timeEnd } = console @@ -28,58 +34,49 @@ export const folderIcon = '🗂️' // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // FILE / PATH HANDLING // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -export class BaseOptions { + +export class Project { project!: string - natives?: string folder?: string + properties: any = {} + + constructor(project: string, folder?: string) { + this.project = project + this.folder = folder + this.safeLoadJson() + } + + get sourceFolder() : string { + return this.folder ? join(this.project, this.folder) : this.project + } + + get packageJsonPath(): string { + return path.join(this.sourceFolder, 'package.json') + } - static new(this: new () => T, config: Partial = {}): T { - const instance = new this() - Object.assign(instance, config) - return instance + save(): void { + const jsonContent = JSON.stringify(this.properties, null, 2) + fs.writeFileSync(this.packageJsonPath, jsonContent, 'utf8') } - private safeLoadJson(this: T, path: string) { + private safeLoadJson() { try { - const rawData = fs.readFileSync(path, 'utf-8') - const jsonData = JSON.parse(rawData) - - Object.entries(jsonData) - .forEach(([key, value]) => { //TODO! I want filter just for this properties, but 'in'operator doesn't work. et all properties work fine - try { - this[key as keyof T] = value as T[keyof T] - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_error) { - // This is not a real error, if package.json has a value of different type then it is a non important value - // Silence here or log? - } - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const rawData = fs.readFileSync(this.packageJsonPath, 'utf-8') + this.properties = JSON.parse(rawData) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { // No package.json or it is invalid. This is not a real problem. // Silence here or log? } } - static load(this: new () => T, partial: Partial): T { - const instance = new this() - - if ('project' in partial) { - const packageJsonPath = path.join(partial.project as string, 'package.json') - instance.safeLoadJson(packageJsonPath) - } - Object.assign(instance, partial) - return instance // Devolver la instancia creada - } - - new(this: T, config: Partial): T { - const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this) - Object.assign(clone, config) - return clone + get natives() : string | undefined { + return this.properties.natives } - get sourceFolder() : string { - return this.folder ? join(this.project, this.folder) : this.project + set natives(value: string|undefined) { + this.properties.natives = value } get nativesFolder(): string { @@ -152,6 +149,7 @@ export const handleError = (error: any): void => { export async function readNatives(nativeFolder: string): Promise { const paths = await globby('**/*.@(ts|js)', { cwd: nativeFolder }) + const debug = logger.getLevel() <= logger.levels.DEBUG if (debug) time('Loading natives files') diff --git a/test/assertions.ts b/test/assertions.ts index ce81bec..4957351 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -129,9 +129,11 @@ export const jsonAssertions: Chai.ChaiPlugin = (chai) => { expectedKeys.forEach((key) => this.assert( getNestedValue(jsonContent, key) !== undefined, - `Expected JSON to have key "${key}"`, + `Expected JSON to have key "${key} but"`, `Expected JSON not to have key "${key}"`, - key + key, + jsonContent, + jsonContent ) ) }) diff --git a/test/diagram.test.ts b/test/diagram.test.ts index e8340a3..24ba8b9 100644 --- a/test/diagram.test.ts +++ b/test/diagram.test.ts @@ -1,7 +1,7 @@ import { should, use } from 'chai' import { join } from 'path' import { interprete, Interpreter } from 'wollok-ts' -import { initializeInterpreter, Options } from '../src/commands/repl' +import { initializeInterpreter } from '../src/commands/repl' import { getDynamicDiagram } from '../src/utils' import { diagramAssertions } from './assertions' @@ -13,14 +13,14 @@ const simpleFile = join(projectPath, 'fish.wlk') const fileWithImports = join(projectPath, 'using-imports', 'base.wlk') describe('Dynamic diagram', () => { - const options = Options.new({ + const options = { project: projectPath, skipValidations: true, port: '8080', host: 'localhost', darkMode: true, skipDiagram: true, // we don't want to open a socket - }) + } let interpreter: Interpreter diff --git a/test/dynamicDiagramClient.test.ts b/test/dynamicDiagramClient.test.ts index 25970c3..b8a254b 100644 --- a/test/dynamicDiagramClient.test.ts +++ b/test/dynamicDiagramClient.test.ts @@ -4,7 +4,7 @@ import chaiHttp from 'chai-http' import { join } from 'path' import { Interface, createInterface as Repl } from 'readline' import { Interpreter } from 'wollok-ts' -import { initializeClient, initializeInterpreter, Options } from '../src/commands/repl' +import { initializeClient, initializeInterpreter } from '../src/commands/repl' chai.should() chai.use(chaiHttp) @@ -14,14 +14,14 @@ const expect = chai.expect describe('dynamic diagram client', () => { const projectPath = join('examples', 'repl-examples') - const options = Options.new({ + const options = { project: projectPath, skipValidations: false, darkMode: true, port: '8080', host: 'localhost', skipDiagram: false, - }) + } let interpreter: Interpreter let repl: Interface @@ -46,7 +46,10 @@ describe('dynamic diagram client', () => { }) it('should return a fake client if skipDiagram is set', async () => { - const skipDiagramOptions = options.new({ skipDiagram: true }) + const skipDiagramOptions = { + ...options, + skipDiagram: true, + } const { enabled, app, server } = await initializeClient(skipDiagramOptions, repl, interpreter) expect(enabled).to.be.false expect(app).to.be.undefined diff --git a/test/fullRepl.test.ts b/test/fullRepl.test.ts index 7117164..068b49f 100644 --- a/test/fullRepl.test.ts +++ b/test/fullRepl.test.ts @@ -14,19 +14,18 @@ chai.use(chaiHttp) chai.use(chaiAsPromised) const expect = chai.expect -const baseOptions = Options.new({ +const baseOptions = { darkMode: true, port: '8080', host: 'localhost', skipDiagram: true, -}) - -const buildOptionsFor = (path: string, skipValidations = false) => - baseOptions.new({ - project: join('examples', 'bad-files-examples', path), - skipValidations: skipValidations, - }) +} +const buildOptionsFor = (path: string, skipValidations = false) => ({ + ...baseOptions, + project: join('examples', 'bad-files-examples', path), + skipValidations, +}) const callRepl = (autoImportPath: string, options: Options) => replFn(join(options.project, autoImportPath), options) @@ -34,14 +33,14 @@ const callRepl = (autoImportPath: string, options: Options) => describe('REPL integration test for valid project', () => { const projectPath = join('examples', 'repl-examples') - const options = Options.new({ + const options = { project: projectPath, skipValidations: false, darkMode: true, host: 'localhost', port: '8080', skipDiagram: true, - }) + } let processExitSpy: sinon.SinonStub let consoleLogSpy: sinon.SinonStub let repl: Interface diff --git a/test/init.test.ts b/test/init.test.ts index a6a2cb0..cf49462 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { readFileSync, rmSync } from 'fs' import sinon from 'sinon' import init, { Options } from '../src/commands/init' -import test, { Options as TestOptions } from '../src/commands/test' +import test from '../src/commands/test' import { pathAssertions, jsonAssertions } from './assertions' chai.should() @@ -17,13 +17,13 @@ const customFolderName = 'custom-folder' const customFolderProject = join(project, customFolderName) const GITHUB_FOLDER = join('.github', 'workflows') -const baseOptions = Options.new({ - project: project, +const baseOptions: Options = { + project, noCI: false, noTest: false, game: false, noGit: false, -}) +} describe('testing init', () => { @@ -41,7 +41,7 @@ describe('testing init', () => { }) it('should create files successfully for default values: ci, no game, example name & git', async () => { - init(baseOptions) + init(undefined, baseOptions) expect(join(project, 'example.wlk')).to.pathExists expect(join(project, 'testExample.wtest')).to.pathExists @@ -54,18 +54,22 @@ describe('testing init', () => { expect(join(project, '.git/HEAD')).to.pathExists expect(getResourceFolder()).to.be.undefined - await test(undefined, TestOptions.new({ - project: project, + await test(undefined, { + project, skipValidations: false, - })) + file: undefined, + describe: undefined, + test: undefined, + }) expect(processExitSpy.callCount).to.equal(0) }) it('should create files successfully for game project with ci & custom example name', () => { - init(baseOptions.new({ + init(undefined, { + ...baseOptions, game: true, name: 'pepita', - })) + }) expect(join(project, 'pepita.wlk')).to.pathExists expect(join(project, 'testPepita.wtest')).to.pathExists @@ -78,11 +82,12 @@ describe('testing init', () => { }) it('should create files successfully for game project with no ci & no test custom example name', async () => { - init(baseOptions.new({ + init(undefined, { + ...baseOptions, noCI: true, noTest: true, name: 'pepita', - })) + }) expect(join(project, 'pepita.wlk')).to.pathExists expect(join(project, 'testPepita.wtest')).to.not.pathExists @@ -94,7 +99,7 @@ describe('testing init', () => { }) it('should create files successfully with an argument for the folder name working in combination with project option', async () => { - init(baseOptions.new({ name: 'pepita', folder: customFolderName })) + init(customFolderName, { ...baseOptions, name:'pepita' } ) expect(join(customFolderProject, 'pepita.wlk')).to.pathExists @@ -107,7 +112,7 @@ describe('testing init', () => { }) it('should skip the initialization of a git repository if notGit flag es enabled', async () => { - init(baseOptions.new({ noGit: true })) + init(undefined, { ...baseOptions, noGit: true }) expect(join(project, '.git')).not.to.pathExists expect(join(project, '.git/HEAD')).not.to.pathExists @@ -122,13 +127,17 @@ describe('testing init', () => { }) it('should exit with code 1 if folder already exists', () => { - init(baseOptions.new({ project: join('examples', 'init-examples', 'existing-folder') })) + init(undefined, { + ...baseOptions, + project: join('examples', 'init-examples', 'existing-folder'), + }) expect(processExitSpy.calledWith(1)).to.be.true }) it('should create a natives folder when it is required', () => { - init(baseOptions.new({ natives: 'myNatives' })) + init(undefined, { ...baseOptions, natives: 'myNatives' }) + expect(join(project, 'myNatives')).to.pathExists expect('package.json') expect(join(project, 'package.json')).jsonMatch({ natives: 'myNatives' }) @@ -137,13 +146,13 @@ describe('testing init', () => { it('should create a natives nested folders when it is required', () => { const nativesFolder =join('myNatives', 'myReallyNatives') - init(baseOptions.new({ natives: nativesFolder })) + init(undefined, { ...baseOptions, natives: nativesFolder }) expect(join(project, 'package.json')).jsonMatch({ natives: nativesFolder }) }) it('should not create a natives folders when it is not specified', () => { - init(baseOptions) + init(undefined, baseOptions) expect(join(project, 'package.json')).not.jsonKeys(['natives']) }) diff --git a/test/repl.test.ts b/test/repl.test.ts index 81b6111..6f99b2a 100644 --- a/test/repl.test.ts +++ b/test/repl.test.ts @@ -1,7 +1,7 @@ import { should } from 'chai' import { join } from 'path' import { Interpreter, REPL } from 'wollok-ts' -import { initializeInterpreter, interpreteLine, Options } from '../src/commands/repl' +import { initializeInterpreter, interpreteLine } from '../src/commands/repl' import { failureDescription, successDescription } from '../src/utils' should() @@ -10,14 +10,14 @@ const projectPath = join('examples', 'repl-examples') describe('REPL', () => { - const options = Options.new({ + const options = { project: projectPath, skipValidations: false, darkMode: true, port: '8080', host: 'localhost', skipDiagram: true, - }) + } let interpreter: Interpreter @@ -261,7 +261,7 @@ describe('REPL', () => { describe('User Natives', () => { const project = join('examples', 'user-natives' ) - const userNativesOptions = options.new({ project: project, natives: 'myNativesFolder' }) + const userNativesOptions = { ...options, project: project, natives: 'myNativesFolder' } it('should execute user natives', async () => { interpreter = await initializeInterpreter(join(project, 'rootFile.wlk'), userNativesOptions) diff --git a/test/run.test.ts b/test/run.test.ts index 164e285..732b1ae 100644 --- a/test/run.test.ts +++ b/test/run.test.ts @@ -19,10 +19,10 @@ const assets = 'assets' describe('testing run', () => { - const buildOptions = (game: boolean, assets: string) => Options.new({ - game: game, - project: project, - assets: assets, + const buildOptions = (game: boolean, assets: string): Options => ({ + game, + project, + assets, skipValidations: false, startDiagram: false, host: 'localhost', @@ -78,10 +78,13 @@ describe('testing run', () => { it('should return all visuals for a simple project', async () => { const imageProject = join('examples', 'run-examples', 'asset-example') - const options = buildOptions(true, 'assets').new({ project: imageProject }) + const options = { + ...buildOptions(true, 'assets'), + project: imageProject, + } const environment = await buildEnvironmentForProgram(options) - const interpreter = getGameInterpreter(environment, await utils.readNatives(options.nativesFolder))! + const interpreter = getGameInterpreter(environment, await utils.readNatives(options.project))! const game = interpreter.object('wollok.game.game') interpreter.send('addVisual', game, interpreter.object('mainGame.elementoVisual')) @@ -175,7 +178,7 @@ describe('testing run', () => { it('should work if program has no errors', async () => { - await run('mainExample.PepitaProgram', Options.new({ + await run('mainExample.PepitaProgram', { project: join('examples', 'run-examples', 'basic-example'), skipValidations: false, game: false, @@ -183,7 +186,7 @@ describe('testing run', () => { assets, host: 'localhost', port: '3000', - })) + }) expect(spyCalledWithSubstring(consoleLogSpy, 'Pepita empieza con 70')).to.be.true expect(spyCalledWithSubstring(consoleLogSpy, 'Vuela')).to.be.true expect(spyCalledWithSubstring(consoleLogSpy, '40')).to.be.true @@ -197,7 +200,7 @@ describe('testing run', () => { }) it('should exit if program has errors', async () => { - await run('mainExample.PepitaProgram', Options.new({ + await run('mainExample.PepitaProgram', { project: join('examples', 'run-examples', 'bad-example'), skipValidations: false, game: false, @@ -205,7 +208,7 @@ describe('testing run', () => { assets, host: 'localhost', port: '3000', - })) + }) expect(processExitSpy.calledWith(21)).to.be.true expect(fileLoggerInfoSpy.calledOnce).to.be.true const fileLoggerArg = fileLoggerInfoSpy.firstCall.firstArg @@ -234,26 +237,30 @@ describe('testing run', () => { }) it('smoke test - should work if program has no errors', async () => { - const ioGame = await run('mainGame.PepitaGame', buildOptions(true, 'specialAssets').new({ + const ioGame = await run('mainGame.PepitaGame', { project: join('examples', 'run-examples', 'basic-game'), skipValidations: false, + game: true, startDiagram: false, + assets: 'specialAssets', port: '3000', host: 'localhost', - })) + }) ioGame?.close() expect(processExitSpy.calledWith(0)).to.be.false expect(errorReturned).to.be.undefined }) it('smoke test - should not work if program has errors', async () => { - const ioGame = await run('mainGame.PepitaGame', buildOptions(true, 'specialAssets').new({ + const ioGame = await run('mainGame.PepitaGame', { project: join('examples', 'run-examples', 'basic-example'), skipValidations: false, + game: true, startDiagram: false, + assets: 'specialAssets', port: '3000', host: 'localhost', - })) + }) ioGame?.close() expect(processExitSpy.calledWith(21)).to.be.false expect(errorReturned).to.equal('Folder image examples/run-examples/basic-example/specialAssets does not exist') diff --git a/test/test.test.ts b/test/test.test.ts index 8177105..27773f4 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -3,7 +3,7 @@ import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' import { Environment } from 'wollok-ts' -import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters, Options } from '../src/commands/test' +import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' import { logger as fileLogger } from '../src/logger' import { buildEnvironmentForProject } from '../src/utils' import { spyCalledWithSubstring } from './assertions' @@ -18,13 +18,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'normal-case') - const emptyOptions = Options.new({ + const emptyOptions = { project: projectPath, skipValidations: false, file: undefined, describe: undefined, test: undefined, - }) + } beforeEach(async () => { environment = await buildEnvironmentForProject(projectPath) @@ -83,34 +83,47 @@ describe('Test', () => { describe('with file/describe/test options', () => { it('should filter by test using test option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ test: 'another test' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + test: 'another test', + }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another test"') }) it('should filter by test using test option - case sensitive', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ test: 'aNother Test' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + test: 'aNother Test', + }) expect(tests.length).to.equal(0) }) it('should filter by describe using describe option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ describe: 'second describe' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + describe: 'second describe', + }) expect(tests.length).to.equal(2) expect(tests[0].name).to.equal('"second test"') expect(tests[1].name).to.equal('"another second test"') }) it('should filter by describe & test using describe & test option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ + const tests = getTarget(environment, undefined, { + ...emptyOptions, describe: 'second describe', test: 'another second test', - })) + }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another second test"') }) it('should filter by file using file option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'test-one.wtest' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + file: 'test-one.wtest', + }) expect(tests.length).to.equal(3) expect(tests[0].name).to.equal('"a test"') expect(tests[1].name).to.equal('"another test"') @@ -118,22 +131,23 @@ describe('Test', () => { }) it('should filter by file & describe using file & describe option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ + const tests = getTarget(environment, undefined, { + ...emptyOptions, file: 'test-one.wtest', describe: 'this describe', - })) + }) expect(tests.length).to.equal(3) expect(tests[0].name).to.equal('"a test"') expect(tests[1].name).to.equal('"another test"') expect(tests[2].name).to.equal('"another test with longer name"') }) - it('should filter by file & describe & test using file & describe & test option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ + const tests = getTarget(environment, undefined, { + ...emptyOptions, file: 'test-one.wtest', describe: 'this describe', test: 'another test', - })) + }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"another test"') }) @@ -145,13 +159,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'only-case') - const emptyOptions = Options.new({ + const emptyOptions = { project: projectPath, skipValidations: false, file: undefined, describe: undefined, test: undefined, - }) + } beforeEach(async () => { environment = await buildEnvironmentForProject(projectPath) @@ -164,7 +178,10 @@ describe('Test', () => { }) it('should execute single test when running a describe using file option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ describe: 'only describe' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + describe: 'only describe', + }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"this is the one"') }) @@ -176,7 +193,10 @@ describe('Test', () => { }) it('should execute single test when running a file using file option', () => { - const tests = getTarget(environment, undefined, emptyOptions.new({ file: 'only-file.wtest' })) + const tests = getTarget(environment, undefined, { + ...emptyOptions, + file: 'only-file.wtest', + }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"this is the one"') }) @@ -198,13 +218,13 @@ describe('Test', () => { describe('validateParameters', () => { - const emptyOptions = Options.new({ + const emptyOptions = { skipValidations: false, project: '', file: undefined, describe: undefined, test: undefined, - }) + } it('should pass if no filter and no options passed', () => { expect(() => { validateParameters(undefined, emptyOptions) }).not.to.throw() @@ -215,27 +235,29 @@ describe('Test', () => { }) it('should pass if options is passed and no filter is passed', () => { - expect(() => { validateParameters(undefined, - emptyOptions.new({ test: 'some test' })) - }).not.to.throw() + expect(() => { validateParameters(undefined, { + ...emptyOptions, + test: 'some test', + }) }).not.to.throw() }) it('should fail if filter and options are passed', () => { - expect(() => { validateParameters('some describe', - emptyOptions.new({ test: 'some test' })) - }).to.throw(/You should either use filter by full name or file/) + expect(() => { validateParameters('some describe', { + ...emptyOptions, + test: 'some test', + }) }).to.throw(/You should either use filter by full name or file/) }) }) describe('matching test description', () => { - const emptyOptions = Options.new({ + const emptyOptions = { project: '', skipValidations: false, file: undefined, describe: undefined, test: undefined, - }) + } it('should return empty string if no filter or options are passed', () => { @@ -247,19 +269,21 @@ describe('Test', () => { }) it('should return options descriptions if options are passed', () => { - expect(matchingTestDescription(undefined, emptyOptions.new({ + expect(matchingTestDescription(undefined, { + ...emptyOptions, file: 'test-one.wtest', describe: 'this describe', test: 'another test', - }))).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') - }) + })).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') + }) it('should return options descriptions with wildcards if options are missing', () => { - expect(matchingTestDescription(undefined, emptyOptions.new({ + expect(matchingTestDescription(undefined, { + ...emptyOptions, file: undefined, describe: 'this discribe', test: undefined, - }))).to.include('*.\'this discribe\'.*') + })).to.include('*.\'this discribe\'.*') }) }) @@ -283,13 +307,13 @@ describe('Test', () => { const projectPath = join('examples', 'test-examples', 'normal-case') - const emptyOptions = Options.new({ + const emptyOptions = { project: projectPath, skipValidations: true, file: undefined, describe: undefined, test: undefined, - }) + } beforeEach(() => { loggerInfoSpy = sinon.stub(logger, 'info') @@ -303,8 +327,10 @@ describe('Test', () => { }) it('passes all the tests successfully and exits normally', async () => { - await test(undefined, emptyOptions.new({ file: 'test-one.wtest' })) - + await test(undefined, { + ...emptyOptions, + file: 'test-one.wtest', + }) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 3 tests')).to.be.true expect(spyCalledWithSubstring(loggerInfoSpy, '3 passed')).to.be.true @@ -315,18 +341,21 @@ describe('Test', () => { }) it('passing a wrong filename runs no tests and logs a warning', async () => { - await test(undefined, emptyOptions.new({ file: 'non-existing-file.wtest' })) - + await test(undefined, { + ...emptyOptions, + file: 'non-existing-file.wtest', + }) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true expect(spyCalledWithSubstring(loggerErrorSpy, 'File \'non-existing-file.wtest\' not found')).to.be.true }) it('passing a wrong describe runs no tests and logs a warning', async () => { - await test(undefined, emptyOptions.new({ + await test(undefined, { + ...emptyOptions, file: 'test-one.wtest', describe: 'non-existing-describe', - })) + }) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true @@ -348,11 +377,12 @@ describe('Test', () => { }) it('returns exit code 2 if one or more tests fail', async () => { - await test(undefined, emptyOptions.new({ + await test(undefined, { + ...emptyOptions, file: 'test-two.wtest', describe: 'third describe', test: 'just a test', - })) + }) expect(processExitSpy.calledWith(2)).to.be.true expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 test')).to.be.true @@ -366,11 +396,12 @@ describe('Test', () => { }) it('returns exit code 2 if one or more tests have errors', async () => { - await test(undefined, emptyOptions.new({ + await test(undefined, { + ...emptyOptions, file: 'test-two.wtest', describe: 'second describe', test: 'second test', - })) + }) expect(processExitSpy.calledWith(2)).to.be.true expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 test')).to.be.true @@ -384,10 +415,11 @@ describe('Test', () => { }) it('returns exit code 1 if tests has parse errors', async () => { - await test(undefined, emptyOptions.new({ + await test(undefined, { + ...emptyOptions, skipValidations: false, project: join('examples', 'test-examples', 'failing-case'), - })) + }) expect(processExitSpy.calledWith(1)).to.be.true }) diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index 585986e..4b0610c 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' -import test, { Options } from '../src/commands/test' +import test from '../src/commands/test' import { logger as fileLogger } from '../src/logger' import { spyCalledWithSubstring } from './assertions' @@ -12,9 +12,14 @@ describe('UserNatives', () => { let processExitSpy: sinon.SinonStub - const options = Options.load({ //load package.json + const options = { project: join('examples', 'user-natives'), - }) + skipValidations: false, + file: undefined, + describe: undefined, + test: undefined, + } + beforeEach(() => { loggerInfoSpy = sinon.stub(logger, 'info') @@ -27,7 +32,7 @@ describe('UserNatives', () => { }) it('passes all the tests successfully and exits normally', async () => { - await test(undefined, options) + await test(undefined, { ...options, natives: 'myNativesFolder' }) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true From e7071522f2bcf100ae644602eba690b539234dec Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Thu, 26 Dec 2024 22:32:16 -0300 Subject: [PATCH 18/24] read natives project inside --- src/commands/repl.ts | 7 +++---- src/commands/run.ts | 11 +++++------ src/commands/test.ts | 9 +++------ src/index.ts | 2 +- src/utils.ts | 17 +++++++---------- test/test.test.ts | 2 +- test/userNatives.test.ts | 2 +- 7 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/commands/repl.ts b/src/commands/repl.ts index 030df89..cf6f2ce 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -10,8 +10,7 @@ import { Server, Socket } from 'socket.io' import { Entity, Environment, Evaluation, Interpreter, Package, REPL, interprete, link } from 'wollok-ts' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' -import { ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, readNatives } from '../utils' -import { join } from 'path' +import { ENTER, buildEnvironmentForProject, failureDescription, getDynamicDiagram, getFQN, handleError, publicPath, replIcon, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, Project } from '../utils' // TODO: // - autocomplete piola @@ -109,7 +108,7 @@ export function interpreteLine(interpreter: Interpreter, line: string): string { export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations, natives }: Options): Promise { let environment: Environment const timeMeasurer = new TimeMeasurer() - const nativesFolder = join(project, natives || '') + const proj = new Project(project) try { environment = await buildEnvironmentForProject(project) @@ -130,7 +129,7 @@ export async function initializeInterpreter(autoImportPath: string | undefined, const replPackage = new Package({ name: REPL }) environment = link([replPackage], environment) } - return new Interpreter(Evaluation.build(environment, await readNatives(nativesFolder))) + return new Interpreter(Evaluation.build(environment, await proj.readNatives())) } catch (error: any) { handleError(error) fileLogger.info({ message: `${replIcon} REPL execution - build failed for ${project}`, timeElapsed: timeMeasurer.elapsedTime(), ok: false, error: sanitizeStackTrace(error) }) diff --git a/src/commands/run.ts b/src/commands/run.ts index 74a2a99..fb0ea54 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -9,7 +9,7 @@ import { Server, Socket } from 'socket.io' import { Asset, boardState, buildKeyPressEvent, queueEvent, SoundState, soundState, VisualState, visualState } from 'wollok-web-tools' import { Environment, GAME_MODULE, interpret, Interpreter, Name, Natives, Package, RuntimeObject, WollokException } from 'wollok-ts' import { logger as fileLogger } from '../logger' -import { buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readNatives, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription } from '../utils' +import { buildEnvironmentForProject, buildEnvironmentIcon, ENTER, failureDescription, folderIcon, gameIcon, getDynamicDiagram, handleError, isValidAsset, isValidImage, isValidSound, programIcon, publicPath, readPackageProperties, sanitizeStackTrace, serverError, successDescription, validateEnvironment, valueDescription, Project } from '../utils' import { DummyProfiler, EventProfiler, TimeMeasurer } from './../time-measurer' const { time, timeEnd } = console @@ -22,7 +22,6 @@ export type Options = { port: string game: boolean, startDiagram: boolean, - natives?: string, } const DEFAULT_PORT = '4200' @@ -33,9 +32,9 @@ type DynamicDiagramClient = { } export default async function (programFQN: Name, options: Options): Promise { - const { game, project, assets, natives } = options + const { game, project, assets } = options const timeMeasurer = new TimeMeasurer() - const nativesFolder = join(project, natives || '') + const proj = new Project(project) try { logger.info(`${game ? gameIcon : programIcon} Running program ${valueDescription(programFQN)} ${runner(game)} on ${valueDescription(project)}`) @@ -56,8 +55,8 @@ export default async function (programFQN: Name, options: Options): Promise(programFQN).parent as Package const dynamicDiagramClient = await initializeDynamicDiagram(programPackage, options, interpreter) const ioGame: Server | undefined = initializeGameClient(options) diff --git a/src/commands/test.ts b/src/commands/test.ts index 78ffcf9..d4c52b9 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -2,12 +2,10 @@ import { bold, red } from 'chalk' import { time, timeEnd } from 'console' import logger from 'loglevel' import { Entity, Environment, Node, Test, is, match, when, interpret, Describe, count } from 'wollok-ts' -import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, readNatives } from '../utils' +import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError, ENTER, sanitizeStackTrace, buildEnvironmentIcon, testIcon, assertionError, warningDescription, Project } from '../utils' import { logger as fileLogger } from '../logger' import { TimeMeasurer } from '../time-measurer' import { Package } from 'wollok-ts' -import { join } from 'path' - const { log } = console @@ -17,7 +15,6 @@ export type Options = { test: string | undefined, project: string skipValidations: boolean, - natives?: string } class TestSearchMissError extends Error{} @@ -96,7 +93,7 @@ type TestExecutionError = { export default async function (filter: string | undefined, options: Options): Promise { try { validateParameters(filter, options) - const nativesFolder = join(options.project, options.natives || '') + const proj = new Project(options.project) const timeMeasurer = new TimeMeasurer() const { project, skipValidations } = options @@ -116,7 +113,7 @@ export default async function (filter: string | undefined, options: Options): Pr const debug = logger.getLevel() <= logger.levels.DEBUG if (debug) time('Run finished') - const interpreter = interpret(environment, await readNatives(nativesFolder)) + const interpreter = interpret(environment, await proj.readNatives()) const testsFailed: TestExecutionError[] = [] let successes = 0 diff --git a/src/index.ts b/src/index.ts index b9e5b35..d691aeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,7 @@ updateNotifier().finally(() => { .option('-t, --noTest', 'avoids creating a test file', false) .option('-c, --noCI', 'avoids creating a file for CI', false) .option('-ng, --noGit', 'avoids initializing a git repository', false) - .option('-N, --natives [natives]', 'folder name for natives files', undefined) + .option('-N, --natives [natives]', 'Folder name for native files and dependencies (defaults to root project).', undefined) .allowUnknownOption() .action(init) diff --git a/src/utils.ts b/src/utils.ts index 553d681..14dfda2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -47,16 +47,17 @@ export class Project { } get sourceFolder() : string { - return this.folder ? join(this.project, this.folder) : this.project + return join(this.project, this.folder || '') } get packageJsonPath(): string { return path.join(this.sourceFolder, 'package.json') } - save(): void { + public save(): void { const jsonContent = JSON.stringify(this.properties, null, 2) fs.writeFileSync(this.packageJsonPath, jsonContent, 'utf8') + //TODO debería hacerlo safe? me parece que no } private safeLoadJson() { @@ -71,16 +72,12 @@ export class Project { } } - get natives() : string | undefined { - return this.properties.natives - } - - set natives(value: string|undefined) { - this.properties.natives = value + get nativesFolder(): string { + return join(this.sourceFolder, this.properties.natives || '') } - get nativesFolder(): string { - return this.natives ? join(this.sourceFolder, this.natives) : this.sourceFolder + public async readNatives(): Promise{ + return readNatives(this.nativesFolder) } } diff --git a/test/test.test.ts b/test/test.test.ts index 27773f4..6a6fe7c 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -275,7 +275,7 @@ describe('Test', () => { describe: 'this describe', test: 'another test', })).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') - }) + }) it('should return options descriptions with wildcards if options are missing', () => { expect(matchingTestDescription(undefined, { diff --git a/test/userNatives.test.ts b/test/userNatives.test.ts index 4b0610c..7527d63 100644 --- a/test/userNatives.test.ts +++ b/test/userNatives.test.ts @@ -32,7 +32,7 @@ describe('UserNatives', () => { }) it('passes all the tests successfully and exits normally', async () => { - await test(undefined, { ...options, natives: 'myNativesFolder' }) + await test(undefined, options ) expect(processExitSpy.callCount).to.equal(0) expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 1 tests')).to.be.true From 79c96223e25eb17998b28d20e970c2d00a8acc01 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Fri, 27 Dec 2024 13:36:04 -0300 Subject: [PATCH 19/24] dependency command --- src/commands/dependencies.ts | 69 +++++++++--------------------------- src/commands/repl.ts | 2 +- src/index.ts | 58 ++++++++++++++---------------- test/repl.test.ts | 6 ++-- 4 files changed, 46 insertions(+), 89 deletions(-) diff --git a/src/commands/dependencies.ts b/src/commands/dependencies.ts index a47f42a..c54c216 100644 --- a/src/commands/dependencies.ts +++ b/src/commands/dependencies.ts @@ -1,65 +1,28 @@ import { execSync } from 'child_process' import path from 'path' -import { existsSync } from 'fs' // Importar existsSync -import console from 'console' -import { Project } from '../utils' +import logger from 'loglevel' -export type Options = { //TODO revisar lo que se necesita! - project: string, - update: boolean, +export type Options = { + project: string + verbose: boolean } -const installDependencies = (projectPath: string): void => { - if (!existsSync(path.join(projectPath, 'node_modules'))) { - console.log('node_modules folder not found. Installing dependencies...') - } else { - console.log('Synchronizing dependencies...') - } - - execSync('npm install', { cwd: projectPath, stdio: 'inherit' }) -} - -const add = ( { project, update=false } : Options, packages: string[]): void => { - const proj = new Project(project) - proj.properties.dependencies = proj.properties.dependencies || {} - - packages.forEach(pkg => { - const [name, version] = pkg.split('@') - if (proj.properties.dependencies[name]) { - console.log(`Updating ${name} to version ${version || 'latest'}`) - } else { - console.log(`Adding ${name} version ${version || 'latest'}`) - } - proj.properties.dependencies[name] = version || 'latest' - }) - proj.save() - if (update) { - installDependencies(project) +const executeNpmCommand = (command: string, project: string, verbose: boolean): void => { + const fullCommand = `npm ${command}` + if (verbose) { + logger.info(`Executing in ${project}: ${fullCommand}`) } + execSync(fullCommand, { cwd: path.resolve(project), stdio: 'inherit' }) } -const remove = ( { project, update }: Options, packages: string[] ): void => { - const proj = new Project(project) - - proj.properties.dependencies = proj.properties.dependencies || {} - - packages.forEach(pkg => { - if (proj.properties.dependencies[pkg]) { - console.log(`Removing ${pkg}`) - delete proj.properties.dependencies[pkg] - } else { - console.log(`${pkg} is not a dependency`) - } - }) - - proj.save() - if (update) { - installDependencies(project) - } +export const addDependency = (pkg: string, { project, verbose }: Options): void => { + executeNpmCommand(`install ${pkg}`, project, verbose) } -const download = (options: Options): void => { - installDependencies(options.project) +export const removeDependency = (pkg: string, { project, verbose }: Options): void => { + executeNpmCommand(`uninstall ${pkg}`, project, verbose) } -export default { add, remove, download } \ No newline at end of file +export const synchronizeDependencies = ({ project, verbose }: Options): void => { + executeNpmCommand('install', project, verbose) +} \ No newline at end of file diff --git a/src/commands/repl.ts b/src/commands/repl.ts index cf6f2ce..4911dbe 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -105,7 +105,7 @@ export function interpreteLine(interpreter: Interpreter, line: string): string { return errored ? failureDescription(result, error) : successDescription(result) } -export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations, natives }: Options): Promise { +export async function initializeInterpreter(autoImportPath: string | undefined, { project, skipValidations }: Options): Promise { let environment: Environment const timeMeasurer = new TimeMeasurer() const proj = new Project(project) diff --git a/src/index.ts b/src/index.ts index d691aeb..db0402c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import repl from './commands/repl' import run from './commands/run' import test from './commands/test' import init from './commands/init' -//import dependencies from './commands/dependencies' +import { addDependency, removeDependency, synchronizeDependencies } from './commands/dependencies' import logger from 'loglevel' import pkg from '../package.json' import { cyan } from 'chalk' @@ -68,40 +68,34 @@ updateNotifier().finally(() => { .allowUnknownOption() .action(init) - // const dependencyCommand = new Command('dependency') - // .description('Manage dependencies for a Wollok project') + const dependencyCommand = new Command('dependency') + .description('Manage dependencies for a Wollok project') - // dependencyCommand - // .command('add') - // .description('Add one or more dependencies to the project') - // .argument('', 'Names of the packages to add (e.g., lodash@latest)') - // .option('-p, --project [path]', 'Path to project', process.cwd()) - // .option('-v, --verbose', 'Print debugging information', false) - // .option('-u, --update', 'Update dependencies after they are added', false) - // .action((packages, options) => { - // dependencies.add(options, packages ) - // }) + dependencyCommand + .command('add') + .description('Add a dependency to the project') + .argument('', 'Name of the package to add (e.g., lodash@latest)') + .option('-p, --project [path]', 'Path to project', process.cwd()) + .option('-v, --verbose', 'Print debugging information', false) + .allowUnknownOption() + .action(addDependency) - // dependencyCommand - // .command('remove') - // .description('Remove one or more dependencies from the project') - // .argument('', 'Names of the packages to remove (e.g., lodash)') - // .option('-p, --project [path]', 'Path to project', process.cwd()) - // .option('-v, --verbose', 'Print debugging information', false) - // .option('-u, --update', 'Update dependencies after they are removed', false) - // .action((packages, options) => { - // dependencies.remove(DependenciesOptions.load({ ...options, packages })) - // }) + dependencyCommand + .command('remove') + .description('Remove a dependency from the project') + .argument('', 'Name of the package to remove (e.g., lodash)') + .option('-p, --project [path]', 'Path to project', process.cwd()) + .option('-v, --verbose', 'Print debugging information', false) + .allowUnknownOption() + .action(removeDependency) - // dependencyCommand - // .command('download') - // .description('Download and synchronize all dependencies') - // .option('-p, --project [path]', 'Path to project', process.cwd()) - // .option('-v, --verbose', 'Print debugging information', false) - // .action((packages, options) => { - // dependencies.add(DependenciesOptions.load({ ...options, packages })) - // }) + dependencyCommand + .command('sync') + .description('Synchronize all dependencies') + .option('-p, --project [path]', 'Path to project', process.cwd()) + .option('-v, --verbose', 'Print debugging information', false) + .action(synchronizeDependencies) - // program.addCommand(dependencyCommand) + program.addCommand(dependencyCommand) program.parseAsync() }) \ No newline at end of file diff --git a/test/repl.test.ts b/test/repl.test.ts index 6f99b2a..8a33f69 100644 --- a/test/repl.test.ts +++ b/test/repl.test.ts @@ -261,17 +261,17 @@ describe('REPL', () => { describe('User Natives', () => { const project = join('examples', 'user-natives' ) - const userNativesOptions = { ...options, project: project, natives: 'myNativesFolder' } + const opt = { ...options, project: project } it('should execute user natives', async () => { - interpreter = await initializeInterpreter(join(project, 'rootFile.wlk'), userNativesOptions) + interpreter = await initializeInterpreter(join(project, 'rootFile.wlk'), opt) const result = interpreteLine(interpreter, 'myModel.nativeOne()') result.should.be.equal(successDescription('1')) }) it('should execute user natives in package', async () => { - interpreter = await initializeInterpreter(join(project, 'myPackage', 'myInnerFile.wlk'), userNativesOptions) + interpreter = await initializeInterpreter(join(project, 'myPackage', 'myInnerFile.wlk'), opt) const result = interpreteLine(interpreter, 'packageModel.nativeTwo()') result.should.be.equal(successDescription('2')) From bb6755133c9c232e9952f3cf05c9162b0f2cfe14 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 29 Dec 2024 12:18:00 -0300 Subject: [PATCH 20/24] dolarito --- .vscode/launch.json | 2 +- src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 618731f..8924830 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "type": "node-terminal" }, { - "command": " npm start -- test -p /home/leo/workspaces/wollok-dev/wollok-ts-cli/examples/user-natives -f userNatives.wtest -v", + "command": " npm start -- run -g --port 4200 'mainDolarito.Doralito' --skipValidations -p 'examples/dolarito' ", "name": "wollok prueba", "request": "launch", "type": "node-terminal" diff --git a/src/utils.ts b/src/utils.ts index 14dfda2..43a5106 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -145,7 +145,7 @@ export const handleError = (error: any): void => { } export async function readNatives(nativeFolder: string): Promise { - const paths = await globby('**/*.@(ts|js)', { cwd: nativeFolder }) + const paths = await globby('**/*.@(ts|cjs|js)', { cwd: nativeFolder }) const debug = logger.getLevel() <= logger.levels.DEBUG From fe89030758e805513f15dd1e9741f1b02a5d593b Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 29 Dec 2024 20:16:17 -0300 Subject: [PATCH 21/24] change dependecy command name and add log to .gitignore --- .vscode/launch.json | 6 ------ examples/init-examples/existing-folder/.gitignore | 4 ++++ src/commands/init.ts | 4 ++++ src/index.ts | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8924830..3cc2dc0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,12 +12,6 @@ "name": "Run a single test file", "request": "launch", "type": "node-terminal" - }, - { - "command": " npm start -- run -g --port 4200 'mainDolarito.Doralito' --skipValidations -p 'examples/dolarito' ", - "name": "wollok prueba", - "request": "launch", - "type": "node-terminal" } ] } diff --git a/examples/init-examples/existing-folder/.gitignore b/examples/init-examples/existing-folder/.gitignore index 9e95028..9e734a5 100644 --- a/examples/init-examples/existing-folder/.gitignore +++ b/examples/init-examples/existing-folder/.gitignore @@ -3,4 +3,8 @@ .history # Wollok Log +log *.log + +# Dependencies +node_modules diff --git a/src/commands/init.ts b/src/commands/init.ts index ceff8f2..a3c122e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -173,5 +173,9 @@ const gitignore = ` .history # Wollok Log +log *.log + +# Dependencies +node_modules ` \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index db0402c..4746b0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ updateNotifier().finally(() => { .allowUnknownOption() .action(init) - const dependencyCommand = new Command('dependency') + const dependencyCommand = new Command('dependencies') .description('Manage dependencies for a Wollok project') dependencyCommand From 0d7eb584b8cf78e1c5feed4702dd3cfcb2715f8d Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 29 Dec 2024 21:15:39 -0300 Subject: [PATCH 22/24] restore wollok-ts dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb24e21..caeda92 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "print-message": "^3.0.1", "socket.io": "^4.5.1", "winston": "^3.11.0", - "wollok-ts": "file:../wollok-ts", + "wollok-ts": "^4.1.9", "wollok-web-tools": "^1.1.7", "typescript": "~5.5.1" }, From 6f1dd1ba0bb366c65a1091c777c22e8276c81e94 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 29 Dec 2024 21:37:07 -0300 Subject: [PATCH 23/24] restore package.json --- examples/init-examples/existing-folder/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/init-examples/existing-folder/package.json b/examples/init-examples/existing-folder/package.json index 155a6ec..ef8cb6a 100644 --- a/examples/init-examples/existing-folder/package.json +++ b/examples/init-examples/existing-folder/package.json @@ -2,6 +2,6 @@ "name": "existing-folder", "version": "1.0.0", "wollokVersion": "4.0.0", - "author": "leo", + "author": "palumbon", "license": "ISC" } From d81b75e7374618fdd662243807368256ea657731 Mon Sep 17 00:00:00 2001 From: Leo Gassman Date: Sun, 29 Dec 2024 21:55:48 -0300 Subject: [PATCH 24/24] remove unused features of project --- src/utils.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 43a5106..ac0705d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,29 +37,21 @@ export const folderIcon = '🗂️' export class Project { project!: string - folder?: string properties: any = {} - constructor(project: string, folder?: string) { + constructor(project: string) { this.project = project - this.folder = folder this.safeLoadJson() } get sourceFolder() : string { - return join(this.project, this.folder || '') + return this.project } get packageJsonPath(): string { return path.join(this.sourceFolder, 'package.json') } - public save(): void { - const jsonContent = JSON.stringify(this.properties, null, 2) - fs.writeFileSync(this.packageJsonPath, jsonContent, 'utf8') - //TODO debería hacerlo safe? me parece que no - } - private safeLoadJson() { try { const rawData = fs.readFileSync(this.packageJsonPath, 'utf-8')