diff --git a/.github/actions/package.json b/.github/actions/package.json index 5f440b990c..d66da94505 100644 --- a/.github/actions/package.json +++ b/.github/actions/package.json @@ -7,7 +7,7 @@ "@sourceacademy/modules-repotools": "workspace:^", "@types/node": "^24.0.0", "typescript": "^6.0.2", - "vitest": "4.1.4" + "vitest": "4.1.5" }, "dependencies": { "@actions/artifact": "^6.0.0", diff --git a/.github/actions/src/__tests__/commons.test.ts b/.github/actions/src/__tests__/commons.test.ts index 3b7598c623..a9ff16b2d9 100644 --- a/.github/actions/src/__tests__/commons.test.ts +++ b/.github/actions/src/__tests__/commons.test.ts @@ -2,7 +2,7 @@ import * as exec from '@actions/exec'; import { describe, expect, it, test, vi } from 'vitest'; import * as commons from '../commons.js'; -vi.mock(import('es-toolkit'), async (importOriginal) => { +vi.mock(import('es-toolkit'), async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -10,7 +10,7 @@ vi.mock(import('es-toolkit'), async (importOriginal) => { }; }); -const mockedExecOutput = vi.spyOn(exec, 'getExecOutput'); +const mockedExecOutput = vi.mocked(exec.getExecOutput); describe(commons.checkDirForChanges, () => { function mockChanges(value: boolean) { @@ -19,18 +19,17 @@ describe(commons.checkDirForChanges, () => { }); } - it('should return true if git diff exits with non zero code', async () => { - mockChanges(true); - await expect(commons.checkDirForChanges('/')).resolves.toEqual(true); - expect(mockedExecOutput).toHaveBeenCalledOnce(); - }); - it('should return false if git diff exits with 0', async () => { mockChanges(false); - await expect(commons.checkDirForChanges('/')).resolves.toEqual(false); expect(mockedExecOutput).toHaveBeenCalledOnce(); }); + + it('should return true if git diff exits with non zero code', async () => { + mockChanges(true); + await expect(commons.checkDirForChanges('/')).resolves.toEqual(true); + expect(mockedExecOutput).toHaveBeenCalledOnce(); + }); }); describe(commons.isPackageRecord, () => { diff --git a/.github/actions/src/artifact.ts b/.github/actions/src/artifact.ts new file mode 100644 index 0000000000..b3b6c20de1 --- /dev/null +++ b/.github/actions/src/artifact.ts @@ -0,0 +1,88 @@ +import fs from 'fs/promises'; +import pathlib from 'path'; +import { ArtifactNotFoundError, DefaultArtifactClient } from '@actions/artifact'; +import * as core from '@actions/core'; +import { exec } from '@actions/exec'; +import { outDir } from '@sourceacademy/modules-repotools/getGitRoot'; +import type { BundleManifest, ResolvedBundle, ResolvedTab } from '@sourceacademy/modules-repotools/types'; +import { filterAsync } from 'es-toolkit'; + +/** + * If the given bundles or tabs have already been built, then restore the built version + * using the {@link DefaultArtifactClient}. Otherwise, focus those workspaces, and then + * run the appropriate build commands. + */ +export async function loadOrBuildAsset(bundles: ResolvedBundle[], tabs: ResolvedTab[], manifest?: boolean) { + const artifact = new DefaultArtifactClient(); + + const tabsPromise = filterAsync(tabs, async ({ name: tabName }) => { + try { + const { artifact: { id } } = await artifact.getArtifact(`${tabName}-tab`); + await artifact.downloadArtifact(id, { path: pathlib.join(outDir, 'tabs') }); + core.info(`Downloaded artifact for ${tabName} tab`); + return false; + } catch (error) { + if (!(error instanceof ArtifactNotFoundError)) { + throw error; + } + core.error(`Error retrieving artifact for ${tabName} tab, need to try building`); + return true; + } + }); + + const bundlesPromise = filterAsync(bundles, async ({ name: bundleName }) => { + try { + const { artifact: { id } } = await artifact.getArtifact(`${bundleName}-bundle`); + await artifact.downloadArtifact(id, { path: pathlib.join(outDir, 'bundles') }); + core.info(`Downloaded artifact for ${bundleName} bundle`); + return false; + } catch (error) { + if (!(error instanceof ArtifactNotFoundError)) { + throw error; + } + core.error(`Error retrieving artifact for ${bundleName} bundle, need to try building`); + return true; + } + }); + + const manifestPromise = (async () => { + if (!manifest || bundles.length === 0) return; + + try { + const { artifact: { id } } = await artifact.getArtifact('manifest'); + await artifact.downloadArtifact(id, { path: outDir }); + } catch (error) { + if (!(error instanceof ArtifactNotFoundError)) { + throw error; + } + + const manifest = bundles.reduce>((res, bundle) => ({ + ...res, + [bundle.name]: bundle.manifest + }), {}); + + const outpath = pathlib.join(outDir, 'modules.json'); + await fs.writeFile(outpath, JSON.stringify(manifest)); + } + })(); + + const [bundlesToBuild, tabsToBuild] = await Promise.all([bundlesPromise, tabsPromise, manifestPromise]); + + if (bundlesToBuild.length === 0 && tabsToBuild.length === 0) return; + + const workspaces = [ + ...bundlesToBuild.map(({ name }) => `@sourceacademy/bundle-${name}`), + ...tabsToBuild.map(({ name }) => `@sourceacademy/tab-${name}`), + ]; + + // focus all at once + await exec('yarn workspaces focus', workspaces, { silent: false }); + + // Then build everything + const workspaceBuildArgs = workspaces.flatMap(each => ['--include', each]); + await exec( + 'yarn workspaces foreach -pA', + [...workspaceBuildArgs, 'run', 'build' ], + { silent: false } + ); +} diff --git a/.github/actions/src/load-artifacts/__tests__/artifact.test.ts b/.github/actions/src/load-artifacts/__tests__/artifact.test.ts index 74fbaf8238..357f1845f7 100644 --- a/.github/actions/src/load-artifacts/__tests__/artifact.test.ts +++ b/.github/actions/src/load-artifacts/__tests__/artifact.test.ts @@ -1,4 +1,4 @@ -import { ArtifactNotFoundError } from '@actions/artifact'; +import { ArtifactNotFoundError, type DefaultArtifactClient } from '@actions/artifact'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as manifest from '@sourceacademy/modules-repotools/manifest'; @@ -14,6 +14,7 @@ vi.mock(import('@actions/core'), async importOriginal => { info: () => { }, startGroup: () => { }, setFailed: vi.fn(), + getInput: vi.fn(), endGroup: () => { } }; }); @@ -35,88 +36,262 @@ vi.mock(import('../../gitRoot.js'), () => ({ })); const mockedResolveAllTabs = vi.spyOn(manifest, 'resolveAllTabs'); -const mockedGetArtifact = vi.fn(); -const mockedExec = vi.spyOn(exec, 'exec').mockResolvedValue(0); +const mockedResolveAllBundles = vi.spyOn(manifest, 'resolveAllBundles'); +const mockedGetArtifact = vi.fn(async name => { + if (name === 'Tab0-tab') { + return { artifact: { id: 0, size: 0, name } }; + } -test('tab resolution errors cause setFailed to be called', async () => { - mockedResolveAllTabs.mockResolvedValueOnce({ - severity: 'error', - errors: ['error1'] - }); + if (name === 'bundle0-bundle') { + return { artifact: { id: 1, size: 0, name } }; + } - await main(); + if (name === 'manifest') { + return { artifact: { id: 2, size: 0, name } }; + } - expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); - expect(core.error).toHaveBeenCalledExactlyOnceWith('error1'); - expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('Tab resolution failed with errors'); + throw new ArtifactNotFoundError(); }); +const mockedGetInput = vi.mocked(core.getInput); +const mockedExec = vi.spyOn(exec, 'exec').mockResolvedValue(0); -test('tabs that can\'t be found are built', async () => { - mockedResolveAllTabs.mockResolvedValueOnce({ - severity: 'success', - tabs: { - Tab0: { - type: 'tab', - directory: 'tab0', - name: 'Tab0', - entryPoint: 'tab0/index.tsx', - }, - Tab1: { - type: 'tab', - directory: 'tab1', - name: 'Tab1', - entryPoint: 'tab1/index.tsx', - }, - } - }); +function testCase( + this: 'skip' | 'only' | void, + desc: string, + input: { loadBundles?: boolean, loadTabs?: boolean, loadManifest?: boolean }, + testFn: () => Promise +) { + let fn: typeof test | typeof test.skip | typeof test.only; + if (this === 'skip') { + fn = test.skip; + } else if (this === 'only') { + fn = test.only; + } else { + fn = test; + } - mockedGetArtifact.mockImplementation(name => { - if (name === 'Tab0-tab') { - return { artifact: { id: 0 } }; - } - throw new ArtifactNotFoundError(); + fn(desc, async () => { + mockedGetInput.mockImplementation((name: string) => { + if (name === 'load-bundles') { + return input.loadBundles ? 'true' : 'false'; + } else if (name === 'load-tabs') { + return input.loadTabs ? 'true' : 'false'; + } else if (name === 'load-manifest') { + return input.loadManifest ? 'true' : 'false'; + } + return ''; + }); + + await testFn(); }); +} - await main(); +testCase.only = testCase.bind('only'); +testCase.skip = testCase.bind('skip'); - expect(mockedGetArtifact).toHaveBeenCalledTimes(2); - expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); +testCase( + 'bundle resolution errors cause setFailed to be called', + { loadBundles: true }, + async () => { + mockedResolveAllBundles.mockResolvedValueOnce({ + severity: 'error', + errors: ['error1'] + }); - const [[artifactCall0], [artifactCall1]] = mockedGetArtifact.mock.calls; - expect(artifactCall0).toEqual('Tab0-tab'); - expect(artifactCall1).toEqual('Tab1-tab'); + await main(); - expect(exec.exec).toHaveBeenCalledTimes(2); - const [[execCmd0, execCall0], [execCmd1, execCall1]] = vi.mocked(exec.exec).mock.calls; + expect(mockedResolveAllBundles).toHaveBeenCalledOnce(); + expect(core.error).toHaveBeenCalledExactlyOnceWith('error1'); + expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('Bundle resolution failed with errors'); + } +); - expect(execCmd0).toEqual('yarn workspaces focus'); - expect(execCall0).toContain('@sourceacademy/tab-Tab1'); - expect(execCall0).not.toContain('@sourceacademy/tab-Tab0'); +testCase( + 'tab resolution errors cause setFailed to be called', + { loadTabs: true }, + async () => { + mockedResolveAllTabs.mockResolvedValueOnce({ + severity: 'error', + errors: ['error1'] + }); - expect(execCmd1).toEqual('yarn workspaces foreach -pA'); - expect(execCall1).toContain('@sourceacademy/tab-Tab1'); - expect(execCall1).not.toContain('@sourceacademy/tab-Tab0'); -}); + await main(); -test('install failure means build doesn\'t happen', async () => { - mockedResolveAllTabs.mockResolvedValueOnce({ - severity: 'success', - tabs: { - Tab0: { - type: 'tab', - directory: 'tab0', - name: 'Tab0', - entryPoint: 'tab0/index.tsx', - }, - } - }); + expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); + expect(core.error).toHaveBeenCalledExactlyOnceWith('error1'); + expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('Tab resolution failed with errors'); + } +); - mockedGetArtifact.mockRejectedValueOnce(new ArtifactNotFoundError()); - mockedExec.mockResolvedValueOnce(1); +testCase( + 'assets that can\'t be found are built', + { loadBundles: true, loadTabs: true, loadManifest: true }, + async () => { + mockedResolveAllTabs.mockResolvedValueOnce({ + severity: 'success', + tabs: { + Tab0: { + type: 'tab', + directory: 'tab0', + name: 'Tab0', + entryPoint: 'tab0/index.tsx', + }, + Tab1: { + type: 'tab', + directory: 'tab1', + name: 'Tab1', + entryPoint: 'tab1/index.tsx', + }, + } + }); - await main(); + mockedResolveAllBundles.mockResolvedValueOnce({ + severity: 'success', + bundles: { + bundle0: { + type: 'bundle', + directory: 'bundle0', + name: 'bundle0', + manifest: {} + }, + bundle1: { + type: 'bundle', + directory: 'bundle1', + name: 'bundle1', + manifest: {} + }, + } + }); - expect(mockedGetArtifact).toHaveBeenCalledOnce(); - expect(exec.exec).toHaveBeenCalledOnce(); - expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('yarn workspace focus failed'); -}); + await expect(main()).resolves.not.toThrow(); + + expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); + expect(mockedResolveAllBundles).toHaveBeenCalledOnce(); + + expect(mockedGetArtifact).toHaveBeenCalledTimes(5); + const [ + [artifactCall0], + [artifactCall1], + [artifactCall2], + [artifactCall3], + [artifactCall4], + ] = mockedGetArtifact.mock.calls; + + expect(artifactCall0).toEqual('Tab0-tab'); + expect(artifactCall1).toEqual('Tab1-tab'); + expect(artifactCall2).toEqual('bundle0-bundle'); + expect(artifactCall3).toEqual('bundle1-bundle'); + expect(artifactCall4).toEqual('manifest'); + + expect(exec.exec).toHaveBeenCalledTimes(2); + const [[execCmd0, execCall0], [execCmd1, execCall1]] = vi.mocked(exec.exec).mock.calls; + + expect(execCmd0).toEqual('yarn workspaces focus'); + expect(execCall0).not.toContain('@sourceacademy/tab-Tab0'); + expect(execCall0).toContain('@sourceacademy/tab-Tab1'); + expect(execCall0).not.toContain('@sourceacademy/bundle-bundle0'); + expect(execCall0).toContain('@sourceacademy/bundle-bundle1'); + + expect(execCmd1).toEqual('yarn workspaces foreach -pA'); + expect(execCall1).not.toContain('@sourceacademy/tab-Tab0'); + expect(execCall1).toContain('@sourceacademy/tab-Tab1'); + expect(execCall1).not.toContain('@sourceacademy/bundle-bundle0'); + expect(execCall1).toContain('@sourceacademy/bundle-bundle1'); + } +); + +testCase( + 'load-bundles only means tabs don\'t get resolved', + { loadBundles: true }, + async () => { + mockedResolveAllBundles.mockResolvedValueOnce({ + severity: 'success', + bundles: {} + }); + + await main(); + + expect(mockedResolveAllTabs).not.toHaveBeenCalled(); + expect(mockedResolveAllBundles).toHaveBeenCalledOnce(); + } +); + +testCase( + 'load-tabs only means bundles don\'t get resolved', + { loadTabs: true }, + async () => { + mockedResolveAllTabs.mockResolvedValueOnce({ + severity: 'success', + tabs: {} + }); + + await main(); + + expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); + expect(mockedResolveAllBundles).not.toHaveBeenCalled(); + } +); + +testCase( + 'install failure means build doesn\'t happen', + { loadTabs: true }, + async () => { + mockedResolveAllTabs.mockResolvedValueOnce({ + severity: 'success', + tabs: { + Tab0: { + type: 'tab', + directory: 'tab0', + name: 'Tab0', + entryPoint: 'tab0/index.tsx', + }, + } + }); + + mockedGetArtifact.mockRejectedValueOnce(new ArtifactNotFoundError()); + mockedExec.mockRejectedValueOnce(new Error('yarn workspace focus failed')); + + await expect(main()).rejects.toThrow('yarn workspace focus failed'); + + expect(mockedResolveAllTabs).toHaveBeenCalledOnce(); + expect(mockedGetArtifact).toHaveBeenCalledOnce(); + expect(exec.exec).toHaveBeenCalledOnce(); + } +); + +testCase( + 'no bundles or tabs to build means exec is never executed', + { loadBundles: true, loadTabs: true }, + async () => { + mockedResolveAllTabs.mockResolvedValueOnce({ + severity: 'success', + tabs: { + Tab0: { + type: 'tab', + directory: 'tab0', + name: 'Tab0', + entryPoint: 'tab0/index.tsx', + }, + } + }); + + mockedResolveAllBundles.mockResolvedValueOnce({ + severity: 'success', + bundles: { + bundle0: { + type: 'bundle', + directory: 'bundle0', + name: 'bundle0', + manifest: {} + }, + } + }); + + await expect(main()).resolves.not.toThrow(); + + expect(mockedResolveAllTabs).toHaveBeenCalled(); + expect(mockedResolveAllBundles).toHaveBeenCalledOnce(); + + expect(exec.exec).not.toHaveBeenCalled(); + } +); diff --git a/.github/actions/src/load-artifacts/action.yml b/.github/actions/src/load-artifacts/action.yml index 367dc271bd..63bff94624 100644 --- a/.github/actions/src/load-artifacts/action.yml +++ b/.github/actions/src/load-artifacts/action.yml @@ -1,5 +1,18 @@ name: Load Artifacts for Dev Server -description: This action is mainly intended for loading all of the compiled tabs for the devserver. +description: This action is mainly intended for loading all of the compiled assets + +inputs: + load-tabs: + required: false + default: false + + load-bundles: + required: false + default: false + + load-manifest: + required: false + default: false runs: using: node24 diff --git a/.github/actions/src/load-artifacts/index.ts b/.github/actions/src/load-artifacts/index.ts index 6aeadbe373..172066ffdd 100644 --- a/.github/actions/src/load-artifacts/index.ts +++ b/.github/actions/src/load-artifacts/index.ts @@ -1,60 +1,43 @@ -import pathlib from 'path'; -import { ArtifactNotFoundError, DefaultArtifactClient } from '@actions/artifact'; import * as core from '@actions/core'; -import { exec } from '@actions/exec'; -import { bundlesDir, outDir, tabsDir } from '@sourceacademy/modules-repotools/getGitRoot'; -import { resolveAllTabs } from '@sourceacademy/modules-repotools/manifest'; +import { bundlesDir, tabsDir } from '@sourceacademy/modules-repotools/getGitRoot'; +import { resolveAllBundles, resolveAllTabs } from '@sourceacademy/modules-repotools/manifest'; +import type { ResolvedBundle, ResolvedTab } from '@sourceacademy/modules-repotools/types'; +import { loadOrBuildAsset } from '../artifact'; export async function main() { - const artifact = new DefaultArtifactClient(); - const tabsResult = await resolveAllTabs(bundlesDir, tabsDir); + let bundles: ResolvedBundle[] = []; + if (core.getInput('load-bundles') === 'true') { + const bundlesResult = await resolveAllBundles(bundlesDir); - if (tabsResult.severity === 'error') { - core.startGroup('Tab Resolution errors'); - for (const error of tabsResult.errors) { - core.error(error); + if (bundlesResult.severity === 'error') { + core.startGroup('Bundles Resolution errors'); + for (const error of bundlesResult.errors) { + core.error(error); + } + core.endGroup(); + core.setFailed('Bundle resolution failed with errors'); + return; } - core.endGroup(); - core.setFailed('Tab resolution failed with errors'); - return; + + bundles = Object.values(bundlesResult.bundles); } - const tabPromises = Object.keys(tabsResult.tabs).map(async (tabName): Promise => { - try { - const { artifact: { id } } = await artifact.getArtifact(`${tabName}-tab`); - await artifact.downloadArtifact(id, { path: pathlib.join(outDir, 'tabs') }); - core.info(`Downloaded artifact for ${tabName}`); - return null; - } catch (error) { - if (!(error instanceof ArtifactNotFoundError)) { - throw error; + let tabs: ResolvedTab[] = []; + if (core.getInput('load-tabs') === 'true') { + const tabsResult = await resolveAllTabs(bundlesDir, tabsDir); + if (tabsResult.severity === 'error') { + core.startGroup('Tab Resolution errors'); + for (const error of tabsResult.errors) { + core.error(error); } - core.error(`Error retrieving artifact for ${tabName}, need to try building`); - return tabName; + core.endGroup(); + core.setFailed('Tab resolution failed with errors'); + return; } - }); - - // Artifacts could not be found, we probably need to build it - const tabsToBuild = (await Promise.all(tabPromises)).filter(x => x !== null); - if (tabsToBuild.length === 0) return; - - // focus all at once - const workspaces = tabsToBuild.map(each => `@sourceacademy/tab-${each}`); - const focusExitCode = await exec('yarn workspaces focus', workspaces, { silent: false }); - if (focusExitCode !== 0) { - core.setFailed('yarn workspace focus failed'); - return; + tabs = Object.values(tabsResult.tabs); } - const workspaceBuildArgs = workspaces.flatMap(each => ['--include', each]); - const buildExitCode = await exec( - 'yarn workspaces foreach -pA', - [...workspaceBuildArgs, 'run', 'build'], - { silent: false } - ); - if (buildExitCode !== 0) { - core.setFailed('Building tabs failed'); - } + await loadOrBuildAsset(bundles, tabs, core.getInput('load-manifest') === 'true'); } if (process.env.GITHUB_ACTIONS) { diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4cebb71bfc..77d7f6e367 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -157,6 +157,12 @@ jobs: yarn build yarn tsc + - name: Upload Tab Artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ matrix.bundleInfo.name }}-bundle + path: ./build/bundles/${{ matrix.bundleInfo.name }}.js + - name: Build Bundle Docs run: | cd ${{ matrix.bundleInfo.directory }} @@ -195,6 +201,8 @@ jobs: - name: Load/Build all tabs uses: ./.github/actions/src/load-artifacts + with: + load-tabs: true - name: Initialize Devserver uses: ./.github/actions/src/init @@ -274,12 +282,11 @@ jobs: - name: Build Manifest run: yarn buildtools manifest - # Not sure if we need to upload this as an artifact - # - name: Upload Manifest - # uses: actions/upload-artifact@v4 - # with: - # name: manifest - # path: ./build/modules.json + - name: Upload Manifest + uses: actions/upload-artifact@v4 + with: + name: manifest + path: ./build/modules.json test: name: Verify all tests pass and build success diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f72155138..ba34630b9a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -66,5 +66,8 @@ "typescriptreact", "yml", "yaml" + ], + "vue.server.includeLanguages": [ + "vue" ] } diff --git a/devserver/package.json b/devserver/package.json index ce8974c31c..850341b470 100644 --- a/devserver/package.json +++ b/devserver/package.json @@ -10,14 +10,14 @@ }, "dependencies": { "@blueprintjs/core": "^6.0.0", - "@blueprintjs/icons": "^6.0.0", "@commander-js/extra-typings": "^14.0.0", "@sourceacademy/modules-lib": "workspace:^", "@vitejs/plugin-react": "^6.0.1", "ace-builds": "^1.25.1", "classnames": "^2.3.1", "commander": "^14.0.0", - "js-slang": "^1.0.85", + "es-toolkit": "^1.44.0", + "js-slang": "^1.0.92", "re-resizable": "^6.9.11", "react": "^19.0.0", "react-ace": "^14.0.0", @@ -29,17 +29,14 @@ "@sourceacademy/modules-buildtools": "workspace:^", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-playwright": "4.1.5", "eslint": "^9.35.0", "playwright": "^1.55.1", "sass": "^1.85.0", "typescript": "^6.0.2", - "vitest": "4.1.4", + "vitest": "4.1.5", "vitest-browser-react": "^2.1.0" }, - "peerDependencies": { - "es-toolkit": "^1.44.0" - }, "scripts": { "dev": "vite", "lint": "eslint src", diff --git a/devserver/src/components/Playground.tsx b/devserver/src/components/Playground.tsx index 763a9daa4f..1a432882c4 100644 --- a/devserver/src/components/Playground.tsx +++ b/devserver/src/components/Playground.tsx @@ -1,12 +1,11 @@ import { Button, Classes, Intent, OverlayToaster, Popover, Tooltip, type ToastProps } from '@blueprintjs/core'; -import { Settings } from '@blueprintjs/icons'; import classNames from 'classnames'; +import { throttle } from 'es-toolkit'; import { SourceDocumentation, getNames, runInContext, type Context } from 'js-slang'; // Importing this straight from js-slang doesn't work for whatever reason import createContext from 'js-slang/dist/createContext'; +import { Chapter, Variant } from 'js-slang/dist/langs'; import { ModuleInternalError } from 'js-slang/dist/modules/errors'; -import { setModulesStaticURL } from 'js-slang/dist/modules/loader'; -import { Chapter, Variant } from 'js-slang/dist/types'; import { stringify } from 'js-slang/dist/utils/stringify'; import React from 'react'; import mockModuleContext from '../mockModuleContext'; @@ -17,7 +16,8 @@ import { ControlBarClearButton } from './controlBar/ControlBarClearButton'; import { ControlBarRefreshButton } from './controlBar/ControlBarRefreshButton'; import { ControlBarRunButton } from './controlBar/ControlBarRunButton'; import testTabContent from './sideContent/TestTab'; -import loadDynamicTabs from './sideContent/importers'; +import { getBundleLoader, loadDynamicTabs } from './sideContent/importers'; +import { getBundleDocsUsingVite, getModulesManifest } from './sideContent/importers/importers'; import type { SideContentTab } from './sideContent/types'; const refreshSuccessToast: ToastProps = { @@ -49,20 +49,14 @@ const createContextHelper = (onConsoleLog: (arg: string) => void) => { return tempContext; }; +const updateEditorLocalStorageValue = throttle((newValue: string) => { + localStorage.setItem('editorValue', newValue); +}, 100); + const Playground: React.FC = () => { const consoleLogs = React.useRef([]); - const [moduleBackend, setModuleBackend] = React.useState(null); - - React.useEffect(() => { - const savedBackend = localStorage.getItem('backend'); - if (savedBackend != undefined) { - setModuleBackend(savedBackend); - setModulesStaticURL(savedBackend); - } - }, []); - - const [useCompiledTabs, setUseCompiledTabs] = React.useState(!!localStorage.getItem('compiledTabs')); + const [useCompiled, setUseCompiled] = React.useState(!!localStorage.getItem('useCompiled')); const [dynamicTabs, setDynamicTabs] = React.useState([]); const [selectedTabId, setSelectedTab] = React.useState(testTabContent.id); const [codeContext, setCodeContext] = React.useState(createContextHelper(str => consoleLogs.current.push(str))); @@ -72,6 +66,9 @@ const Playground: React.FC = () => { const toaster = React.useRef(null); + const manifestImporter = getModulesManifest; + const docsImporter = getBundleDocsUsingVite; + const showToast = (props: ToastProps) => { if (toaster.current) { toaster.current.show({ @@ -81,43 +78,41 @@ const Playground: React.FC = () => { } }; - const getAutoComplete = React.useCallback((row: number, col: number, callback: any) => { - getNames(editorValue, row, col, codeContext) - .then(([editorNames, displaySuggestions]) => { - if (!displaySuggestions) { - callback(); - return; - } - - const editorSuggestions = editorNames.map((editorName: any) => ({ - ...editorName, - caption: editorName.name, - value: editorName.name, - score: editorName.score ? editorName.score + 1000 : 1000, - name: undefined - })); - - const builtins: Record = SourceDocumentation.builtins[Chapter.SOURCE_4]; - const builtinSuggestions = Object.entries(builtins) - .map(([builtin, thing]) => ({ - ...thing, - caption: builtin, - value: builtin, - score: 100, - name: builtin, - docHTML: thing.description - })); + const getAutoComplete = async (row: number, col: number, callback: any) => { + const [editorNames, displaySuggestions] = await getNames(editorValue, row, col, codeContext, { manifestImporter, docsImporter }); + if (!displaySuggestions) { + callback(); + return; + } - callback(null, [ - ...builtinSuggestions, - ...editorSuggestions - ]); - }); - }, [editorValue, codeContext]); + const editorSuggestions = editorNames.map((editorName: any) => ({ + ...editorName, + caption: editorName.name, + value: editorName.name, + score: editorName.score ? editorName.score + 1000 : 1000, + name: undefined + })); + + const builtins: Record = SourceDocumentation.builtins[Chapter.SOURCE_4]; + const builtinSuggestions = Object.entries(builtins) + .map(([builtin, thing]) => ({ + ...thing, + caption: builtin, + value: builtin, + score: 100, + name: builtin, + docHTML: thing.description + })); + + callback(null, [ + ...builtinSuggestions, + ...editorSuggestions + ]); + }; const loadTabs = async () => { try { - const tabs = await loadDynamicTabs(codeContext, useCompiledTabs); + const tabs = await loadDynamicTabs(codeContext, useCompiled); setDynamicTabs(tabs); const newIds = tabs.map(({ id }) => id); @@ -127,52 +122,55 @@ const Playground: React.FC = () => { setSelectedTab(testTabContent.id); } setAlerts(newIds); - } catch (error) { showToast(errorToast); console.log(error); } }; - const evalCode = () => { + const evalCode = async () => { codeContext.errors = []; codeContext.moduleContexts = mockModuleContext.moduleContexts = {}; consoleLogs.current = []; - runInContext(editorValue, codeContext, { + const result = await runInContext(editorValue, codeContext, { importOptions: { - loadTabs: useCompiledTabs - } - }) - .then((result) => { - if (codeContext.errors.length > 0) { - showToast(errorToast); - } else { - loadTabs() - .then(() => showToast(evalSuccessToast)); + loadTabs: false, + sourceBundleImporter: getBundleLoader(useCompiled), + docsImporter, + resolverOptions: { + manifestImporter } + } + }); - if (result.status === 'finished') { - setReplOutput({ - type: 'result', - // code: editorValue, - consoleLogs: consoleLogs.current, - value: stringify(result.value) - }); - } else if (result.status === 'error') { - codeContext.errors.forEach(error => { - if (error instanceof ModuleInternalError) { - console.error(error.error); - } - }); + if (codeContext.errors.length > 0) { + showToast(errorToast); + } else { + loadTabs() + .then(() => showToast(evalSuccessToast)); + } - setReplOutput({ - type: 'errors', - errors: codeContext.errors, - consoleLogs: consoleLogs.current - }); + if (result.status === 'finished') { + setReplOutput({ + type: 'result', + // code: editorValue, + consoleLogs: consoleLogs.current, + value: stringify(result.value) + }); + } else if (result.status === 'error') { + codeContext.errors.forEach(error => { + if (error instanceof ModuleInternalError) { + console.error(error.error); } }); + + setReplOutput({ + type: 'errors', + errors: codeContext.errors, + consoleLogs: consoleLogs.current + }); + } }; const resetEditor = () => { @@ -200,28 +198,19 @@ const Playground: React.FC = () => { interactionKind='click' placement="right" content={ { - setModuleBackend(value); - setModulesStaticURL(value); - localStorage.setItem('backend', value); - }} - useCompiledForTabs={useCompiledTabs} + useCompiled={useCompiled} onUseCompiledChange={value => { - setUseCompiledTabs(value); - localStorage.setItem('compiledTabs', value ? 'true' : ''); + setUseCompiled(value); + localStorage.setItem('useCompiled', value ? 'true' : ''); }} />} - renderTarget={({ isOpen: _isOpen, ...targetProps }) => { - return ( - - -

+

} + icon='arrow-right' tabIndex={0} onClick={() => { changeStep(currentStep + 1); diff --git a/lib/modules-lib/src/tabs/NumberSelector.tsx b/lib/modules-lib/src/tabs/NumberSelector.tsx index d2d87ff18b..48facdf83c 100644 --- a/lib/modules-lib/src/tabs/NumberSelector.tsx +++ b/lib/modules-lib/src/tabs/NumberSelector.tsx @@ -24,6 +24,8 @@ export type NumberSelectorProps = { /** * React component for wrapping around a {@link EditableText} to provide automatic * validation for number values + * + * @category Components */ export default function NumberSelector({ value, diff --git a/lib/modules-lib/src/tabs/PlayButton.tsx b/lib/modules-lib/src/tabs/PlayButton.tsx index 3b76ec719e..0502560ff4 100644 --- a/lib/modules-lib/src/tabs/PlayButton.tsx +++ b/lib/modules-lib/src/tabs/PlayButton.tsx @@ -1,23 +1,49 @@ -/* [Imports] */ -import { Icon, Tooltip } from '@blueprintjs/core'; -import { Pause, Play } from '@blueprintjs/icons'; +import { Icon, Tooltip, type IconProps } from '@blueprintjs/core'; import ButtonComponent, { type ButtonComponentProps } from './ButtonComponent'; -/* [Exports] */ -export type PlayButtonProps = ButtonComponentProps & { +export type PlayButtonProps = Omit & { isPlaying: boolean; + + /** + * Tooltip string for the button when `isPlaying` is true. Defaults to `Pause`. + */ + playingText?: string; + + /** + * Tooltip string for the button when `isPlaying` is false. Defaults to `Play`. + */ + pausedText?: string; + + /** + * Icon for the button when `isPlaying` is true. Defaults to `pause`. + */ + playingIcon?: IconProps['icon']; + + /** + * Icon for the button when `isPlaying` is false. Defaults to `play`. + */ + pausedIcon?: IconProps['icon']; }; -/* [Main] */ -export default function PlayButton(props: PlayButtonProps) { +/** + * A {@link ButtonComponent|Button} that toggles between two states: playing and not playing. + * + * @category Components + */ +export default function PlayButton({ + playingText = 'Pause', + playingIcon = 'pause', + pausedText = 'Play', + pausedIcon = 'play', + isPlaying, + ...props +}: PlayButtonProps) { return - : } - /> + ; } diff --git a/lib/modules-lib/src/tabs/WebGLCanvas.tsx b/lib/modules-lib/src/tabs/WebGLCanvas.tsx index 936e3a5a5e..ccc96c1593 100644 --- a/lib/modules-lib/src/tabs/WebGLCanvas.tsx +++ b/lib/modules-lib/src/tabs/WebGLCanvas.tsx @@ -12,6 +12,8 @@ export type WebGLCanvasProps = DetailedHTMLProps( (props, ref) => { diff --git a/lib/modules-lib/src/tabs/__tests__/utils.test.ts b/lib/modules-lib/src/tabs/__tests__/utils.test.ts new file mode 100644 index 0000000000..db7214a483 --- /dev/null +++ b/lib/modules-lib/src/tabs/__tests__/utils.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vitest'; +import { getModuleState } from '../utils'; + +describe(getModuleState, () => { + test('returns null when module isn\'t present on module state', () => { + const context = { + context: { + moduleContexts: {} + } + }; + + expect(getModuleState(context as any, 'module')).toBeNull(); + }); + + test('returns null when module hasn\'t been intialized', () => { + const context = { + context: { + moduleContexts: { + module: { + state: null, + tabs: null + } + } + } + }; + + expect(getModuleState(context as any, 'module')).toBeNull(); + }); +}); diff --git a/lib/modules-lib/src/tabs/index.ts b/lib/modules-lib/src/tabs/index.ts index 847b702fb4..38fd6cce1f 100644 --- a/lib/modules-lib/src/tabs/index.ts +++ b/lib/modules-lib/src/tabs/index.ts @@ -1,7 +1,7 @@ /** * Reusable React Components and styling utilities designed for use with SA Module Tabs - * @module Tabs - * @title Tabs Library + * @module tabs + * @disableGroups */ // This file is necessary so that the documentation generated by typedoc comes out in diff --git a/lib/modules-lib/src/tabs/useAnimation.ts b/lib/modules-lib/src/tabs/useAnimation.ts index 3445441e27..c74bb9592c 100644 --- a/lib/modules-lib/src/tabs/useAnimation.ts +++ b/lib/modules-lib/src/tabs/useAnimation.ts @@ -102,9 +102,9 @@ function useRerender() { /** * Hook for animations based around the `requestAnimationFrame` function. Calls the provided callback periodically. + * @category Hooks * @returns Animation Hook utilities */ - export function useAnimation({ animationDuration, autoLoop, @@ -172,7 +172,7 @@ export function useAnimation({ * - Sets elapsed to 0 and draws the 0 frame to the canvas * - Sets lastFrameTimestamp to null * - Cancels the current animation request - * - If there was a an animation callback scheduled, call `requestFrame` again + * - If there was an animation callback scheduled, call `requestFrame` again */ function reset() { setElapsed(0); diff --git a/lib/modules-lib/src/tabs/utils.ts b/lib/modules-lib/src/tabs/utils.ts index 43a9eef76b..ee9b4e7eac 100644 --- a/lib/modules-lib/src/tabs/utils.ts +++ b/lib/modules-lib/src/tabs/utils.ts @@ -8,6 +8,8 @@ import type { DebuggerContext, ModuleSideContent } from '../types'; * @param debuggerContext DebuggerContext as returned by the frontend * @param name Name of your bundle * @returns The state object of your bundle + * + * @category Utilities */ export function getModuleState(debuggerContext: DebuggerContext, name: string): T | null { const { context: { moduleContexts } } = debuggerContext; @@ -16,6 +18,7 @@ export function getModuleState(debuggerContext: DebuggerContext, name: string /** * Helper for typing tabs + * @category Utilities */ export function defineTab(tab: T) { return tab; diff --git a/lib/modules-lib/src/types/index.ts b/lib/modules-lib/src/types/index.ts index 68cd9c1fe5..2c24a3357c 100644 --- a/lib/modules-lib/src/types/index.ts +++ b/lib/modules-lib/src/types/index.ts @@ -1,4 +1,4 @@ -import type { IconName } from '@blueprintjs/icons'; +import type { IconName } from '@blueprintjs/core'; import type { Context } from 'js-slang'; import type React from 'react'; @@ -35,17 +35,22 @@ export abstract class glAnimation { /** * Because of some quirks in the way tabs and bundles are built, an `instanceof` check might fail at runtime. - * You should use this function instead of an `instanceof` check to check for `glAnimations`. + * So we need to rewrite the instanceof check */ - public static isAnimation(obj: unknown): obj is glAnimation { - if (typeof obj !== 'object' || obj === null) return false; + static [Symbol.hasInstance](constructor: unknown): boolean { + if (typeof constructor !== 'object' || constructor === null) return false; + return '_anim_symbol' in constructor && constructor._anim_symbol === glAnimationSymbol; + } - return '_anim_symbol' in obj && obj._anim_symbol === glAnimationSymbol; + public static isAnimation(obj: unknown): obj is glAnimation { + return obj instanceof glAnimation; } } + export interface AnimFrame { draw: (canvas: HTMLCanvasElement) => void; } + export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T; @@ -53,24 +58,20 @@ export type DeepPartial = T extends object ? { /** * DebuggerContext type used by frontend to assist typing information */ -export type DebuggerContext = { +export interface DebuggerContext { result: any; lastDebuggerResult: any; code: string; context: Context; workspaceLocation?: any; -}; +} export type ModuleContexts = Context['moduleContexts']; -/** - * Interface to represent objects that require a string representation in the - * REPL - */ -export interface ReplResult { - toReplString: () => string; -} +export type { ReplResult } from 'js-slang/dist/types'; +// We don't use the context property to avoid confusion with the context +// property on React class components export type ModuleTab = (props: { debuggerCtx: DebuggerContext }) => React.ReactNode; export interface ModuleSideContent { @@ -93,5 +94,5 @@ export interface ModuleSideContent { * This function will be called to render the module tab in the side contents * on Source Academy frontend. */ - body: (context: DebuggerContext) => React.ReactElement; + body: (context: DebuggerContext) => React.ReactNode; }; diff --git a/lib/modules-lib/src/utilities.ts b/lib/modules-lib/src/utilities.ts index f1641ff242..6c8a5bb419 100644 --- a/lib/modules-lib/src/utilities.ts +++ b/lib/modules-lib/src/utilities.ts @@ -4,6 +4,7 @@ * @title Utilities */ +import { GeneralRuntimeError, InvalidParameterTypeError } from './errors'; import type { DebuggerContext } from './types'; /** @@ -30,12 +31,17 @@ export function radiansToDegrees(radians: number): number { * @returns Tuple of three numbers representing the R, G and B components */ export function hexToColor(hex: string, func_name?: string): [r: number, g: number, b: number] { + func_name = func_name ?? hexToColor.name; + + if (typeof hex !== 'string') { + throw new InvalidParameterTypeError('string', hex, func_name); + } + const regex = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/igu; const groups = regex.exec(hex); - if (groups == undefined) { - func_name = func_name ?? hexToColor.name; - throw new Error(`${func_name}: Invalid color hex string: ${hex}`); + if (!groups) { + throw new GeneralRuntimeError(`${func_name}: Invalid color hex string: ${hex}`); }; return [ @@ -63,21 +69,16 @@ export function mockDebuggerContext(moduleState: T, name: string) { } as unknown as DebuggerContext; } -type TupleOfLengthHelper = - V['length'] extends T ? V : TupleOfLengthHelper; - -/** - * Utility type that represents a tuple of a specific length - */ -export type TupleOfLength = TupleOfLengthHelper; +export { + isNumberWithinRange, + assertNumberWithinRange, + isFunctionOfLength, + assertFunctionOfLength, + isTupleOfLength, + assertTupleOfLength +} from 'js-slang/dist/utils/rttc'; -/** - * Type guard for checking that a function has the specified number of parameters. Of course at runtime parameter types - * are not checked, so this is only useful when combined with TypeScript types. - */ -export function isFunctionOfLength any>(f: (...args: any) => any, l: Parameters['length']): f is T; -export function isFunctionOfLength(f: unknown, l: T): f is (...args: TupleOfLength) => unknown; -export function isFunctionOfLength(f: unknown, l: number) { - // TODO: Need a variation for rest parameters - return typeof f === 'function' && f.length === l; -} +export { + callIfFuncAndRightArgs, + wrap as wrapFunction +} from 'js-slang/dist/utils/operators'; diff --git a/lib/modules-lib/typedoc.config.js b/lib/modules-lib/typedoc.config.js index 42650769be..2ce2b739e4 100644 --- a/lib/modules-lib/typedoc.config.js +++ b/lib/modules-lib/typedoc.config.js @@ -1,5 +1,7 @@ import { OptionDefaults } from 'typedoc'; +// typedoc options reference: https://typedoc.org/documents/Options.html + /** * @type { * import('typedoc').TypeDocOptions & @@ -23,6 +25,15 @@ const typedocOptions = { readme: 'none', router: 'module', skipErrorChecking: true, + externalSymbolLinkMappings: { + '@blueprintjs/core': { + EditableText: 'https://blueprintjs.com/docs/#core/components/editable-text', + Switch: 'https://blueprintjs.com/docs/#core/components/switch' + }, + 'js-slang': { + RuntimeSourceError: '#' + } + }, // This lets us define some custom block tags blockTags: [ @@ -42,9 +53,17 @@ const typedocOptions = { parametersFormat: 'htmlTable', typeAliasPropertiesFormat: 'htmlTable', useCodeBlocks: true, + + // Organizational Options + categorizeByGroup: true, + categoryOrder: ['*', 'Other'], + navigation: { + includeCategories: true, + includeGroups: false + }, sort: [ 'alphabetical', - 'kind' + 'kind', ] }; diff --git a/lib/modules-lib/vitest.config.ts b/lib/modules-lib/vitest.config.ts index 3f4642b168..560815f49d 100644 --- a/lib/modules-lib/vitest.config.ts +++ b/lib/modules-lib/vitest.config.ts @@ -12,8 +12,9 @@ export default mergeConfig( include: [ '@blueprintjs/core', '@blueprintjs/icons', - 'es-toolkit', 'vitest-browser-react', + 'js-slang/dist/errors/runtimeSourceError', + 'js-slang/dist/utils/stringify' ] }, plugins: [react()], diff --git a/lib/repotools/package.json b/lib/repotools/package.json index 7e2319bd71..aa7fbb5bb1 100644 --- a/lib/repotools/package.json +++ b/lib/repotools/package.json @@ -8,13 +8,13 @@ "@commander-js/extra-typings": "^14.0.0", "@types/node": "^24.0.0", "@vitejs/plugin-react": "^6.0.1", - "@vitest/coverage-v8": "4.1.4", + "@vitest/coverage-v8": "4.1.5", "typescript": "^6.0.2", - "vitest": "4.1.4", + "vitest": "4.1.5", "vitest-browser-react": "^2.1.0" }, "dependencies": { - "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-playwright": "4.1.5", "chalk": "^5.0.1", "commander": "^14.0.0", "es-toolkit": "^1.44.0", diff --git a/lib/vitest-reporter/package.json b/lib/vitest-reporter/package.json index e4164f7fbb..c9377f8a27 100644 --- a/lib/vitest-reporter/package.json +++ b/lib/vitest-reporter/package.json @@ -5,12 +5,12 @@ "type": "module", "dependencies": { "istanbul-lib-report": "^3.0.1", - "vitest": "4.1.4" + "vitest": "4.1.5" }, "devDependencies": { "@types/istanbul-lib-report": "^3.0.3", "@types/node": "^24.0.0", - "@vitest/coverage-v8": "4.1.4", + "@vitest/coverage-v8": "4.1.5", "esbuild": "^0.28.0", "typescript": "^6.0.2" }, diff --git a/package.json b/package.json index f547640d7e..53f8cdc06d 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,16 @@ "build:bundles": "Builds all bundles", "build:docs": "Build all documentation for all bundles, then builds the HTML documentation", "build:libs": "Build all module library code", + "build:manifest": "Build the manifest", "build:modules": "Build all bundles and tabs", "compile:bundles": "Compile all bundles", "devserver": "Run the modules development server", "lint:all": "Lint all code in the repository", + "lint:bundles": "Lint only bundles", "lint:global": "Lint all code in the repository that isn't a bundle or a tab", "lint:inspect": "Run the interactive ESLint config inspector", "lint:modules": "Lint only bundles and tabs", + "lint:tabs": "Lint only tabs", "prepare": "Enable git hooks", "run:bundles": "Run the given command on every single bundle workspace", "serve": "Start the HTTP server to serve all files in ./build, with the same directory structure", @@ -32,13 +35,16 @@ "build:docs": "yarn run:bundles buildtools build docs && yarn buildtools html && yarn workspaces foreach -A --include \"@sourceacademy/modules-docserver\" run build", "build:bundles": "yarn run:bundles build", "build:libs": "yarn workspaces foreach -ptW --from \"./lib/*\" run build", + "build:manifest": "buildtools manifest", "build:modules": "yarn workspaces foreach -ptW --from \"./src/{bundles,tabs}/*\" run buildtools build && buildtools manifest", "compile:bundles": "yarn workspaces foreach -ptW --from \"./src/bundles/*\" run buildtools compile", "devserver": "node ./devserver/bin.js", "lint:all": "yarn lint:global && yarn lint:modules", + "lint:bundles": "yarn workspaces foreach -j 5 -pW --from \"./src/bundles/*\" run lint", "lint:global": "node --max-old-space-size=8192 $(yarn bin buildtools) lintglobal", "lint:inspect": "yarn dlx eslint --inspect-config", "lint:modules": "yarn workspaces foreach -j 5 -pW --from \"./src/{bundles,tabs}/*\" run lint", + "lint:tabs": "yarn workspaces foreach -j 5 -pW --from \"./src/tabs/*\" run lint", "prepare": "husky", "run:bundles": "yarn workspaces foreach -ptW --from \"./src/bundles/*\" run", "serve": "yarn buildtools serve", @@ -47,6 +53,7 @@ "test:devserver": "yarn workspaces foreach -A --include \"@sourceacademy/modules-devserver\" run test", "test:libs": "yarn workspaces foreach -ptW --from \"./lib/*\" run test", "test:modules": "yarn workspaces foreach -ptW --from \"./src/{bundles,tabs}/*\" run test", + "test:tabs": "yarn workspaces foreach -ptW --from \"./src/tabs/*\" run test", "tsc:all": "yarn workspaces foreach -pt --all --exclude \"@sourceacademy/modules\" run tsc", "tsc:devserver": "tsc --project ./devserver/tsconfig.json", "tsc:modules": "yarn workspaces foreach -ptW --from \"./src/{bundles,tabs}/*\" run tsc" @@ -56,14 +63,16 @@ "@eslint/markdown": "^7.5.1", "@sourceacademy/lint-plugin": "workspace:^", "@sourceacademy/modules-buildtools": "workspace:^", + "@sourceacademy/modules-lib": "workspace:^", "@sourceacademy/modules-repotools": "workspace:^", "@sourceacademy/vitest-reporter": "workspace:^", "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^24.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@vitest/coverage-v8": "4.1.4", + "@vitest/coverage-v8": "4.1.5", "@vitest/eslint-plugin": "^1.6.14", + "@vitest/ui": "4.1.5", "@yarnpkg/types": "^4.0.1", "esbuild": "^0.28.0", "eslint": "^9.35.0", @@ -80,15 +89,15 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.0", - "vitest": "4.1.4", + "typescript-eslint": "^8.58.2", + "vitest": "4.1.5", "vitest-browser-react": "^2.1.0" }, "peerDependencies": { "@blueprintjs/core": "^6.0.0", "@blueprintjs/icons": "^6.0.0", "es-toolkit": "^1.44.0", - "js-slang": "^1.0.85", + "js-slang": "^1.0.92", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -100,7 +109,7 @@ }, "runtime": { "name": "node", - "version": "^22.16.0", + "version": "^24.14.1", "onFail": "error" } }, diff --git a/src/archive/.vscode/settings.json b/src/archive/.vscode/settings.json new file mode 100644 index 0000000000..f2370a27cb --- /dev/null +++ b/src/archive/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsserver.enable": false +} \ No newline at end of file diff --git a/src/bundles/arcade_2d/package.json b/src/bundles/arcade_2d/package.json index dfcc6d5ed1..e6f70da0cb 100644 --- a/src/bundles/arcade_2d/package.json +++ b/src/bundles/arcade_2d/package.json @@ -1,6 +1,6 @@ { "name": "@sourceacademy/bundle-arcade_2d", - "version": "1.0.0", + "version": "1.0.1", "private": true, "dependencies": { "phaser": "^3.54.0" @@ -20,8 +20,9 @@ "lint": "buildtools lint .", "tsc": "buildtools tsc .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/arcade_2d/src/audio.ts b/src/bundles/arcade_2d/src/audio.ts index 28c22a9183..2392b1035a 100644 --- a/src/bundles/arcade_2d/src/audio.ts +++ b/src/bundles/arcade_2d/src/audio.ts @@ -1,12 +1,14 @@ /** * This file contains Arcade2D's representation of audio clips and sound. */ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; +import type { ReplResult } from '@sourceacademy/modules-lib/types'; /** * Encapsulates the representation of AudioClips. * AudioClips are unique - there are no AudioClips with the same URL. */ -export class AudioClip { +export class AudioClip implements ReplResult { private static audioClipCount: number = 0; // Stores AudioClip index with the URL as a unique key. private static audioClipsIndexMap: Map = new Map(); @@ -33,7 +35,7 @@ export class AudioClip { */ public static of(url: string, volumeLevel: number): AudioClip { if (url === '') { - throw new Error('AudioClip URL cannot be empty'); + throw new GeneralRuntimeError('AudioClip URL cannot be empty'); } if (AudioClip.audioClipsIndexMap.has(url)) { return AudioClip.audioClipsArray[AudioClip.audioClipsIndexMap.get(url) as number]; @@ -83,6 +85,5 @@ export class AudioClip { public toReplString = () => ''; - /** @override */ public toString = () => this.toReplString(); } diff --git a/src/bundles/arcade_2d/src/functions.ts b/src/bundles/arcade_2d/src/functions.ts index bb3398a299..22adb6fa90 100644 --- a/src/bundles/arcade_2d/src/functions.ts +++ b/src/bundles/arcade_2d/src/functions.ts @@ -9,6 +9,8 @@ * @author Xenos Fiorenzo Anong */ +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertNumberWithinRange, assertTupleOfLength } from '@sourceacademy/modules-lib/utilities'; import { AudioClip } from './audio'; import { DEFAULT_DEBUG_STATE, @@ -89,7 +91,7 @@ export const config = { * ``` * @category GameObject */ -export function create_rectangle(width: number, height: number): ShapeGameObject { +export function create_rectangle(width: number, height: number): ShapeGameObject { const rectangle = { width, height @@ -107,7 +109,7 @@ export function create_rectangle(width: number, height: number): ShapeGameObject * ``` * @category GameObject */ -export function create_circle(radius: number): ShapeGameObject { +export function create_circle(radius: number): ShapeGameObject { const circle = { radius } as CircleProps; @@ -124,7 +126,7 @@ export function create_circle(radius: number): ShapeGameObject { * ``` * @category GameObject */ -export function create_triangle(width: number, height: number): ShapeGameObject { +export function create_triangle(width: number, height: number): ShapeGameObject { const triangle = { x1: 0, y1: 0, @@ -171,12 +173,14 @@ export function create_text(text: string): TextGameObject { * @category GameObject */ export function create_sprite(image_url: string): SpriteGameObject { - if (image_url === '') { - throw new Error('image_url cannot be empty'); - } if (typeof image_url !== 'string') { - throw new Error('image_url must be a string'); + throw new InvalidParameterTypeError('string', image_url, create_sprite.name); + } + + if (image_url === '') { + throw new GeneralRuntimeError(`${create_sprite.name}: image_url cannot be empty`); } + const sprite: Sprite = { imageUrl: image_url } as Sprite; @@ -187,6 +191,17 @@ export function create_sprite(image_url: string): SpriteGameObject { // Manipulation of GameObjects // ============================================================================= +function throwIfNotGameObject( + obj: unknown, + guard: (v: unknown) => v is T, + func_name: string, + param_name?: string +): asserts obj is T { + if (!guard(obj)) { + throw new InvalidParameterTypeError('GameObject', obj, func_name, param_name); + } +} + /** * Updates the position transform of the GameObject. * @@ -200,14 +215,13 @@ export function create_sprite(image_url: string): SpriteGameObject { * @category GameObject */ export function update_position(gameObject: GameObject, [x, y]: PositionXY): GameObject { - if (gameObject instanceof GameObject) { - gameObject.setTransform({ - ...gameObject.getTransform(), - position: [x, y] - }); - return gameObject; - } - throw new TypeError('Cannot update position of a non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, update_position.name); + + gameObject.setTransform({ + ...gameObject.getTransform(), + position: [x, y] + }); + return gameObject; } /** @@ -223,14 +237,13 @@ export function update_position(gameObject: GameObject, [x, y]: PositionXY): Gam * @category GameObject */ export function update_scale(gameObject: GameObject, [x, y]: ScaleXY): GameObject { - if (gameObject instanceof GameObject) { - gameObject.setTransform({ - ...gameObject.getTransform(), - scale: [x, y] - }); - return gameObject; - } - throw new TypeError('Cannot update scale of a non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, update_scale.name); + + gameObject.setTransform({ + ...gameObject.getTransform(), + scale: [x, y] + }); + return gameObject; } /** @@ -246,14 +259,12 @@ export function update_scale(gameObject: GameObject, [x, y]: ScaleXY): GameObjec * @category GameObject */ export function update_rotation(gameObject: GameObject, radians: number): GameObject { - if (gameObject instanceof GameObject) { - gameObject.setTransform({ - ...gameObject.getTransform(), - rotation: radians - }); - return gameObject; - } - throw new TypeError('Cannot update rotation of a non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, update_rotation.name); + gameObject.setTransform({ + ...gameObject.getTransform(), + rotation: radians + }); + return gameObject; } /** @@ -270,17 +281,14 @@ export function update_rotation(gameObject: GameObject, radians: number): GameOb * @category GameObject */ export function update_color(gameObject: GameObject, color: ColorRGBA): GameObject { - if (color.length !== 4) { - throw new Error('color must be a 4-element array'); - } - if (gameObject instanceof RenderableGameObject) { - gameObject.setRenderState({ - ...gameObject.getRenderState(), - color - }); - return gameObject; - } - throw new TypeError('Cannot update color of a non-GameObject'); + assertTupleOfLength(color, 4, update_color.name, 'color'); + throwIfNotGameObject(gameObject, obj => obj instanceof RenderableGameObject, update_color.name, 'gameObject'); + + gameObject.setRenderState({ + ...gameObject.getRenderState(), + color + }); + return gameObject; } /** @@ -296,17 +304,14 @@ export function update_color(gameObject: GameObject, color: ColorRGBA): GameObje * @category GameObject */ export function update_flip(gameObject: GameObject, flip: FlipXY): GameObject { - if (flip.length !== 2) { - throw new Error('flip must be a 2-element array'); - } - if (gameObject instanceof RenderableGameObject) { - gameObject.setRenderState({ - ...gameObject.getRenderState(), - flip - }); - return gameObject; - } - throw new TypeError('Cannot update flip of a non-GameObject'); + assertTupleOfLength(flip, 2, update_flip.name, 'flip'); + throwIfNotGameObject(gameObject, obj => obj instanceof RenderableGameObject, update_flip.name, 'gameObject'); + + gameObject.setRenderState({ + ...gameObject.getRenderState(), + flip + }); + return gameObject; } /** @@ -323,13 +328,13 @@ export function update_flip(gameObject: GameObject, flip: FlipXY): GameObject { * @category GameObject */ export function update_text(textGameObject: TextGameObject, text: string): GameObject { - if (textGameObject instanceof TextGameObject) { - textGameObject.setText({ - text - } as DisplayText); - return textGameObject; + if (typeof text !== 'string') { + throw new InvalidParameterTypeError('string', text, update_text.name, 'text'); } - throw new TypeError('Cannot update text onto a non-TextGameObject'); + + throwIfNotGameObject(textGameObject, obj => obj instanceof TextGameObject, update_text.name, 'textGameObject'); + textGameObject.setText({ text }); + return textGameObject; } /** @@ -343,11 +348,9 @@ export function update_text(textGameObject: TextGameObject, text: string): GameO * @category GameObject */ export function update_to_top(gameObject: GameObject): GameObject { - if (gameObject instanceof RenderableGameObject) { - gameObject.setBringToTopFlag(); - return gameObject; - } - throw new TypeError('Cannot update to top a non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof RenderableGameObject, update_to_top.name); + gameObject.setBringToTopFlag(); + return gameObject; } // ============================================================================= @@ -370,10 +373,8 @@ export function update_to_top(gameObject: GameObject): GameObject { * @category GameObject */ export function query_id(gameObject: GameObject): number { - if (gameObject instanceof GameObject) { - return gameObject.id; - } - throw new TypeError('Cannot query id of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, query_id.name); + return gameObject.id; } /** @@ -389,10 +390,8 @@ export function query_id(gameObject: GameObject): number { * @category GameObject */ export function query_position(gameObject: GameObject): PositionXY { - if (gameObject instanceof GameObject) { - return [...gameObject.getTransform().position]; - } - throw new TypeError('Cannot query position of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, query_position.name); + return [...gameObject.getTransform().position]; } /** @@ -408,10 +407,9 @@ export function query_position(gameObject: GameObject): PositionXY { * @category GameObject */ export function query_rotation(gameObject: GameObject): number { - if (gameObject instanceof GameObject) { - return gameObject.getTransform().rotation; - } - throw new TypeError('Cannot query rotation of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, query_rotation.name); + + return gameObject.getTransform().rotation; } /** @@ -427,10 +425,8 @@ export function query_rotation(gameObject: GameObject): number { * @category GameObject */ export function query_scale(gameObject: GameObject): ScaleXY { - if (gameObject instanceof GameObject) { - return [...gameObject.getTransform().scale]; - } - throw new TypeError('Cannot query scale of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, query_scale.name); + return [...gameObject.getTransform().scale]; } /** @@ -446,10 +442,8 @@ export function query_scale(gameObject: GameObject): ScaleXY { * @category GameObject */ export function query_color(gameObject: RenderableGameObject): ColorRGBA { - if (gameObject instanceof RenderableGameObject) { - return [...gameObject.getColor()]; - } - throw new TypeError('Cannot query color of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof RenderableGameObject, query_color.name); + return [...gameObject.getColor()]; } /** @@ -465,10 +459,8 @@ export function query_color(gameObject: RenderableGameObject): ColorRGBA { * @category GameObject */ export function query_flip(gameObject: RenderableGameObject): FlipXY { - if (gameObject instanceof RenderableGameObject) { - return [...gameObject.getFlipState()]; - } - throw new TypeError('Cannot query flip of non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof RenderableGameObject, query_flip.name); + return [...gameObject.getFlipState()]; } /** @@ -485,10 +477,8 @@ export function query_flip(gameObject: RenderableGameObject): FlipXY { * @category GameObject */ export function query_text(textGameObject: TextGameObject): string { - if (textGameObject instanceof TextGameObject) { - return textGameObject.getText().text; - } - throw new TypeError('Cannot query text of non-TextGameObject'); + throwIfNotGameObject(textGameObject, obj => obj instanceof TextGameObject, query_text.name); + return textGameObject.getText().text; } /** @@ -510,26 +500,6 @@ export function query_pointer_position(): PositionXY { // Game configuration // ============================================================================= -/** - * Private function to set the allowed range for a value. - * - * @param num the numeric value - * @param min the minimum value allowed for that number - * @param max the maximum value allowed for that number - * @returns a number within the interval - * @hidden - */ -const withinRange: (num: number, min: number, max: number) => number - = (num: number, min: number, max: number) => { - if (num > max) { - return max; - } - if (num < min) { - return min; - } - return num; - }; - /** * Sets the frames per second of the canvas, which should be between the MIN_FPS and MAX_FPS. * It ranges between 1 and 120, with the default target as 30. @@ -543,7 +513,8 @@ const withinRange: (num: number, min: number, max: number) => number * ``` */ export function set_fps(fps: number) { - config.fps = withinRange(fps, MIN_FPS, MAX_FPS); + assertNumberWithinRange(fps, set_fps.name, MIN_FPS, MAX_FPS); + config.fps = fps; } /** @@ -558,11 +529,13 @@ export function set_fps(fps: number) { * ``` */ export function set_dimensions(dimensions: DimensionsXY) { - if (dimensions.length !== 2) { - throw new Error('dimensions must be a 2-element array'); - } - config.width = withinRange(dimensions[0], MIN_WIDTH, MAX_WIDTH); - config.height = withinRange(dimensions[1], MIN_HEIGHT, MAX_HEIGHT); + assertTupleOfLength(dimensions, 2, set_dimensions.name, 'dimensions'); + + assertNumberWithinRange(dimensions[0], set_dimensions.name, MIN_WIDTH, MAX_WIDTH); + config.width = dimensions[0]; + + assertNumberWithinRange(dimensions[1], set_dimensions.name, MIN_HEIGHT, MAX_HEIGHT); + config.height = dimensions[1]; } /** @@ -579,7 +552,8 @@ export function set_dimensions(dimensions: DimensionsXY) { * ``` */ export function set_scale(scale: number) { - config.scale = withinRange(scale, MIN_SCALE, MAX_SCALE); + assertNumberWithinRange(scale, set_scale.name, MIN_SCALE, MAX_SCALE); + config.scale = scale; } /** @@ -695,10 +669,8 @@ export function input_right_mouse_down() { * @category Logic */ export function pointer_over_gameobject(gameObject: GameObject) { - if (gameObject instanceof GameObject) { - return gameState.pointerProps.pointerOverGameObjectsId.has(gameObject.id); - } - throw new TypeError('Cannot check pointer over non-GameObject'); + throwIfNotGameObject(gameObject, obj => obj instanceof GameObject, pointer_over_gameobject.name); + return gameState.pointerProps.pointerOverGameObjectsId.has(gameObject.id); } /** @@ -720,10 +692,10 @@ export function pointer_over_gameobject(gameObject: GameObject) { * @category Logic */ export function gameobjects_overlap(gameObject1: InteractableGameObject, gameObject2: InteractableGameObject) { - if (gameObject1 instanceof InteractableGameObject && gameObject2 instanceof InteractableGameObject) { - return gameObject1.isOverlapping(gameObject2); - } - throw new TypeError('Cannot check overlap of non-GameObject'); + throwIfNotGameObject(gameObject1, obj => obj instanceof InteractableGameObject, gameobjects_overlap.name, 'gameObject1'); + throwIfNotGameObject(gameObject2, obj => obj instanceof InteractableGameObject, gameobjects_overlap.name, 'gameObject2'); + + return gameObject1.isOverlapping(gameObject2); } /** @@ -862,12 +834,11 @@ export function build_game(): BuildGame { */ export function create_audio(audio_url: string, volume_level: number) { if (typeof audio_url !== 'string') { - throw new Error('audio_url must be a string'); + throw new InvalidParameterTypeError('string', audio_url, create_audio.name); } - if (typeof volume_level !== 'number') { - throw new Error('volume_level must be a number'); - } - return AudioClip.of(audio_url, withinRange(volume_level, MIN_VOLUME, MAX_VOLUME)); + + assertNumberWithinRange(volume_level, create_audio.name, MIN_VOLUME, MAX_VOLUME); + return AudioClip.of(audio_url, volume_level); } /** @@ -887,7 +858,7 @@ export function loop_audio(audio_clip: AudioClip) { audio_clip.setShouldAudioClipLoop(true); return audio_clip; } - throw new TypeError('Cannot loop a non-AudioClip'); + throw new InvalidParameterTypeError('AudioClip', audio_clip, loop_audio.name); } /** @@ -906,7 +877,7 @@ export function play_audio(audio_clip: AudioClip) { audio_clip.setShouldAudioClipPlay(true); return audio_clip; } - throw new TypeError('Cannot play a non-AudioClip'); + throw new InvalidParameterTypeError('AudioClip', audio_clip, play_audio.name); } /** @@ -925,5 +896,5 @@ export function stop_audio(audio_clip: AudioClip) { audio_clip.setShouldAudioClipPlay(false); return audio_clip; } - throw new TypeError('Cannot stop a non-AudioClip'); + throw new InvalidParameterTypeError('AudioClip', audio_clip, stop_audio.name); } diff --git a/src/bundles/arcade_2d/src/gameobject.ts b/src/bundles/arcade_2d/src/gameobject.ts index 5eb0ea8f3b..2b77835fcd 100644 --- a/src/bundles/arcade_2d/src/gameobject.ts +++ b/src/bundles/arcade_2d/src/gameobject.ts @@ -44,6 +44,8 @@ export abstract class GameObject implements Transformable, ReplResult { } public toReplString = () => ''; + + public toString = () => this.toReplString(); } /** @@ -152,24 +154,21 @@ export abstract class InteractableGameObject extends RenderableGameObject implem /** * Encapsulates the data-representation of a ShapeGameObject. */ -export abstract class ShapeGameObject extends InteractableGameObject { +export abstract class ShapeGameObject extends InteractableGameObject { /** * Gets the shape properties of the ShapeGameObject. * @returns The shape properties. */ - public abstract getShape(); + public abstract getShape(): T; - /** @override */ - public toReplString = () => ''; + public override toReplString = () => ''; - /** @override */ - public toString = () => this.toReplString(); } /** * Encapsulates the data-representation of a RectangleGameObject. */ -export class RectangleGameObject extends ShapeGameObject { +export class RectangleGameObject extends ShapeGameObject { constructor( transformProps: types.TransformProps, renderProps: types.RenderProps, @@ -178,8 +177,8 @@ export class RectangleGameObject extends ShapeGameObject { ) { super(transformProps, renderProps, interactableProps); } - /** @override */ - getShape(): types.RectangleProps { + + override getShape(): types.RectangleProps { return this.rectangle; } } @@ -187,7 +186,7 @@ export class RectangleGameObject extends ShapeGameObject { /** * Encapsulates the data-representation of a CircleGameObject. */ -export class CircleGameObject extends ShapeGameObject { +export class CircleGameObject extends ShapeGameObject { constructor( transformProps: types.TransformProps, renderProps: types.RenderProps, @@ -196,8 +195,8 @@ export class CircleGameObject extends ShapeGameObject { ) { super(transformProps, renderProps, interactableProps); } - /** @override */ - getShape(): types.CircleProps { + + override getShape(): types.CircleProps { return this.circle; } } @@ -205,7 +204,7 @@ export class CircleGameObject extends ShapeGameObject { /** * Encapsulates the data-representation of a TriangleGameObject. */ -export class TriangleGameObject extends ShapeGameObject { +export class TriangleGameObject extends ShapeGameObject { constructor( transformProps: types.TransformProps, renderProps: types.RenderProps, @@ -240,11 +239,7 @@ export class SpriteGameObject extends InteractableGameObject { return this.sprite; } - /** @override */ - public toReplString = () => ''; - - /** @override */ - public toString = () => this.toReplString(); + public override toReplString = () => ''; } /** @@ -275,11 +270,7 @@ export class TextGameObject extends InteractableGameObject { return this.displayText; } - /** @override */ - public toReplString = () => ''; - - /** @override */ - public toString = () => this.toReplString(); + public override toReplString = () => ''; } // ============================================================================= @@ -293,7 +284,7 @@ interface Transformable { /** * @param transformProps The transform properties of the GameObject. */ - setTransform(transformProps: types.TransformProps); + setTransform(transformProps: types.TransformProps): void; /** * @returns The render properties of the GameObject. @@ -309,7 +300,7 @@ interface Transformable { /** * Should be called when the GameObject's transform has been updated in the canvas. */ - setTransformUpdated(); + setTransformUpdated(): void; } /** @@ -319,7 +310,7 @@ interface Renderable { /** * @param renderProps The render properties of the GameObject. */ - setRenderState(renderProps: types.RenderProps); + setRenderState(renderProps: types.RenderProps): void; /** * @returns The render properties of the GameObject. @@ -335,7 +326,7 @@ interface Renderable { /** * Should be called when the GameObject's rendered image has been updated in the canvas. */ - setRenderUpdated(); + setRenderUpdated(): void; } /** @@ -345,7 +336,7 @@ interface Interactable { /** * @param interactableProps The hitbox state of the GameObject in detecting overlaps. */ - setHitboxState(interactableProps: types.InteractableProps); + setHitboxState(interactableProps: types.InteractableProps): void; /** * @returns The hitbox state of the GameObject in detecting overlaps. @@ -361,5 +352,5 @@ interface Interactable { /** * Should be called when the GameObject's hitbox has been updated in the canvas. */ - setHitboxUpdated(); + setHitboxUpdated(): void; } diff --git a/src/bundles/arcade_2d/src/phaserScene.ts b/src/bundles/arcade_2d/src/phaserScene.ts index d4dcd9d866..ff62899672 100644 --- a/src/bundles/arcade_2d/src/phaserScene.ts +++ b/src/bundles/arcade_2d/src/phaserScene.ts @@ -111,7 +111,7 @@ export class PhaserScene extends Phaser.Scene { .setScale(config.scale < 1 ? 1 / config.scale : 1); } - update(time, delta) { + override update(time: number, delta: number) { // Set the time and delta gameState.gameTime += delta; gameState.loopCount++; diff --git a/src/bundles/arcade_2d/src/types.ts b/src/bundles/arcade_2d/src/types.ts index 875801d933..81533dea61 100644 --- a/src/bundles/arcade_2d/src/types.ts +++ b/src/bundles/arcade_2d/src/types.ts @@ -100,7 +100,7 @@ export type Sprite = { */ export type BuildGame = { toReplString: () => string; - gameConfig; + gameConfig: Record; }; /** diff --git a/src/bundles/binary_tree/package.json b/src/bundles/binary_tree/package.json index e2a417357c..f24a37db00 100644 --- a/src/bundles/binary_tree/package.json +++ b/src/bundles/binary_tree/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "private": true, "dependencies": { - "js-slang": "^1.0.85" + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -19,8 +20,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/binary_tree/src/__tests__/index.test.ts b/src/bundles/binary_tree/src/__tests__/index.test.ts index 4b7562ca24..53437a4042 100644 --- a/src/bundles/binary_tree/src/__tests__/index.test.ts +++ b/src/bundles/binary_tree/src/__tests__/index.test.ts @@ -9,7 +9,7 @@ describe(funcs.is_tree, () => { }); it('returns false when argument is a list of 4 elements', () => { - const arg = list(0, funcs.make_empty_tree(), funcs.make_empty_tree(), funcs.make_empty_tree()); + const arg = list(0, funcs.make_empty_tree(), funcs.make_empty_tree(), funcs.make_empty_tree()); expect(funcs.is_tree(arg)).toEqual(false); }); @@ -17,7 +17,7 @@ describe(funcs.is_tree, () => { const not_tree = list(0, 1, 2); expect(funcs.is_tree(not_tree)).toEqual(false); - const also_not_tree = list(1, not_tree, null); + const also_not_tree = list(1, not_tree, null); expect(funcs.is_tree(also_not_tree)).toEqual(false); }); @@ -47,13 +47,23 @@ describe(funcs.is_tree, () => { }); }); +describe(funcs.make_tree, () => { + it('throws an error when \'left\' is not a tree', () => { + expect(() => funcs.make_tree(0, 0 as any, null)).toThrow('make_tree: Expected binary tree for left, got 0.'); + }); + + it('throws an error when \'right\' is not a tree', () => { + expect(() => funcs.make_tree(0, null, 0 as any)).toThrow('make_tree: Expected binary tree for right, got 0.'); + }); +}); + describe(funcs.entry, () => { it('throws when argument is not a tree', () => { - expect(() => funcs.entry(0 as any)).toThrowError('entry expects binary tree, received: 0'); + expect(() => funcs.entry(0 as any)).toThrow('entry: Expected binary tree, got 0.'); }); it('throws when argument is an empty tree', () => { - expect(() => funcs.entry(null)).toThrowError('entry received an empty binary tree!'); + expect(() => funcs.entry(null)).toThrow('entry: Expected non-empty binary tree, got null.'); }); it('works', () => { @@ -64,11 +74,11 @@ describe(funcs.entry, () => { describe(funcs.left_branch, () => { it('throws when argument is not a tree', () => { - expect(() => funcs.left_branch(0 as any)).toThrowError('left_branch expects binary tree, received: 0'); + expect(() => funcs.left_branch(0 as any)).toThrow('left_branch: Expected binary tree, got 0.'); }); it('throws when argument is an empty tree', () => { - expect(() => funcs.left_branch(null)).toThrowError('left_branch received an empty binary tree!'); + expect(() => funcs.left_branch(null)).toThrow('left_branch: Expected non-empty binary tree, got null.'); }); it('works (simple)', () => { @@ -87,11 +97,11 @@ describe(funcs.left_branch, () => { describe(funcs.right_branch, () => { it('throws when argument is not a tree', () => { - expect(() => funcs.right_branch(0 as any)).toThrowError('right_branch expects binary tree, received: 0'); + expect(() => funcs.right_branch(0 as any)).toThrow('right_branch: Expected binary tree, got 0.'); }); it('throws when argument is an empty tree', () => { - expect(() => funcs.right_branch(null)).toThrowError('right_branch received an empty binary tree!'); + expect(() => funcs.right_branch(null)).toThrow('right_branch: Expected non-empty binary tree, got null.'); }); it('works (simple)', () => { diff --git a/src/bundles/binary_tree/src/functions.ts b/src/bundles/binary_tree/src/functions.ts index 06f4507ca5..9f479edebe 100644 --- a/src/bundles/binary_tree/src/functions.ts +++ b/src/bundles/binary_tree/src/functions.ts @@ -1,4 +1,5 @@ -import { head, is_list, is_pair, list, tail } from 'js-slang/dist/stdlib/list'; +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { head, is_null, is_pair, tail } from 'js-slang/dist/stdlib/list'; import type { BinaryTree, EmptyBinaryTree, NonEmptyBinaryTree } from './types'; /** @@ -26,7 +27,15 @@ export function make_empty_tree(): BinaryTree { * @returns A binary tree */ export function make_tree(value: any, left: BinaryTree, right: BinaryTree): BinaryTree { - return list(value, left, right); + if (!is_tree(left)) { + throw new InvalidParameterTypeError('binary tree', left, make_tree.name, 'left'); + } + + if (!is_tree(right)) { + throw new InvalidParameterTypeError('binary tree', right, make_tree.name, 'right'); + } + + return [value, [left, [right, null]]]; } /** @@ -39,19 +48,18 @@ export function make_tree(value: any, left: BinaryTree, right: BinaryTree): Bina * ``` * @param value Value to be tested */ -export function is_tree(value: any): value is BinaryTree { - // TODO: value parameter should be of type unknown - if (!is_list(value)) return false; - +export function is_tree(value: unknown): value is BinaryTree { if (is_empty_tree(value)) return true; + if (!is_pair(value)) return false; + const left = tail(value); - if (!is_list(left) || !is_tree(head(left))) return false; + if (!is_pair(left) || !is_tree(head(left))) return false; const right = tail(left); if (!is_pair(right) || !is_tree(head(right))) return false; - return tail(right) === null; + return is_null(tail(right)); } /** @@ -65,17 +73,17 @@ export function is_tree(value: any): value is BinaryTree { * @param value Value to be tested * @returns bool */ -export function is_empty_tree(value: BinaryTree): value is EmptyBinaryTree { +export function is_empty_tree(value: unknown): value is EmptyBinaryTree { return value === null; } function throwIfNotNonEmptyTree(value: unknown, func_name: string): asserts value is NonEmptyBinaryTree { if (!is_tree(value)) { - throw new Error(`${func_name} expects binary tree, received: ${value}`); + throw new InvalidParameterTypeError('binary tree', value, func_name); } if (is_empty_tree(value)) { - throw new Error(`${func_name} received an empty binary tree!`); + throw new InvalidParameterTypeError('non-empty binary tree', value, func_name); } } @@ -91,7 +99,7 @@ function throwIfNotNonEmptyTree(value: unknown, func_name: string): asserts valu */ export function entry(t: BinaryTree): any { throwIfNotNonEmptyTree(t, entry.name); - return t[0]; + return head(t); } /** @@ -106,7 +114,7 @@ export function entry(t: BinaryTree): any { */ export function left_branch(t: BinaryTree): BinaryTree { throwIfNotNonEmptyTree(t, left_branch.name); - return head(tail(t)); + return head(tail(t)!); } /** @@ -121,5 +129,5 @@ export function left_branch(t: BinaryTree): BinaryTree { */ export function right_branch(t: BinaryTree): BinaryTree { throwIfNotNonEmptyTree(t, right_branch.name); - return head(tail(tail(t))); + return head(tail(tail(t)!)!); } diff --git a/src/bundles/communication/package.json b/src/bundles/communication/package.json index 3ccd590683..e7adb2047a 100644 --- a/src/bundles/communication/package.json +++ b/src/bundles/communication/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { + "@sourceacademy/modules-lib": "workspace:^", "mqtt": "^4.3.7", "os": "^0.1.2", "uniqid": "^5.4.0" @@ -22,8 +23,9 @@ "test": "buildtools test --project .", "tsc": "buildtools tsc .", "lint": "buildtools lint .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/communication/src/Communications.ts b/src/bundles/communication/src/Communications.ts index 6a960b2b5e..72caeb1a3f 100644 --- a/src/bundles/communication/src/Communications.ts +++ b/src/bundles/communication/src/Communications.ts @@ -1,3 +1,4 @@ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import context from 'js-slang/context'; import { GlobalStateController } from './GlobalStateController'; import { MultiUserController } from './MultiUserController'; @@ -30,9 +31,11 @@ export function initCommunications( user: string, password: string, ) { - if (getModuleState() instanceof CommunicationModuleState) { + const { state: oldState } = context.moduleContexts.communication; + if (oldState instanceof CommunicationModuleState) { return; } + const newModuleState = new CommunicationModuleState( address, port, @@ -42,8 +45,13 @@ export function initCommunications( context.moduleContexts.communication.state = newModuleState; } -function getModuleState() { - return context.moduleContexts.communication.state; +function getModuleState(func_name: string) { + const { state } = context.moduleContexts.communication; + if (state instanceof CommunicationModuleState) { + return state; + } + + throw new GeneralRuntimeError(`${func_name}: Communication module not initialized.`); } // Loop @@ -79,19 +87,15 @@ export function initGlobalState( topicHeader: string, callback: (state: any) => void, ) { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - if (moduleState.globalState instanceof GlobalStateController) { - return; - } - moduleState.globalState = new GlobalStateController( - topicHeader, - moduleState.multiUser, - callback, - ); + const moduleState = getModuleState(initGlobalState.name); + if (moduleState.globalState !== null) { return; } - throw new Error('Error: Communication module not initialized.'); + moduleState.globalState = new GlobalStateController( + topicHeader, + moduleState.multiUser, + callback, + ); } /** @@ -100,11 +104,8 @@ export function initGlobalState( * @returns Current global state. */ export function getGlobalState() { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - return moduleState.globalState?.globalState; - } - throw new Error('Error: Communication module not initialized.'); + const moduleState = getModuleState(getGlobalState.name); + return moduleState.globalState?.globalState; } /** @@ -115,12 +116,8 @@ export function getGlobalState() { * @param updatedState Replacement value at specified path. */ export function updateGlobalState(path: string, updatedState: any) { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - moduleState.globalState?.updateGlobalState(path, updatedState); - return; - } - throw new Error('Error: Communication module not initialized.'); + const moduleState = getModuleState(updateGlobalState.name); + moduleState.globalState?.updateGlobalState(path, updatedState); } // Rpc @@ -132,16 +129,12 @@ export function updateGlobalState(path: string, updatedState: any) { * @param userId Identifier for this user, set undefined to generate a random ID. */ export function initRpc(topicHeader: string, userId?: string) { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - moduleState.rpc = new RpcController( - topicHeader, - moduleState.multiUser, - userId, - ); - return; - } - throw new Error('Error: Communication module not initialized.'); + const moduleState = getModuleState(initRpc.name); + moduleState.rpc = new RpcController( + topicHeader, + moduleState.multiUser, + userId, + ); } /** @@ -150,15 +143,12 @@ export function initRpc(topicHeader: string, userId?: string) { * @returns String for user ID. */ export function getUserId(): string { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - const userId = moduleState.rpc?.getUserId(); - if (userId) { - return userId; - } - throw new Error('Error: UserID not found.'); + const moduleState = getModuleState(getUserId.name); + const userId = moduleState.rpc?.getUserId(); + if (userId) { + return userId; } - throw new Error('Error: Communication module not initialized.'); + throw new GeneralRuntimeError(`${getUserId.name}: UserID not found.`); } /** @@ -169,12 +159,8 @@ export function getUserId(): string { * @param func Function to call when request received. */ export function expose(name: string, func: (...args: any[]) => any) { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - moduleState.rpc?.expose(name, func); - return; - } - throw new Error('Error: Communication module not initialized.'); + const moduleState = getModuleState(expose.name); + moduleState.rpc?.expose(name, func); } /** @@ -191,10 +177,6 @@ export function callFunction( args: any[], callback: (args: any[]) => void, ) { - const moduleState = getModuleState(); - if (moduleState instanceof CommunicationModuleState) { - moduleState.rpc?.callFunction(receiver, name, args, callback); - return; - } - throw new Error('Error: Communication module not initialized.'); + const moduleState = getModuleState(callFunction.name); + moduleState.rpc?.callFunction(receiver, name, args, callback); } diff --git a/src/bundles/copy_gc/package.json b/src/bundles/copy_gc/package.json index 638cc6131e..4d97d2fc4d 100644 --- a/src/bundles/copy_gc/package.json +++ b/src/bundles/copy_gc/package.json @@ -1,11 +1,15 @@ { "name": "@sourceacademy/bundle-copy_gc", - "version": "1.0.0", + "version": "2.0.0", "private": true, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", "typescript": "^6.0.2" }, + "dependencies": { + "@sourceacademy/modules-lib": "workspace:^", + "es-toolkit": "^1.44.0" + }, "type": "module", "exports": { ".": "./dist/index.js", @@ -16,8 +20,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/copy_gc/src/__tests__/index.test.ts b/src/bundles/copy_gc/src/__tests__/index.test.ts new file mode 100644 index 0000000000..7f45f26e81 --- /dev/null +++ b/src/bundles/copy_gc/src/__tests__/index.test.ts @@ -0,0 +1,134 @@ +import { range } from 'es-toolkit'; +import { beforeEach, describe, expect, it } from 'vitest'; +import * as funcs from '..'; +import { COMMAND } from '../types'; + +beforeEach(() => { + funcs.globalState.ROW = 10; + funcs.globalState.MEMORY_SIZE = -99; + funcs.globalState.TO_SPACE = -1; + funcs.globalState.FROM_SPACE = -1; + funcs.globalState.memory = []; + funcs.globalState.memoryHeaps = []; + funcs.globalState.commandHeap.splice(0, funcs.globalState.commandHeap.length); + funcs.globalState.toMemoryMatrix = []; + funcs.globalState.fromMemoryMatrix = []; + funcs.globalState.tags = []; + funcs.globalState.typeTag = []; + funcs.globalState.flips.splice(0, funcs.globalState.flips.length); + funcs.globalState.TAG_SLOT = 0; + funcs.globalState.SIZE_SLOT = 1; + funcs.globalState.FIRST_CHILD_SLOT = 2; + funcs.globalState.LAST_CHILD_SLOT = 3; + funcs.globalState.ROOTS = []; +}); + +describe(funcs.initialize_memory, () => { + it('works when memory size is a multiple of column size', () => { + expect(funcs.initialize_memory(128)).toBeUndefined(); + + expect(funcs.globalState.MEMORY_SIZE).toEqual(128); + expect(funcs.globalState.ROW).toEqual(4); + expect(funcs.globalState.TO_SPACE).toEqual(0); + expect(funcs.globalState.FROM_SPACE).toEqual(64); + + expect(funcs.globalState.toMemoryMatrix[0]).toEqual(range(0, 32)); + expect(funcs.globalState.toMemoryMatrix[1]).toEqual(range(32, 64)); + + expect(funcs.globalState.fromMemoryMatrix[0]).toEqual(range(64, 96)); + expect(funcs.globalState.fromMemoryMatrix[1]).toEqual(range(96, 128)); + + expect(funcs.globalState.commandHeap).toHaveLength(1); + expect(funcs.globalState.commandHeap[0]).toMatchObject({ + type: COMMAND.INIT, + to: 0, + from: 64, + heap: [], + left: -1, + right: -1, + sizeLeft: 0, + sizeRight: 0, + desc: 'Memory initially empty.', + leftDesc: '', + rightDesc: '', + scan: -1, + free: -1 + }); + }); + + it('works when memory size is a not multiple of column size', () => { + expect(funcs.initialize_memory(91)).toBeUndefined(); + + expect(funcs.globalState.MEMORY_SIZE).toEqual(92); + expect(funcs.globalState.ROW).toEqual(3); + expect(funcs.globalState.TO_SPACE).toEqual(0); + expect(funcs.globalState.FROM_SPACE).toEqual(46); + + expect(funcs.globalState.toMemoryMatrix[0]).toEqual(range(0, 32)); + expect(funcs.globalState.toMemoryMatrix[1]).toEqual(range(32, 46)); + + expect(funcs.globalState.fromMemoryMatrix[0]).toEqual(range(46, 78)); + expect(funcs.globalState.fromMemoryMatrix[1]).toEqual(range(78, 92)); + + expect(funcs.globalState.commandHeap).toHaveLength(1); + expect(funcs.globalState.commandHeap[0]).toMatchObject({ + type: COMMAND.INIT, + to: 0, + from: 46, + heap: [], + left: -1, + right: -1, + sizeLeft: 0, + sizeRight: 0, + desc: 'Memory initially empty.', + leftDesc: '', + rightDesc: '', + scan: -1, + free: -1 + }); + }); +}); + +describe(funcs.resetSpace, () => { + it('resets the to space properly', () => { + funcs.initialize_memory(50); + const testHeap = range(50); + + const newHeap = funcs.resetSpace('to', testHeap); + expect(newHeap).toHaveLength(50); + expect(funcs.globalState.FROM_SPACE).toEqual(25); + + for (let i = 0; i < 25; i++) { + expect(newHeap[i]).toEqual(i); + } + + for (let i = 25; i < 50; i++) { + expect(newHeap[i]).toEqual(0); + } + }); + + it('resets the from space properly', () => { + funcs.initialize_memory(50); + const testHeap = range(50); + + const newHeap = funcs.resetSpace('from', testHeap); + expect(newHeap).toHaveLength(50); + expect(funcs.globalState.FROM_SPACE).toEqual(25); + + for (let i = 0; i < 25; i++) { + expect(newHeap[i]).toEqual(0); + } + + for (let i = 25; i < 50; i++) { + expect(newHeap[i]).toEqual(i); + } + }); + + it('throws if the provided heap isn\'t the correct size', () => { + funcs.initialize_memory(50); + const testHeap = range(10); + + expect(() => funcs.resetSpace('to', testHeap)) + .toThrow('resetSpace: Provided heap was of size 10, required size 50'); + }); +}); diff --git a/src/bundles/copy_gc/src/index.ts b/src/bundles/copy_gc/src/index.ts index 59f8170fc1..30f49fdf90 100644 --- a/src/bundles/copy_gc/src/index.ts +++ b/src/bundles/copy_gc/src/index.ts @@ -2,64 +2,65 @@ * @module copy_gc */ -import { COMMAND, type CommandHeapObject, type Memory, type MemoryHeaps, type Tag } from './types'; - -// Global Variables -let ROW: number = 10; -const COLUMN: number = 32; -let MEMORY_SIZE: number = -99; -let TO_SPACE: number; -let FROM_SPACE: number; -let memory: Memory; -let memoryHeaps: Memory[] = []; -const commandHeap: CommandHeapObject[] = []; -let toMemoryMatrix: number[][]; -let fromMemoryMatrix: number[][]; -let tags: Tag[]; -let typeTag: string[]; -const flips: number[] = []; -let TAG_SLOT: number = 0; -let SIZE_SLOT: number = 1; -let FIRST_CHILD_SLOT: number = 2; -let LAST_CHILD_SLOT: number = 3; -let ROOTS: number[] = []; +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; +import { chunk, clone, last, range } from 'es-toolkit'; +import context from 'js-slang/context'; +import { COMMAND, type CommandHeapObject, type CopyGCGlobalState } from './types'; + +function getInitialState(): CopyGCGlobalState { + // Global Variables + return { + ROW: 10, + COLUMN: 32, + MEMORY_SIZE: -99, + TO_SPACE: -1, + FROM_SPACE: -1, + memory: [], + memoryHeaps: [], + commandHeap: [], + toMemoryMatrix: [], + fromMemoryMatrix: [], + tags: [], + typeTag: [], + flips: [], + TAG_SLOT: 0, + SIZE_SLOT: 1, + FIRST_CHILD_SLOT: 2, + LAST_CHILD_SLOT: 3, + ROOTS: [], + }; +} + +/** + * Exported for testing + * @hidden + */ +export const globalState = getInitialState(); export function initialize_tag(allTag: number[], types: string[]): void { - tags = allTag; - typeTag = types; + globalState.tags = allTag; + globalState.typeTag = types; } export function allHeap(newHeap: number[][]): void { - memoryHeaps = newHeap; + globalState.memoryHeaps = newHeap; } function updateFlip(): void { - flips.push(commandHeap.length - 1); + globalState.flips.push(globalState.commandHeap.length - 1); } export function generateMemory(): void { - toMemoryMatrix = []; - for (let i = 0; i < ROW / 2; i += 1) { - memory = []; - for (let j = 0; j < COLUMN && i * COLUMN + j < MEMORY_SIZE / 2; j += 1) { - memory.push(i * COLUMN + j); - } - toMemoryMatrix.push(memory); - } + const rawToMemory = range(globalState.FROM_SPACE); + const rawFromMemory = range(globalState.FROM_SPACE, globalState.MEMORY_SIZE); - fromMemoryMatrix = []; - for (let i = ROW / 2; i < ROW; i += 1) { - memory = []; - for (let j = 0; j < COLUMN && i * COLUMN + j < MEMORY_SIZE; j += 1) { - memory.push(i * COLUMN + j); - } - fromMemoryMatrix.push(memory); - } + globalState.toMemoryMatrix = chunk(rawToMemory, globalState.COLUMN); + globalState.fromMemoryMatrix = chunk(rawFromMemory, globalState.COLUMN); const obj: CommandHeapObject = { type: COMMAND.INIT, - to: TO_SPACE, - from: FROM_SPACE, + to: globalState.TO_SPACE, + from: globalState.FROM_SPACE, heap: [], left: -1, right: -1, @@ -72,24 +73,28 @@ export function generateMemory(): void { free: -1 }; - commandHeap.push(obj); + globalState.commandHeap.push(obj); } -export function resetFromSpace(fromSpace, heap): number[] { +export function resetSpace(space: 'to' | 'from', heap: number[]): number[] { + if (heap.length !== globalState.MEMORY_SIZE) { + throw new GeneralRuntimeError(`${resetSpace.name}: Provided heap was of size ${heap.length}, required size ${globalState.MEMORY_SIZE}`); + } + const newHeap: number[] = []; - if (fromSpace > 0) { - for (let i = 0; i < MEMORY_SIZE / 2; i += 1) { + if (space === 'to') { + for (let i = 0; i < globalState.FROM_SPACE; i += 1) { newHeap.push(heap[i]); } - for (let i = MEMORY_SIZE / 2; i < MEMORY_SIZE; i += 1) { + for (let i = globalState.FROM_SPACE; i < globalState.MEMORY_SIZE; i += 1) { newHeap.push(0); } } else { // to space between 0...M/2 - for (let i = 0; i < MEMORY_SIZE / 2; i += 1) { + for (let i = 0; i < globalState.FROM_SPACE; i += 1) { newHeap.push(0); } - for (let i = MEMORY_SIZE / 2; i < MEMORY_SIZE; i += 1) { + for (let i = globalState.FROM_SPACE; i < globalState.MEMORY_SIZE; i += 1) { newHeap.push(heap[i]); } } @@ -97,67 +102,58 @@ export function resetFromSpace(fromSpace, heap): number[] { } export function initialize_memory(memorySize: number): void { - MEMORY_SIZE = memorySize; - ROW = MEMORY_SIZE / COLUMN; - TO_SPACE = 0; - FROM_SPACE = MEMORY_SIZE / 2; + // Round up to the nearest even number + if (memorySize % 2 !== 0) memorySize++; + + globalState.MEMORY_SIZE = memorySize; + globalState.ROW = Math.ceil(globalState.MEMORY_SIZE / globalState.COLUMN); + globalState.TO_SPACE = 0; + globalState.FROM_SPACE = globalState.MEMORY_SIZE / 2; + + context.moduleContexts.copy_gc.state = globalState; + generateMemory(); } export function newCommand( - type, - toSpace, - fromSpace, - left, - right, - sizeLeft, - sizeRight, - heap, - description, - firstDesc, - lastDesc + type: string, + toSpace: number, + fromSpace: number, + left: number, + right: number, + sizeLeft: number, + sizeRight: number, + heap: number[], + description: string, + firstDesc: string, + lastDesc: string ): void { - const newType = type; - const newToSpace = toSpace; - const newFromSpace = fromSpace; - const newLeft = left; - const newRight = right; - const newSizeLeft = sizeLeft; - const newSizeRight = sizeRight; - const newDesc = description; - const newFirstDesc = firstDesc; - const newLastDesc = lastDesc; - - memory = []; - for (let j = 0; j < heap.length; j += 1) { - memory.push(heap[j]); - } + globalState.memory = []; + globalState.memory.push(...heap); const obj: CommandHeapObject = { - type: newType, - to: newToSpace, - from: newFromSpace, - heap: memory, - left: newLeft, - right: newRight, - sizeLeft: newSizeLeft, - sizeRight: newSizeRight, - desc: newDesc, - leftDesc: newFirstDesc, - rightDesc: newLastDesc, + type, + to: toSpace, + from: fromSpace, + heap: globalState.memory, + left, + right, + sizeLeft, + sizeRight, + desc: description, + leftDesc: firstDesc, + rightDesc: lastDesc, scan: -1, free: -1 }; - commandHeap.push(obj); + globalState.commandHeap.push(obj); } -export function newCopy(left, right, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; - const newSizeLeft = heap[left + SIZE_SLOT]; - const newSizeRight = heap[right + SIZE_SLOT]; +export function newCopy(left: number, right: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; + const newSizeLeft = heap[left + globalState.SIZE_SLOT]; + const newSizeRight = heap[right + globalState.SIZE_SLOT]; const desc = `Copying node ${left} to ${right}`; newCommand( COMMAND.COPY, @@ -174,11 +170,9 @@ export function newCopy(left, right, heap): void { ); } -export function endFlip(left, heap): void { - const { length } = commandHeap; - const fromSpace = commandHeap[length - 1].from; - const toSpace = commandHeap[length - 1].to; - const newSizeLeft = heap[left + SIZE_SLOT]; +export function endFlip(left: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; + const newSizeLeft = heap[left + globalState.SIZE_SLOT]; const desc = 'Flip finished'; newCommand( COMMAND.FLIP, @@ -196,17 +190,15 @@ export function endFlip(left, heap): void { updateFlip(); } -export function updateRoots(array): void { - for (let i = 0; i < array.length; i += 1) { - ROOTS.push(array[i]); - } +export function updateRoots(array: number[]): void { + globalState.ROOTS.push(...array); } export function resetRoots(): void { - ROOTS = []; + globalState.ROOTS = []; } -export function startFlip(toSpace, fromSpace, heap): void { +export function startFlip(toSpace: number, fromSpace: number, heap: number[]): void { const desc = 'Memory is exhausted. Start stop and copy garbage collector.'; newCommand( 'Start of Cheneys', @@ -224,10 +216,8 @@ export function startFlip(toSpace, fromSpace, heap): void { updateFlip(); } -export function newPush(left, right, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; +export function newPush(left: number, right: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; const desc = `Push OS update memory ${left} and ${right}.`; newCommand( COMMAND.PUSH, @@ -244,10 +234,8 @@ export function newPush(left, right, heap): void { ); } -export function newPop(res, left, right, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; +export function newPop(res: any, left: number, right: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; const newRes = res; const desc = `Pop OS from memory ${left}, with value ${newRes}.`; newCommand( @@ -265,7 +253,7 @@ export function newPop(res, left, right, heap): void { ); } -export function doneShowRoot(heap): void { +export function doneShowRoot(heap: number[]): void { const toSpace = 0; const fromSpace = 0; const desc = 'All root nodes are copied'; @@ -284,11 +272,9 @@ export function doneShowRoot(heap): void { ); } -export function showRoots(left, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; - const newSizeLeft = heap[left + SIZE_SLOT]; +export function showRoots(left: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; + const newSizeLeft = heap[left + globalState.SIZE_SLOT]; const desc = `Roots: node ${left}`; newCommand( 'Showing Roots', @@ -305,10 +291,8 @@ export function showRoots(left, heap): void { ); } -export function newAssign(res, left, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; +export function newAssign(res: any, left: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; const newRes = res; const desc = `Assign memory [${left}] with ${newRes}.`; newCommand( @@ -326,11 +310,9 @@ export function newAssign(res, left, heap): void { ); } -export function newNew(left, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; - const newSizeLeft = heap[left + SIZE_SLOT]; +export function newNew(left: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; + const newSizeLeft = heap[left + globalState.SIZE_SLOT]; const desc = `New node starts in [${left}].`; newCommand( COMMAND.NEW, @@ -347,19 +329,10 @@ export function newNew(left, heap): void { ); } -export function scanFlip(left, right, scan, free, heap): void { - const { length } = commandHeap; - const toSpace = commandHeap[length - 1].to; - const fromSpace = commandHeap[length - 1].from; - memory = []; - for (let j = 0; j < heap.length; j += 1) { - memory.push(heap[j]); - } +export function scanFlip(left: number, right: number, scan: number, free: number, heap: number[]): void { + const { from: fromSpace, to: toSpace } = last(globalState.commandHeap)!; + globalState.memory = clone(heap); - const newLeft = left; - const newRight = right; - const newScan = scan; - const newFree = free; let newDesc = `Scanning node at ${left} for children node ${scan} and ${free}`; if (scan) { if (free) { @@ -375,19 +348,19 @@ export function scanFlip(left, right, scan, free, heap): void { type: COMMAND.SCAN, to: toSpace, from: fromSpace, - heap: memory, - left: newLeft, - right: newRight, + heap: globalState.memory, + left, + right, sizeLeft: 1, sizeRight: 1, - scan: newScan, - free: newFree, + scan, + free, desc: newDesc, leftDesc: 'scan', rightDesc: 'free' }; - commandHeap.push(obj); + globalState.commandHeap.push(obj); } export function updateSlotSegment( @@ -397,91 +370,15 @@ export function updateSlotSegment( last: number ): void { if (tag >= 0) { - TAG_SLOT = tag; + globalState.TAG_SLOT = tag; } if (size >= 0) { - SIZE_SLOT = size; + globalState.SIZE_SLOT = size; } if (first >= 0) { - FIRST_CHILD_SLOT = first; + globalState.FIRST_CHILD_SLOT = first; } if (last >= 0) { - LAST_CHILD_SLOT = last; + globalState.LAST_CHILD_SLOT = last; } } - -function get_memory_size(): number { - return MEMORY_SIZE; -} - -function get_tags(): Tag[] { - return tags; -} - -function get_command(): CommandHeapObject[] { - return commandHeap; -} - -function get_flips(): number[] { - return flips; -} - -function get_types(): string[] { - return typeTag; -} - -function get_from_space(): number { - return FROM_SPACE; -} - -function get_memory_heap(): MemoryHeaps { - return memoryHeaps; -} - -function get_to_memory_matrix(): MemoryHeaps { - return toMemoryMatrix; -} - -function get_from_memory_matrix(): MemoryHeaps { - return fromMemoryMatrix; -} - -function get_roots(): number[] { - return ROOTS; -} - -function get_slots(): number[] { - return [TAG_SLOT, SIZE_SLOT, FIRST_CHILD_SLOT, LAST_CHILD_SLOT]; -} - -function get_to_space(): number { - return TO_SPACE; -} - -function get_column_size(): number { - return COLUMN; -} - -function get_row_size(): number { - return ROW; -} - -export function init() { - return { - toReplString: () => '', - get_memory_size, - get_from_space, - get_to_space, - get_memory_heap, - get_tags, - get_types, - get_column_size, - get_row_size, - get_from_memory_matrix, - get_to_memory_matrix, - get_flips, - get_slots, - get_command, - get_roots - }; -} diff --git a/src/bundles/copy_gc/src/types.ts b/src/bundles/copy_gc/src/types.ts index b03400dbb1..cff0d571fe 100644 --- a/src/bundles/copy_gc/src/types.ts +++ b/src/bundles/copy_gc/src/types.ts @@ -15,7 +15,7 @@ export enum COMMAND { INIT = 'Initialize Memory', } -export type CommandHeapObject = { +export interface CommandHeapObject { type: string; to: number; from: number; @@ -30,3 +30,24 @@ export type CommandHeapObject = { rightDesc: string; free: number; }; + +export interface CopyGCGlobalState { + ROW: number; + readonly COLUMN: number; + MEMORY_SIZE: number; + TO_SPACE: number; + FROM_SPACE: number; + memory: Memory; + memoryHeaps: Memory[]; + readonly commandHeap: CommandHeapObject[]; + toMemoryMatrix: number[][]; + fromMemoryMatrix: number[][]; + tags: Tag[]; + typeTag: string[]; + readonly flips: number[]; + TAG_SLOT: number; + SIZE_SLOT: number; + FIRST_CHILD_SLOT: number; + LAST_CHILD_SLOT: number; + ROOTS: number[]; +} diff --git a/src/bundles/csg/package.json b/src/bundles/csg/package.json index a5dfed749a..e0489ed887 100644 --- a/src/bundles/csg/package.json +++ b/src/bundles/csg/package.json @@ -7,7 +7,7 @@ "@jscad/regl-renderer": "^2.6.1", "@jscad/stl-serializer": "2.1.11", "@sourceacademy/modules-lib": "workspace:^", - "js-slang": "^1.0.85", + "js-slang": "^1.0.92", "save-file": "^2.3.1" }, "exports": { @@ -24,8 +24,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/csg/src/ambient.d.ts b/src/bundles/csg/src/ambient.d.ts new file mode 100644 index 0000000000..e2279a6439 --- /dev/null +++ b/src/bundles/csg/src/ambient.d.ts @@ -0,0 +1,15 @@ +/** + * File for providing the missing type information for @jscad/stl-serializer + * Actual function signature can be found [here](https://github.com/jscad/OpenJSCAD.org/blob/master/packages/io/stl-serializer/index.js) + */ +declare module '@jscad/stl-serializer' { + type SerializeOptions = { + binary?: boolean + statusCallback?: (progress: number) => void + } + + export function serialize( + options: SerializeOptions, + ...objects: any[] + ): string[]; +} diff --git a/src/bundles/csg/src/functions.ts b/src/bundles/csg/src/functions.ts index b50f41fba6..aef7df9a84 100644 --- a/src/bundles/csg/src/functions.ts +++ b/src/bundles/csg/src/functions.ts @@ -1,10 +1,7 @@ /* [Imports] */ import { primitives } from '@jscad/modeling'; import { colorize as colorSolid } from '@jscad/modeling/src/colors'; -import { - measureBoundingBox, - type BoundingBox -} from '@jscad/modeling/src/measurements'; +import { measureBoundingBox } from '@jscad/modeling/src/measurements'; import { intersect as _intersect, subtract as _subtract, @@ -12,17 +9,11 @@ import { } from '@jscad/modeling/src/operations/booleans'; import { extrudeLinear } from '@jscad/modeling/src/operations/extrusions'; import { serialize } from '@jscad/stl-serializer'; -import { degreesToRadians, hexToColor } from '@sourceacademy/modules-lib/utilities'; -import { - head, - is_list, - list, - tail, - type List -} from 'js-slang/dist/stdlib/list'; +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertNumberWithinRange, degreesToRadians, hexToColor } from '@sourceacademy/modules-lib/utilities'; +import { is_list, list_to_vector, vector_to_list, type List } from 'js-slang/dist/stdlib/list'; import save from 'save-file'; import { Core } from './core'; -import type { Solid } from './jscad/types'; import { Group, Shape, @@ -44,20 +35,6 @@ import { When a user passes in a List, we convert it to arrays here so that the rest of the underlying code is free to operate with arrays. */ -export function listToArray(l: List): Operable[] { - const operables: Operable[] = []; - while (l !== null) { - const operable: Operable = head(l); - operables.push(operable); - l = tail(l); - } - return operables; -} - -export function arrayToList(array: Operable[]): List { - return list(...array); -} - /* [Exports] */ // [Variables - Colors] @@ -196,8 +173,8 @@ export function empty_shape(): Shape { * @category Primitives */ export function cube(hex: string): Shape { - const solid: Solid = primitives.cube({ size: 1 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const solid = primitives.cube({ size: 1 }); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -212,8 +189,8 @@ export function cube(hex: string): Shape { * @category Primitives */ export function rounded_cube(hex: string): Shape { - const solid: Solid = primitives.roundedCuboid({ size: [1, 1, 1] }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const solid = primitives.roundedCuboid({ size: [1, 1, 1] }); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -229,11 +206,11 @@ export function rounded_cube(hex: string): Shape { * @category Primitives */ export function cylinder(hex: string): Shape { - const solid: Solid = primitives.cylinder({ + const solid = primitives.cylinder({ height: 1, radius: 0.5 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -249,11 +226,11 @@ export function cylinder(hex: string): Shape { * @category Primitives */ export function rounded_cylinder(hex: string): Shape { - const solid: Solid = primitives.roundedCylinder({ + const solid = primitives.roundedCylinder({ height: 1, radius: 0.5 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -268,8 +245,8 @@ export function rounded_cylinder(hex: string): Shape { * @category Primitives */ export function sphere(hex: string): Shape { - const solid: Solid = primitives.sphere({ radius: 0.5 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const solid = primitives.sphere({ radius: 0.5 }); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -284,8 +261,8 @@ export function sphere(hex: string): Shape { * @category Primitives */ export function geodesic_sphere(hex: string): Shape { - const solid: Solid = primitives.geodesicSphere({ radius: 0.5 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const solid = primitives.geodesicSphere({ radius: 0.5 }); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -301,9 +278,9 @@ export function geodesic_sphere(hex: string): Shape { * @category Primitives */ export function pyramid(hex: string): Shape { - const pythagorasSide: number = Math.sqrt(2); // sqrt(1^2 + 1^2) + const pythagorasSide = Math.sqrt(2); // sqrt(1^2 + 1^2) const radius = pythagorasSide / 2; - const solid: Solid = primitives.cylinderElliptic({ + const solid = primitives.cylinderElliptic({ height: 1, // Base starting radius startRadius: [radius, radius], @@ -311,7 +288,7 @@ export function pyramid(hex: string): Shape { endRadius: [0, 0], segments: 4 }); - let shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + let shape = new Shape(colorSolid(hexToColor(hex), solid)); shape = rotate(shape, 0, 0, degreesToRadians(45)) as Shape; return centerPrimitive(shape); } @@ -328,12 +305,12 @@ export function pyramid(hex: string): Shape { * @category Primitives */ export function cone(hex: string): Shape { - const solid: Solid = primitives.cylinderElliptic({ + const solid = primitives.cylinderElliptic({ height: 1, startRadius: [0.5, 0.5], endRadius: [0, 0] }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -349,8 +326,8 @@ export function cone(hex: string): Shape { * @category Primitives */ export function prism(hex: string): Shape { - const solid: Solid = extrudeLinear({ height: 1 }, primitives.triangle()); - let shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const solid = extrudeLinear({ height: 1 }, primitives.triangle()); + let shape = new Shape(colorSolid(hexToColor(hex), solid)); shape = rotate(shape, 0, 0, degreesToRadians(-90)) as Shape; return centerPrimitive(shape); } @@ -366,11 +343,11 @@ export function prism(hex: string): Shape { * @category Primitives */ export function star(hex: string): Shape { - const solid: Solid = extrudeLinear( + const solid = extrudeLinear( { height: 1 }, primitives.star({ outerRadius: 0.5 }) ); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -386,11 +363,11 @@ export function star(hex: string): Shape { * @category Primitives */ export function torus(hex: string): Shape { - const solid: Solid = primitives.torus({ + const solid = primitives.torus({ innerRadius: 0.15, outerRadius: 0.35 }); - const shape: Shape = new Shape(colorSolid(hexToColor(hex), solid)); + const shape = new Shape(colorSolid(hexToColor(hex), solid)); return centerPrimitive(shape); } @@ -406,11 +383,15 @@ export function torus(hex: string): Shape { * @category Operations */ export function union(first: Shape, second: Shape): Shape { - if (!is_shape(first) || !is_shape(second)) { - throw new Error('Failed to union, only Shapes can be operated on'); + if (!is_shape(first)) { + throw new InvalidParameterTypeError('Shape', first, union.name, 'first'); } - const solid: Solid = _union(first.solid, second.solid); + if (!is_shape(second)) { + throw new InvalidParameterTypeError('Shape', second, union.name, 'second'); + } + + const solid = _union(first.solid, second.solid); return new Shape(solid); } @@ -425,11 +406,15 @@ export function union(first: Shape, second: Shape): Shape { * @category Operations */ export function subtract(target: Shape, subtractedShape: Shape): Shape { - if (!is_shape(target) || !is_shape(subtractedShape)) { - throw new Error('Failed to subtract, only Shapes can be operated on'); + if (!is_shape(target)) { + throw new InvalidParameterTypeError('Shape', target, subtract.name, 'target'); + } + + if (!is_shape(subtractedShape)) { + throw new InvalidParameterTypeError('Shape', subtractedShape, subtract.name, 'subtractedShape'); } - const solid: Solid = _subtract(target.solid, subtractedShape.solid); + const solid = _subtract(target.solid, subtractedShape.solid); return new Shape(solid); } @@ -443,11 +428,15 @@ export function subtract(target: Shape, subtractedShape: Shape): Shape { * @category Operations */ export function intersect(first: Shape, second: Shape): Shape { - if (!is_shape(first) || !is_shape(second)) { - throw new Error('Failed to intersect, only Shapes can be operated on'); + if (!is_shape(first)) { + throw new InvalidParameterTypeError('Shape', first, intersect.name, 'first'); } - const solid: Solid = _intersect(first.solid, second.solid); + if (!is_shape(second)) { + throw new InvalidParameterTypeError('Shape', second, intersect.name, 'second'); + } + + const solid = _intersect(first.solid, second.solid); return new Shape(solid); } @@ -521,10 +510,9 @@ export function scale( yFactor: number, zFactor: number ): Operable { - if (xFactor <= 0 || yFactor <= 0 || zFactor <= 0) { - // JSCAD library does not allow factors <= 0 - throw new Error('Scaling factor must be greater than 0'); - } + assertNumberWithinRange(xFactor, scale.name, 0, undefined, false, 'xFactor'); + assertNumberWithinRange(yFactor, scale.name, 0, undefined, false, 'yFactor'); + assertNumberWithinRange(zFactor, scale.name, 0, undefined, false, 'zFactor'); return operable.scale([xFactor, yFactor, zFactor]); } @@ -544,12 +532,12 @@ export function scale( * * @category Utilities */ -export function group(operables: List): Group { +export function group(operables: List): Group { if (!is_list(operables)) { - throw new Error('Only lists of Operables can be grouped'); + throw new InvalidParameterTypeError('list of Operables', operables, group.name); } - return new Group(listToArray(operables)); + return new Group(list_to_vector(operables)); } /** @@ -563,10 +551,10 @@ export function group(operables: List): Group { */ export function ungroup(g: Group): List { if (!is_group(g)) { - throw new Error('Only Groups can be ungrouped'); + throw new InvalidParameterTypeError('Group', g, ungroup.name); } - return arrayToList(g.ungroup()); + return vector_to_list(g.ungroup()); } /** @@ -577,7 +565,7 @@ export function ungroup(g: Group): List { * * @category Utilities */ -export function is_shape(parameter: unknown): boolean { +export function is_shape(parameter: unknown) { return parameter instanceof Shape; } @@ -589,7 +577,7 @@ export function is_shape(parameter: unknown): boolean { * * @category Utilities */ -export function is_group(parameter: unknown): boolean { +export function is_group(parameter: unknown) { return parameter instanceof Group; } @@ -618,22 +606,39 @@ export function is_group(parameter: unknown): boolean { * @category Utilities */ export function bounding_box(shape: Shape): (axis: string, minMax: string) => number { - const bounds: BoundingBox = measureBoundingBox(shape.solid); + const bounds = measureBoundingBox(shape.solid); return (axis: string, minMax: string): number => { let j: number; - if (axis === 'x') j = 0; - else if (axis === 'y') j = 1; - else if (axis === 'z') j = 2; - else { - throw new Error(`Bounding box getter function expected "x", "y", or "z" as first parameter, but got ${axis}`); + switch (axis) { + case 'x': { + j = 0; + break; + } + case 'y': { + j = 1; + break; + } + case 'z': { + j = 2; + break; + } + default: + throw new InvalidParameterTypeError('"x", "y" or "z"', axis, 'BoundingBox'); } let i: number; - if (minMax === 'min') i = 0; - else if (minMax === 'max') i = 1; - else { - throw new Error(`Bounding box getter function expected "min" or "max" as second parameter, but got ${minMax}`); + switch (minMax) { + case 'min': { + i = 0; + break; + } + case 'max': { + i = 1; + break; + } + default: + throw new InvalidParameterTypeError('"min" or "max"', minMax, 'BoundingBox'); } return bounds[i][j]; @@ -655,16 +660,9 @@ export function rgb( greenValue: number, blueValue: number ): string { - if ( - redValue < 0 - || redValue > 255 - || greenValue < 0 - || greenValue > 255 - || blueValue < 0 - || blueValue > 255 - ) { - throw new Error('RGB values must be between 0 and 255 (inclusive)'); - } + assertNumberWithinRange(redValue, rgb.name, 0, 255, true, 'redValue'); + assertNumberWithinRange(blueValue, rgb.name, 0, 255, true, 'blueValue'); + assertNumberWithinRange(greenValue, rgb.name, 0, 255, true, 'greenValue'); return `#${redValue.toString(16)}${greenValue.toString(16)}${blueValue.toString(16)}`; } @@ -680,7 +678,7 @@ export function rgb( */ export async function download_shape_stl(shape: Shape): Promise { if (!is_shape(shape)) { - throw new Error('Failed to export, only Shapes can be converted to STL'); + throw new InvalidParameterTypeError('Shape', shape, download_shape_stl.name); } await save( @@ -700,7 +698,7 @@ export async function download_shape_stl(shape: Shape): Promise { */ export function render(operable: Operable): RenderGroup { if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); + throw new InvalidParameterTypeError('Operable', operable, render.name); } operable.store(); @@ -720,7 +718,7 @@ export function render(operable: Operable): RenderGroup { */ export function render_grid(operable: Operable): RenderGroup { if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); + throw new InvalidParameterTypeError('Operable', operable, render.name); } operable.store(); @@ -738,7 +736,7 @@ export function render_grid(operable: Operable): RenderGroup { */ export function render_axes(operable: Operable): RenderGroup { if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); + throw new InvalidParameterTypeError('Operable', operable, render.name); } operable.store(); @@ -756,7 +754,7 @@ export function render_axes(operable: Operable): RenderGroup { */ export function render_grid_axes(operable: Operable): RenderGroup { if (!(operable instanceof Shape || operable instanceof Group)) { - throw new Error('Only Operables can be rendered'); + throw new InvalidParameterTypeError('Operable', operable, render.name); } operable.store(); diff --git a/src/bundles/curve/package.json b/src/bundles/curve/package.json index d54daf391e..5aa893fdef 100644 --- a/src/bundles/curve/package.json +++ b/src/bundles/curve/package.json @@ -6,7 +6,7 @@ "@sourceacademy/modules-lib": "workspace:^", "es-toolkit": "^1.44.0", "gl-matrix": "^3.3.0", - "js-slang": "^1.0.85" + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -25,8 +25,9 @@ "prepare": "yarn tsc", "test": "buildtools test --project .", "tsc": "buildtools tsc .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/curve/src/__tests__/curve.test.ts b/src/bundles/curve/src/__tests__/curve.test.ts index 9754277f0f..73bd0857d3 100644 --- a/src/bundles/curve/src/__tests__/curve.test.ts +++ b/src/bundles/curve/src/__tests__/curve.test.ts @@ -1,3 +1,4 @@ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; import { stringify } from 'js-slang/dist/utils/stringify'; import { describe, expect, it, test } from 'vitest'; import type { Color, Curve } from '../curves_webgl'; @@ -27,16 +28,12 @@ describe('Ensure that invalid curves and animations error gracefully', () => { }); test('Curve that takes multiple parameters should throw error', () => { - expect(() => drawers.draw_connected(200)(((t, u) => funcs.make_point(t, u)) as any)) - .toThrow( - 'The provided curve is not a valid Curve function. ' + - 'A Curve function must take exactly one parameter (a number t between 0 and 1) ' + - 'and return a Point or 3D Point depending on whether it is a 2D or 3D curve.' - ); + expect(() => drawers.draw_connected(200)(((t: number, u: number) => funcs.make_point(t, u)) as any)) + .toThrowErrorMatchingInlineSnapshot(`[Error: RenderFunction: Expected Curve, got (t, u) => __vite_ssr_import_4__.make_point(t, u).]`); }); test('CurveAnimation that doesn\'t return a curve should throw error', () => { - const anim = new AnimatedCurve(1, 30, (_t => 0) as any, drawers.draw_connected(200), false); + const anim = new AnimatedCurve(1, 30, ((_t: number) => 0) as any, drawers.draw_connected(200), false); expect(() => anim.getFrame(0)).toThrow('CurveAnimation did not return a Curve at timestamp 0'); }); @@ -52,10 +49,13 @@ describe('Ensure that invalid curves and animations error gracefully', () => { }); describe('Render function creators', () => { - const names = Object.getOwnPropertyNames(drawers.RenderFunctionCreators); - const renderFuncCreators = names.reduce<[string, RenderFunctionCreator][]>((res, name) => { - if (typeof drawers.RenderFunctionCreators[name] !== 'function') return res; - return [...res, [name, drawers.RenderFunctionCreators[name]]]; + type FunctionNames = keyof (typeof drawers.RenderFunctionCreators); + + const names = Object.getOwnPropertyNames(drawers.RenderFunctionCreators) as FunctionNames[]; + const renderFuncCreators = names.reduce<[FunctionNames, RenderFunctionCreator][]>((res, name) => { + const value = drawers.RenderFunctionCreators[name]; + if (typeof value !== 'function') return res; + return [...res, [name, value] as [FunctionNames, RenderFunctionCreator]]; }, []); describe.each(renderFuncCreators)('%s', (name, func) => { @@ -77,28 +77,33 @@ describe('Render function creators', () => { } else if (func.name.includes('connected')) { expect(func.drawMode).toEqual('lines'); } else { - throw new Error(`Unknown draw mode for render function creator: ${func.name}`); + expect.fail(`Unknown draw mode for render function creator: ${func.name}`); } }); it('throws when numPoints is less than 0', () => { - expect(() => func(0)).toThrowError( - `${name}: The number of points must be a positive integer less than or equal to 65535. Got: 0` + expect(() => func(-1)).toThrow( + `${name}: Expected integer between 0 and 65535, got -1.` ); }); it('throws when numPoints is greater than 65535', () => { - expect(() => func(70000)).toThrowError( - `${name}: The number of points must be a positive integer less than or equal to 65535. Got: 70000` + expect(() => func(70000)).toThrow( + `${name}: Expected integer between 0 and 65535, got 70000.` ); }); it('throws when numPoints is not an integer', () => { - expect(() => func(3.14)).toThrowError( - `${name}: The number of points must be a positive integer less than or equal to 65535. Got: 3.14` + expect(() => func(3.14)).toThrow( + `${name}: Expected integer between 0 and 65535, got 3.14.` ); }); + test('returned render function throws when called with an invalid curve', () => { + const creator = func(200); + expect(() => creator(0 as any)).toThrow('RenderFunction: Expected Curve, got 0.'); + }); + test('returned render functions have nice string representations', () => { const renderFunc = func(250); if (renderFunc.is3D) { @@ -125,7 +130,8 @@ describe('Coloured Points', () => { }); it('throws when argument is not a point', () => { - expect(() => funcs.r_of(0 as any)).toThrowError('r_of expects a point as argument'); + expect(() => funcs.r_of(0 as any)).toThrow(InvalidParameterTypeError); + // expect(() => funcs.r_of(0 as any)).toThrowError('r_of: Expected Point, got 0'); }); }); @@ -136,7 +142,8 @@ describe('Coloured Points', () => { }); it('throws when argument is not a point', () => { - expect(() => funcs.g_of(0 as any)).toThrowError('g_of expects a point as argument'); + expect(() => funcs.g_of(0 as any)).toThrow(InvalidParameterTypeError); + // expect(() => funcs.g_of(0 as any)).toThrowError('g_of: Expected Point, got 0'); }); }); @@ -147,7 +154,8 @@ describe('Coloured Points', () => { }); it('throws when argument is not a point', () => { - expect(() => funcs.b_of(0 as any)).toThrowError('b_of expects a point as argument'); + expect(() => funcs.b_of(0 as any)).toThrow(InvalidParameterTypeError); + // expect(() => funcs.b_of(0 as any)).toThrowError('b_of: Expected Point, got 0'); }); }); }); @@ -161,6 +169,11 @@ describe(funcs.unit_line_at, () => { expect(y).toEqual(0.5); } }); + + it('throws an error when argument is not a number', () => { + expect(() => funcs.unit_line_at('a' as any)) + .toThrowError('unit_line_at: Expected number, got "a".'); + }); }); describe(funcs.translate, () => { @@ -198,6 +211,11 @@ describe(funcs.translate, () => { expect(g).toBeCloseTo(0.5); } }); + + test('toReplString representation', () => { + const transformer = funcs.translate(1, 1, 1); + expect(stringify(transformer)).toEqual(''); + }); }); describe(funcs.scale, () => { @@ -227,6 +245,11 @@ describe(funcs.scale, () => { expect(g).toBeCloseTo(0.5); } }); + + test('toReplString representation', () => { + const transformer = funcs.scale(1, 1, 1); + expect(stringify(transformer)).toEqual(''); + }); }); describe(funcs.put_in_standard_position, () => { @@ -243,4 +266,13 @@ describe(funcs.put_in_standard_position, () => { expect(xn).toBeCloseTo(1, 1); expect(yn).toBeCloseTo(0, 1); }); + + test('toReplString representation', () => { + const transformer = funcs.put_in_standard_position; + expect(stringify(transformer)).toEqual(''); + }); + + test('name', () => { + expect(funcs.put_in_standard_position.name).toEqual('put_in_standard_position'); + }); }); diff --git a/src/bundles/curve/src/curves_webgl.ts b/src/bundles/curve/src/curves_webgl.ts index 6b8f2466f4..4252c79f7c 100644 --- a/src/bundles/curve/src/curves_webgl.ts +++ b/src/bundles/curve/src/curves_webgl.ts @@ -1,12 +1,9 @@ +import { GeneralRuntimeError, InternalRuntimeError } from '@sourceacademy/modules-lib/errors'; import type { ReplResult } from '@sourceacademy/modules-lib/types'; import { mat4, vec3 } from 'gl-matrix'; import { stringify } from 'js-slang/dist/utils/stringify'; - import type { CurveSpace, DrawMode, ScaleMode } from './types'; -/** @hidden */ -export const drawnCurves: CurveDrawn[] = []; - // Vertex shader program const vsS: string = ` attribute vec4 aFragColor; @@ -53,7 +50,7 @@ function loadShader( ): WebGLShader { const shader = gl.createShader(type); if (!shader) { - throw new Error('WebGLShader not available.'); + throw new InternalRuntimeError('WebGLShader not available.'); } gl.shaderSource(shader, source); gl.compileShader(shader); @@ -77,7 +74,7 @@ function initShaderProgram( const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); const shaderProgram = gl.createProgram(); if (!shaderProgram) { - throw new Error('Unable to initialize the shader program.'); + throw new InternalRuntimeError('Unable to initialize the shader program.'); } gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); @@ -121,7 +118,7 @@ export class Point implements ReplResult { public readonly y: number, public readonly z: number, public readonly color: Color - ) {} + ) { } public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; } @@ -159,7 +156,7 @@ export class CurveDrawn implements ReplResult { public init = (canvas: HTMLCanvasElement) => { this.renderingContext = canvas.getContext('webgl'); if (!this.renderingContext) { - throw new Error('Rendering context cannot be null.'); + throw new InternalRuntimeError('Rendering context cannot be null.'); } const cubeBuffer = this.renderingContext.createBuffer(); this.renderingContext.bindBuffer( @@ -338,7 +335,7 @@ export function generateCurve( const point = func(i / numPoints); if (!(point instanceof Point)) { - throw new Error(`Expected curve to return a point, got '${stringify(point)}' at t=${i / numPoints}`); + throw new GeneralRuntimeError(`Expected curve to return a point, got '${stringify(point)}' at t=${i / numPoints}`); } const x = point.x * 2 - 1; diff --git a/src/bundles/curve/src/drawers.ts b/src/bundles/curve/src/drawers.ts index 1aa95b4593..73f8255403 100644 --- a/src/bundles/curve/src/drawers.ts +++ b/src/bundles/curve/src/drawers.ts @@ -1,4 +1,5 @@ -import { isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; import context from 'js-slang/context'; import { generateCurve, type Curve, type CurveDrawn } from './curves_webgl'; @@ -18,7 +19,7 @@ context.moduleContexts.curve.state = { drawnCurves }; -function createDrawFunction( +function getRenderFunctionCreator( scaleMode: ScaleMode, drawMode: DrawMode, space: CurveSpace, @@ -26,21 +27,10 @@ function createDrawFunction( name: string ): RenderFunctionCreator { function renderFuncCreator(numPoints: number) { - if (numPoints <= 0 || numPoints > 65535 || !Number.isInteger(numPoints)) { - throw new Error( - `${name}: The number of points must be a positive integer less than or equal to 65535. ` + - `Got: ${numPoints}` - ); - } + assertNumberWithinRange(numPoints, name, 0, 65535); function renderFunc(curve: Curve) { - if (!isFunctionOfLength(curve, 1)) { - throw new Error( - 'The provided curve is not a valid Curve function. ' + - 'A Curve function must take exactly one parameter (a number t between 0 and 1) ' + - 'and return a Point or 3D Point depending on whether it is a 2D or 3D curve.' - ); - } + assertFunctionOfLength(curve, 1, 'RenderFunction', 'Curve'); const curveDrawn = generateCurve( scaleMode, @@ -59,12 +49,7 @@ function createDrawFunction( } renderFunc.is3D = space === '3D'; - - const stringifier = () => `<${space === '3D' ? '3D' : ''}RenderFunction(${numPoints})>`; - - // Retain both properties for compatibility - renderFunc.toString = stringifier; - renderFunc.toReplString = stringifier; + renderFunc.toReplString = () => `<${space === '3D' ? '3D' : ''}RenderFunction(${numPoints})>`; return renderFunc; } @@ -89,10 +74,10 @@ function createDrawFunction( /** @hidden */ export class RenderFunctionCreators { @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_connected = createDrawFunction('none', 'lines', '2D', false, 'draw_connected'); + static draw_connected = getRenderFunctionCreator('none', 'lines', '2D', false, 'draw_connected'); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_connected_full_view = createDrawFunction( + static draw_connected_full_view = getRenderFunctionCreator( 'stretch', 'lines', '2D', @@ -101,7 +86,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_connected_full_view_proportional = createDrawFunction( + static draw_connected_full_view_proportional = getRenderFunctionCreator( 'fit', 'lines', '2D', @@ -110,10 +95,10 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_points = createDrawFunction('none', 'points', '2D', false, 'draw_points'); + static draw_points = getRenderFunctionCreator('none', 'points', '2D', false, 'draw_points'); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_points_full_view = createDrawFunction( + static draw_points_full_view = getRenderFunctionCreator( 'stretch', 'points', '2D', @@ -122,7 +107,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_points_full_view_proportional = createDrawFunction( + static draw_points_full_view_proportional = getRenderFunctionCreator( 'fit', 'points', '2D', @@ -131,7 +116,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_connected = createDrawFunction( + static draw_3D_connected = getRenderFunctionCreator( 'none', 'lines', '3D', @@ -140,7 +125,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_connected_full_view = createDrawFunction( + static draw_3D_connected_full_view = getRenderFunctionCreator( 'stretch', 'lines', '3D', @@ -149,7 +134,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_connected_full_view_proportional = createDrawFunction( + static draw_3D_connected_full_view_proportional = getRenderFunctionCreator( 'fit', 'lines', '3D', @@ -158,10 +143,10 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_points = createDrawFunction('none', 'points', '3D', false, 'draw_3D_points'); + static draw_3D_points = getRenderFunctionCreator('none', 'points', '3D', false, 'draw_3D_points'); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_points_full_view = createDrawFunction( + static draw_3D_points_full_view = getRenderFunctionCreator( 'stretch', 'points', '3D', @@ -170,7 +155,7 @@ export class RenderFunctionCreators { ); @functionDeclaration('numPoints: number', '(func: Curve) => Curve') - static draw_3D_points_full_view_proportional = createDrawFunction( + static draw_3D_points_full_view_proportional = getRenderFunctionCreator( 'fit', 'points', '3D', @@ -393,9 +378,11 @@ class CurveAnimators { func: CurveAnimation ): AnimatedCurve { if (drawer.is3D) { - throw new Error(`${animate_curve.name} cannot be used with 3D draw function!`); + throw new GeneralRuntimeError(`${animate_curve.name} cannot be used with 3D draw function!`); } + assertFunctionOfLength(func, 1, CurveAnimators.animate_curve.name, 'CurveAnimation'); + const anim = new AnimatedCurve(duration, fps, func, drawer, false); drawnCurves.push(anim); return anim; @@ -409,9 +396,11 @@ class CurveAnimators { func: CurveAnimation ): AnimatedCurve { if (!drawer.is3D) { - throw new Error(`${animate_3D_curve.name} cannot be used with 2D draw function!`); + throw new GeneralRuntimeError(`${animate_3D_curve.name} cannot be used with 2D draw function!`); } + assertFunctionOfLength(func, 1, CurveAnimators.animate_3D_curve.name, 'CurveAnimation'); + const anim = new AnimatedCurve(duration, fps, func, drawer, true); drawnCurves.push(anim); return anim; @@ -425,6 +414,7 @@ class CurveAnimators { * @param drawer Draw function to the generated curves with * @param func Curve generating function. Takes in a timestamp value and returns a curve * @returns Curve Animation + * @function */ export const animate_curve = CurveAnimators.animate_curve; @@ -435,5 +425,6 @@ export const animate_curve = CurveAnimators.animate_curve; * @param drawer Draw function to the generated curves with * @param func Curve generating function. Takes in a timestamp value and returns a curve * @returns 3D Curve Animation + * @function */ export const animate_3D_curve = CurveAnimators.animate_3D_curve; diff --git a/src/bundles/curve/src/functions.ts b/src/bundles/curve/src/functions.ts index ff13a3a224..bc1b0c8e51 100644 --- a/src/bundles/curve/src/functions.ts +++ b/src/bundles/curve/src/functions.ts @@ -1,3 +1,5 @@ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; import { clamp } from 'es-toolkit'; import { Point, type Curve } from './curves_webgl'; import { functionDeclaration } from './type_interface'; @@ -5,18 +7,44 @@ import type { CurveTransformer } from './types'; function throwIfNotPoint(obj: unknown, func_name: string): asserts obj is Point { if (!(obj instanceof Point)) { - throw new Error(`${func_name} expects a point as argument`); + throw new InvalidParameterTypeError('Point', obj, func_name); } } +function throwIfNotCurve(obj: unknown, func_name: string, param_name?: string): asserts obj is Curve { + assertFunctionOfLength(obj, 1, func_name, 'Curve', param_name); +} + +function defineCurveTransformer(f: (arg: Curve) => Curve, name?: string): CurveTransformer { + const transformer: CurveTransformer = curve => { + throwIfNotCurve(curve, 'CurveTransformer'); + return f(curve); + }; + + transformer.toReplString = () => ''; + + if (name !== undefined) { + Object.defineProperty(transformer, 'name', { value: name }); + } + + return transformer; +} + class CurveFunctions { @functionDeclaration('x: number, y: number', 'Point') static make_point(x: number, y: number): Point { + assertNumberWithinRange(x, { func_name: CurveFunctions.make_point.name, param_name: 'x', integer: false }); + assertNumberWithinRange(y, { func_name: CurveFunctions.make_point.name, param_name: 'y', integer: false }); + return new Point(x, y, 0, [0, 0, 0, 1]); } @functionDeclaration('x: number, y: number, z: number', 'Point') static make_3D_point(x: number, y: number, z: number): Point { + assertNumberWithinRange(x, { func_name: CurveFunctions.make_3D_point.name, param_name: 'x', integer: false }); + assertNumberWithinRange(y, { func_name: CurveFunctions.make_3D_point.name, param_name: 'y', integer: false }); + assertNumberWithinRange(z, { func_name: CurveFunctions.make_3D_point.name, param_name: 'z', integer: false }); + return new Point(x, y, z, [0, 0, 0, 1]); } @@ -53,6 +81,9 @@ class CurveFunctions { @functionDeclaration('curve1: Curve, curve2: Curve', 'Curve') static connect_ends(curve1: Curve, curve2: Curve): Curve { + throwIfNotCurve(curve1, CurveFunctions.connect_ends.name, 'curve1'); + throwIfNotCurve(curve2, CurveFunctions.connect_ends.name, 'curve2'); + const startPointOfCurve2 = curve2(0); const endPointOfCurve1 = curve1(1); return connect_rigidly( @@ -67,12 +98,19 @@ class CurveFunctions { @functionDeclaration('curve1: Curve, curve2: Curve', 'Curve') static connect_rigidly(curve1: Curve, curve2: Curve): Curve { - return (t) => (t < 1 / 2 ? curve1(2 * t) : curve2(2 * t - 1)); + throwIfNotCurve(curve1, CurveFunctions.connect_rigidly.name, 'curve1'); + throwIfNotCurve(curve2, CurveFunctions.connect_rigidly.name, 'curve2'); + + return t => (t < 0.5 ? curve1(2 * t) : curve2(2 * t - 1)); } @functionDeclaration('x0: number, y0: number, z0: number', '(c: Curve) => Curve') static translate(x0: number, y0: number, z0: number): CurveTransformer { - return curve => t => { + assertNumberWithinRange(x0, { func_name: CurveFunctions.translate.name, param_name: 'x0', integer: false }); + assertNumberWithinRange(y0, { func_name: CurveFunctions.translate.name, param_name: 'y0', integer: false }); + assertNumberWithinRange(z0, { func_name: CurveFunctions.translate.name, param_name: 'z0', integer: false }); + + return defineCurveTransformer(curve => t => { const ct = curve(t); return new Point( x0 + ct.x, @@ -80,14 +118,14 @@ class CurveFunctions { z0 + ct.z, [ct.color[0], ct.color[1], ct.color[2], 1] ); - }; + }); } @functionDeclaration('curve: Curve', 'Curve') - static invert: CurveTransformer = original => t => original(1 - t); + static invert: CurveTransformer = defineCurveTransformer(original => t => original(1 - t), 'invert'); @functionDeclaration('curve: Curve', 'Curve') - static put_in_standard_position: CurveTransformer = curve => { + static put_in_standard_position: CurveTransformer = defineCurveTransformer(curve => { const start_point = curve(0); const curve_started_at_origin = translate( -x_of(start_point), @@ -103,18 +141,23 @@ class CurveFunctions { )(curve_started_at_origin); const end_point_on_x_axis = x_of(curve_ended_at_x_axis(1)); return scale_proportional(1 / end_point_on_x_axis)(curve_ended_at_x_axis); - }; + }, 'put_in_standard_position'); @functionDeclaration('a: number, b: number, c: number', '(c: Curve) => Curve') static rotate_around_origin_3D(a: number, b: number, c: number): CurveTransformer { + assertNumberWithinRange(a, { func_name: CurveFunctions.rotate_around_origin_3D.name, integer: false, param_name: 'a' }); const cthx = Math.cos(a); const sthx = Math.sin(a); + + assertNumberWithinRange(b, { func_name: CurveFunctions.rotate_around_origin_3D.name, integer: false, param_name: 'b' }); const cthy = Math.cos(b); const sthy = Math.sin(b); + + assertNumberWithinRange(c, { func_name: CurveFunctions.rotate_around_origin_3D.name, integer: false, param_name: 'c' }); const cthz = Math.cos(c); const sthz = Math.sin(c); - return curve => t => { + return defineCurveTransformer(curve => t => { const ct = curve(t); const coord = [ct.x, ct.y, ct.z]; const mat = [ @@ -139,15 +182,18 @@ class CurveFunctions { zf += mat[2][i] * coord[i]; } return new Point(xf, yf, zf, [ct.color[0], ct.color[1], ct.color[2], 1]); - }; + }); } @functionDeclaration('a: number', '(c: Curve) => Curve') static rotate_around_origin(a: number): CurveTransformer { + assertNumberWithinRange(a, { func_name: CurveFunctions.rotate_around_origin.name, integer: false }); + // 1 args const cth = Math.cos(a); const sth = Math.sin(a); - return curve => t => { + + return defineCurveTransformer(curve => t => { const ct = curve(t); return new Point( cth * ct.x - sth * ct.y, @@ -155,12 +201,16 @@ class CurveFunctions { ct.z, [ct.color[0], ct.color[1], ct.color[2], 1] ); - }; + }); } @functionDeclaration('x: number, y: number, z: number', '(c: Curve) => Curve') static scale(x: number, y: number, z: number): CurveTransformer { - return curve => t => { + assertNumberWithinRange(x, { func_name: CurveFunctions.scale.name, param_name: 'x', integer: false }); + assertNumberWithinRange(y, { func_name: CurveFunctions.scale.name, param_name: 'y', integer: false }); + assertNumberWithinRange(z, { func_name: CurveFunctions.scale.name, param_name: 'z', integer: false }); + + return defineCurveTransformer(curve => t => { const ct = curve(t); return new Point( @@ -169,7 +219,7 @@ class CurveFunctions { z * ct.z, [ct.color[0], ct.color[1], ct.color[2], 1] ); - }; + }); } @functionDeclaration('s: number', '(c: Curve) => Curve') @@ -214,22 +264,19 @@ class CurveFunctions { } @functionDeclaration('t: number', 'Point') - static unit_circle: Curve = t => { - return make_point(Math.cos(2 * Math.PI * t), Math.sin(2 * Math.PI * t)); - }; + static unit_circle: Curve = t => make_point(Math.cos(2 * Math.PI * t), Math.sin(2 * Math.PI * t)); @functionDeclaration('t: number', 'Point') static unit_line: Curve = t => make_point(t, 0); @functionDeclaration('t: number', 'Curve') static unit_line_at(y: number): Curve { + assertNumberWithinRange(y, { func_name: CurveFunctions.unit_line_at.name, integer: false }); return t => make_point(t, y); } @functionDeclaration('t: number', 'Point') - static arc: Curve = t => { - return make_point(Math.sin(Math.PI * t), Math.cos(Math.PI * t)); - }; + static arc: Curve = t => make_point(Math.sin(Math.PI * t), Math.cos(Math.PI * t)); } /** @@ -238,6 +285,7 @@ class CurveFunctions { * @param x x-coordinate of new point * @param y y-coordinate of new point * @returns with x and y as coordinates + * @function * @example * ``` * const point = make_point(0.5, 0.5); @@ -251,6 +299,7 @@ export const make_point = CurveFunctions.make_point; * @param x x-coordinate of new point * @param y y-coordinate of new point * @param z z-coordinate of new point + * @function * @returns with x, y and z as coordinates * @example * ``` @@ -269,6 +318,7 @@ export const make_3D_point = CurveFunctions.make_3D_point; * @param r red component of new point * @param g green component of new point * @param b blue component of new point + * @function * @returns with x and y as coordinates, and r, g and b as RGB values * @example * ``` @@ -288,6 +338,7 @@ export const make_color_point = CurveFunctions.make_color_point; * @param r red component of new point * @param g green component of new point * @param b blue component of new point + * @function * @returns with x, y and z as coordinates, and r, g and b as RGB values * @example * ``` @@ -301,6 +352,7 @@ export const make_3D_color_point = CurveFunctions.make_3D_color_point; * * @param pt given point * @returns x-coordinate of the Point + * @function * @example * ``` * const point = make_color_point(1, 2, 3, 50, 100, 150); @@ -327,6 +379,7 @@ export const y_of = CurveFunctions.y_of; * * @param pt given point * @returns z-coordinate of the Point + * @function * @example * ``` * const point = make_color_point(1, 2, 3, 50, 100, 150); @@ -353,6 +406,7 @@ export const r_of = CurveFunctions.r_of; * * @param pt given point * @returns Green component of the Point as a value between [0,255] + * @function * @example * ``` * const point = make_color_point(1, 2, 3, 50, 100, 150); @@ -366,6 +420,7 @@ export const g_of = CurveFunctions.g_of; * * @param pt given point * @returns Blue component of the Point as a value between [0,255] + * @function * @example * ``` * const point = make_color_point(1, 2, 3, 50, 100, 150); @@ -409,6 +464,7 @@ export const translate = CurveFunctions.translate; * @param b given angle * @param c given angle * @returns function that takes a Curve and returns a Curve + * @function */ export const rotate_around_origin_3D = CurveFunctions.rotate_around_origin_3D; @@ -420,6 +476,7 @@ export const rotate_around_origin_3D = CurveFunctions.rotate_around_origin_3D; * * @param a given angle * @returns function that takes a Curve and returns a Curve + * @function */ export const rotate_around_origin = CurveFunctions.rotate_around_origin; @@ -433,6 +490,7 @@ export const rotate_around_origin = CurveFunctions.rotate_around_origin; * @param y scaling factor in y-direction * @param z scaling factor in z-direction * @returns function that takes a Curve and returns a Curve + * @function */ export const scale = CurveFunctions.scale; @@ -442,6 +500,7 @@ export const scale = CurveFunctions.scale; * * @param s scaling factor * @returns function that takes a Curve and returns a Curve + * @function */ export const scale_proportional = CurveFunctions.scale_proportional; @@ -469,6 +528,7 @@ export const put_in_standard_position = CurveFunctions.put_in_standard_position; * @param curve1 first Curve * @param curve2 second Curve * @returns result Curve + * @function */ export const connect_rigidly = CurveFunctions.connect_rigidly; @@ -483,6 +543,7 @@ export const connect_rigidly = CurveFunctions.connect_rigidly; * @param curve1 first Curve * @param curve2 second Curve * @returns result Curve + * @function */ export const connect_ends = CurveFunctions.connect_ends; @@ -512,6 +573,7 @@ export const unit_line = CurveFunctions.unit_line; * * @param y fraction between 0 and 1 * @returns horizontal Curve + * @function */ export const unit_line_at = CurveFunctions.unit_line_at; diff --git a/src/bundles/curve/src/index.ts b/src/bundles/curve/src/index.ts index 001782b89f..a9816e9da6 100644 --- a/src/bundles/curve/src/index.ts +++ b/src/bundles/curve/src/index.ts @@ -1,10 +1,10 @@ /** - * drawing *curves*, i.e. collections of *points*, on a canvas in a tools tab + * Module for drawing *curves*, i.e. collections of *points*, on a canvas in a tools tab * * A *point* is defined by its coordinates (x, y and z), and the color assigned to * it (r, g, and b). A few constructors for points is given, for example - * `make_color_point`. Selectors allow access to the coordinates and color - * components, for example `x_of`. + * {@link make_color_point}. Selectors allow access to the coordinates and color + * components, for example {@link x_of}. * * A *curve* is a * unary function which takes a number argument within the unit interval `[0,1]` @@ -12,13 +12,13 @@ * is always `C(0)`, and the ending point is always `C(1)`. * * A *curve transformation* is a function that takes a curve as argument and - * returns a curve. Examples of curve transformations are `scale` and `translate`. + * returns a curve. Examples of curve transformations are {@link scale|scale} and {@link translate|translate}. * - * A *curve drawer* is function that takes a number argument and returns + * A *render function* is function that takes a number argument and returns * a function that takes a curve as argument and visualises it in the output screen is * shown in the Source Academy in the tab with the "Curves Canvas" icon (image). * The following [example](https://share.sourceacademy.org/unitcircle) uses - * the curve drawer `draw_connected_full_view` to display a curve called + * the render function {@link draw_connected_full_view|draw_connected_full_view} to display a curve called * `unit_circle`. * ``` * import { make_point, draw_connected_full_view } from "curve"; @@ -34,6 +34,13 @@ * @author Lee Zheng Han * @author Ng Yong Xiang */ + +import { draw_connected_full_view } from './drawers'; +import { scale, translate, x_of } from './functions'; + +// import and re-export to make links in the module summary work +export { draw_connected_full_view, scale, translate, x_of }; + export { arc, b_of, @@ -48,13 +55,10 @@ export { put_in_standard_position, r_of, rotate_around_origin, - scale, scale_proportional, - translate, unit_circle, unit_line, unit_line_at, - x_of, y_of, z_of } from './functions'; @@ -69,7 +73,6 @@ export { draw_3D_points_full_view, draw_3D_points_full_view_proportional, draw_connected, - draw_connected_full_view, draw_connected_full_view_proportional, draw_points, draw_points_full_view, diff --git a/src/bundles/curve/src/types.ts b/src/bundles/curve/src/types.ts index b2508d96a9..349fa14af7 100644 --- a/src/bundles/curve/src/types.ts +++ b/src/bundles/curve/src/types.ts @@ -1,3 +1,4 @@ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import { glAnimation, type AnimFrame, type ReplResult } from '@sourceacademy/modules-lib/types'; import { isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import type { Curve, CurveDrawn } from './curves_webgl'; @@ -7,7 +8,9 @@ export type CurveModuleState = { }; /** A function that takes in CurveFunction and returns a tranformed CurveFunction. */ -export type CurveTransformer = (c: Curve) => Curve; +export interface CurveTransformer extends ReplResult { + (c: Curve): Curve; +} export type DrawMode = 'lines' | 'points'; export type ScaleMode = 'fit' | 'none' | 'stretch'; @@ -56,7 +59,7 @@ export class AnimatedCurve extends glAnimation implements ReplResult { const curve = this.func(timestamp); if (!isFunctionOfLength(curve, 1)) { - throw new Error(`CurveAnimation did not return a Curve at timestamp ${timestamp}`); + throw new GeneralRuntimeError(`CurveAnimation did not return a Curve at timestamp ${timestamp}`); } curve.shouldNotAppend = true; diff --git a/src/bundles/game/package.json b/src/bundles/game/package.json index d6580dfff1..8c917d2c39 100644 --- a/src/bundles/game/package.json +++ b/src/bundles/game/package.json @@ -1,9 +1,9 @@ { "name": "@sourceacademy/bundle-game", - "version": "1.0.0", + "version": "1.0.1", "private": true, "dependencies": { - "js-slang": "^1.0.85", + "js-slang": "^1.0.92", "phaser": "^3.54.0" }, "devDependencies": { @@ -20,8 +20,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/game/src/functions.ts b/src/bundles/game/src/functions.ts index dd265ab19d..2b017e18c2 100644 --- a/src/bundles/game/src/functions.ts +++ b/src/bundles/game/src/functions.ts @@ -15,7 +15,8 @@ */ import context from 'js-slang/context'; -import { accumulate, head, is_pair, tail, type List } from 'js-slang/dist/stdlib/list'; +import { GeneralRuntimeError } from 'js-slang/dist/errors/base'; +import { for_each, head, is_pair, tail, type List, type Pair } from 'js-slang/dist/stdlib/list'; import Phaser from 'phaser'; import { defaultGameParams, @@ -62,7 +63,7 @@ const ObjTypes = Object.values(ObjectTypes); const nullFn = () => { }; -const mandatory = (obj, errMsg: string) => { +const mandatory = (obj: any, errMsg: string) => { if (!obj) { throw_error(errMsg); } @@ -149,7 +150,7 @@ function set_type( * @hidden */ function throw_error(message: string): never { - throw new Error(`${arguments.callee.caller.name}: ${message}`); + throw new GeneralRuntimeError(`${arguments.callee.caller.name}: ${message}`); } // ============================================================================= @@ -177,15 +178,16 @@ export function prepend_remote_url(asset_key: string): string { * @param lst the list to be turned into object config. * @returns object config */ -export function create_config(lst: List): ObjectConfig { - const config = {}; - accumulate((xs: [any, any], _) => { +export function create_config(lst: List>): ObjectConfig { + const config: ObjectConfig = {}; + + for_each(xs => { if (!is_pair(xs)) { throw_error('config element is not a pair!'); } config[head(xs)] = tail(xs); - return null; - }, null, lst); + }, lst); + return config; } diff --git a/src/bundles/mark_sweep/package.json b/src/bundles/mark_sweep/package.json index 3858f5fc32..dd83849926 100644 --- a/src/bundles/mark_sweep/package.json +++ b/src/bundles/mark_sweep/package.json @@ -1,6 +1,6 @@ { "name": "@sourceacademy/bundle-mark_sweep", - "version": "1.0.0", + "version": "2.0.0", "private": true, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -16,8 +16,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", @@ -25,5 +26,8 @@ "serve": "Starts the modules server", "test": "Runs unit tests defined for the bundle", "tsc": "Runs the Typescript compiler and produces the library version of the bundle" + }, + "dependencies": { + "es-toolkit": "^1.44.0" } } diff --git a/src/bundles/mark_sweep/src/__tests__/index.test.ts b/src/bundles/mark_sweep/src/__tests__/index.test.ts new file mode 100644 index 0000000000..62a2c099fc --- /dev/null +++ b/src/bundles/mark_sweep/src/__tests__/index.test.ts @@ -0,0 +1,116 @@ +import { range } from 'es-toolkit/math'; +import { beforeEach, describe, expect, it } from 'vitest'; +import * as funcs from '..'; +import { COMMAND } from '../types'; + +beforeEach(() => { + funcs.globalState.rowCount = 10; + funcs.globalState.NODE_SIZE = 0; + funcs.globalState.MEMORY_SIZE = -99; + funcs.globalState.memory = []; + funcs.globalState.memoryHeaps = []; + funcs.globalState.commandHeap.splice(0, funcs.globalState.commandHeap.length); + funcs.globalState.memoryMatrix = []; + funcs.globalState.tags = []; + funcs.globalState.typeTag = []; + funcs.globalState.flips.splice(0, funcs.globalState.flips.length); + funcs.globalState.TAG_SLOT = 0; + funcs.globalState.SIZE_SLOT = 1; + funcs.globalState.FIRST_CHILD_SLOT = 2; + funcs.globalState.LAST_CHILD_SLOT = 3; + funcs.globalState.MARKED = 1; + funcs.globalState.UNMARKED = 0; + funcs.globalState.ROOTS = []; +}); + +describe(funcs.addRoots, () => { + it('works', () => { + expect(funcs.addRoots([1, 2, 3])).toBeUndefined(); + expect(funcs.globalState.ROOTS).toEqual([1, 2, 3]); + }); +}); + +describe(funcs.initialize_memory, () => { + it('works memory size is exact multiple of columns', () => { + funcs.initialize_memory(128, 4, 1, 1); + + expect(funcs.globalState.MEMORY_SIZE).toEqual(128); + expect(funcs.globalState.NODE_SIZE).toEqual(4); + expect(funcs.globalState.rowCount).toEqual(4); + expect(funcs.globalState.MARKED).toEqual(1); + expect(funcs.globalState.UNMARKED).toEqual(1); + + expect(funcs.globalState.memoryMatrix[0]).toEqual(range(32)); + expect(funcs.globalState.memoryMatrix[1]).toEqual(range(32, 64)); + expect(funcs.globalState.memoryMatrix[2]).toEqual(range(64, 96)); + expect(funcs.globalState.memoryMatrix[3]).toEqual(range(96, 128)); + }); + + it('works when memory size is not exact multiple of columns', () => { + funcs.initialize_memory(120, 3, 1, 1); + + expect(funcs.globalState.MEMORY_SIZE).toEqual(120); + expect(funcs.globalState.NODE_SIZE).toEqual(3); + expect(funcs.globalState.rowCount).toEqual(4); + expect(funcs.globalState.MARKED).toEqual(1); + expect(funcs.globalState.UNMARKED).toEqual(1); + + expect(funcs.globalState.memoryMatrix[0]).toEqual(range(32)); + expect(funcs.globalState.memoryMatrix[1]).toEqual(range(32, 64)); + expect(funcs.globalState.memoryMatrix[2]).toEqual(range(64, 96)); + expect(funcs.globalState.memoryMatrix[3]).toEqual(range(96, 120)); + + expect(funcs.globalState.commandHeap).toHaveLength(1); + expect(funcs.globalState.commandHeap[0]).toMatchObject({ + type: COMMAND.INIT, + heap: [], + left: -1, + right: -1, + sizeLeft: 0, + sizeRight: 0, + desc: 'Memory initially empty.', + leftDesc: '', + rightDesc: '', + queue: [] + }); + }); +}); + +describe(funcs.newGC, () => { + it('works', () => { + funcs.globalState.flips.push(1); + expect(funcs.newGC([1, 2, 3])).toBeUndefined(); + expect(funcs.globalState.flips).toEqual([1, 0]); + expect(funcs.globalState.commandHeap).toHaveLength(1); + expect(funcs.globalState.commandHeap[0]).toMatchObject({ + type: COMMAND.START, + left: -1, + right: -1, + sizeLeft: 0, + sizeRight: 0, + heap: [1, 2, 3], + desc: 'Memory exhausted, start Mark and Sweep Algorithm', + leftDesc: '', + rightDesc: '', + queue: [] + }); + }); +}); + +describe(funcs.showRoots, () => { + it('works', () => { + funcs.addRoots([1, 2, 3]); + expect(funcs.showRoots([1, 2, 3])).toBeUndefined(); + + expect(funcs.globalState.commandHeap).toHaveLength(3); + expect(funcs.globalState.ROOTS).toEqual([]); + }); +}); + +describe(funcs.updateRoots, () => { + it('works', () => { + funcs.globalState.ROOTS = [1, 2, 3]; + expect(funcs.updateRoots([4, 5, 6])).toBeUndefined(); + expect(funcs.globalState.ROOTS).toEqual([1, 2, 3, 4, 5, 6]); + }); +}); diff --git a/src/bundles/mark_sweep/src/index.ts b/src/bundles/mark_sweep/src/index.ts index 53884b281c..793db9efde 100644 --- a/src/bundles/mark_sweep/src/index.ts +++ b/src/bundles/mark_sweep/src/index.ts @@ -2,137 +2,128 @@ * @module mark_sweep */ -import { COMMAND, type CommandHeapObject, type Memory, type MemoryHeaps, type Tag } from './types'; +import { chunk, clone, range } from 'es-toolkit'; +import context from 'js-slang/context'; +import { COMMAND, type CommandHeapObject, type MarkSweepGlobalState, type Memory } from './types'; -// Global Variables -let ROW: number = 10; -const COLUMN: number = 32; -let NODE_SIZE: number = 0; -let MEMORY_SIZE: number = -99; -let memory: Memory; -let memoryHeaps: Memory[] = []; -const commandHeap: CommandHeapObject[] = []; -let memoryMatrix: number[][]; -let tags: Tag[]; -let typeTag: string[]; -const flips: number[] = []; -let TAG_SLOT: number = 0; -let SIZE_SLOT: number = 1; -let FIRST_CHILD_SLOT: number = 2; -let LAST_CHILD_SLOT: number = 3; -let MARKED: number = 1; -let UNMARKED: number = 0; -let ROOTS: number[] = []; +function getInitialState(): MarkSweepGlobalState { + return { + rowCount: 10, + columnCount: 32, + NODE_SIZE: 0, + MEMORY_SIZE: -99, + memory: [], + memoryHeaps: [], + commandHeap: [], + memoryMatrix: [], + tags: [], + typeTag: [], + flips: [], + TAG_SLOT: 0, + SIZE_SLOT: 1, + FIRST_CHILD_SLOT: 2, + LAST_CHILD_SLOT: 3, + MARKED: 1, + UNMARKED: 0, + ROOTS: [], + }; +} -function generateMemory(): void { - memoryMatrix = []; - for (let i = 0; i < ROW; i += 1) { - memory = []; - for (let j = 0; j < COLUMN && i * COLUMN + j < MEMORY_SIZE; j += 1) { - memory.push(i * COLUMN + j); - } - memoryMatrix.push(memory); - } +/** + * Exported for testing + * @hidden + */ +export const globalState = getInitialState(); - const obj: CommandHeapObject = { - type: COMMAND.INIT, - heap: [], - left: -1, - right: -1, - sizeLeft: 0, - sizeRight: 0, - desc: 'Memory initially empty.', - leftDesc: '', - rightDesc: '', - queue: [] - }; +export function generateMemory(): void { + globalState.memoryMatrix = chunk( + range(globalState.MEMORY_SIZE), + globalState.columnCount + ); - commandHeap.push(obj); + newCommand( + COMMAND.INIT, + -1, + -1, + 0, + 0, + [], + 'Memory initially empty.', + '', + '', + [] + ); } -function updateRoots(array): void { - for (let i = 0; i < array.length; i += 1) { - ROOTS.push(array[i]); - } +export function updateRoots(array: number[]): void { + globalState.ROOTS.push(...array); } -function initialize_memory( +export function initialize_memory( memorySize: number, - nodeSize, - marked, - unmarked + nodeSize: number, + marked: number, + unmarked: number ): void { - MEMORY_SIZE = memorySize; - NODE_SIZE = nodeSize; - const excess = MEMORY_SIZE % NODE_SIZE; - MEMORY_SIZE -= excess; - ROW = MEMORY_SIZE / COLUMN; - MARKED = marked; - UNMARKED = unmarked; + context.moduleContexts.mark_sweep.state = globalState; + + globalState.MEMORY_SIZE = memorySize; + globalState.NODE_SIZE = nodeSize; + const excess = globalState.MEMORY_SIZE % globalState.NODE_SIZE; + globalState.MEMORY_SIZE -= excess; + globalState.rowCount = Math.ceil(globalState.MEMORY_SIZE / globalState.columnCount); + globalState.MARKED = marked; + globalState.UNMARKED = unmarked; generateMemory(); } -function initialize_tag(allTag: number[], types: string[]): void { - tags = allTag; - typeTag = types; +export function initialize_tag(allTag: number[], types: string[]): void { + globalState.tags = allTag; + globalState.typeTag = types; } -function allHeap(newHeap: number[][]): void { - memoryHeaps = newHeap; +export function allHeap(newHeap: number[][]): void { + globalState.memoryHeaps = newHeap; } function updateFlip(): void { - flips.push(commandHeap.length - 1); -} - -function newCommand( - type, - left, - right, - sizeLeft, - sizeRight, - heap, - description, - firstDesc, - lastDesc, - queue = [] + globalState.flips.push(globalState.commandHeap.length - 1); +} + +export function newCommand( + type: COMMAND, + left: number, + right: number, + sizeLeft: number, + sizeRight: number, + heap: Memory, + description: string, + firstDesc: string, + lastDesc: string, + queue: number[] = [] ): void { - const newType = type; - const newLeft = left; - const newRight = right; - const newSizeLeft = sizeLeft; - const newSizeRight = sizeRight; - const newDesc = description; - const newFirstDesc = firstDesc; - const newLastDesc = lastDesc; - - memory = []; - for (let j = 0; j < heap.length; j += 1) { - memory.push(heap[j]); - } - const newQueue: number[] = []; - for (let j = 0; j < queue.length; j += 1) { - newQueue.push(queue[j]); - } + globalState.memory = []; + globalState.memory.push(...heap); + const newQueue = clone(queue); const obj: CommandHeapObject = { - type: newType, - heap: memory, - left: newLeft, - right: newRight, - sizeLeft: newSizeLeft, - sizeRight: newSizeRight, - desc: newDesc, - leftDesc: newFirstDesc, - rightDesc: newLastDesc, + type, + heap: globalState.memory, + left, + right, + sizeLeft, + sizeRight, + desc: description, + leftDesc: firstDesc, + rightDesc: lastDesc, queue: newQueue }; - commandHeap.push(obj); + globalState.commandHeap.push(obj); } -function newSweep(left, heap): void { - const newSizeLeft = NODE_SIZE; +export function newSweep(left: number, heap: Memory): void { + const newSizeLeft = globalState.NODE_SIZE; const desc = `Freeing node ${left}`; newCommand( COMMAND.SWEEP, @@ -147,8 +138,8 @@ function newSweep(left, heap): void { ); } -function newMark(left, heap, queue): void { - const newSizeLeft = NODE_SIZE; +export function newMark(left: number, heap: Memory, queue: number[]): void { + const newSizeLeft = globalState.NODE_SIZE; const desc = `Marking node ${left} to be live memory`; newCommand( COMMAND.MARK, @@ -164,32 +155,30 @@ function newMark(left, heap, queue): void { ); } -function addRoots(arr): void { - for (let i = 0; i < arr.length; i += 1) { - ROOTS.push(arr[i]); - } +export function addRoots(arr: number[]): void { + globalState.ROOTS.push(...arr); } -function showRoot(heap): void { +export function showRoot(heap: Memory): void { const desc = 'All root nodes are marked'; newCommand(COMMAND.SHOW_MARKED, -1, -1, 0, 0, heap, desc, '', ''); } -function showRoots(heap): void { - for (let i = 0; i < ROOTS.length; i += 1) { +export function showRoots(heap: Memory): void { + for (let i = 0; i < globalState.ROOTS.length; i += 1) { showRoot(heap); } - ROOTS = []; + globalState.ROOTS = []; } -function newUpdateSweep(right, heap): void { +export function newUpdateSweep(right: number, heap: Memory): void { const desc = `Set node ${right} to freelist`; newCommand( COMMAND.RESET, -1, right, 0, - NODE_SIZE, + globalState.NODE_SIZE, heap, desc, 'free node', @@ -197,7 +186,7 @@ function newUpdateSweep(right, heap): void { ); } -function newPush(left, right, heap): void { +export function newPush(left: number, right: number, heap: Memory): void { const desc = `Push OS update memory ${left} and ${right}.`; newCommand( COMMAND.PUSH, @@ -212,7 +201,7 @@ function newPush(left, right, heap): void { ); } -function newPop(res, left, right, heap): void { +export function newPop(res: any, left: number, right: number, heap: Memory): void { const newRes = res; const desc = `Pop OS from memory ${left}, with value ${newRes}.`; newCommand( @@ -228,14 +217,14 @@ function newPop(res, left, right, heap): void { ); } -function newAssign(res, left, heap): void { +export function newAssign(res: any, left: number, heap: Memory): void { const newRes = res; const desc = `Assign memory [${left}] with ${newRes}.`; newCommand(COMMAND.ASSIGN, left, -1, 1, 1, heap, desc, 'assigned memory', ''); } -function newNew(left, heap): void { - const newSizeLeft = NODE_SIZE; +export function newNew(left: number, heap: Memory): void { + const newSizeLeft = globalState.NODE_SIZE; const desc = `New node starts in [${left}].`; newCommand( COMMAND.NEW, @@ -250,129 +239,34 @@ function newNew(left, heap): void { ); } -function newGC(heap): void { +export function newGC(heap: Memory): void { const desc = 'Memory exhausted, start Mark and Sweep Algorithm'; newCommand(COMMAND.START, -1, -1, 0, 0, heap, desc, '', ''); updateFlip(); } -function endGC(heap): void { +export function endGC(heap: Memory): void { const desc = 'Result of free memory'; newCommand(COMMAND.END, -1, -1, 0, 0, heap, desc, '', ''); updateFlip(); } -function updateSlotSegment( +export function updateSlotSegment( tag: number, size: number, first: number, last: number ): void { if (tag >= 0) { - TAG_SLOT = tag; + globalState.TAG_SLOT = tag; } if (size >= 0) { - SIZE_SLOT = size; + globalState.SIZE_SLOT = size; } if (first >= 0) { - FIRST_CHILD_SLOT = first; + globalState.FIRST_CHILD_SLOT = first; } if (last >= 0) { - LAST_CHILD_SLOT = last; + globalState.LAST_CHILD_SLOT = last; } } - -function get_memory_size(): number { - return MEMORY_SIZE; -} - -function get_tags(): Tag[] { - return tags; -} - -function get_command(): CommandHeapObject[] { - return commandHeap; -} - -function get_flips(): number[] { - return flips; -} - -function get_types(): string[] { - return typeTag; -} - -function get_memory_heap(): MemoryHeaps { - return memoryHeaps; -} - -function get_memory_matrix(): MemoryHeaps { - return memoryMatrix; -} - -function get_roots(): number[] { - return ROOTS; -} - -function get_slots(): number[] { - return [TAG_SLOT, SIZE_SLOT, FIRST_CHILD_SLOT, LAST_CHILD_SLOT]; -} - -function get_column_size(): number { - return COLUMN; -} - -function get_row_size(): number { - return ROW; -} - -function get_unmarked(): number { - return UNMARKED; -} - -function get_marked(): number { - return MARKED; -} - -function init() { - return { - toReplString: () => '', - get_memory_size, - get_memory_heap, - get_tags, - get_types, - get_column_size, - get_row_size, - get_memory_matrix, - get_flips, - get_slots, - get_command, - get_unmarked, - get_marked, - get_roots - }; -} - -export { - init, - // initialisation - initialize_memory, - initialize_tag, - generateMemory, - allHeap, - updateSlotSegment, - newCommand, - newMark, - newPush, - newPop, - newAssign, - newNew, - newGC, - newSweep, - updateRoots, - newUpdateSweep, - showRoots, - endGC, - addRoots, - showRoot -}; diff --git a/src/bundles/mark_sweep/src/types.ts b/src/bundles/mark_sweep/src/types.ts index 21aa49671f..80150cec0d 100644 --- a/src/bundles/mark_sweep/src/types.ts +++ b/src/bundles/mark_sweep/src/types.ts @@ -18,8 +18,8 @@ export enum COMMAND { INIT = 'Initialize Memory', } -export type CommandHeapObject = { - type: string; +export interface CommandHeapObject { + type: COMMAND; heap: number[]; left: number; right: number; @@ -29,4 +29,25 @@ export type CommandHeapObject = { leftDesc: string; rightDesc: string; queue: number[]; -}; +} + +export interface MarkSweepGlobalState { + rowCount: number; + readonly columnCount: number; + NODE_SIZE: number; + MEMORY_SIZE: number; + memory: Memory; + memoryHeaps: Memory[]; + readonly commandHeap: CommandHeapObject[]; + memoryMatrix: number[][]; + tags: Tag[]; + typeTag: string[]; + readonly flips: number[]; + TAG_SLOT: number; + SIZE_SLOT: number; + FIRST_CHILD_SLOT: number; + LAST_CHILD_SLOT: number; + MARKED: number; + UNMARKED: number; + ROOTS: number[]; +} diff --git a/src/bundles/midi/package.json b/src/bundles/midi/package.json index 7cfbdc2201..31d1d281d8 100644 --- a/src/bundles/midi/package.json +++ b/src/bundles/midi/package.json @@ -7,7 +7,8 @@ "typescript": "^6.0.2" }, "dependencies": { - "js-slang": "^1.0.85" + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92" }, "type": "module", "scripts": { @@ -16,8 +17,9 @@ "prepare": "yarn tsc", "tsc": "buildtools tsc .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "exports": { ".": "./dist/index.js", diff --git a/src/bundles/midi/src/__tests__/index.test.ts b/src/bundles/midi/src/__tests__/index.test.ts index 1ef04b31f6..30ef854419 100644 --- a/src/bundles/midi/src/__tests__/index.test.ts +++ b/src/bundles/midi/src/__tests__/index.test.ts @@ -1,32 +1,32 @@ import { list_to_vector } from 'js-slang/dist/stdlib/list'; import { describe, expect, test } from 'vitest'; -import { letter_name_to_midi_note, midi_note_to_letter_name } from '..'; +import * as funcs from '..'; import { major_scale, minor_scale } from '../scales'; import { Accidental, type Note, type NoteWithOctave } from '../types'; import { noteToValues } from '../utils'; describe('scales', () => { test('major_scale', () => { - const c0 = letter_name_to_midi_note('C0'); + const c0 = funcs.letter_name_to_midi_note('C0'); const scale = major_scale(c0); expect(list_to_vector(scale)).toMatchObject([12, 14, 16, 17, 19, 21, 23, 24]); }); test('minor_scale', () => { - const a0 = letter_name_to_midi_note('A0'); + const a0 = funcs.letter_name_to_midi_note('A0'); const scale = minor_scale(a0); expect(list_to_vector(scale)).toMatchObject([21, 23, 24, 26, 28, 29, 31, 33]); }); }); -describe(midi_note_to_letter_name, () => { +describe(funcs.midi_note_to_letter_name, () => { describe('Test with sharps', () => { test.each([ [12, 'C0'], [13, 'C#0'], [36, 'C2'], [69, 'A4'], - ] as [number, NoteWithOctave][])('%i should equal %s', (note, noteName) => expect(midi_note_to_letter_name(note, 'sharp')).toEqual(noteName)); + ] as [number, NoteWithOctave][])('%i should equal %s', (note, noteName) => expect(funcs.midi_note_to_letter_name(note, Accidental.SHARP)).toEqual(noteName)); }); describe('Test with flats', () => { @@ -35,7 +35,7 @@ describe(midi_note_to_letter_name, () => { [13, 'Db0'], [36, 'C2'], [69, 'A4'], - ] as [number, NoteWithOctave][])('%i should equal %s', (note, noteName) => expect(midi_note_to_letter_name(note, 'flat')).toEqual(noteName)); + ] as [number, NoteWithOctave][])('%i should equal %s', (note, noteName) => expect(funcs.midi_note_to_letter_name(note, Accidental.FLAT)).toEqual(noteName)); }); }); @@ -49,13 +49,72 @@ describe(noteToValues, () => { // Leaving out octave should set it to 4 automatically ['a', 'A', Accidental.NATURAL, 4] ] as [NoteWithOctave, Note, Accidental, number][])('%s', (note, expectedNote, expectedAccidental, expectedOctave) => { - const [actualNote, actualAccidental, actualOctave] = noteToValues(note); + const [actualNote, actualAccidental, actualOctave] = noteToValues(note, ''); expect(actualNote).toEqual(expectedNote); expect(actualAccidental).toEqual(expectedAccidental); expect(actualOctave).toEqual(expectedOctave); }); test('Invalid note should throw an error', () => { - expect(() => noteToValues('Fb9' as any)).toThrowError('noteToValues: Invalid Note with Octave: Fb9'); + expect(() => noteToValues('Fb9' as any, 'noteToValues')).toThrowError('noteToValues: Invalid Note with Octave: Fb9'); + }); +}); + +describe(funcs.add_octave_to_note, () => { + test('Valid note and octave', () => { + expect(funcs.add_octave_to_note('C', 4)).toEqual('C4'); + expect(funcs.add_octave_to_note('F#', 0)).toEqual('F#0'); + }); + + test('Invalid octave should throw an error', () => { + expect(() => funcs.add_octave_to_note('C', -1)).toThrowError('add_octave_to_note: Expected integer greater than 0 for octave, got -1.'); + expect(() => funcs.add_octave_to_note('C', 2.5)).toThrowError('add_octave_to_note: Expected integer greater than 0 for octave, got 2.5.'); + }); +}); + +describe(funcs.get_octave, () => { + test('Valid note with octave', () => { + expect(funcs.get_octave('C4')).toEqual(4); + expect(funcs.get_octave('F#0')).toEqual(0); + + // If octave is left out, it should default to 4 + expect(funcs.get_octave('F')).toEqual(4); + }); + + test('Invalid note should throw an error', () => { + expect(() => funcs.get_octave('Fb9' as any)).toThrowError('get_octave: Invalid Note with Octave: Fb9'); + }); +}); + +describe(funcs.key_signature_to_keys, () => { + test('Valid key signatures', () => { + expect(funcs.key_signature_to_keys(Accidental.SHARP, 0)).toEqual('C'); + expect(funcs.key_signature_to_keys(Accidental.SHARP, 2)).toEqual('D'); + expect(funcs.key_signature_to_keys(Accidental.FLAT, 3)).toEqual('Eb'); + }); + + test('Invalid number of accidentals should throw an error', () => { + expect(() => funcs.key_signature_to_keys(Accidental.SHARP, -1)).toThrowError('key_signature_to_keys: Expected integer between 0 and 6 for numAccidentals, got -1.'); + expect(() => funcs.key_signature_to_keys(Accidental.SHARP, 7)).toThrowError('key_signature_to_keys: Expected integer between 0 and 6 for numAccidentals, got 7.'); + expect(() => funcs.key_signature_to_keys(Accidental.SHARP, 2.5)).toThrowError('key_signature_to_keys: Expected integer between 0 and 6 for numAccidentals, got 2.5.'); + }); + + test('Invalid accidental should throw an error', () => { + expect(() => funcs.key_signature_to_keys('invalid' as any, 2)).toThrowError('key_signature_to_keys: Expected sharp or flat for accidental, got "invalid".'); + }); +}); + +describe(funcs.is_note_with_octave, () => { + test('Valid NoteWithOctaves', () => { + expect(funcs.is_note_with_octave('C4')).toBe(true); + expect(funcs.is_note_with_octave('F#0')).toBe(true); + expect(funcs.is_note_with_octave('Ab9')).toBe(true); + expect(funcs.is_note_with_octave('C')).toBe(true); + expect(funcs.is_note_with_octave('F#')).toBe(true); + }); + + test('Invalid NoteWithOctaves', () => { + expect(funcs.is_note_with_octave('Invalid')).toBe(false); + expect(funcs.is_note_with_octave(123)).toBe(false); }); }); diff --git a/src/bundles/midi/src/index.ts b/src/bundles/midi/src/index.ts index 83d6f6af23..7a1829c53f 100644 --- a/src/bundles/midi/src/index.ts +++ b/src/bundles/midi/src/index.ts @@ -6,8 +6,18 @@ * @author leeyi45 */ -import { Accidental, type MIDINote, type NoteWithOctave } from './types'; -import { midiNoteToNoteName, noteToValues } from './utils'; +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; +import { Accidental, type MIDINote, type Note, type NoteWithOctave } from './types'; +import { midiNoteToNoteName, noteToValues, parseNoteWithOctave } from './utils'; + +/** + * Returns a boolean value indicating whether the given value is a {@link NoteWithOctave|note name with octave}. + */ +export function is_note_with_octave(value: unknown): value is NoteWithOctave { + const res = parseNoteWithOctave(value); + return res !== null; +} /** * Converts a letter name to its corresponding MIDI note. @@ -76,41 +86,125 @@ export function letter_name_to_midi_note(note: NoteWithOctave): MIDINote { } /** - * Convert a MIDI note into its letter representation + * Convert a {@link MIDINote|MIDI note} into its {@link NoteWithOctave|letter representation} + * * @param midiNote Note to convert * @param accidental Whether to return the letter as with a sharp or with a flat * @function + * @example + * ``` + * midi_note_to_letter_name(61, SHARP); // Returns "C#4" + * midi_note_to_letter_name(61, FLAT); // Returns "Db4" + * + * // Notes without accidentals return the same letter name + * // regardless of whether SHARP or FLAT is passed in + * midi_note_to_letter_name(60, FLAT); // Returns "C4" + * midi_note_to_letter_name(60, SHARP); // Returns "C4" + * ``` */ -export function midi_note_to_letter_name(midiNote: MIDINote, accidental: 'flat' | 'sharp'): NoteWithOctave { +export function midi_note_to_letter_name(midiNote: MIDINote, accidental: Accidental.FLAT | Accidental.SHARP): NoteWithOctave { const octave = Math.floor(midiNote / 12) - 1; const note = midiNoteToNoteName(midiNote, accidental, midi_note_to_letter_name.name); return `${note}${octave}`; } /** - * Converts a MIDI note to its corresponding frequency. + * Converts a {@link MIDINote|MIDI note} to its corresponding frequency. * * @param note given MIDI note * @returns the frequency of the MIDI note * @function - * @example midi_note_to_frequency(69); // Returns 440 + * @example + * ``` + * midi_note_to_frequency(69); // Returns 440 + * ``` */ export function midi_note_to_frequency(note: MIDINote): number { + assertNumberWithinRange(note, midi_note_to_frequency.name); // A4 = 440Hz = midi note 69 return 440 * 2 ** ((note - 69) / 12); } /** - * Converts a letter name to its corresponding frequency. + * Converts a {@link NoteWithOctave|note name} to its corresponding frequency. * * @param note given letter name - * @returns the corresponding frequency - * @example letter_name_to_frequency("A4"); // Returns 440 + * @returns the corresponding frequency (in Hz) + * @example + * ``` + * letter_name_to_frequency('A4'); // Returns 440 + * ``` */ export function letter_name_to_frequency(note: NoteWithOctave): number { return midi_note_to_frequency(letter_name_to_midi_note(note)); } +/** + * Takes the given {@link Note} and adds the octave number to it. + * @example + * ``` + * add_octave_to_note('C', 4); // Returns "C4" + * ``` + */ +export function add_octave_to_note(note: Note, octave: number): NoteWithOctave { + assertNumberWithinRange(octave, add_octave_to_note.name, 0, undefined, true, 'octave'); + return `${note}${octave}`; +} + +/** + * Gets the octave number from a given {@link NoteWithOctave|note name with octave}. + */ +export function get_octave(note: NoteWithOctave): number { + const [,, octave] = noteToValues(note, get_octave.name); + return octave; +} + +/** + * Gets the letter name from a given {@link NoteWithOctave|note name with octave} (without the accidental). + * @example + * ``` + * get_note_name('C#4'); // Returns "C" + * get_note_name('Eb3'); // Returns "E" + * ``` + */ +export function get_note_name(note: NoteWithOctave): Note { + const [noteName] = noteToValues(note, get_note_name.name); + return noteName; +} + +/** + * Gets the accidental from a given {@link NoteWithOctave|note name with octave}. + */ +export function get_accidental(note: NoteWithOctave): Accidental { + const [, accidental] = noteToValues(note, get_accidental.name); + return accidental; +} + +/** + * Converts the key signature to the corresponding key + * @example + * ``` + * key_signature_to_keys(SHARP, 2); // Returns "D", since the key of D has 2 sharps + * key_signature_to_keys(FLAT, 3); // Returns "Eb", since the key of Eb has 3 flats + * ``` + */ +export function key_signature_to_keys(accidental: Accidental.FLAT | Accidental.SHARP, numAccidentals: number): Note { + assertNumberWithinRange(numAccidentals, key_signature_to_keys.name, 0, 6, true, 'numAccidentals'); + + switch (accidental) { + case Accidental.SHARP: { + const keys: Note[] = ['C', 'G', 'D', 'A', 'E', 'B', 'F#']; + return keys[numAccidentals]; + } + case Accidental.FLAT: { + const keys: Note[] = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb']; + return keys[numAccidentals]; + } + default: + throw new InvalidParameterTypeError('sharp or flat', accidental, key_signature_to_keys.name, 'accidental'); + } +} + export * from './scales'; /** diff --git a/src/bundles/midi/src/scales.ts b/src/bundles/midi/src/scales.ts index 246677bd13..4f8ce95fb5 100644 --- a/src/bundles/midi/src/scales.ts +++ b/src/bundles/midi/src/scales.ts @@ -6,14 +6,14 @@ const major_intervals = [2, 2, 1, 2, 2, 2, 1]; /** * A musical scale is simply a list of {@link MIDINote| MIDI notes}. */ -export type Scale = List; +export type Scale = List; /** * There are 7 modes of the major scale that are just made by shuffling the major scale's * intervals around, so we can reuse this function. */ function make_from_major_scale(root: MIDINote, mode: number): Scale { - let output: List = pair(root + 12, null); + let output: List = pair(root + 12, null); let note = root + 12; for (let i = major_intervals.length - 1; i >= 0; i--) { diff --git a/src/bundles/midi/src/utils.ts b/src/bundles/midi/src/utils.ts index 7504409b7d..16b11b1b94 100644 --- a/src/bundles/midi/src/utils.ts +++ b/src/bundles/midi/src/utils.ts @@ -1,23 +1,23 @@ +import { GeneralRuntimeError, InternalRuntimeError } from '@sourceacademy/modules-lib/errors'; import { Accidental, type MIDINote, type Note, type NoteName, type NoteWithOctave } from './types'; -export function noteToValues(note: NoteWithOctave, func_name: string = noteToValues.name) { +export function parseNoteWithOctave(note: NoteWithOctave): [NoteName, Accidental, number]; +export function parseNoteWithOctave(note: unknown): [NoteName, Accidental, number] | null; +export function parseNoteWithOctave(note: unknown): [NoteName, Accidental, number] | null { + if (typeof note !== 'string') return null; + const match = /^([A-Ga-g])([#♮b]?)(\d*)$/.exec(note); - if (match === null) throw new Error(`${func_name}: Invalid Note with Octave: ${note}`); + if (match === null) return null; const [, noteName, accidental, octaveStr] = match; switch (accidental) { case Accidental.SHARP: { - if (noteName === 'B' || noteName === 'E') { - throw new Error(`${func_name}: Invalid Note with Octave: ${note}`); - } - + if (noteName === 'B' || noteName === 'E') return null; break; } case Accidental.FLAT: { - if (noteName === 'F' || noteName === 'C') { - throw new Error(`${func_name}: Invalid Note with Octave: ${note}`); - } + if (noteName === 'F' || noteName === 'C') return null; break; } } @@ -30,33 +30,45 @@ export function noteToValues(note: NoteWithOctave, func_name: string = noteToVal ] as [NoteName, Accidental, number]; } -export function midiNoteToNoteName(midiNote: MIDINote, accidental: 'flat' | 'sharp', func_name: string): Note { +export function noteToValues(note: NoteWithOctave, func_name: string): [NoteName, Accidental, number] { + const res = parseNoteWithOctave(note); + if (res === null) { + throw new GeneralRuntimeError(`${func_name}: Invalid Note with Octave: ${note}`); + } + return res; +} + +export function midiNoteToNoteName( + midiNote: MIDINote, + accidental: Accidental.FLAT | Accidental.SHARP, + func_name: string +): Note { switch (midiNote % 12) { case 0: return 'C'; case 1: - return accidental === 'sharp' ? `C${Accidental.SHARP}` : `D${Accidental.FLAT}`; + return accidental === Accidental.SHARP ? `C${Accidental.SHARP}` : `D${Accidental.FLAT}`; case 2: return 'D'; case 3: - return accidental === 'sharp' ? `D${Accidental.SHARP}` : `E${Accidental.FLAT}`; + return accidental === Accidental.SHARP ? `D${Accidental.SHARP}` : `E${Accidental.FLAT}`; case 4: return 'E'; case 5: return 'F'; case 6: - return accidental === 'sharp' ? `F${Accidental.SHARP}` : `G${Accidental.FLAT}`; + return accidental === Accidental.SHARP ? `F${Accidental.SHARP}` : `G${Accidental.FLAT}`; case 7: return 'G'; case 8: - return accidental === 'sharp' ? `G${Accidental.SHARP}` : `A${Accidental.FLAT}`; + return accidental === Accidental.SHARP ? `G${Accidental.SHARP}` : `A${Accidental.FLAT}`; case 9: return 'A'; case 10: - return accidental === 'sharp' ? `A${Accidental.SHARP}` : `B${Accidental.FLAT}`; + return accidental === Accidental.SHARP ? `A${Accidental.SHARP}` : `B${Accidental.FLAT}`; case 11: return 'B'; default: - throw new Error(`${func_name}: Invalid MIDI note value ${midiNote}`); + throw new InternalRuntimeError(`${func_name}: Invalid MIDI note value ${midiNote}`); } } diff --git a/src/bundles/nbody/package.json b/src/bundles/nbody/package.json index e334efcaa2..47092ee32c 100644 --- a/src/bundles/nbody/package.json +++ b/src/bundles/nbody/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { + "@sourceacademy/modules-lib": "workspace:^", "nbody": "^0.2.0", "plotly.js-dist": "^2.17.1", "three": "^0.183.0" @@ -23,8 +24,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/nbody/src/Simulation.ts b/src/bundles/nbody/src/Simulation.ts index 3151bc7949..b8b9ed614c 100644 --- a/src/bundles/nbody/src/Simulation.ts +++ b/src/bundles/nbody/src/Simulation.ts @@ -1,5 +1,7 @@ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; import context from 'js-slang/context'; import { RecordingVisualizer3D, RecordingVisualizer, Simulation, type Universe, type VisType } from 'nbody'; +import type { Visualizer } from 'nbody/dist/types/src/Visualizer'; import type { RecordInfo } from './types'; /** @@ -43,8 +45,8 @@ context.moduleContexts.nbody.state = { recordInfo }; -function isRecordingBased(sim: Simulation): boolean { - return sim.visualizer instanceof RecordingVisualizer || sim.visualizer instanceof RecordingVisualizer3D; +function isRecordingBased(visualizer: Visualizer) { + return visualizer instanceof RecordingVisualizer || visualizer instanceof RecordingVisualizer3D; } /** @@ -56,8 +58,8 @@ export function playSim(sim: Simulation): void { while (simulations.length > 0) { simulations.pop()!.stop(); } - if (isRecordingBased(sim)) { - throw new Error('playSim expects non-recording simulations'); + if (isRecordingBased(sim.visualizer)) { + throw new InvalidParameterTypeError('non-recording simulation', sim, playSim.name); } recordInfo.isRecording = false; simulations.push(sim); @@ -74,8 +76,8 @@ export function recordSim(sim: Simulation, recordFor: number, recordSpeed: numbe while (simulations.length > 0) { simulations.pop()!.stop(); } - if (!isRecordingBased(sim)) { - throw new Error('recordSim expects recording simulations'); + if (!isRecordingBased(sim.visualizer)) { + throw new InvalidParameterTypeError('recording simulation', sim, recordSim.name); } recordInfo.isRecording = true; recordInfo.recordFor = recordFor; diff --git a/src/bundles/painter/package.json b/src/bundles/painter/package.json index c598586250..d03ca84636 100644 --- a/src/bundles/painter/package.json +++ b/src/bundles/painter/package.json @@ -21,8 +21,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/physics_2d/package.json b/src/bundles/physics_2d/package.json index c7fd153e01..13ed7b8223 100644 --- a/src/bundles/physics_2d/package.json +++ b/src/bundles/physics_2d/package.json @@ -20,8 +20,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/physics_2d/src/__tests__/functions.test.ts b/src/bundles/physics_2d/src/__tests__/functions.test.ts new file mode 100644 index 0000000000..a7b5a38d8d --- /dev/null +++ b/src/bundles/physics_2d/src/__tests__/functions.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import * as funcs from '../functions'; + +beforeEach(() => { + funcs.resetWorld(); +}); + +describe(funcs.add_box_object, () => { + it('throws when no world has been created', () => { + expect(() => funcs.add_box_object( + funcs.make_vector(1, 1), + 0, + funcs.make_vector(1, 1), + funcs.make_vector(1, 1), + true + )).toThrow( + 'add_box_object: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.add_circle_object, () => { + it('throws when no world has been created', () => { + expect(() => funcs.add_circle_object( + funcs.make_vector(1, 1), + 0, + funcs.make_vector(1, 1), + 1, + true + )).toThrow( + 'add_circle_object: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.add_triangle_object, () => { + it('throws when no world has been created', () => { + expect(() => funcs.add_triangle_object( + funcs.make_vector(1, 1), + 0, + funcs.make_vector(1, 1), + 1, + 1, + true + )).toThrow( + 'add_triangle_object: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.add_wall, () => { + it('throws when no world has been created', () => { + expect(() => funcs.add_wall( + funcs.make_vector(1, 1), + 0, + funcs.make_vector(1, 1), + )).toThrow( + 'add_wall: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.impact_start_time, () => { + it('throws when no world has been created', () => { + expect(() => funcs.impact_start_time({} as any, {} as any)).toThrow( + 'impact_start_time: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.make_ground, () => { + it('throws when no world has been created', () => { + expect(() => funcs.make_ground(0, 0)).toThrow( + 'make_ground: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.scale_size, () => { + it('throws when no world has been created', () => { + expect(() => funcs.scale_size({} as any, 0)).toThrow( + 'scale_size: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.set_gravity, () => { + it('throws when called multiple times', () => { + expect(funcs.set_gravity(funcs.make_vector(0, -9.8))).toBeUndefined(); + expect(() => funcs.set_gravity(funcs.make_vector(0, -9.8))).toThrow('set_gravity: You may only call set_gravity once!'); + }); +}); + +describe(funcs.simulate_world, () => { + it('throws when no world has been created', () => { + expect(() => funcs.simulate_world(0)).toThrow( + 'simulate_world: Please call set_gravity first!' + ); + }); +}); + +describe(funcs.update_world, () => { + it('throws when no world has been created', () => { + expect(() => funcs.update_world(0)).toThrow( + 'update_world: Please call set_gravity first!' + ); + }); +}); diff --git a/src/bundles/physics_2d/src/functions.ts b/src/bundles/physics_2d/src/functions.ts index c747d3471f..b687a23c60 100644 --- a/src/bundles/physics_2d/src/functions.ts +++ b/src/bundles/physics_2d/src/functions.ts @@ -5,6 +5,7 @@ */ import { b2CircleShape, b2PolygonShape } from '@box2d/core'; +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import context from 'js-slang/context'; import { PhysicsObject } from './PhysicsObject'; @@ -13,9 +14,28 @@ import { Vector2, type Force } from './types'; // Global Variables -let world: PhysicsWorld | null = null; -const NO_WORLD = new Error('Please call set_gravity first!'); -const MULTIPLE_WORLDS = new Error('You may only call set_gravity once!'); +/** + * exported for testing + * @hidden + */ +export let world: PhysicsWorld | null = null; + +/** + * exported for testing + * @hidden + */ +export function resetWorld() { + world = null; +} + +const NO_WORLD = 'Please call set_gravity first!'; +const MULTIPLE_WORLDS = 'You may only call set_gravity once!'; + +function throwIfNoWorld(world: PhysicsWorld | null, func_name: string): asserts world is PhysicsWorld { + if (!world) { + throw new GeneralRuntimeError(`${func_name}: ${NO_WORLD}`); + } +} // Module's Exposed Functions @@ -64,14 +84,14 @@ export function make_force( * @param v gravity vector * @example * ``` - * set_gravity(0, -9.8); // gravity vector for real world + * set_gravity(make_vector(0, -9.8)); // gravity vector for real world * ``` * * @category Main */ export function set_gravity(v: Vector2) { if (world) { - throw MULTIPLE_WORLDS; + throw new GeneralRuntimeError(`${set_gravity.name}: ${MULTIPLE_WORLDS}`); } world = new PhysicsWorld(); @@ -90,10 +110,7 @@ export function set_gravity(v: Vector2) { * @category Main */ export function make_ground(height: number, friction: number) { - if (!world) { - throw NO_WORLD; - } - + throwIfNoWorld(world, make_ground.name); world.makeGround(height, friction); } @@ -108,9 +125,7 @@ export function make_ground(height: number, friction: number) { * @category Main */ export function add_wall(pos: Vector2, rot: number, size: Vector2) { - if (!world) { - throw NO_WORLD; - } + throwIfNoWorld(world, add_wall.name); return world.addObject(new PhysicsObject( pos, @@ -140,10 +155,9 @@ export function add_box_object( size: Vector2, isStatic: boolean ): PhysicsObject { - if (!world) { - throw NO_WORLD; - } - const newObj: PhysicsObject = new PhysicsObject( + throwIfNoWorld(world, add_box_object.name); + + const newObj = new PhysicsObject( pos, rot, new b2PolygonShape() @@ -173,10 +187,9 @@ export function add_circle_object( radius: number, isStatic: boolean ): PhysicsObject { - if (!world) { - throw NO_WORLD; - } - const newObj: PhysicsObject = new PhysicsObject( + throwIfNoWorld(world, add_circle_object.name); + + const newObj = new PhysicsObject( pos, rot, new b2CircleShape() @@ -208,10 +221,9 @@ export function add_triangle_object( height: number, isStatic: boolean ): PhysicsObject { - if (!world) { - throw NO_WORLD; - } - const newObj: PhysicsObject = new PhysicsObject( + throwIfNoWorld(world, add_triangle_object.name); + + const newObj = new PhysicsObject( pos, rot, new b2PolygonShape() @@ -235,10 +247,7 @@ export function add_triangle_object( * @category Main */ export function update_world(dt: number) { - if (!world) { - throw NO_WORLD; - } - + throwIfNoWorld(world, update_world.name); world.update(dt); } @@ -250,10 +259,7 @@ export function update_world(dt: number) { * @category Main */ export function simulate_world(total_time: number) { - if (!world) { - throw NO_WORLD; - } - + throwIfNoWorld(world, simulate_world.name); world.simulate(total_time); } @@ -374,9 +380,7 @@ export function set_density(obj: PhysicsObject, density: number) { * @category Body */ export function scale_size(obj: PhysicsObject, scale: number) { - if (!world) { - throw NO_WORLD; - } + throwIfNoWorld(world, scale_size.name); obj.scale_size(scale); } @@ -416,10 +420,7 @@ export function is_touching(obj1: PhysicsObject, obj2: PhysicsObject) { * @category Dynamics */ export function impact_start_time(obj1: PhysicsObject, obj2: PhysicsObject) { - if (!world) { - throw NO_WORLD; - } - + throwIfNoWorld(world, impact_start_time.name); return world.findImpact(obj1, obj2); } diff --git a/src/bundles/physics_2d/src/types.ts b/src/bundles/physics_2d/src/types.ts index f33988a894..4d6f793ed1 100644 --- a/src/bundles/physics_2d/src/types.ts +++ b/src/bundles/physics_2d/src/types.ts @@ -2,6 +2,7 @@ import { b2Vec2 } from '@box2d/core'; import type { ReplResult } from '@sourceacademy/modules-lib/types'; export const ACCURACY = 2; + export class Vector2 extends b2Vec2 implements ReplResult { public toReplString = () => `Vector2D: [${this.x}, ${this.y}]`; } diff --git a/src/bundles/pix_n_flix/package.json b/src/bundles/pix_n_flix/package.json index bfa9cd3852..040c7c64c4 100644 --- a/src/bundles/pix_n_flix/package.json +++ b/src/bundles/pix_n_flix/package.json @@ -5,14 +5,17 @@ "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", "@types/react": "^19.0.0", - "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-playwright": "4.1.5", "playwright": "^1.55.1", "react": "^19.0.0", "react-dom": "^19.0.0", "typescript": "^6.0.2", - "vitest": "4.1.4", + "vitest": "4.1.5", "vitest-browser-react": "^2.1.0" }, + "dependencies": { + "@sourceacademy/modules-lib": "workspace:^" + }, "type": "module", "exports": { ".": "./dist/index.js", @@ -23,8 +26,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/pix_n_flix/src/__tests__/index.test.tsx b/src/bundles/pix_n_flix/src/__tests__/index.test.tsx index 84b47f273f..b470578e60 100644 --- a/src/bundles/pix_n_flix/src/__tests__/index.test.tsx +++ b/src/bundles/pix_n_flix/src/__tests__/index.test.tsx @@ -1,13 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, test as baseTest, vi } from 'vitest'; import { cleanup, render, type RenderResult } from 'vitest-browser-react'; import * as funcs from '../functions'; -import type { Pixel, VideoElement } from '../types'; +import type { BundlePacket, Pixel, VideoElement } from '../types'; interface Fixtures { canvas: HTMLCanvasElement; image: HTMLImageElement; video: VideoElement; - reinit: () => ReturnType['init']>; + reinit: () => BundlePacket; errLogger: () => void; } @@ -26,6 +26,7 @@ const test = baseTest.extend<{ cleanup(); }, fixtures: async ({ screen }, fixture) => { + funcs.start(); const { init, deinit } = funcs.start(); const errLogger = vi.fn(); const canvas = screen @@ -70,24 +71,40 @@ describe('pixel manipulation functions', () => { it('works', () => { expect(funcs.alpha_of([0, 0, 0, 255])).toEqual(255); }); + + it('throws error when argument is not a pixel', () => { + expect(() => funcs.alpha_of(0 as any)).toThrow('alpha_of: Expected pixel, got 0.'); + }); }); describe(funcs.red_of, () => { it('works', () => { expect(funcs.red_of([255, 0, 0, 0])).toEqual(255); }); + + it('throws error when argument is not a pixel', () => { + expect(() => funcs.red_of(0 as any)).toThrow('red_of: Expected pixel, got 0.'); + }); }); describe(funcs.green_of, () => { it('works', () => { expect(funcs.green_of([0, 255, 0, 0])).toEqual(255); }); + + it('throws error when argument is not a pixel', () => { + expect(() => funcs.green_of(0 as any)).toThrow('green_of: Expected pixel, got 0.'); + }); }); describe(funcs.blue_of, () => { it('works', () => { expect(funcs.blue_of([0, 0, 255, 0])).toEqual(255); }); + + it('throws error when argument is not a pixel', () => { + expect(() => funcs.blue_of(0 as any)).toThrow('blue_of: Expected pixel, got 0.'); + }); }); describe(funcs.set_rgba, () => { @@ -98,6 +115,10 @@ describe('pixel manipulation functions', () => { expect(pixel[i]).toEqual(i + 1); } }); + + it('throws error when first argument is not a pixel', () => { + expect(() => funcs.set_rgba(0 as any, 1, 2, 3, 4)).toThrow('set_rgba: Expected pixel for pixel, got 0.'); + }); }); }); @@ -138,7 +159,7 @@ describe(funcs.writeToBuffer, () => { test('with invalid data', ({ fixtures: { errLogger } }) => { const img = funcs.new_image(); - funcs.set_rgba(img[0][0], 999, 999, 999, 999); + img[0][0] = [999, 999, 999, 999]; const imageData = new ImageData(width, height); const buffer = imageData.data; @@ -153,6 +174,19 @@ describe(funcs.writeToBuffer, () => { }); }); +describe(funcs.install_filter, () => { + it('throws an error when passed an invalid filter', () => { + expect(() => funcs.install_filter(0 as any)).toThrow('install_filter: Expected filter, got 0.'); + }); +}); + +describe(funcs.compose_filter, () => { + it('throws an error when passed invalid filters', () => { + expect(() => funcs.compose_filter(0 as any, (_s, _d) => {})).toThrow('compose_filter: Expected filter for filter1, got 0.'); + expect(() => funcs.compose_filter((_s, _d) => {}, 0 as any)).toThrow('compose_filter: Expected filter for filter2, got 0.'); + }); +}); + describe('video functions', () => { test('startVideo and stopVideo', ({ fixtures: { errLogger } }) => { const filter = vi.fn(funcs.copy_image); @@ -197,4 +231,11 @@ describe('video functions', () => { expect(FPS).toEqual(60); }); }); + + describe(funcs.set_loop_count, () => { + it('throws an error when given not an integer', () => { + expect(() => funcs.set_loop_count('a' as any)).toThrow('set_loop_count: Expected integer, got "a".'); + expect(() => funcs.set_loop_count(0.5)).toThrow('set_loop_count: Expected integer, got 0.5.'); + }); + }); }); diff --git a/src/bundles/pix_n_flix/src/functions.ts b/src/bundles/pix_n_flix/src/functions.ts index 5e062b1c8a..da750b31ca 100644 --- a/src/bundles/pix_n_flix/src/functions.ts +++ b/src/bundles/pix_n_flix/src/functions.ts @@ -1,3 +1,6 @@ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; +import context from 'js-slang/context'; import { DEFAULT_FPS, DEFAULT_HEIGHT, @@ -107,7 +110,8 @@ function setupData(): void { export function isPixelValid(pixel: Pixel): boolean { let ok = true; for (let i = 0; i < 4; i += 1) { - if (pixel[i] >= 0 && pixel[i] <= 255) { + const value = pixel[i]; + if (typeof value === 'number' && value >= 0 && value <= 255) { continue; } ok = false; @@ -281,7 +285,7 @@ function setAspectRatioDimensions(w: number, h: number): void { /** @hidden */ function loadMedia(): void { - if (!navigator.mediaDevices.getUserMedia) { + if (!navigator.mediaDevices?.getUserMedia) { const errMsg = 'The browser you are using does not support getUserMedia'; console.error(errMsg); errorLogger(errMsg, false); @@ -290,8 +294,7 @@ function loadMedia(): void { // If video is already part of bundle state if (videoElement.srcObject) return; - navigator.mediaDevices - .getUserMedia({ video: true }) + navigator.mediaDevices?.getUserMedia({ video: true }) .then((stream) => { videoElement.srcObject = stream; videoElement.onloadedmetadata = () => setAspectRatioDimensions( @@ -503,6 +506,16 @@ function deinit(): void { }); } +function throwIfNotPixel(obj: unknown, func_name: string, param_name?: string): asserts obj is Pixel { + if ( + !Array.isArray(obj) || + obj.length !== 4 || + obj.some(each => typeof each !== 'number') + ) { + throw new InvalidParameterTypeError('pixel', obj, func_name, param_name); + } +} + // ============================================================================= // Module's Exposed Functions // ============================================================================= @@ -510,17 +523,25 @@ function deinit(): void { /** * Starts processing the image or video using the installed filter. */ -export function start(): StartPacket { - return { - toReplString: () => '[Pix N Flix]', +export function start() { + const startPacket: StartPacket = { init, deinit, startVideo, stopVideo, updateFPS, updateVolume, - updateDimensions + updateDimensions, + toReplString: () => '[Pix N Flix]', }; + + if (!context.moduleContexts.pix_n_flix.state) { + context.moduleContexts.pix_n_flix.state = { + pixnflix: startPacket + }; + } + + return startPacket; } /** @@ -530,7 +551,7 @@ export function start(): StartPacket { * @returns The red component as a number between 0 and 255 */ export function red_of(pixel: Pixel): number { - // returns the red value of pixel respectively + throwIfNotPixel(pixel, red_of.name); return pixel[0]; } @@ -541,7 +562,7 @@ export function red_of(pixel: Pixel): number { * @returns The green component as a number between 0 and 255 */ export function green_of(pixel: Pixel): number { - // returns the green value of pixel respectively + throwIfNotPixel(pixel, green_of.name); return pixel[1]; } @@ -552,7 +573,7 @@ export function green_of(pixel: Pixel): number { * @returns The blue component as a number between 0 and 255 */ export function blue_of(pixel: Pixel): number { - // returns the blue value of pixel respectively + throwIfNotPixel(pixel, blue_of.name); return pixel[2]; } @@ -563,7 +584,7 @@ export function blue_of(pixel: Pixel): number { * @returns The alpha component as a number between 0 and 255 */ export function alpha_of(pixel: Pixel): number { - // returns the alpha value of pixel respectively + throwIfNotPixel(pixel, alpha_of.name); return pixel[3]; } @@ -584,6 +605,12 @@ export function set_rgba( b: number, a: number ): void { + throwIfNotPixel(pixel, set_rgba.name, 'pixel'); + assertNumberWithinRange(r, set_rgba.name, 0, 255, true, 'r'); + assertNumberWithinRange(g, set_rgba.name, 0, 255, true, 'g'); + assertNumberWithinRange(b, set_rgba.name, 0, 255, true, 'b'); + assertNumberWithinRange(a, set_rgba.name, 0, 255, true, 'a'); + // assigns the r,g,b values to this pixel pixel[0] = r; pixel[1] = g; @@ -618,7 +645,7 @@ export function image_width(): number { * @param src Source image * @param dest Destination image */ -export function copy_image(src: Pixels, dest: Pixels): void { +export function copy_image(src: Pixels, dest: Pixels) { for (let i = 0; i < HEIGHT; i += 1) { for (let j = 0; j < WIDTH; j += 1) { dest[i][j] = src[i][j]; @@ -638,6 +665,7 @@ export function copy_image(src: Pixels, dest: Pixels): void { * @param _filter The filter to be installed */ export function install_filter(_filter: Filter): void { + assertFunctionOfLength(_filter, 2, install_filter.name, 'filter'); filter = _filter; } @@ -657,6 +685,9 @@ export function reset_filter(): void { * @returns The filter equivalent to applying filter1 and then filter2 */ export function compose_filter(filter1: Filter, filter2: Filter): Filter { + assertFunctionOfLength(filter1, 2, compose_filter.name, 'filter', 'filter1'); + assertFunctionOfLength(filter2, 2, compose_filter.name, 'filter', 'filter2'); + return (src, dest) => { const temp = new_image(); filter1(src, temp); @@ -670,11 +701,12 @@ export function compose_filter(filter1: Filter, filter2: Filter): Filter { * @param pause_time Time in ms after the video starts. */ export function pause_at(pause_time: number): void { - // prevent negative pause_time + assertNumberWithinRange(pause_time, pause_at.name, 0); + lateEnqueue(() => { setTimeout( tabsPackage.onClickStill, - pause_time >= 0 ? pause_time : -pause_time + pause_time ); }); } @@ -687,6 +719,9 @@ export function pause_at(pause_time: number): void { * @param height The height of the displayed images (default value: 400) */ export function set_dimensions(width: number, height: number): void { + assertNumberWithinRange(width, set_dimensions.name, MIN_WIDTH, MAX_WIDTH, true, 'width'); + assertNumberWithinRange(height, set_dimensions.name, MIN_HEIGHT, MAX_HEIGHT, true, 'height'); + enqueue(() => updateDimensions(width, height)); } @@ -697,6 +732,7 @@ export function set_dimensions(width: number, height: number): void { * @param fps FPS of video (default value: 10) */ export function set_fps(fps: number): void { + assertNumberWithinRange(fps, set_fps.name, MIN_FPS, MAX_FPS); enqueue(() => updateFPS(fps)); } @@ -707,7 +743,14 @@ export function set_fps(fps: number): void { * @param volume Volume of video (Default value of 50) */ export function set_volume(volume: number): void { - enqueue(() => updateVolume(Math.max(0, Math.min(100, volume) / 100.0))); + assertNumberWithinRange(volume, set_volume.name); + + if (volume > 100) volume = 100; + else if (volume < 0) volume = 0; + + volume /= 100; + + enqueue(() => updateVolume(volume)); } /** @@ -725,6 +768,10 @@ export function use_local_file(): void { * @param URL URL of the image */ export function use_image_url(URL: string): void { + if (typeof URL !== 'string') { + throw new InvalidParameterTypeError('string', URL, use_image_url.name); + } + inputFeed = InputFeed.ImageURL; url = URL; } @@ -736,6 +783,10 @@ export function use_image_url(URL: string): void { * @param URL URL of the video */ export function use_video_url(URL: string): void { + if (typeof URL !== 'string') { + throw new InvalidParameterTypeError('string', URL, use_video_url.name); + } + inputFeed = InputFeed.VideoURL; url = URL; } @@ -755,6 +806,10 @@ export function get_video_time(): number { * @param _keepAspectRatio to keep aspect ratio. (Default value of true) */ export function keep_aspect_ratio(_keepAspectRatio: boolean): void { + if (typeof _keepAspectRatio !== 'boolean') { + throw new InvalidParameterTypeError('boolean', URL, keep_aspect_ratio.name); + } + keepAspectRatio = _keepAspectRatio; } @@ -765,5 +820,7 @@ export function keep_aspect_ratio(_keepAspectRatio: boolean): void { * @param n number of times the video repeats after the first iteration. If n < 1, n will be taken to be 1. (Default value of Infinity) */ export function set_loop_count(n: number): void { + assertNumberWithinRange(n, set_loop_count.name); + LOOP_COUNT = n; } diff --git a/src/bundles/pix_n_flix/src/types.ts b/src/bundles/pix_n_flix/src/types.ts index 3aaddf838b..108a760bc3 100644 --- a/src/bundles/pix_n_flix/src/types.ts +++ b/src/bundles/pix_n_flix/src/types.ts @@ -1,3 +1,5 @@ +import type { ReplResult } from '@sourceacademy/modules-lib/types'; + export type VideoElement = HTMLVideoElement & { srcObject?: MediaStream }; export type ImageElement = HTMLImageElement; export type CanvasElement = HTMLCanvasElement; @@ -23,7 +25,8 @@ export type BundlePacket = { inputFeed: InputFeed; }; export type Queue = () => void; -export type StartPacket = { + +export interface StartPacket extends ReplResult { toReplString: () => string; init: ( image: ImageElement, @@ -38,7 +41,17 @@ export type StartPacket = { updateFPS: (fps: number) => void; updateVolume: (volume: number) => void; updateDimensions: (width: number, height: number) => void; -}; +} + +export interface PixNFlixModuleState { + pixnflix: StartPacket | null; +} + export type Pixel = [r: number, g: number, b: number, a: number]; export type Pixels = Pixel[][]; + +/** + * A `void` returning function that takes the pixel data in `src`, + * transforms it, and then writes the output to `dest`. + */ export type Filter = (src: Pixels, dest: Pixels) => void; diff --git a/src/bundles/plotly/package.json b/src/bundles/plotly/package.json index cad26e4fd3..54c7b285ad 100644 --- a/src/bundles/plotly/package.json +++ b/src/bundles/plotly/package.json @@ -5,7 +5,8 @@ "dependencies": { "@sourceacademy/bundle-curve": "workspace:^", "@sourceacademy/bundle-sound": "workspace:^", - "js-slang": "^1.0.85", + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92", "plotly.js-dist": "^3.0.0" }, "devDependencies": { @@ -23,8 +24,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/plotly/src/__tests__/index.test.ts b/src/bundles/plotly/src/__tests__/index.test.ts new file mode 100644 index 0000000000..778d24d696 --- /dev/null +++ b/src/bundles/plotly/src/__tests__/index.test.ts @@ -0,0 +1,18 @@ +import { list, pair } from 'js-slang/dist/stdlib/list'; +import { describe, expect, it } from 'vitest'; +import * as funcs from '../functions'; + +describe(funcs.add_fields_to_data, () => { + it('works', () => { + const data = {}; + funcs.add_fields_to_data(data, list( + pair('x', 0), + pair('y', 1), + pair('z', 2), + )); + + expect(data).toHaveProperty('x', 0); + expect(data).toHaveProperty('y', 1); + expect(data).toHaveProperty('z', 2); + }); +}); diff --git a/src/bundles/plotly/src/functions.ts b/src/bundles/plotly/src/functions.ts index f7e07f1fe9..8398fd8f0c 100644 --- a/src/bundles/plotly/src/functions.ts +++ b/src/bundles/plotly/src/functions.ts @@ -6,7 +6,9 @@ import type { Curve } from '@sourceacademy/bundle-curve/curves_webgl'; import { get_duration, get_wave, is_sound } from '@sourceacademy/bundle-sound/functions'; import type { Sound } from '@sourceacademy/bundle-sound/types'; +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; import context from 'js-slang/context'; +import { accumulate, head, is_pair, tail, type List } from 'js-slang/dist/stdlib/list'; import Plotly, { type Data, type Layout } from 'plotly.js-dist'; import { generatePlot } from './curve_functions'; import { @@ -97,7 +99,8 @@ export function new_plot_json(data: any): void { * @param divId The id of the div element on which the plot will be displayed */ function draw_new_plot(data: ListOfPairs, divId: string) { - const plotlyData = convert_to_plotly_data(data); + const plotlyData: Data = {}; + add_fields_to_data(plotlyData, data); Plotly.newPlot(divId, [plotlyData]); } @@ -110,30 +113,28 @@ function draw_new_plot_json(data: any, divId: string) { Plotly.newPlot(divId, data); } -/** - * @param data The list of pairs given by the user - * @returns The converted data that can be used by the plotly.js function - */ -function convert_to_plotly_data(data: ListOfPairs): Data { - const convertedData: Data = {}; - if (Array.isArray(data) && data.length === 2) { - add_fields_to_data(convertedData, data); - } - return convertedData; -} - /** * @param convertedData Stores the Javascript object which is used by plotly.js * @param data The list of pairs data used by source + * @hidden */ +export function add_fields_to_data(convertedData: Data, data: ListOfPairs) { + accumulate((entry, result) => { + if (!is_pair(entry)) { + throw new GeneralRuntimeError(`${add_fields_to_data.name}: Expected list of pairs, got ${entry}`); + } -function add_fields_to_data(convertedData: Data, data: ListOfPairs) { - if (Array.isArray(data) && data.length === 2 && data[0].length === 2) { - const field = data[0][0]; - const value = data[0][1]; - convertedData[field] = value; - add_fields_to_data(convertedData, data[1]); - } + const field = head(entry); + + if (typeof field !== 'string') { + throw new GeneralRuntimeError(`${add_fields_to_data.name}: Expected head of pair to be string, got ${field}`); + } + + const value = tail(entry); + + (result as any)[field] = value; + return result; + }, convertedData, data as List); } function createPlotFunction( @@ -214,7 +215,7 @@ export const draw_connected_3d = createPlotFunction( * Curve at num sample points. The Drawing consists of isolated points, and does not connect them. * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, * - * * @param num determines the number of points, lower than 65535, to be sampled. + * @param num determines the number of points, lower than 65535, to be sampled. * Including 0 and 1, there are `num + 1` evenly spaced sample points * @function * @returns function of type 2D Curve → Drawing @@ -241,7 +242,7 @@ export const draw_points_2d = createPlotFunction( * 3D Curve at num sample points. The Drawing consists of isolated points, and does not connect them. * When a program evaluates to a Drawing, the Source system displays it graphically, in a window, * - * * @param num determines the number of points, lower than 65535, to be sampled. + * @param num determines the number of points, lower than 65535, to be sampled. * Including 0 and 1, there are `num + 1` evenly spaced sample points * @function * @returns function of type 3D Curve → Drawing @@ -263,10 +264,10 @@ export const draw_points_3d = createPlotFunction( export function draw_sound_2d(sound: Sound) { const FS: number = 44100; // Output sample rate if (!is_sound(sound)) { - throw new Error(`draw_sound_2d is expecting sound, but encountered ${sound}`); + throw new InvalidParameterTypeError('sound', sound, draw_sound_2d.name); // If a sound is already displayed, terminate execution. } else if (get_duration(sound) < 0) { - throw new Error('draw_sound_2d: duration of sound is negative'); + throw new GeneralRuntimeError(`${draw_sound_2d.name}: duration of sound is negative`); } else { // Instantiate audio context if it has not been instantiated. // Create mono buffer diff --git a/src/bundles/repeat/package.json b/src/bundles/repeat/package.json index 32efe86f98..de374340c2 100644 --- a/src/bundles/repeat/package.json +++ b/src/bundles/repeat/package.json @@ -1,7 +1,10 @@ { "name": "@sourceacademy/bundle-repeat", - "version": "1.0.0", + "version": "1.1.0", "private": true, + "dependencies": { + "@sourceacademy/modules-lib": "workspace:^" + }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", "typescript": "^6.0.2" @@ -16,8 +19,9 @@ "test": "buildtools test --project .", "tsc": "buildtools tsc .", "lint": "buildtools lint .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/repeat/src/__tests__/index.test.ts b/src/bundles/repeat/src/__tests__/index.test.ts index 3a3cff9e81..1fd8a73d18 100644 --- a/src/bundles/repeat/src/__tests__/index.test.ts +++ b/src/bundles/repeat/src/__tests__/index.test.ts @@ -1,19 +1,43 @@ -import { expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; +import * as funcs from '../functions'; -import { repeat, thrice, twice } from '../functions'; +vi.spyOn(funcs, 'repeat'); -// Test functions -test('repeat works correctly and repeats function n times', () => { - expect(repeat((x: number) => x + 1, 5)(1)) - .toBe(6); +describe(funcs.repeat, () => { + test('repeat works correctly and repeats unary function n times', () => { + expect(funcs.repeat((x: number) => x + 1, 5)(1)) + .toEqual(6); + }); + + test('returns the identity function when n = 0', () => { + expect(funcs.repeat((x: number) => x + 1, 0)(0)).toEqual(0); + }); + + test('throws an error when the function doesn\'t take 1 parameter', () => { + expect(() => funcs.repeat((x: number, y: number) => x + y, 2)) + .toThrow('repeat: Expected function with 1 parameter, got (x, y) => x + y.'); + + expect(() => funcs.repeat(() => 2, 2)) + .toThrow('repeat: Expected function with 1 parameter, got () => 2.'); + }); + + test('throws an error when provided incorrect values', () => { + expect(() => funcs.repeat((x: number) => x, -1)) + .toThrow('repeat: Expected integer greater than 0, got -1.'); + + expect(() => funcs.repeat((x: number) => x, 1.5)) + .toThrow('repeat: Expected integer greater than 0, got 1.5.'); + }); }); test('twice works correctly and repeats function twice', () => { - expect(twice((x: number) => x + 1)(1)) - .toBe(3); + expect(funcs.twice((x: number) => x + 1)(1)) + .toEqual(3); + expect(funcs.repeat).not.toHaveBeenCalled(); }); test('thrice works correctly and repeats function thrice', () => { - expect(thrice((x: number) => x + 1)(1)) - .toBe(4); + expect(funcs.thrice((x: number) => x + 1)(1)) + .toEqual(4); + expect(funcs.repeat).not.toHaveBeenCalled(); }); diff --git a/src/bundles/repeat/src/functions.ts b/src/bundles/repeat/src/functions.ts index 04b659da1b..0c7e935a89 100644 --- a/src/bundles/repeat/src/functions.ts +++ b/src/bundles/repeat/src/functions.ts @@ -3,6 +3,22 @@ * @module repeat */ +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; + +/** + * Represents a function that takes in 1 parameter and returns a + * value of the same type + */ +type UnaryFunction = (x: T) => T; + +/** + * Internal implementation of the repeat function that doesn't perform type checking + * @hidden + */ +export function repeat_internal(f: UnaryFunction, n: number): UnaryFunction { + return n === 0 ? x => x : x => f(repeat_internal(f, n - 1)(x)); +} + /** * Returns a new function which when applied to an argument, has the same effect * as applying the specified function to the same argument n times. @@ -16,7 +32,10 @@ * @returns the new function that has the same effect as func repeated n times */ export function repeat(func: Function, n: number): Function { - return n === 0 ? (x: any) => x : (x: any) => func(repeat(func, n - 1)(x)); + assertFunctionOfLength(func, 1, repeat.name); + assertNumberWithinRange(n, repeat.name, 0); + + return repeat_internal(func, n); } /** @@ -31,7 +50,8 @@ export function repeat(func: Function, n: number): Function { * @returns the new function that has the same effect as `(x => func(func(x)))` */ export function twice(func: Function): Function { - return repeat(func, 2); + assertFunctionOfLength(func, 1, twice.name); + return repeat_internal(func, 2); } /** @@ -46,5 +66,6 @@ export function twice(func: Function): Function { * @returns the new function that has the same effect as `(x => func(func(func(x))))` */ export function thrice(func: Function): Function { - return repeat(func, 3); + assertFunctionOfLength(func, 1, thrice.name); + return repeat_internal(func, 3); } diff --git a/src/bundles/repl/package.json b/src/bundles/repl/package.json index ca7cda9421..f668263ed8 100644 --- a/src/bundles/repl/package.json +++ b/src/bundles/repl/package.json @@ -1,9 +1,10 @@ { "name": "@sourceacademy/bundle-repl", - "version": "1.0.0", + "version": "2.0.0", "private": true, "dependencies": { - "js-slang": "^1.0.85" + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -17,10 +18,11 @@ "scripts": { "tsc": "buildtools tsc .", "build": "buildtools build bundle .", + "compile": "buildtools compile", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "buildtools serve" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/repl/src/__tests__/index.test.ts b/src/bundles/repl/src/__tests__/index.test.ts index fc288676e6..b55d0ce0d8 100644 --- a/src/bundles/repl/src/__tests__/index.test.ts +++ b/src/bundles/repl/src/__tests__/index.test.ts @@ -1,61 +1,160 @@ import * as slang from 'js-slang'; +import { pair } from 'js-slang/dist/stdlib/list'; import { stringify } from 'js-slang/dist/utils/stringify'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as funcs from '../functions'; import { ProgrammableRepl } from '../programmable_repl'; +const repl = funcs.INSTANCE; +const tabRerender = vi.fn(() => {}); + +repl.tabRerenderer = tabRerender; + +vi.spyOn(repl, 'easterEggFunction'); + +beforeEach(() => { + repl.outputStrings.splice(0, repl.outputStrings.length); + repl.evalFunction = null; + repl.customizedEditorProps = { + backgroundImageUrl: null, + backgroundColorAlpha: 1, + fontSize: 17 + }; + + tabRerender.mockClear(); +}); + describe(ProgrammableRepl, () => { - const replIt = it.extend<{ repl: ProgrammableRepl }>({ - repl: ({}, use) => { - const repl = new ProgrammableRepl(); - repl.setTabReactComponentInstance({ - setState: () => {} - }); - return use(repl); - } - }); - - replIt('calls js-slang when default_js_slang is the evaluator', ({ repl }) => { - vi.spyOn(slang, 'runFilesInContext').mockResolvedValueOnce({ - status: 'error' + it('calls js-slang when default_js_slang is the evaluator', async () => { + vi.spyOn(slang, 'runInContext').mockResolvedValueOnce({ + status: 'error', + context: {} as any }); repl.InvokeREPL_Internal(funcs.default_js_slang); - repl.runCode(); + await repl.runCode('display();', slang.createContext()); - expect(slang.runFilesInContext).toHaveBeenCalledOnce(); + expect(slang.runInContext).toHaveBeenCalledOnce(); + expect(tabRerender).toHaveBeenCalledOnce(); }); - replIt('calls the evaluator when another evaluator is provided', ({ repl }) => { + it('calls the evaluator when another evaluator is provided', async () => { const evaller = vi.fn(() => 0); repl.InvokeREPL_Internal(evaller); - repl.runCode(); + await repl.runCode('display();', {} as any); expect(evaller).toHaveBeenCalledOnce(); + expect(tabRerender).toHaveBeenCalledOnce(); }); - replIt('calls the easter egg function when no evaluator is provided', ({ repl }) => { - vi.spyOn(repl, 'easterEggFunction'); - repl.runCode(); + it('calls the easter egg function when no evaluator is provided', async () => { + await repl.runCode('display();', {} as any); expect(repl.easterEggFunction).toHaveBeenCalledOnce(); + expect(tabRerender).toHaveBeenCalledOnce(); + }); + + describe(funcs.rich_repl_display, () => { + it('works when passed a string', () => { + expect(() => funcs.rich_repl_display('test')).not.toThrow(); + expect(repl.outputStrings).toEqual([{ + content: 'test', + color: '', + outputMethod: 'richtext' + }]); + }); + + it('works when passed a pair', () => { + expect(() => funcs.rich_repl_display(pair('test', 'clrt#112233'))).not.toThrow(); + expect(repl.outputStrings).toEqual([{ + content: 'test', + color: '', + outputMethod: 'richtext' + }]); + }); + + it('works when passed multiple pairs', () => { + expect(() => funcs.rich_repl_display( + pair(pair('test', 'clrt#112233'), 'bold') + )).not.toThrow(); + + expect(repl.outputStrings).toEqual([ + { + content: 'test', + color: '', + outputMethod: 'richtext' + } + ]); + }); + + it('throws an error when not passed proper content', () => { + expect(() => funcs.rich_repl_display(0 as any)).toThrow(); + expect(() => funcs.rich_repl_display(pair(0, 0) as any)).toThrow(); + }); + + it('throws an error when given an invalid colour directive', () => { + expect(() => funcs.rich_repl_display(pair('test', 'clrx#112233'))).toThrow('rich_repl_display: Unknown colour type "clrx".'); + expect(() => funcs.rich_repl_display(pair('test', 'clrt#gggggg'))) + .toThrow('rich_repl_display: Invalid html colour string "#gggggg". It should start with # and followed by 6 characters representing a hex number.'); + }); }); }); describe(funcs.default_js_slang, () => { it('default_js_slang throws when called', () => { expect(() => funcs.default_js_slang('')) - .toThrowError('Invaild Call: Function "default_js_slang" can not be directly called by user\'s code in editor. You should use it as the parameter of the function "set_evaluator"'); + .toThrow('default_js_slang: Cannot be called directly. You should use it as the parameter of the function "set_evaluator"'); }); }); describe(funcs.set_evaluator, () => { it('returns a value that indicates that the repl is initialized', () => { - expect(stringify(funcs.set_evaluator(() => 0))).toEqual(''); + const f = (_t: string) => 0; + expect(stringify(funcs.set_evaluator(f))).toEqual(''); + expect(repl.evalFunction).toBe(f); }); it('throws when the parameter isn\'t a function', () => { expect(() => funcs.set_evaluator(0 as any)) - .toThrowError('set_evaluator expects a function as parameter'); + .toThrow('set_evaluator: Expected function with 1 parameter, got 0.'); + }); +}); + +describe(funcs.set_background_image, () => { + it('sets the background image and alpha', () => { + funcs.set_background_image('https://example.com/image.png', 0.5); + expect(repl.customizedEditorProps.backgroundImageUrl).toBe('https://example.com/image.png'); + expect(repl.customizedEditorProps.backgroundColorAlpha).toBe(0.5); + }); + + it('throws when the alpha is out of range', () => { + expect(() => funcs.set_background_image('https://example.com/image.png', -0.1)) + .toThrow('set_background_image: Expected number between 0 and 1 for background_color_alpha, got -0.1.'); + + expect(() => funcs.set_background_image('https://example.com/image.png', 1.5)) + .toThrow('set_background_image: Expected number between 0 and 1 for background_color_alpha, got 1.5.'); + }); + + it('throws when the image url isn\'t a string', () => { + expect(() => funcs.set_background_image(0 as any, 0.5)) + .toThrow('set_background_image: Expected string for img_url, got 0.'); + }); +}); + +describe(funcs.set_font_size, () => { + it('sets the font size', () => { + funcs.set_font_size(20); + expect(repl.customizedEditorProps.fontSize).toBe(20); + }); + + it('throws when the font size is invalid', () => { + expect(() => funcs.set_font_size(-1)) + .toThrow('set_font_size: Expected integer greater than 0, got -1.'); + + expect(() => funcs.set_font_size(0.5)) + .toThrow('set_font_size: Expected integer greater than 0, got 0.5.'); + + expect(() => funcs.set_font_size('invalid' as any)) + .toThrow('set_font_size: Expected integer greater than 0, got "invalid".'); }); }); diff --git a/src/bundles/repl/src/functions.ts b/src/bundles/repl/src/functions.ts index b2302e6c5d..99aabd5711 100644 --- a/src/bundles/repl/src/functions.ts +++ b/src/bundles/repl/src/functions.ts @@ -4,11 +4,16 @@ * @author Wang Zihan */ +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; import context from 'js-slang/context'; +import type { Value } from 'js-slang/dist/types'; +import { stringify } from 'js-slang/dist/utils/stringify'; import { COLOR_REPL_DISPLAY_DEFAULT } from './config'; -import { ProgrammableRepl } from './programmable_repl'; +import { ProgrammableRepl, processRichDisplayContent, type RichDisplayContent } from './programmable_repl'; -const INSTANCE = new ProgrammableRepl(); +/* Exported for testing */ +export const INSTANCE = new ProgrammableRepl(); context.moduleContexts.repl.state = INSTANCE; /** @@ -25,9 +30,8 @@ context.moduleContexts.repl.state = INSTANCE; * @category Main */ export function set_evaluator(evalFunc: (code: string) => any) { - if (typeof evalFunc !== 'function') { - throw new Error(`${set_evaluator.name} expects a function as parameter`); - } + assertFunctionOfLength(evalFunc, 1, set_evaluator.name); + INSTANCE.evalFunction = evalFunc; return { toReplString: () => '' @@ -36,25 +40,32 @@ export function set_evaluator(evalFunc: (code: string) => any) { /** * Display message in Programmable Repl Tab - * If you give a pair as the parameter, it will use the given pair to generate rich text and use rich text display mode to display the string in Programmable Repl Tab with undefined return value (see module description for more information). - * If you give other things as the parameter, it will simply display the toString value of the parameter in Programmable Repl Tab and returns the displayed string itself. + * If given a pair as the parameter, it will use the given pair to generate rich text and use rich text display mode to display + * the string in Programmable Repl Tab * * **Rich Text Display** - * - First you need to `import { repl_display } from "repl";` + * - First you need to `import { rich_repl_display } from "repl";` * - Format: pair(pair("string",style),style)... * - Examples: * * ```js * // A large italic underlined "Hello World" - * repl_display(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic")); + * rich_repl_display(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic")); * * // A large italic underlined "Hello World" in blue - * repl_display(pair(pair(pair(pair(pair("Hello World", "underline"),"italic"), "bold"), "gigantic"), "clrt#0000ff")); + * rich_repl_display(pair(pair(pair(pair(pair("Hello World", "underline"),"italic"), "bold"), "gigantic"), "clrt#0000ff")); * * // A large italic underlined "Hello World" with orange foreground and purple background - * repl_display(pair(pair(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic"), "clrb#A000A0"),"clrt#ff9700")); + * rich_repl_display(pair(pair(pair(pair(pair(pair("Hello World", "underline"), "italic"), "bold"), "gigantic"), "clrb#A000A0"),"clrt#ff9700")); + * ``` + * To display rich text from within REPL code, you should use the `raw_display` function instead: + * + * ```js + * raw_display(pair("Hello World", "gigantic")); * ``` * + * If the content you provided isn't valid as rich text, then it will be treated as a regular object and displayed as regular text. + * * - Coloring: * - `clrt` stands for text color, `clrb` stands for background color. The color string are in hexadecimal begin with "#" and followed by 6 hexadecimal digits. * - Example: `pair("123","clrt#ff0000")` will produce a red "123"; `pair("456","clrb#00ff00")` will produce a green "456". @@ -72,12 +83,25 @@ export function set_evaluator(evalFunc: (code: string) => any) { * @param content the content you want to display * @category Main */ -export function repl_display(content: any): any { - if (INSTANCE.richDisplayInternal(content) === 'not_rich_text_pair') { - INSTANCE.pushOutputString(content.toString(), COLOR_REPL_DISPLAY_DEFAULT, 'plaintext');// students may set the value of the parameter "str" to types other than a string (for example "repl_display(1)" ). So here I need to first convert the parameter "str" into a string before preceding. - return content; - } - return undefined; +export function rich_repl_display(content: RichDisplayContent): RichDisplayContent { + const result = processRichDisplayContent(content, rich_repl_display.name); + const output = `', 'script', 'javascript', 'eval', 'document', 'window', 'console', 'location']; + for (const word of forbiddenWords) { + if (tmp.indexOf(word) !== -1) { + return word; + } + } + return 'safe'; +} + +const pairStyleToCssStyle: { [pairStyle: string]: string } = { + bold: 'font-weight:bold;', + italic: 'font-style:italic;', + small: 'font-size: 14px;', + medium: 'font-size: 20px;', + large: 'font-size: 25px;', + gigantic: 'font-size: 50px;', + underline: 'text-decoration: underline;' +}; + +/** + * Checks if the given string is a valid hex color identifier + */ +function checkColorStringValidity(htmlColor: string) { + return /#[0-9a-f]{6}/.test(htmlColor.toLowerCase()); +} + +export function processRichDisplayContent(pair_rich_text: RichDisplayContent, func_name: string): string { + if (typeof pair_rich_text === 'string') { + // There MUST be a safe check on users' strings, because users may insert something that can be interpreted as executable JavaScript code when outputing rich text. + const safeCheckResult = xssStringCheck(pair_rich_text); + if (safeCheckResult !== 'safe') { + throw new GeneralRuntimeError(`${func_name}: For safety, the character/word ${safeCheckResult} is not allowed in rich text output. Please remove it or use plain text output mode and try again.`); + } + return `">${pair_rich_text}`; + } + + if (!is_pair(pair_rich_text)) { + throw new InvalidParameterTypeError('pair or string', pair_rich_text, func_name); + } + + const config_str = tail(pair_rich_text); + if (typeof config_str !== 'string') { + throw new GeneralRuntimeError(`${func_name}: The tail in style pair should always be a string, but got ${config_str}.`); + } + let style = ''; + if (config_str.substring(0, 3) === 'clr') { + let prefix: string; + switch (config_str[3]) { + case 't': { + prefix = 'color'; + break; + } + case 'b': { + prefix = 'background-color'; + break; + } + default: + throw new GeneralRuntimeError(`${func_name}: Unknown colour type "${config_str.substring(0, 4)}".`); + } + + const colorHex = config_str.substring(4); + + if (!checkColorStringValidity(colorHex)) { + throw new GeneralRuntimeError(`${func_name}: Invalid html colour string "${colorHex}". It should start with # and followed by 6 characters representing a hex number.`); + } + + style = `${prefix}:${colorHex};`; + } else { + style = pairStyleToCssStyle[config_str]; + if (style === undefined) { + throw new GeneralRuntimeError(`${func_name}: Found undefined style "${config_str}" while processing rich text.`); + } + } + return style + processRichDisplayContent(head(pair_rich_text), func_name); +} + export class ProgrammableRepl { - public evalFunction: (code: string) => any; - public userCodeInEditor: string; - public outputStrings: any[]; - private _editorInstance; - private _tabReactComponent: any; + public evalFunction: ((code: string) => any) | null; + public outputStrings: OutputStringEntry[]; + public tabRerenderer?: () => void; + + /** + * Function to call when user code is updated **after** the editor + * has been rendered + */ + public updateUserCode?: (newCode: string) => void; + + /** + * Code that gets displayed when the editor is first rendered. If this is not set, the editor + * will try and load from `localStorage`. + */ + public defaultCode?: string; + // I store editorHeight value separately in here although it is already stored in the module's Tab React component state because I need to keep the editor height // when the Tab component is re-mounted due to the user drags the area between the module's Tab and Source Academy's original REPL to resize the module's Tab height. public editorHeight: number; - public customizedEditorProps = { - backgroundImageUrl: 'no-background-image', + public customizedEditorProps: CustomEditorProps = { + backgroundImageUrl: null, backgroundColorAlpha: 1, fontSize: 17 }; constructor() { - this.evalFunction = () => this.easterEggFunction(); - this.userCodeInEditor = this.getSavedEditorContent(); + this.evalFunction = null; this.outputStrings = []; - this._editorInstance = null;// To be set when calling "SetEditorInstance" in the ProgrammableRepl Tab React Component render function. this.editorHeight = DEFAULT_EDITOR_HEIGHT; - developmentLog(this); } InvokeREPL_Internal(evalFunc: (code: string) => any) { this.evalFunction = evalFunc; } - runCode() { + async runCode(code: string, context: Context) { this.outputStrings = []; let retVal: any; - try { - if (evaluatorSymbol in this.evalFunction) { - retVal = this.runInJsSlang(this.userCodeInEditor); - } else { - retVal = this.evalFunction(this.userCodeInEditor); + + if (this.evalFunction === null) { + retVal = this.easterEggFunction(); + } else if (evaluatorSymbol in this.evalFunction) { + const evalResult = await this.runInJsSlang(code, context); + + if (evalResult.status !== 'finished') { + this.reRenderTab(); + return; } - } catch (exception: any) { - developmentLog(exception); - // If the exception has a start line of -1 and an undefined error property, then this exception is most likely to be "incorrect number of arguments" caused by incorrect number of parameters in the evaluator entry function provided by students with set_evaluator. - if (exception.location.start.line === -1 && exception.error === undefined) { - this.pushOutputString('Error: Unable to use your evaluator to run the code. Does your evaluator entry function contain and only contain exactly one parameter?', COLOR_ERROR_MESSAGE); - } else { + + retVal = evalResult.value; + } else { + try { + retVal = this.evalFunction(code); + } catch (exception: any) { + console.error(exception); this.pushOutputString(`Line ${exception.location.start.line.toString()}: ${exception.error?.message}`, COLOR_ERROR_MESSAGE); + this.reRenderTab(); + return; } - this.reRenderTab(); - return; - } - if (typeof retVal === 'string') { - retVal = `"${retVal}"`; } + // Here must use plain text output mode because retVal contains strings from the users. - this.pushOutputString(retVal, COLOR_RUN_CODE_RESULT); + this.pushOutputString(typeof retVal === 'string' ? retVal : stringify(retVal), COLOR_RUN_CODE_RESULT); this.reRenderTab(); - developmentLog('RunCode finished'); - } - - updateUserCode(code: string) { - this.userCodeInEditor = code; } - // Rich text output method allow output strings to have html tags and css styles. - pushOutputString(content: string, textColor: string, outputMethod: string = 'plaintext') { + /** + * Method for outputting to the REPL instance's own REPL output area. + * Rich text output method allow output strings to have html tags and css styles. + */ + pushOutputString(content: string, textColor: string, outputMethod: OutputStringMethods = 'plaintext') { const tmp = { content: content === undefined ? 'undefined' : content === null ? 'null' : content, color: textColor, @@ -81,164 +191,61 @@ export class ProgrammableRepl { this.outputStrings.push(tmp); } - setEditorInstance(instance: any) { - if (instance === undefined) return; // It seems that when calling this function in gui->render->ref, the React internal calls this function for multiple times (at least two times) , and in at least one call the parameter 'instance' is set to 'undefined'. If I don't add this if statement, the program will throw a runtime error when rendering tab. - this._editorInstance = instance; - this._editorInstance.on('guttermousedown', (e) => { - const breakpointLine = e.getDocumentPosition().row; - developmentLog(breakpointLine); - }); - - this._editorInstance.setOptions({ fontSize: `${this.customizedEditorProps.fontSize.toString()}pt` }); - } - - richDisplayInternal(pair_rich_text) { - developmentLog(pair_rich_text); - const head = (pair) => pair[0]; - const tail = (pair) => pair[1]; - const is_pair = (obj) => obj instanceof Array && obj.length === 2; - if (!is_pair(pair_rich_text)) return 'not_rich_text_pair'; - function checkColorStringValidity(htmlColor: string) { - if (htmlColor.length !== 7) return false; - if (htmlColor[0] !== '#') return false; - for (let i = 1; i < 7; i++) { - const char = htmlColor[i]; - developmentLog(` ${char}`); - if (!((char >= '0' && char <= '9') || (char >= 'A' && char <= 'F') || (char >= 'a' && char <= 'f'))) { - return false; - } - } - return true; - } - function recursiveHelper(thisInstance, param): string { - if (typeof param === 'string') { - // There MUST be a safe check on users' strings, because users may insert something that can be interpreted as executable JavaScript code when outputing rich text. - const safeCheckResult = thisInstance.userStringSafeCheck(param); - if (safeCheckResult !== 'safe') { - throw new Error(`For safety matters, the character/word ${safeCheckResult} is not allowed in rich text output. Please remove it or use plain text output mode and try again.`); - } - developmentLog(head(param)); - return `">${param}`; - } - if (!is_pair(param)) { - throw new Error(`Unexpected data type ${typeof param} when processing rich text. It should be a pair.`); - } else { - const pairStyleToCssStyle: { [pairStyle: string]: string } = { - bold: 'font-weight:bold;', - italic: 'font-style:italic;', - small: 'font-size: 14px;', - medium: 'font-size: 20px;', - large: 'font-size: 25px;', - gigantic: 'font-size: 50px;', - underline: 'text-decoration: underline;' - }; - if (typeof tail(param) !== 'string') { - throw new Error(`The tail in style pair should always be a string, but got ${typeof tail(param)}.`); - } - let style = ''; - if (tail(param) - .substring(0, 3) === 'clr') { - let prefix = ''; - if (tail(param)[3] === 't') prefix = 'color:'; - else if (tail(param)[3] === 'b') prefix = 'background-color:'; - else throw new Error('Error when decoding rich text color data'); - const colorHex = tail(param) - .substring(4); - if (!checkColorStringValidity(colorHex)) { - throw new Error(`Invalid html color string ${colorHex}. It should start with # and followed by 6 characters representing a hex number.`); - } - style = `${prefix + colorHex};`; - } else { - style = pairStyleToCssStyle[tail(param)]; - if (style === undefined) { - throw new Error(`Found undefined style ${tail(param)} during processing rich text.`); - } - } - return style + recursiveHelper(thisInstance, head(param)); - } - } - this.pushOutputString(`', 'script', 'javascript', 'eval', 'document', 'window', 'console', 'location']; - for (const word of forbiddenWords) { - if (tmp.indexOf(word) !== -1) { - return word; - } - } - return 'safe'; - } - /* Directly invoking Source Academy's builtin js-slang runner. Needs hard-coded support from js-slang part for the "sourceRunner" function and "backupContext" property in the content object for this to work. */ - runInJsSlang(code: string): string { - developmentLog('js-slang context:'); - // console.log(context); + async runInJsSlang(code: string, context: Context): Promise { const options: Partial = { originalMaxExecTime: 1000, stepLimit: 1000, throwInfiniteLoops: true, useSubst: false }; - context.prelude = 'const display=(x)=>repl_display(x);'; - context.errors = []; // Here if I don't manually clear the "errors" array in context, the remaining errors from the last evaluation will stop the function "preprocessFileImports" in preprocessor.ts of js-slang thus stop the whole evaluation. - const sourceFile: Record = { - '/ReplModuleUserCode.js': code - }; - runFilesInContext(sourceFile, '/ReplModuleUserCode.js', context, options) - .then((evalResult) => { - if (evalResult.status === 'suspended-cse-eval') { - throw new Error('This should not happen'); - } - if (evalResult.status !== 'error') { - this.pushOutputString('js-slang program finished with value:', COLOR_RUN_CODE_RESULT); - // Here must use plain text output mode because evalResult.value contains strings from the users. - this.pushOutputString(evalResult.value === undefined ? 'undefined' : evalResult.value.toString(), COLOR_RUN_CODE_RESULT); - } else { - const errors = context.errors; - console.log(errors); - const errorCount = errors.length; - for (let i = 0; i < errorCount; i++) { - const error = errors[i]; - if (error.explain() - .indexOf('Name repl_display not declared.') !== -1) { - this.pushOutputString('[Error] It seems that you haven\'t imported the function "repl_display" correctly when calling "set_evaluator" in Source Academy\'s main editor.', COLOR_ERROR_MESSAGE); - } else this.pushOutputString(`Line ${error.location.start.line}: ${error.type} Error: ${error.explain()} (${error.elaborate()})`, COLOR_ERROR_MESSAGE); + const evalContext = createContext( + context.chapter, + context.variant, + context.languageOptions, + context.externalSymbols, + context.externalContext, + { + rawDisplay: value => { + if (is_pair(value)) { + try { + // Try to decode the value as rich display content + const result = processRichDisplayContent(value as any, 'display'); + const output = ` ({ @@ -28,45 +28,38 @@ vi.mock(import('three'), async importOriginal => { }); const mockedMeshFactory = vi.mocked(MeshFactory); +mockedMeshFactory.addCuboid.mockReturnValue(new THREE.Mesh()); + const mockedEntityFactory = vi.mocked(EntityFactory); describe(ChassisWrapper, () => { - let physicsMock; - let rendererMock; - let chassisWrapper; - let config; - - beforeEach(() => { - physicsMock = vi.fn() as unknown as Physics; - rendererMock = { add:vi.fn() } as unknown as Renderer; - config = { + const it = baseIt + .extend('physicsMock', () => vi.fn() as unknown as Physics) + .extend('rendererMock', { add:vi.fn() } as unknown as Renderer) + .extend('config', { dimension: { width: 1, height: 1, depth: 1 }, orientation: { x: 0, y: 0, z: 0, w: 1 }, debug: true - }; + } as unknown as ChassisWrapperConfig) + .extend('chassisWrapper', ({ physicsMock, rendererMock, config }) => new ChassisWrapper(physicsMock, rendererMock, config)); - mockedMeshFactory.addCuboid.mockReturnValue(new THREE.Mesh()); - chassisWrapper = new ChassisWrapper(physicsMock, rendererMock, config); - - }); - - it('should initialize with a debug mesh if debug is true', () => { + it('should initialize with a debug mesh if debug is true', ({ rendererMock, chassisWrapper, config }) => { expect(MeshFactory.addCuboid).toHaveBeenCalledWith({ orientation: config.orientation, dimension: config.dimension, color: expect.any(THREE.Color), debug: true }); - expect(rendererMock.add).toBeCalled(); + expect(rendererMock.add).toHaveBeenCalledOnce(); expect(chassisWrapper.debugMesh.visible).toBe(true); }); - it('should throw if getEntity is called before chassis is initialized', () => { + it('should throw if getEntity is called before chassis is initialized', ({ chassisWrapper }) => { expect(chassisWrapper.chassis).toBe(null); expect(() => chassisWrapper.getEntity()).toThrow('Chassis not initialized'); }); - it('should correctly initialize the chassis entity on start', async () => { + it('should correctly initialize the chassis entity on start', async ({ chassisWrapper, physicsMock, config }) => { const mockEntity = { getTranslation: vi.fn(), getRotation: vi.fn() }; mockedEntityFactory.addCuboid.mockReturnValue(mockEntity as any); await chassisWrapper.start(); @@ -75,12 +68,13 @@ describe(ChassisWrapper, () => { expect(EntityFactory.addCuboid).toHaveBeenCalledWith(physicsMock, config); }); - it('should update the position and orientation of the debug mesh to match the chassis entity', () => { + it('should update the position and orientation of the debug mesh to match the chassis entity', ({ chassisWrapper }) => { const mockEntity = { getTranslation: vi.fn().mockReturnValue(new THREE.Vector3()), getRotation: vi.fn().mockReturnValue(new THREE.Quaternion()) - }; - mockedEntityFactory.addCuboid.mockReturnValue(mockEntity as any); + } as any; + + mockedEntityFactory.addCuboid.mockReturnValue(mockEntity); chassisWrapper.chassis = mockEntity; chassisWrapper.update(); diff --git a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Mesh.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Mesh.test.ts index f359403814..dfca85d2a9 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Mesh.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Mesh.test.ts @@ -1,9 +1,9 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Renderer } from '../../../../engine'; +import { describe, expect, it as baseIt, vi } from 'vitest'; +import type { Renderer } from '../../../../engine'; import { loadGLTF } from '../../../../engine/Render/helpers/GLTF'; import { ChassisWrapper } from '../Chassis'; -import { Mesh } from '../Mesh'; +import { Mesh, type MeshConfig } from '../Mesh'; vi.mock(import('three'), async importOriginal => { return { @@ -43,14 +43,9 @@ vi.mock(import('../../../../engine'), () => ({ }) as any); describe(Mesh, () => { - let mesh; - let mockChassisWrapper; - let mockRenderer; - let mockConfig; - - beforeEach(() => { - mockRenderer = { add: vi.fn() } as unknown as Renderer; - mockChassisWrapper = { + const it = baseIt + .extend('mockRenderer', { add: vi.fn() } as unknown as Renderer) + .extend('mockChassisWrapper', { getEntity: vi.fn().mockReturnValue({ getTranslation: vi.fn().mockReturnValue(new THREE.Vector3()), getRotation: vi.fn().mockReturnValue(new THREE.Quaternion()), @@ -68,38 +63,32 @@ describe(Mesh, () => { z: 0, w: 1, }, - }, + } } - } as unknown as ChassisWrapper; - mockConfig = { + } as unknown as ChassisWrapper) + .extend('mockConfig', { url: 'path/to/mesh', dimension: { width: 1, height: 2, depth: 3 }, offset: { x: 0.5, y: 0.5, z: 0.5 }, - }; - - // mockLoadGLTF.mockResolvedValue({ - // scene: new THREE.GLTF().scene - // } as any); - - mesh = new Mesh(mockChassisWrapper, mockRenderer, mockConfig); - }); + } as unknown as MeshConfig) + .extend('mesh', ({ mockChassisWrapper, mockRenderer, mockConfig }) => new Mesh(mockChassisWrapper, mockRenderer, mockConfig)); - it('should initialize correctly with given configurations', () => { + it('should initialize correctly with given configurations', ({ mesh, mockConfig }) => { expect(mesh.config.url).toBe(mockConfig.url); expect(mesh.offset.x).toBe(0.5); }); - it('should load the mesh and add it to the renderer on start', async () => { + it('should load the mesh and add it to the renderer on start', async ({ mesh, mockConfig, mockRenderer }) => { await mesh.start(); expect(loadGLTF).toHaveBeenCalledWith(mockConfig.url, mockConfig.dimension); expect(mockRenderer.add).toHaveBeenCalledWith(expect.any(Object)); // Checks if mesh scene is added to renderer }); - it('should update mesh position and orientation according to chassis', async () => { + it('should update mesh position and orientation according to chassis', async ({ mesh }) => { await mesh.start(); - mesh.update({ residualFactor: 0.5 }); + mesh.update({ residualFactor: 0.5 } as any); - expect(mesh.mesh.scene.position.copy).toHaveBeenCalled(); - expect(mesh.mesh.scene.quaternion.copy).toHaveBeenCalled(); + expect(mesh.mesh!.scene.position.copy).toHaveBeenCalled(); + expect(mesh.mesh!.scene.quaternion.copy).toHaveBeenCalled(); }); }); diff --git a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Motor.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Motor.test.ts index 5d0b75c510..8a758fda76 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Motor.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Motor.test.ts @@ -1,10 +1,10 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it as baseIt, vi } from 'vitest'; import { Physics, Renderer } from '../../../../engine'; import { loadGLTF } from '../../../../engine/Render/helpers/GLTF'; import { ev3Config } from '../../ev3/default/config'; import { ChassisWrapper } from '../Chassis'; -import { Motor } from '../Motor'; +import { Motor, type MotorConfig } from '../Motor'; vi.mock(import('../../../../engine/Render/helpers/GLTF'), () => ({ loadGLTF: vi.fn().mockResolvedValue({ @@ -42,18 +42,10 @@ vi.mock(import('../Chassis'), () => ({ } as any)); describe(Motor, () => { - let motor; - let mockChassisWrapper; - let mockPhysics; - let mockRenderer; - let mockConfig; - - beforeEach(() => { - mockPhysics = { - applyImpulse: vi.fn(), - } as unknown as Physics; - mockRenderer = { add: vi.fn() } as unknown as Renderer; - mockConfig = { + const it = baseIt + .extend('mockPhysics', { applyImpulse: vi.fn() } as unknown as Physics) + .extend('mockRenderer', { add: vi.fn() } as unknown as Renderer) + .extend('mockConfig', { displacement: { x: 1, y: 0, z: 0 }, pid: { proportionalGain: 1, @@ -64,18 +56,15 @@ describe(Motor, () => { url: 'path/to/mesh', dimension: { height: 1, width: 1, depth: 1 }, }, - }; - const config = ev3Config.motors[0]; - mockChassisWrapper = new ChassisWrapper(mockPhysics, mockRenderer, config); - motor = new Motor( - mockChassisWrapper, - mockPhysics, - mockRenderer, - mockConfig + } as unknown as MotorConfig) + // @ts-expect-error Ignore ev3config errors + .extend('mockChassisWrapper', ({ mockPhysics, mockRenderer }) => new ChassisWrapper(mockPhysics, mockRenderer, ev3Config.motors[0])) + .extend( + 'motor', + ({ mockChassisWrapper, mockConfig, mockPhysics, mockRenderer }) => new Motor(mockChassisWrapper, mockPhysics, mockRenderer, mockConfig ) ); - }); - it('should initialize correctly and load the mesh', async () => { + it('should initialize correctly and load the mesh', async ({ motor, mockConfig, mockRenderer }) => { await motor.start(); expect(loadGLTF).toHaveBeenCalledWith( mockConfig.mesh.url, @@ -84,29 +73,29 @@ describe(Motor, () => { expect(mockRenderer.add).toHaveBeenCalled(); }); - it('sets motor velocity and schedules stop with distance', () => { + it('sets motor velocity and schedules stop with distance', ({ motor }) => { motor.setSpeedDistance(10, 100); expect(motor.motorVelocity).toBe(10); }); - it('updates the motor velocity and applies impulse', () => { - motor.fixedUpdate({ deltaTime: 1 }); + it('updates the motor velocity and applies impulse', ({ motor, mockChassisWrapper }) => { + motor.fixedUpdate({ deltaTime: 1 } as any); expect(mockChassisWrapper.getEntity().applyImpulse).toHaveBeenCalled(); }); - it('updates mesh', async () => { + it('updates mesh', async ({ motor }) => { await motor.start(); - motor.update({ frameDuration: 1 }); + motor.update({ frameDuration: 1 } as any); - expect(motor.mesh.scene.position.copy).toBeCalled(); - expect(motor.mesh.scene.quaternion.copy).toBeCalled(); + expect(motor.mesh!.scene.position.copy).toBeCalled(); + expect(motor.mesh!.scene.quaternion.copy).toBeCalled(); }); - it('rotates the mesh if on the left side', async () => { + it('rotates the mesh if on the left side', async ({ motor }) => { motor.wheelSide = 'left'; await motor.start(); - motor.update({ frameDuration: 1 }); + motor.update({ frameDuration: 1 } as any); - expect(motor.mesh.scene.rotateZ).toBeCalled(); + expect(motor.mesh!.scene.rotateZ).toHaveBeenCalledOnce(); }); }); diff --git a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Wheel.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Wheel.test.ts index bb5ef0ed94..0d75221bb2 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Wheel.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/components/__tests__/Wheel.test.ts @@ -1,6 +1,8 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Wheel } from '../Wheel'; +import { describe, expect, it as baseIt, vi } from 'vitest'; +import type { Physics, Renderer } from '../../../../engine'; +import type { ChassisWrapper } from '../Chassis'; +import { Wheel, type WheelConfig } from '../Wheel'; vi.mock(import('../../../../engine/Render/debug/DebugArrow'), () => ({ DebugArrow: class { @@ -10,20 +12,10 @@ vi.mock(import('../../../../engine/Render/debug/DebugArrow'), () => ({ } as any)); describe(Wheel, () => { - let wheel; - let mockChassisWrapper; - let mockPhysics; - let mockRenderer; - let mockConfig; - - beforeEach(() => { - mockPhysics = { - castRay: vi.fn(), - }; - mockRenderer = { - add: vi.fn(), - }; - mockChassisWrapper = { + const it = baseIt + .extend('mockPhysics', { castRay: vi.fn() } as unknown as Physics) + .extend('mockRenderer', { add: vi.fn() } as unknown as Renderer) + .extend('mockChassisWrapper', { getEntity: vi.fn().mockReturnValue({ worldTranslation: vi.fn().mockImplementation(() => new THREE.Vector3()), transformDirection: vi.fn().mockImplementation(() => new THREE.Vector3()), @@ -31,8 +23,8 @@ describe(Wheel, () => { getMass: vi.fn().mockReturnValue(1), getCollider: vi.fn().mockReturnValue({}), }), - }; - mockConfig = { + } as unknown as ChassisWrapper) + .extend('mockConfig', { displacement: { x: 1, y: 0, z: 0 }, pid: { proportionalGain: 1, @@ -42,29 +34,26 @@ describe(Wheel, () => { gapToFloor: 0.5, maxRayDistance: 5, debug: true, - }; - wheel = new Wheel( - mockChassisWrapper, - mockPhysics, - mockRenderer, - mockConfig + } as WheelConfig) + .extend( + 'wheel', + ({ mockChassisWrapper, mockPhysics, mockRenderer, mockConfig }) => new Wheel(mockChassisWrapper, mockPhysics, mockRenderer, mockConfig) ); - }); - it('should initialize with a debug arrow if debug is true', () => { + it('should initialize with a debug arrow if debug is true', ({ wheel, mockRenderer }) => { expect(wheel.arrowHelper).toBeDefined(); expect(mockRenderer.add).toHaveBeenCalled(); }); - it('should correctly calculate physics interactions in fixedUpdate', () => { + it('should correctly calculate physics interactions in fixedUpdate', ({ mockPhysics, wheel, mockChassisWrapper, mockConfig }) => { const timingInfo = { timestep: 16 }; // 16 ms timestep const mockResult = { distance: 0.3, normal: new THREE.Vector3(0, 1, 0), }; - mockPhysics.castRay.mockReturnValue(mockResult); + vi.mocked(mockPhysics.castRay).mockReturnValue(mockResult); - wheel.fixedUpdate(timingInfo); + wheel.fixedUpdate(timingInfo as any); expect(mockPhysics.castRay).toHaveBeenCalledWith( expect.any(THREE.Vector3), @@ -76,24 +65,24 @@ describe(Wheel, () => { expect(wheel.arrowHelper.update).toHaveBeenCalled(); }); - it('should handle null result from castRay indicating no ground contact', () => { + it('should handle null result from castRay indicating no ground contact', ({ mockPhysics, mockChassisWrapper, wheel }) => { const timingInfo = { timestep: 16 }; - mockPhysics.castRay.mockReturnValue(null); + vi.mocked(mockPhysics.castRay).mockReturnValue(null); - wheel.fixedUpdate(timingInfo); + wheel.fixedUpdate(timingInfo as any); expect(mockChassisWrapper.getEntity().applyImpulse).not.toHaveBeenCalled(); }); - it('if wheelDistance is 0, the normal should be pointing up', () => { + it('if wheelDistance is 0, the normal should be pointing up', ({ wheel, mockPhysics, mockChassisWrapper }) => { const timingInfo = { timestep: 16 }; // 16 ms timestep const mockResult = { distance: 0, normal: new THREE.Vector3(0, 0, 0), }; - mockPhysics.castRay.mockReturnValue(mockResult); + vi.mocked(mockPhysics.castRay).mockReturnValue(mockResult); - wheel.fixedUpdate(timingInfo); + wheel.fixedUpdate(timingInfo as any); expect(mockChassisWrapper.getEntity().applyImpulse).toHaveBeenCalledWith( expect.objectContaining({ x: 0, z: 0 }), // y value can be anything diff --git a/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/PidController.ts b/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/PidController.ts index accc41d3cc..16cfdde262 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/PidController.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/PidController.ts @@ -21,7 +21,7 @@ type PIDControllerOptions = { derivativeGain: number; }; -class PIDController { +export class PIDController { zero: NullaryFunction; add: BinaryFunction; subtract: BinaryFunction; diff --git a/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/__tests__/PidController.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/__tests__/PidController.test.ts index f0c3a02559..2c7e1ce401 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/__tests__/PidController.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/feedback_control/__tests__/PidController.test.ts @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { describe, expect, it as baseIt } from 'vitest'; import { NumberPidController, VectorPidController } from '../PidController'; const resetPid = (pidController: NumberPidController) => { @@ -13,23 +13,19 @@ const resetVectorPid = (pidController: VectorPidController) => { }; describe(NumberPidController, () => { - let pidController; + const it = baseIt.extend('pidController', new NumberPidController({ + proportionalGain: 0.1, + integralGain: 0.01, + derivativeGain: 0.05, + })); - beforeEach(() => { - pidController = new NumberPidController({ - proportionalGain: 0.1, - integralGain: 0.01, - derivativeGain: 0.05, - }); - }); - - it('should initialize correctly', () => { + it('should initialize correctly', ({ pidController }) => { expect(pidController.proportionalGain).toEqual(0.1); expect(pidController.integralGain).toEqual(0.01); expect(pidController.derivativeGain).toEqual(0.05); }); - it('should calculate correct PID output', () => { + it('should calculate correct PID output', ({ pidController }) => { const setpoint = 10; let output; @@ -51,21 +47,16 @@ describe(NumberPidController, () => { }); describe(VectorPidController, () => { - let pidController; - - beforeEach(() => { - pidController = new VectorPidController({ - proportionalGain: 0.1, - integralGain: 0.01, - derivativeGain: 0.05, - }); - }); - - it('should initialize correctly', () => { + const it = baseIt.extend('pidController', new VectorPidController({ + proportionalGain: 0.1, + integralGain: 0.01, + derivativeGain: 0.05, + })); + it('should initialize correctly', ({ pidController }) => { expect(pidController.proportionalGain).toEqual(0.1); }); - it('should calculate correct PID output for vector inputs', () => { + it('should calculate correct PID output for vector inputs', ({ pidController }) => { const setpoint = new THREE.Vector3(10, 10, 10); let output; diff --git a/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/ColorSensor.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/ColorSensor.test.ts index 33ca39676a..9dc72d51df 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/ColorSensor.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/ColorSensor.test.ts @@ -1,6 +1,8 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ColorSensor } from '../ColorSensor'; +import { describe, expect, it as baseIt, vi } from 'vitest'; +import type { Renderer } from '../../../../engine'; +import type { ChassisWrapper } from '../../components/Chassis'; +import { ColorSensor, type ColorSensorConfig } from '../ColorSensor'; vi.mock(import('../../../../engine'), () => ({ Renderer: vi.fn(class { @@ -17,24 +19,19 @@ vi.mock(import('../../../../engine/Render/helpers/Camera'), () => ({ })); describe(ColorSensor, () => { - let sensor; - let mockChassisWrapper; - let mockRenderer; - let mockConfig; - - beforeEach(() => { - mockChassisWrapper = { + const it = baseIt + .extend('mockChassisWrapper', { getEntity: vi.fn(() => ({ worldTranslation: vi.fn().mockReturnValue(new THREE.Vector3()), })), - }; - mockRenderer = { + } as unknown as ChassisWrapper) + .extend('mockRenderer', { add: vi.fn(), scene: vi.fn(), render: vi.fn(), getElement: vi.fn(() => document.createElement('canvas')), - }; - mockConfig = { + } as unknown as Renderer) + .extend('mockConfig', { tickRateInSeconds: 0.1, displacement: { x: 0.04, @@ -53,10 +50,13 @@ describe(ColorSensor, () => { far: 1, }, debug: true, - }; - - sensor = new ColorSensor(mockChassisWrapper, mockRenderer, mockConfig); + } as ColorSensorConfig) + .extend( + 'sensor', + ({ mockChassisWrapper, mockRenderer, mockConfig }) => new ColorSensor(mockChassisWrapper, mockRenderer, mockConfig) + ); + it.beforeEach(() => { const mockCtx = { getImageData: vi.fn(() => ({ data: new Uint8ClampedArray([255, 255, 255, 255]), @@ -75,19 +75,19 @@ describe(ColorSensor, () => { }); }); - it('should initialize correctly', () => { + it('should initialize correctly', ({ sensor, mockRenderer }) => { expect(sensor).toBeDefined(); expect(mockRenderer.add).toHaveBeenCalled(); }); - it('should update color only after accumulating sufficient time', () => { + it('should update color only after accumulating sufficient time', ({ sensor, mockRenderer }) => { const timingInfo = { timestep: 50 }; - sensor.fixedUpdate(timingInfo); + sensor.fixedUpdate(timingInfo as any); expect(mockRenderer.render).not.toHaveBeenCalled(); - sensor.fixedUpdate(timingInfo); + sensor.fixedUpdate(timingInfo as any); }); - it('should give correct response for sense', () => { + it('should give correct response for sense', ({ sensor }) => { const colorSensed = { r: 10, g: 20, b: 30 }; sensor.colorSensed = colorSensed; const result = sensor.sense(); diff --git a/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/UltrasonicSensor.test.ts b/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/UltrasonicSensor.test.ts index ba94763461..4dd921176f 100644 --- a/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/UltrasonicSensor.test.ts +++ b/src/bundles/robot_simulation/src/controllers/ev3/sensor/__tests__/UltrasonicSensor.test.ts @@ -1,5 +1,7 @@ import * as THREE from 'three'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it as baseIt, vi } from 'vitest'; +import type { Physics, Renderer } from '../../../../engine'; +import type { ChassisWrapper } from '../../components/Chassis'; import { UltrasonicSensor } from '../UltrasonicSensor'; vi.mock(import('three'), () => ({ @@ -18,46 +20,37 @@ vi.mock(import('three'), () => ({ }) as any); describe(UltrasonicSensor, () => { - let sensor: UltrasonicSensor; - let mockChassisWrapper; - let mockPhysics; - let mockRenderer; - let mockConfig; - - beforeEach(() => { - mockChassisWrapper = { + const it = baseIt + .extend('mockChassisWrapper', { getEntity: vi.fn(() => ({ worldTranslation: vi.fn().mockReturnValue(new THREE.Vector3()), transformDirection: vi.fn().mockReturnValue(new THREE.Vector3()), getCollider: vi.fn() })) - }; - mockPhysics = { - castRay: vi.fn().mockReturnValue({ distance: 5 }) - }; - mockRenderer = { - add: vi.fn() - }; - mockConfig = { + } as unknown as ChassisWrapper) + .extend('mockPhysics', { castRay: vi.fn().mockReturnValue({ distance: 5 }) } as unknown as Physics) + .extend('mockRenderer', { add: vi.fn() } as unknown as Renderer) + .extend('mockConfig', { displacement: { x: 1, y: 1, z: 1 }, direction: { x: 0, y: 1, z: 0 }, debug: true - }; - - sensor = new UltrasonicSensor(mockChassisWrapper, mockPhysics, mockRenderer, mockConfig); - }); + }) + .extend( + 'sensor', + ({ mockChassisWrapper, mockPhysics, mockRenderer, mockConfig }) => new UltrasonicSensor(mockChassisWrapper, mockPhysics, mockRenderer, mockConfig) + ); - it('should create instances and set initial properties', () => { + it('should create instances and set initial properties', ({ sensor, mockRenderer }) => { expect(sensor).toBeDefined(); expect(THREE.Vector3).toHaveBeenCalledTimes(2); // Called for displacement and direction expect(mockRenderer.add).toHaveBeenCalledWith(sensor.debugArrow); }); - it('should return initial distance sensed as 0', () => { + it('should return initial distance sensed as 0', ({ sensor }) => { expect(sensor.sense()).toEqual(0); }); - it('should calculate distance when fixedUpdate is called', () => { + it('should calculate distance when fixedUpdate is called', ({ sensor, mockPhysics }) => { sensor.fixedUpdate(); expect(sensor.distanceSensed).toEqual(5); expect(mockPhysics.castRay).toHaveBeenCalled(); @@ -65,8 +58,8 @@ describe(UltrasonicSensor, () => { expect(sensor.debugArrow.setDirection).toHaveBeenCalled(); }); - it('should handle null results from castRay indicating no collision detected', () => { - mockPhysics.castRay.mockReturnValue(null); + it('should handle null results from castRay indicating no collision detected', ({ sensor, mockPhysics }) => { + vi.mocked(mockPhysics.castRay).mockReturnValue(null); sensor.fixedUpdate(); expect(sensor.distanceSensed).toEqual(0); expect(mockPhysics.castRay).toHaveBeenCalled(); diff --git a/src/bundles/robot_simulation/src/controllers/program/Program.ts b/src/bundles/robot_simulation/src/controllers/program/Program.ts index b29d468028..948a49cd49 100644 --- a/src/bundles/robot_simulation/src/controllers/program/Program.ts +++ b/src/bundles/robot_simulation/src/controllers/program/Program.ts @@ -1,3 +1,4 @@ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import type { DeepPartial } from '@sourceacademy/modules-lib/types'; import type { IOptions } from 'js-slang'; import context from 'js-slang/context'; @@ -57,7 +58,7 @@ export class Program implements Controller { fixedUpdate() { try { if (!this.iterator) { - throw Error('Program not started'); + throw new GeneralRuntimeError('Program not started'); } if (this.isPaused) { diff --git a/src/bundles/robot_simulation/src/controllers/program/error.ts b/src/bundles/robot_simulation/src/controllers/program/error.ts index 6fe991fb56..1edfa5c9cb 100644 --- a/src/bundles/robot_simulation/src/controllers/program/error.ts +++ b/src/bundles/robot_simulation/src/controllers/program/error.ts @@ -1,6 +1,11 @@ -export class ProgramError extends Error { - constructor(message) { - super(message); - this.name = 'ProgramError'; +import { RuntimeSourceError } from '@sourceacademy/modules-lib/errors'; + +export class ProgramError extends RuntimeSourceError { + constructor(public readonly explanation: string) { + super(undefined); + } + + public override explain(): string { + return this.explanation; } } diff --git a/src/bundles/robot_simulation/src/controllers/program/evaluate.ts b/src/bundles/robot_simulation/src/controllers/program/evaluate.ts index 940d030875..5647052bdb 100644 --- a/src/bundles/robot_simulation/src/controllers/program/evaluate.ts +++ b/src/bundles/robot_simulation/src/controllers/program/evaluate.ts @@ -4,8 +4,9 @@ import { Stash, generateCSEMachineStateStream, } from 'js-slang/dist/cse-machine/interpreter'; +import { Variant } from 'js-slang/dist/langs'; import { parse } from 'js-slang/dist/parser/parser'; -import { Variant, type Context } from 'js-slang/dist/types'; +import type { Context } from 'js-slang/dist/types'; export const DEFAULT_SOURCE_OPTIONS = { scheduler: 'async', diff --git a/src/bundles/robot_simulation/src/engine/Core/Controller.ts b/src/bundles/robot_simulation/src/engine/Core/Controller.ts index 6d11663260..f65b8e80fc 100644 --- a/src/bundles/robot_simulation/src/engine/Core/Controller.ts +++ b/src/bundles/robot_simulation/src/engine/Core/Controller.ts @@ -62,7 +62,7 @@ export class ControllerGroup implements Controller { this.controllers.push(...controllers); } - async start?(): Promise { + async start(): Promise { await Promise.all(this.controllers.map(async (controller) => { await controller.start?.(); }),); diff --git a/src/bundles/robot_simulation/src/engine/Core/RobotConsole.ts b/src/bundles/robot_simulation/src/engine/Core/RobotConsole.ts index 950e601264..85c11e5059 100644 --- a/src/bundles/robot_simulation/src/engine/Core/RobotConsole.ts +++ b/src/bundles/robot_simulation/src/engine/Core/RobotConsole.ts @@ -13,7 +13,7 @@ export class RobotConsole { this.logs = []; } - log(message: string, level) { + log(message: string, level: LogLevel) { this.logs.push({ message, level, diff --git a/src/bundles/robot_simulation/src/engine/Core/__tests__/Controller.test.ts b/src/bundles/robot_simulation/src/engine/Core/__tests__/Controller.test.ts index bec91a6b1e..60e9e0e323 100644 --- a/src/bundles/robot_simulation/src/engine/Core/__tests__/Controller.test.ts +++ b/src/bundles/robot_simulation/src/engine/Core/__tests__/Controller.test.ts @@ -13,10 +13,15 @@ const createTimingInfo = () => { return { stepCount: 1, timestep: 2 } as PhysicsTimingInfo; }; -describe('ControllerMap methods', () => { +// Helper type to extract properties that are assignable to functions +type FunctionKeys = keyof { + [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K] +}; + +describe(ControllerMap, () => { // Define test cases in an array of arrays. Each inner array represents parameters for a single test case. const methodsTestData: Array< - [string, Mock, { async: boolean, args?: any[] }] + [FunctionKeys>>, Mock, { async: boolean, args?: any[] }] > = [ ['start', vi.fn(), { async: true }], ['update', vi.fn(), { async: false, args: [createTimingInfo()] }], @@ -38,11 +43,13 @@ describe('ControllerMap methods', () => { { [methodName]: mockMethod, notMethodName: notCalledMethod } ); + const method: any = controllerMap[methodName].bind(controllerMap); + // If the method is async, await it. Otherwise, call it directly. if (async) { - await controllerMap[methodName](...args); + await method(...args); } else { - controllerMap[methodName](...args); + method(...args); } // Assert that each controller's method was called once @@ -55,14 +62,16 @@ describe('ControllerMap methods', () => { ); test.each(methodsTestData)( - 'no calls if missing callbacks object', + '%s: no calls if missing callbacks object', async (methodName, mockMethod, { async, args = [] }) => { mockMethod.mockClear(); const controllerMap = new ControllerMap({}); + const method: any = controllerMap[methodName].bind(controllerMap); + if (async) { - await controllerMap[methodName](...args); + await method(...args); } else { - controllerMap[methodName](...args); + method(...args); } expect(mockMethod).toHaveBeenCalledTimes(0); } @@ -96,7 +105,7 @@ describe('ControllerMap methods', () => { describe(ControllerGroup, () => { // Define test data for each method - const methodsTestData: Array<[string, { async: boolean, args: any[] }]> = [ + const methodsTestData: Array<[FunctionKeys, { async: boolean, args: any[] }]> = [ ['start', { async: true, args: [] }], ['update', { async: false, args: [{ stepCount: 1, timestep: 20 }] }], // Assuming createTimingInfo() returns something similar ['fixedUpdate', { async: false, args: [{ stepCount: 2, timestep: 15 }] }], @@ -116,11 +125,13 @@ describe(ControllerGroup, () => { const controllerGroup = new ControllerGroup(); controllerGroup.addController(controller); + const method: any = controllerGroup[methodName].bind(controllerGroup); + // Execute the method if (async) { - await controllerGroup[methodName](...args); + await method(...args); } else { - controllerGroup[methodName](...args); + method(...args); } // Assertions @@ -149,10 +160,12 @@ describe(ControllerGroup, () => { const controllerGroup = new ControllerGroup(); controllerGroup.addController(controller); + const method: any = controllerGroup[methodName].bind(controllerGroup); + if (async) { - await controllerGroup[methodName](...args); + await method(...args); } else { - controllerGroup[methodName](...args); + method(...args); } expect(notCalledMethod).not.toHaveBeenCalled(); diff --git a/src/bundles/robot_simulation/src/engine/Core/__tests__/Timer.test.ts b/src/bundles/robot_simulation/src/engine/Core/__tests__/Timer.test.ts index 5ec6171238..acb62be1b3 100644 --- a/src/bundles/robot_simulation/src/engine/Core/__tests__/Timer.test.ts +++ b/src/bundles/robot_simulation/src/engine/Core/__tests__/Timer.test.ts @@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { Timer } from '../Timer'; // Adjust the import path as per your project structure describe(Timer, () => { - let timer; - let mockTimestamp; + let timer: Timer; + let mockTimestamp: number; beforeEach(() => { timer = new Timer(); diff --git a/src/bundles/robot_simulation/src/engine/Entity/Entity.ts b/src/bundles/robot_simulation/src/engine/Entity/Entity.ts index 3f9d004426..465e95a2a4 100644 --- a/src/bundles/robot_simulation/src/engine/Entity/Entity.ts +++ b/src/bundles/robot_simulation/src/engine/Entity/Entity.ts @@ -89,7 +89,7 @@ export class Entity { * @param localPoint - The point for which to calculate the tangential velocity. * @returns The tangential velocity vector of the point. */ - tangentialVelocityOfPoint(localPoint): THREE.Vector3 { + tangentialVelocityOfPoint(localPoint: THREE.Vector3): THREE.Vector3 { // Calculate the distance vector from the point to the rotational axis const distanceVector = this.distanceVectorOfPointToRotationalAxis(localPoint); diff --git a/src/bundles/robot_simulation/src/engine/Physics.ts b/src/bundles/robot_simulation/src/engine/Physics.ts index 748621750e..88633705c5 100644 --- a/src/bundles/robot_simulation/src/engine/Physics.ts +++ b/src/bundles/robot_simulation/src/engine/Physics.ts @@ -1,5 +1,5 @@ import rapier from '@dimforge/rapier3d-compat'; - +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import type * as THREE from 'three'; import { TypedEventTarget } from './Core/Events'; @@ -75,7 +75,7 @@ export class Physics extends TypedEventTarget { createRigidBody(rigidBodyDesc: rapier.RigidBodyDesc): rapier.RigidBody { if (this.internals.initialized === false) { - throw Error("Physics engine hasn't been initialized yet"); + throw new GeneralRuntimeError("Physics engine hasn't been initialized yet"); } return this.internals.world.createRigidBody(rigidBodyDesc); @@ -86,7 +86,7 @@ export class Physics extends TypedEventTarget { rigidBody: rapier.RigidBody, ): rapier.Collider { if (this.internals.initialized === false) { - throw Error("Physics engine hasn't been initialized yet"); + throw new GeneralRuntimeError("Physics engine hasn't been initialized yet"); } return this.internals.world.createCollider(colliderDesc, rigidBody); } @@ -101,7 +101,7 @@ export class Physics extends TypedEventTarget { normal: SimpleVector; } | null { if (this.internals.initialized === false) { - throw Error("Physics engine hasn't been initialized yet"); + throw new GeneralRuntimeError("Physics engine hasn't been initialized yet"); } const ray = new this.RAPIER.Ray(globalPosition, globalDirection); @@ -130,7 +130,7 @@ export class Physics extends TypedEventTarget { step(timing: FrameTimingInfo): PhysicsTimingInfo { if (this.internals.initialized === false) { - throw Error("Physics engine hasn't been initialized yet"); + throw new GeneralRuntimeError("Physics engine hasn't been initialized yet"); } const maxFrameTime = 0.05; diff --git a/src/bundles/robot_simulation/src/engine/Render/__tests__/MeshFactory.test.ts b/src/bundles/robot_simulation/src/engine/Render/__tests__/MeshFactory.test.ts index 06fa0ab0a8..a9cf6dad2b 100644 --- a/src/bundles/robot_simulation/src/engine/Render/__tests__/MeshFactory.test.ts +++ b/src/bundles/robot_simulation/src/engine/Render/__tests__/MeshFactory.test.ts @@ -16,7 +16,7 @@ vi.mock(import('three'), async importOriginal => { this.z = z; } - copy(vector) { + copy(vector: Vector3) { this.x = vector.x; this.y = vector.y; this.z = vector.z; @@ -37,7 +37,7 @@ vi.mock(import('three'), async importOriginal => { this.w = w; } - copy(quaternion) { + copy(quaternion: Quaternion) { this.x = quaternion.x; this.y = quaternion.y; this.z = quaternion.z; diff --git a/src/bundles/robot_simulation/src/engine/Render/helpers/Camera.ts b/src/bundles/robot_simulation/src/engine/Render/helpers/Camera.ts index 1838aee315..5b3a0ead92 100644 --- a/src/bundles/robot_simulation/src/engine/Render/helpers/Camera.ts +++ b/src/bundles/robot_simulation/src/engine/Render/helpers/Camera.ts @@ -1,3 +1,4 @@ +import { InternalRuntimeError } from '@sourceacademy/modules-lib/errors'; import * as THREE from 'three'; type OrthographicCameraOptions = { @@ -40,9 +41,8 @@ export function getCamera(cameraOptions: CameraOptions): THREE.Camera { return camera; } default: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _: never = cameraOptions; - throw new Error('Unknown camera type'); + // @ts-expect-error Ignore the never + throw new InternalRuntimeError(`Unknown camera type: ${cameraOptions.type}`); } } } diff --git a/src/bundles/robot_simulation/src/engine/__tests__/Physics.test.ts b/src/bundles/robot_simulation/src/engine/__tests__/Physics.test.ts index ebbd630a83..d57f72ea57 100644 --- a/src/bundles/robot_simulation/src/engine/__tests__/Physics.test.ts +++ b/src/bundles/robot_simulation/src/engine/__tests__/Physics.test.ts @@ -1,6 +1,6 @@ // physics.test.js import rapier from '@dimforge/rapier3d-compat'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { assert, describe, expect, test as baseTest, vi } from 'vitest'; import { Physics } from '../Physics'; // Mock rapier @@ -24,18 +24,15 @@ vi.mock(import('@dimforge/rapier3d-compat'), () => { }); describe(Physics, () => { - let physics; - const config = { gravity: { x: 0, y: -9.81, z: 0 }, timestep: 1 / 60 }; + const test = baseTest + .extend('config', { gravity: { x: 0, y: -9.81, z: 0 }, timestep: 1 / 60 }) + .extend('physics', ({ config }) => new Physics(config)); - beforeEach(() => { - physics = new Physics(config); - }); - - test('constructor initializes configuration', () => { + test('constructor initializes configuration', ({ physics, config }) => { expect(physics.configuration).toEqual(config); }); - test('start initializes the physics world', async () => { + test('start initializes the physics world', async ({ physics }) => { await physics.start(); expect(rapier.init).toHaveBeenCalled(); expect(physics.internals).toHaveProperty('initialized', true); @@ -43,34 +40,39 @@ describe(Physics, () => { expect(physics.internals).toHaveProperty('accumulator', physics.configuration.timestep); }); - test('createRigidBody throws if not initialized', () => { - expect(() => physics.createRigidBody({})).toThrow("Physics engine hasn't been initialized yet"); + test('createRigidBody throws if not initialized', ({ physics }) => { + expect(() => physics.createRigidBody({} as any)).toThrow("Physics engine hasn't been initialized yet"); }); - test('createRigidBody creates a rigid body when initialized', async () => { + test('createRigidBody creates a rigid body when initialized', async ({ physics }) => { await physics.start(); // Initialize const rigidBodyDesc = {}; // Mocked rigid body descriptor - physics.createRigidBody(rigidBodyDesc); + physics.createRigidBody(rigidBodyDesc as any); + + assert(physics.internals.initialized); expect(physics.internals.world.createRigidBody).toHaveBeenCalledWith(rigidBodyDesc); }); - test('createCollider creates a collider when initialized', async () => { + test('createCollider creates a collider when initialized', async ({ physics }) => { await physics.start(); // Initialize - const colliderDesc = {}; // Mocked collider descriptor - const rigidBody = {}; // Mocked rigid body + const colliderDesc: any = {}; // Mocked collider descriptor + const rigidBody: any = {}; // Mocked rigid body physics.createCollider(colliderDesc, rigidBody); + + assert(physics.internals.initialized); expect(physics.internals.world.createCollider).toHaveBeenCalledWith(colliderDesc, rigidBody); }); - test('castRay returns correct result when initialized', async () => { + test('castRay returns correct result when initialized', async ({ physics }) => { await physics.start(); // Initialize - const globalPosition = {}; // Mocked global position - const globalDirection = {}; // Mocked global direction + const globalPosition: any = {}; // Mocked global position + const globalDirection: any = {}; // Mocked global direction const maxDistance = 100; // Mock the return value of castRayAndGetNormal - const expectedResult = { toi: 10, normal: { x: 0, y: 1, z: 0 } }; - physics.internals.world.castRayAndGetNormal.mockReturnValue(expectedResult); + const expectedResult: any = { toi: 10, normal: { x: 0, y: 1, z: 0 } }; + assert(physics.internals.initialized); + vi.mocked(physics.internals.world.castRayAndGetNormal).mockReturnValue(expectedResult); const result = physics.castRay(globalPosition, globalDirection, maxDistance); expect(result).toEqual({ @@ -80,42 +82,44 @@ describe(Physics, () => { expect(physics.internals.world.castRayAndGetNormal).toHaveBeenCalledWith(expect.anything(), maxDistance, true, undefined, undefined, undefined); }); - test('castRay returns null result castRayAndGetNormal returns null', async () => { + test('castRay returns null result castRayAndGetNormal returns null', async ({ physics }) => { await physics.start(); // Initialize - const globalPosition = {}; // Mocked global position - const globalDirection = {}; // Mocked global direction + const globalPosition: any = {}; // Mocked global position + const globalDirection: any = {}; // Mocked global direction const maxDistance = 100; // Mock the return value of castRayAndGetNormal const expectedResult = null; - physics.internals.world.castRayAndGetNormal.mockReturnValue(expectedResult); + assert(physics.internals.initialized); + vi.mocked(physics.internals.world.castRayAndGetNormal).mockReturnValue(expectedResult); const result = physics.castRay(globalPosition, globalDirection, maxDistance); expect(result).toEqual(null); }); - test('step advances physics world by correct timestep', async () => { + test('step advances physics world by correct timestep', async ({ physics }) => { await physics.start(); // Initialize - const frameTimingInfo = { frameDuration: 1000 / 60 }; // 60 FPS + const frameTimingInfo: any = { frameDuration: 1000 / 60 }; // 60 FPS physics.step(frameTimingInfo); + assert(physics.internals.initialized); expect(physics.internals.world.step).toHaveBeenCalledTimes(2); }); - test('castRay throws if not initialized', () => { + test('castRay throws if not initialized', ({ physics }) => { expect(() => { - physics.castRay({}, {}, 100); + physics.castRay({} as any, {} as any, 100); }).toThrow("Physics engine hasn't been initialized yet"); }); - test('createCollider throws if not initialized', () => { + test('createCollider throws if not initialized', ({ physics }) => { expect(() => { - physics.createCollider({}, {}, 100); + physics.createCollider({} as any, {} as any); }).toThrow("Physics engine hasn't been initialized yet"); }); - test('step throws if not initialized', () => { + test('step throws if not initialized', ({ physics }) => { expect(() => { - physics.step({ frameDuration: 1000 / 60 }); + physics.step({ frameDuration: 1000 / 60 } as any); }).toThrow("Physics engine hasn't been initialized yet"); }); }); diff --git a/src/bundles/robot_simulation/src/helper_functions.ts b/src/bundles/robot_simulation/src/helper_functions.ts index 49ac78a161..2fe73fd4cc 100644 --- a/src/bundles/robot_simulation/src/helper_functions.ts +++ b/src/bundles/robot_simulation/src/helper_functions.ts @@ -1,3 +1,4 @@ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import { interrupt } from '@sourceacademy/modules-lib/specialErrors'; import context from 'js-slang/context'; import { sceneConfig } from './config'; @@ -33,7 +34,7 @@ import { createScene } from './engine/Render/helpers/Scene'; export function getWorldFromContext(): World { const world = context.moduleContexts.robot_simulation.state?.world; if (world === undefined) { - throw new Error('World not initialized'); + throw new GeneralRuntimeError('World not initialized'); } return world as World; } @@ -47,7 +48,7 @@ export function getWorldFromContext(): World { export function getEv3FromContext(): DefaultEv3 { const ev3 = context.moduleContexts.robot_simulation.state?.ev3; if (ev3 === undefined) { - throw new Error('ev3 not initialized'); + throw new GeneralRuntimeError('ev3 not initialized'); } return ev3 as DefaultEv3; } @@ -264,7 +265,7 @@ export function createCuboid( bodyType: string ) { if (isRigidBodyType(bodyType) === false) { - throw new Error('Invalid body type'); + throw new GeneralRuntimeError('Invalid body type'); } const config: CuboidConfig = { diff --git a/src/bundles/rune/package.json b/src/bundles/rune/package.json index 2b4d3346b0..2180a40ca3 100644 --- a/src/bundles/rune/package.json +++ b/src/bundles/rune/package.json @@ -3,13 +3,14 @@ "version": "1.0.0", "private": true, "dependencies": { + "@sourceacademy/bundle-repeat": "workspace:^", "@sourceacademy/modules-lib": "workspace:^", "es-toolkit": "^1.44.0", "gl-matrix": "^3.3.0" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", - "js-slang": "^1.0.85", + "js-slang": "^1.0.92", "typescript": "^6.0.2" }, "type": "module", @@ -22,8 +23,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/rune/src/__tests__/index.test.ts b/src/bundles/rune/src/__tests__/index.test.ts index 826a57cb22..335c2fe8cb 100644 --- a/src/bundles/rune/src/__tests__/index.test.ts +++ b/src/bundles/rune/src/__tests__/index.test.ts @@ -6,7 +6,7 @@ import type { Rune } from '../rune'; describe(display.anaglyph, () => { it('throws when argument is not rune', () => { - expect(() => display.anaglyph(0 as any)).toThrowError('anaglyph expects a rune as argument'); + expect(() => display.anaglyph(0 as any)).toThrow('anaglyph: Expected Rune, got 0.'); }); it('returns the rune passed to it', () => { @@ -16,7 +16,7 @@ describe(display.anaglyph, () => { describe(display.hollusion, () => { it('throws when argument is not rune', () => { - expect(() => display.hollusion(0 as any)).toThrowError('hollusion expects a rune as argument'); + expect(() => display.hollusion(0 as any)).toThrow('hollusion: Expected Rune, got 0.'); }); it('returns the rune passed to it', () => { @@ -26,7 +26,7 @@ describe(display.hollusion, () => { describe(display.show, () => { it('throws when argument is not rune', () => { - expect(() => display.show(0 as any)).toThrowError('show expects a rune as argument'); + expect(() => display.show(0 as any)).toThrow('show: Expected Rune, got 0.'); }); it('returns the rune passed to it', () => { @@ -36,7 +36,7 @@ describe(display.show, () => { describe('Hollusion Rune tests', () => { it('has isHollusion as true', () => { - const hollusion = new funcs.HollusionRune(funcs.blank, 0); + const hollusion = new funcs.DrawnHollusionRune(funcs.blank, 0); expect(hollusion.isHollusion).toEqual(true); }); }); @@ -55,25 +55,25 @@ describe(funcs.color, () => { }); it('throws when argument is not rune', () => { - expect(() => funcs.color(0 as any, 0, 0, 0)).toThrowError('color expects a rune as argument'); + expect(() => funcs.color(0 as any, 0, 0, 0)).toThrow('color: Expected Rune, got 0.'); }); it('throws when any color parameter is invalid', () => { - expect(() => funcs.color(funcs.heart, 100, 0, 0)).toThrowError('r cannot be greater than 1!'); - expect(() => funcs.color(funcs.heart, 0, -1, 0)).toThrowError('g cannot be less than 0!'); - expect(() => funcs.color(funcs.heart, 0, 0, 'hi' as any)).toThrowError('b must be a number!'); + expect(() => funcs.color(funcs.heart, 100, 0, 0)).toThrow('color: Expected number between 0 and 1 for r, got 100.'); + expect(() => funcs.color(funcs.heart, 0, -1, 0)).toThrow('color: Expected number between 0 and 1 for g, got -1.'); + expect(() => funcs.color(funcs.heart, 0, 0, 'hi' as any)).toThrow('color: Expected number between 0 and 1 for b, got "hi".'); }); }); describe(funcs.beside_frac, () => { it('throws when argument is not rune', () => { - expect(() => funcs.beside_frac(0, 0 as any, funcs.heart)).toThrowError('beside_frac expects a rune as argument'); - expect(() => funcs.beside_frac(0, funcs.heart, 0 as any)).toThrowError('beside_frac expects a rune as argument'); + expect(() => funcs.beside_frac(0, 0 as any, funcs.heart)).toThrow('beside_frac: Expected Rune for rune1, got 0.'); + expect(() => funcs.beside_frac(0, funcs.heart, 0 as any)).toThrow('beside_frac: Expected Rune for rune2, got 0.'); }); it('throws when frac is out of range', () => { - expect(() => funcs.beside_frac(-1, funcs.heart, funcs.heart)).toThrowError('beside_frac: frac cannot be less than 0!'); - expect(() => funcs.beside_frac(10, funcs.heart, funcs.heart)).toThrowError('beside_frac: frac cannot be greater than 1!'); + expect(() => funcs.beside_frac(-1, funcs.heart, funcs.heart)).toThrow('beside_frac: Expected number between 0 and 1 for frac, got -1.'); + expect(() => funcs.beside_frac(10, funcs.heart, funcs.heart)).toThrow('beside_frac: Expected number between 0 and 1 for frac, got 10.'); }); }); @@ -88,13 +88,13 @@ describe(funcs.beside, () => { describe(funcs.stack_frac, () => { it('throws when argument is not rune', () => { - expect(() => funcs.stack_frac(0, 0 as any, funcs.heart)).toThrowError('stack_frac expects a rune as argument'); - expect(() => funcs.stack_frac(0, funcs.heart, 0 as any)).toThrowError('stack_frac expects a rune as argument'); + expect(() => funcs.stack_frac(0, 0 as any, funcs.heart)).toThrow('stack_frac: Expected Rune for rune1, got 0.'); + expect(() => funcs.stack_frac(0, funcs.heart, 0 as any)).toThrow('stack_frac: Expected Rune for rune2, got 0.'); }); it('throws when frac is out of range', () => { - expect(() => funcs.stack_frac(-1, funcs.heart, funcs.heart)).toThrowError('stack_frac: frac cannot be less than 0!'); - expect(() => funcs.stack_frac(10, funcs.heart, funcs.heart)).toThrowError('stack_frac: frac cannot be greater than 1!'); + expect(() => funcs.stack_frac(-1, funcs.heart, funcs.heart)).toThrow('stack_frac: Expected number between 0 and 1 for frac, got -1.'); + expect(() => funcs.stack_frac(10, funcs.heart, funcs.heart)).toThrow('stack_frac: Expected number between 0 and 1 for frac, got 10.'); }); }); @@ -102,11 +102,11 @@ describe(funcs.stackn, () => { vi.spyOn(funcs.RuneFunctions, 'stack_frac'); it('throws when argument is not rune', () => { - expect(() => funcs.stackn(0, 0 as any)).toThrowError('stackn expects a rune as argument'); + expect(() => funcs.stackn(0, 0 as any)).toThrow('stackn: Expected Rune, got 0.'); }); it('throws when n is not an integer', () => { - expect(() => funcs.stackn(0.1, funcs.heart)).toThrowError('stackn expects an integer'); + expect(() => funcs.stackn(0.1, funcs.heart)).toThrow('stackn: Expected integer, got 0.1.'); }); it('simply returns when n <= 1', () => { @@ -123,34 +123,47 @@ describe(funcs.stackn, () => { describe(funcs.repeat_pattern, () => { it('simply returns if n <= 0', () => { - const mockPattern = vi.fn(); + const mockPattern = vi.fn(x => x); expect(funcs.repeat_pattern(0, mockPattern, funcs.blank)).toBe(funcs.blank); expect(mockPattern).not.toHaveBeenCalled(); }); + + it('works', () => { + const mockPattern = vi.fn(x => x); + expect(funcs.repeat_pattern(5, mockPattern, funcs.blank)).toBe(funcs.blank); + expect(mockPattern).toHaveBeenCalledTimes(5); + }); + + it('throws if initial is not a rune', () => { + expect(() => funcs.repeat_pattern(5, x => x, 0 as any)) + .toThrow('repeat_pattern: Expected Rune for initial, got 0.'); + }); }); describe(funcs.overlay_frac, () => { it('throws when argument is not rune', () => { - expect(() => funcs.overlay_frac(0, 0 as any, funcs.heart)).toThrowError('overlay_frac expects a rune as argument'); - expect(() => funcs.overlay_frac(0, funcs.heart, 0 as any)).toThrowError('overlay_frac expects a rune as argument'); + expect(() => funcs.overlay_frac(0, 0 as any, funcs.heart)).toThrow('overlay_frac: Expected Rune for rune1, got 0.'); + expect(() => funcs.overlay_frac(0, funcs.heart, 0 as any)).toThrow('overlay_frac: Expected Rune for rune2, got 0.'); }); it('throws when frac is out of range', () => { - expect(() => funcs.overlay_frac(-1, funcs.heart, funcs.heart)).toThrowError('overlay_frac: frac cannot be less than 0!'); - expect(() => funcs.overlay_frac(10, funcs.heart, funcs.heart)).toThrowError('overlay_frac: frac cannot be greater than 1!'); + expect(() => funcs.overlay_frac(-1, funcs.heart, funcs.heart)).toThrow('overlay_frac: Expected number between 0 and 1 for frac, got -1.'); + expect(() => funcs.overlay_frac(10, funcs.heart, funcs.heart)).toThrow('overlay_frac: Expected number between 0 and 1 for frac, got 10.'); }); }); describe('Colouring functions', () => { - const names = Object.getOwnPropertyNames(funcs.RuneColours); - const colourers = names.reduce<[string, (r: Rune) => Rune][]>((res, name) => { + type FunctionName = keyof (typeof funcs.RuneColours); + + const names = Object.getOwnPropertyNames(funcs.RuneColours) as FunctionName[]; + const colourers = names.reduce<[FunctionName, (r: Rune) => Rune][]>((res, name) => { if (typeof funcs.RuneColours[name] !== 'function') return res; - return [...res, [name, funcs.RuneColours[name]]]; + return [...res, [name, funcs.RuneColours[name]] as [FunctionName, (r: Rune) => Rune]]; }, []); describe.each(colourers)('%s', (_, f) => { it('throws when argument is not rune', () => { - expect(() => f(0 as any)).toThrowError(`${f.name} expects a rune as argument`); + expect(() => f(0 as any)).toThrow(`${f.name}: Expected Rune, got 0.`); }); it('does not modify the original rune', () => { diff --git a/src/bundles/rune/src/display.ts b/src/bundles/rune/src/display.ts index c00eb000de..dfaf0b7e3a 100644 --- a/src/bundles/rune/src/display.ts +++ b/src/bundles/rune/src/display.ts @@ -1,6 +1,7 @@ +import { assertFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import context from 'js-slang/context'; -import { AnaglyphRune, HollusionRune } from './functions'; -import { AnimatedRune, NormalRune, Rune, type DrawnRune, type RuneAnimation } from './rune'; +import { DrawnAnaglyphRune, DrawnHollusionRune } from './functions'; +import { AnimatedRune, DrawnNormalRune, Rune, type DrawnRune, type RuneAnimation } from './rune'; import { throwIfNotRune } from './runes_ops'; import { functionDeclaration } from './type_map'; @@ -17,21 +18,21 @@ class RuneDisplay { @functionDeclaration('rune: Rune', 'Rune') static show(rune: Rune): Rune { throwIfNotRune(RuneDisplay.show.name, rune); - drawnRunes.push(new NormalRune(rune)); + drawnRunes.push(new DrawnNormalRune(rune)); return rune; } @functionDeclaration('rune: Rune', 'Rune') static anaglyph(rune: Rune): Rune { throwIfNotRune(RuneDisplay.anaglyph.name, rune); - drawnRunes.push(new AnaglyphRune(rune)); + drawnRunes.push(new DrawnAnaglyphRune(rune)); return rune; } @functionDeclaration('rune: Rune, magnitude: number', 'Rune') static hollusion_magnitude(rune: Rune, magnitude: number): Rune { throwIfNotRune(RuneDisplay.hollusion_magnitude.name, rune); - drawnRunes.push(new HollusionRune(rune, magnitude)); + drawnRunes.push(new DrawnHollusionRune(rune, magnitude)); return rune; } @@ -43,10 +44,12 @@ class RuneDisplay { @functionDeclaration('duration: number, fps: number, func: RuneAnimation', 'AnimatedRune') static animate_rune(duration: number, fps: number, func: RuneAnimation) { - const anim = new AnimatedRune(duration, fps, (n) => { + assertFunctionOfLength(func, 1, RuneDisplay.animate_rune.name, 'RuneAnimation'); + + const anim = new AnimatedRune(duration, fps, n => { const rune = func(n); throwIfNotRune(RuneDisplay.animate_rune.name, rune); - return new NormalRune(rune); + return new DrawnNormalRune(rune); }); drawnRunes.push(anim); return anim; @@ -54,10 +57,12 @@ class RuneDisplay { @functionDeclaration('duration: number, fps: number, func: RuneAnimation', 'AnimatedRune') static animate_anaglyph(duration: number, fps: number, func: RuneAnimation) { - const anim = new AnimatedRune(duration, fps, (n) => { + assertFunctionOfLength(func, 1, RuneDisplay.animate_anaglyph.name, 'RuneAnimation'); + + const anim = new AnimatedRune(duration, fps, n => { const rune = func(n); throwIfNotRune(RuneDisplay.animate_anaglyph.name, rune); - return new AnaglyphRune(rune); + return new DrawnAnaglyphRune(rune); }); drawnRunes.push(anim); return anim; diff --git a/src/bundles/rune/src/functions.ts b/src/bundles/rune/src/functions.ts index 51f8310f38..f51d005cad 100644 --- a/src/bundles/rune/src/functions.ts +++ b/src/bundles/rune/src/functions.ts @@ -1,4 +1,6 @@ -import { clamp } from 'es-toolkit'; +import { repeat_internal } from '@sourceacademy/bundle-repeat/functions'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; +import { clamp, sample } from 'es-toolkit'; import { mat4, vec3 } from 'gl-matrix'; import { DrawnRune, @@ -35,15 +37,7 @@ export type RuneModuleState = { }; function throwIfNotFraction(val: unknown, param_name: string, func_name: string): asserts val is number { - if (typeof val !== 'number') throw new Error(`${func_name}: ${param_name} must be a number!`); - - if (val < 0) { - throw new Error(`${func_name}: ${param_name} cannot be less than 0!`); - } - - if (val > 1) { - throw new Error(`${func_name}: ${param_name} cannot be greater than 1!`); - } + assertNumberWithinRange(val, func_name, 0, 1, false, param_name); } // ============================================================================= @@ -109,6 +103,9 @@ export class RuneFunctions { rune: Rune ): Rune { throwIfNotRune(RuneFunctions.scale_independent.name, rune); + assertNumberWithinRange(ratio_x, { func_name: RuneFunctions.scale_independent.name, param_name: 'ratio_x', integer: false }); + assertNumberWithinRange(ratio_y, { func_name: RuneFunctions.scale_independent.name, param_name: 'ratio_y', integer: false }); + const scaleVec = vec3.fromValues(ratio_x, ratio_y, 1); const scaleMat = mat4.create(); mat4.scale(scaleMat, scaleMat, scaleVec); @@ -158,8 +155,8 @@ export class RuneFunctions { @functionDeclaration('frac: number, rune1: Rune, rune2: Rune', 'Rune') static stack_frac(frac: number, rune1: Rune, rune2: Rune): Rune { - throwIfNotRune(RuneFunctions.stack_frac.name, rune1); - throwIfNotRune(RuneFunctions.stack_frac.name, rune2); + throwIfNotRune(RuneFunctions.stack_frac.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.stack_frac.name, rune2, 'rune2'); throwIfNotFraction(frac, 'frac', RuneFunctions.stack_frac.name); const upper = RuneFunctions.translate(0, -(1 - frac), RuneFunctions.scale_independent(1, frac, rune1)); @@ -171,21 +168,23 @@ export class RuneFunctions { @functionDeclaration('rune1: Rune, rune2: Rune', 'Rune') static stack(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune(RuneFunctions.stack.name, rune1); - throwIfNotRune(RuneFunctions.stack.name, rune2); + throwIfNotRune(RuneFunctions.stack.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.stack.name, rune2, 'rune2'); return RuneFunctions.stack_frac(1 / 2, rune1, rune2); } @functionDeclaration('n: number, rune: Rune', 'Rune') static stackn(n: number, rune: Rune): Rune { throwIfNotRune(RuneFunctions.stackn.name, rune); - if (!Number.isInteger(n)) { - throw new Error(`${RuneFunctions.stackn.name} expects an integer!`); - } + + assertNumberWithinRange(n, { + func_name: RuneFunctions.stackn.name + }); if (n <= 1) { return rune; } + return RuneFunctions.stack_frac(1 / n, rune, RuneFunctions.stackn(n - 1, rune)); } @@ -209,8 +208,8 @@ export class RuneFunctions { @functionDeclaration('frac: number, rune1: Rune, rune2: Rune', 'Rune') static beside_frac(frac: number, rune1: Rune, rune2: Rune): Rune { - throwIfNotRune(RuneFunctions.beside_frac.name, rune1); - throwIfNotRune(RuneFunctions.beside_frac.name, rune2); + throwIfNotRune(RuneFunctions.beside_frac.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.beside_frac.name, rune2, 'rune2'); throwIfNotFraction(frac, 'frac', RuneFunctions.beside_frac.name); const left = RuneFunctions.translate(-(1 - frac), 0, RuneFunctions.scale_independent(frac, 1, rune1)); @@ -222,8 +221,8 @@ export class RuneFunctions { @functionDeclaration('rune1: Rune, rune2: Rune', 'Rune') static beside(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune(RuneFunctions.beside.name, rune1); - throwIfNotRune(RuneFunctions.beside.name, rune2); + throwIfNotRune(RuneFunctions.beside.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.beside.name, rune2, 'rune2'); return RuneFunctions.beside_frac(0.5, rune1, rune2); } @@ -254,11 +253,10 @@ export class RuneFunctions { pattern: (a: Rune) => Rune, initial: Rune ): Rune { - if (n <= 0) { - return initial; - } - - return pattern(RuneFunctions.repeat_pattern(n - 1, pattern, initial)); + throwIfNotRune(RuneFunctions.repeat_pattern.name, initial, 'initial'); + assertFunctionOfLength(pattern, 1, RuneFunctions.repeat_pattern.name); + const repeated = repeat_internal(pattern, n); + return repeated(initial); } // ============================================================================= @@ -269,8 +267,8 @@ export class RuneFunctions { static overlay_frac(frac: number, rune1: Rune, rune2: Rune): Rune { // to developer: please read https://www.tutorialspoint.com/webgl/webgl_basics.htm to understand the webgl z-axis interpretation. // The key point is that positive z is closer to the screen. Hence, the image at the back should have smaller z value. Primitive runes have z = 0. - throwIfNotRune(RuneFunctions.overlay_frac.name, rune1); - throwIfNotRune(RuneFunctions.overlay_frac.name, rune2); + throwIfNotRune(RuneFunctions.overlay_frac.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.overlay_frac.name, rune2, 'rune2'); throwIfNotFraction(frac, 'frac', RuneFunctions.overlay_frac.name); // by definition, when frac == 0 or 1, the back rune will overlap with the front rune. @@ -305,8 +303,8 @@ export class RuneFunctions { @functionDeclaration('rune1: Rune, rune2: Rune', 'Rune') static overlay(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune(RuneFunctions.overlay.name, rune1); - throwIfNotRune(RuneFunctions.overlay.name, rune2); + throwIfNotRune(RuneFunctions.overlay.name, rune1, 'rune1'); + throwIfNotRune(RuneFunctions.overlay.name, rune2, 'rune2'); return RuneFunctions.overlay_frac(0.5, rune1, rune2); } @@ -412,9 +410,8 @@ export class RuneColours { @functionDeclaration('rune: Rune', 'Rune') static random_color(rune: Rune): Rune { throwIfNotRune(RuneColours.random_color.name, rune); - const colourNames = Object.keys(RuneColours.colours); - const colourName = colourNames[Math.floor(Math.random() * colourNames.length)]; - const randomColor = hexToColor(RuneColours.colours[colourName]); + const colorVal = sample(Object.values(RuneColours.colours)); + const randomColor = hexToColor(colorVal); return Rune.of({ colors: new Float32Array(randomColor), @@ -424,7 +421,7 @@ export class RuneColours { } /** @hidden */ -export class AnaglyphRune extends DrawnRune { +export class DrawnAnaglyphRune extends DrawnRune { private static readonly anaglyphVertexShader = ` precision mediump float; attribute vec4 a_position; @@ -503,8 +500,8 @@ export class AnaglyphRune extends DrawnRune { // prepare the shader program to combine the left/right eye images const shaderProgram = initShaderProgram( gl, - AnaglyphRune.anaglyphVertexShader, - AnaglyphRune.anaglyphFragmentShader + DrawnAnaglyphRune.anaglyphVertexShader, + DrawnAnaglyphRune.anaglyphFragmentShader ); gl.useProgram(shaderProgram); const reduPt = gl.getUniformLocation(shaderProgram, 'u_sampler_red'); @@ -534,7 +531,7 @@ export class AnaglyphRune extends DrawnRune { } /** @hidden */ -export class HollusionRune extends DrawnRune { +export class DrawnHollusionRune extends DrawnRune { constructor(rune: Rune, magnitude: number) { super(rune, true); this.rune.hollusionDistance = magnitude; @@ -610,8 +607,8 @@ export class HollusionRune extends DrawnRune { // Then, draw a frame from framebuffer for each update const copyShaderProgram = initShaderProgram( gl, - HollusionRune.copyVertexShader, - HollusionRune.copyFragmentShader + DrawnHollusionRune.copyVertexShader, + DrawnHollusionRune.copyFragmentShader ); gl.useProgram(copyShaderProgram); const texturePt = gl.getUniformLocation(copyShaderProgram, 'uTexture'); @@ -650,7 +647,7 @@ export class HollusionRune extends DrawnRune { } /** @hidden */ -export function isHollusionRune(rune: DrawnRune): rune is HollusionRune { +export function isHollusionRune(rune: DrawnRune): rune is DrawnHollusionRune { return rune.isHollusion; } @@ -957,7 +954,7 @@ export const red = RuneColours.red; * @param {function} pattern - Unary function from Rune to Rune * @param {Rune} initial - The initial Rune * @returns {Rune} - Result of n times application of pattern to initial: - * pattern(pattern(...pattern(pattern(initial))...)) + * `pattern(pattern(...pattern(pattern(initial))...))` * @function * * @category Main diff --git a/src/bundles/rune/src/rune.ts b/src/bundles/rune/src/rune.ts index 77426d7890..c7de44d47b 100644 --- a/src/bundles/rune/src/rune.ts +++ b/src/bundles/rune/src/rune.ts @@ -1,3 +1,4 @@ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import { glAnimation, type AnimFrame, type ReplResult } from '@sourceacademy/modules-lib/types'; import { mat4 } from 'gl-matrix'; import { getWebGlFromCanvas, initShaderProgram } from './runes_webgl'; @@ -50,6 +51,16 @@ void main(void) { gl_FragColor.a = 1.0; } `; + +interface RuneOfParams { + vertices?: Float32Array; + colors?: Float32Array | null; + transformMatrix?: mat4; + subRunes?: Rune[]; + texture?: HTMLImageElement | null; + hollusionDistance?: number; +} + /** * The basic data-representation of a Rune. When the Rune is drawn, every 3 consecutive vertex will form a triangle. */ @@ -57,7 +68,7 @@ void main(void) { export class Rune { constructor( /** - * A list of vertex coordinates, each vertex has 4 coordiante (x,y,z,t). + * A list of vertex coordinates, each vertex has 4 coordiants (x,y,z,t). */ public vertices: Float32Array, @@ -120,15 +131,8 @@ export class Rune { return runeList; }; - public static of = (params: { - vertices?: Float32Array; - colors?: Float32Array | null; - transformMatrix?: mat4; - subRunes?: Rune[]; - texture?: HTMLImageElement | null; - hollusionDistance?: number; - } = {}) => { - const paramGetter = (name: string, defaultValue: () => any) => (params[name] === undefined ? defaultValue() : params[name]); + public static of = (params: RuneOfParams = {}) => { + const paramGetter = (name: keyof RuneOfParams, defaultValue: () => any) => params[name] ?? defaultValue(); return new Rune( paramGetter('vertices', () => new Float32Array()), @@ -167,7 +171,7 @@ export function drawRunesToFrameBuffer( ); gl.useProgram(shaderProgram); if (gl === null) { - throw Error('Rendering Context not initialized for drawRune.'); + throw new GeneralRuntimeError('Rendering Context not initialized for drawRune.'); } // create pointers to the data-entries of the shader program @@ -226,7 +230,7 @@ export function drawRunesToFrameBuffer( const loadTexture = (image: HTMLImageElement): WebGLTexture | null => { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); - function isPowerOf2(value) { + function isPowerOf2(value: number) { return (value & (value - 1)) === 0; } // Because images have to be downloaded over the internet @@ -375,7 +379,7 @@ export abstract class DrawnRune implements ReplResult { public abstract draw: (canvas: HTMLCanvasElement) => void; } -export class NormalRune extends DrawnRune { +export class DrawnNormalRune extends DrawnRune { constructor(rune: Rune) { super(rune, false); } diff --git a/src/bundles/rune/src/runes_ops.ts b/src/bundles/rune/src/runes_ops.ts index 2f327ceb4a..5b053b7250 100644 --- a/src/bundles/rune/src/runes_ops.ts +++ b/src/bundles/rune/src/runes_ops.ts @@ -1,14 +1,17 @@ /** * This file contains the bundle's private functions for runes. */ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; import { hexToColor as hexToColorUtil } from '@sourceacademy/modules-lib/utilities'; import { Rune } from './rune'; // ============================================================================= // Utility Functions // ============================================================================= -export function throwIfNotRune(name: string, rune: unknown): asserts rune is Rune { - if (!(rune instanceof Rune)) throw new Error(`${name} expects a rune as argument.`); +export function throwIfNotRune(func_name: string, rune: unknown, param_name?: string): asserts rune is Rune { + if (!(rune instanceof Rune)) { + throw new InvalidParameterTypeError('Rune', rune, func_name, param_name); + } } // ============================================================================= diff --git a/src/bundles/rune/src/runes_webgl.ts b/src/bundles/rune/src/runes_webgl.ts index 1e519dfc86..f17f9f910f 100644 --- a/src/bundles/rune/src/runes_webgl.ts +++ b/src/bundles/rune/src/runes_webgl.ts @@ -2,6 +2,8 @@ * This file contains the module's private functions that handles various webgl operations. */ +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; + export type FrameBufferWithTexture = { framebuffer: WebGLFramebuffer; texture: WebGLTexture; @@ -23,14 +25,14 @@ function loadShader( ): WebGLShader { const shader = gl.createShader(type); if (!shader) { - throw new Error('WebGLShader not available.'); + throw new GeneralRuntimeError('WebGLShader not available.'); } gl.shaderSource(shader, source); gl.compileShader(shader); const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!compiled) { const compilationLog = gl.getShaderInfoLog(shader); - throw Error(`Shader compilation failed: ${compilationLog}`); + throw new GeneralRuntimeError(`Shader compilation failed: ${compilationLog}`); } return shader; } @@ -52,7 +54,7 @@ export function initShaderProgram( const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); const shaderProgram = gl.createProgram(); if (!shaderProgram) { - throw new Error('Unable to initialize the shader program.'); + throw new GeneralRuntimeError('Unable to initialize the shader program.'); } gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); @@ -68,7 +70,7 @@ export function initShaderProgram( export function getWebGlFromCanvas(canvas: HTMLCanvasElement): WebGLRenderingContext { const gl: WebGLRenderingContext | null = canvas.getContext('webgl'); if (!gl) { - throw Error('Unable to initialize WebGL.'); + throw new GeneralRuntimeError('Unable to initialize WebGL.'); } gl.clearColor(1.0, 1.0, 1.0, 1.0); // Set clear color to white, fully opaque gl.enable(gl.DEPTH_TEST); // Enable depth testing @@ -86,13 +88,13 @@ export function initFramebufferObject(gl: WebGLRenderingContext): FrameBufferWit // create a framebuffer object const framebuffer = gl.createFramebuffer(); if (!framebuffer) { - throw Error('Failed to create frame buffer object'); + throw new GeneralRuntimeError('Failed to create frame buffer object'); } // create a texture object and set its size and parameters const texture = gl.createTexture(); if (!texture) { - throw Error('Failed to create texture object'); + throw new GeneralRuntimeError('Failed to create texture object'); } gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( @@ -111,7 +113,7 @@ export function initFramebufferObject(gl: WebGLRenderingContext): FrameBufferWit // create a renderbuffer for depth buffer const depthBuffer = gl.createRenderbuffer(); if (!depthBuffer) { - throw Error('Failed to create renderbuffer object'); + throw new GeneralRuntimeError('Failed to create renderbuffer object'); } // bind renderbuffer object to target and set size @@ -143,7 +145,7 @@ export function initFramebufferObject(gl: WebGLRenderingContext): FrameBufferWit // check whether the framebuffer is configured correctly const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (gl.FRAMEBUFFER_COMPLETE !== e) { - throw Error(`Frame buffer object is incomplete:${e.toString()}`); + throw new GeneralRuntimeError(`Frame buffer object is incomplete:${e.toString()}`); } // Unbind the buffer object diff --git a/src/bundles/rune_in_words/package.json b/src/bundles/rune_in_words/package.json index 1da8ec7ac8..8bbfc2c538 100644 --- a/src/bundles/rune_in_words/package.json +++ b/src/bundles/rune_in_words/package.json @@ -1,11 +1,14 @@ { "name": "@sourceacademy/bundle-rune_in_words", - "version": "1.0.0", + "version": "1.0.1", "private": true, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", "typescript": "^6.0.2" }, + "dependencies": { + "@sourceacademy/modules-lib": "workspace:^" + }, "type": "module", "exports": { ".": "./dist/index.js", @@ -16,8 +19,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/rune_in_words/src/functions.ts b/src/bundles/rune_in_words/src/functions.ts index b359c37875..fd1a07b5a3 100644 --- a/src/bundles/rune_in_words/src/functions.ts +++ b/src/bundles/rune_in_words/src/functions.ts @@ -23,13 +23,13 @@ import { * * @category Primitive */ -export const square: string = getSquare(); +export const square: Rune = getSquare(); /** * Rune with the shape of a blank square * * @category Primitive */ -export const blank: string = getBlank(); +export const blank: Rune = getBlank(); /** * Rune with the shape of a * small square inside a large square, @@ -38,26 +38,26 @@ export const blank: string = getBlank(); * * @category Primitive */ -export const rcross: string = getRcross(); +export const rcross: Rune = getRcross(); /** * Rune with the shape of a sail * * @category Primitive */ -export const sail: string = getSail(); +export const sail: Rune = getSail(); /** * Rune with the shape of a triangle * * @category Primitive */ -export const triangle: string = getTriangle(); +export const triangle: Rune = getTriangle(); /** * Rune with black triangle, * filling upper right corner * * @category Primitive */ -export const corner: string = getCorner(); +export const corner: Rune = getCorner(); /** * Rune with the shape of two overlapping * triangles, residing in the upper half @@ -65,32 +65,32 @@ export const corner: string = getCorner(); * * @category Primitive */ -export const nova: string = getNova(); +export const nova: Rune = getNova(); /** * Rune with the shape of a circle * * @category Primitive */ -export const circle: string = getCircle(); +export const circle: Rune = getCircle(); /** * Rune with the shape of a heart * * @category Primitive */ -export const heart: string = getHeart(); +export const heart: Rune = getHeart(); /** * Rune with the shape of a pentagram * * @category Primitive */ -export const pentagram: string = getPentagram(); +export const pentagram: Rune = getPentagram(); /** * Rune with the shape of a ribbon * winding outwards in an anticlockwise spiral * * @category Primitive */ -export const ribbon: string = getRibbon(); +export const ribbon: Rune = getRibbon(); // ============================================================================= // Textured Runes @@ -103,7 +103,7 @@ export const ribbon: string = getRibbon(); * * @category Main */ -export function from_url(imageUrl: string): string { +export function from_url(imageUrl: string): Rune { return `url(${imageUrl})`; } @@ -123,8 +123,8 @@ export function from_url(imageUrl: string): string { export function scale_independent( ratio_x: number, ratio_y: number, - rune: string -): string { + rune: Rune +): Rune { throwIfNotRune(scale_independent.name, rune); return `scaled(${rune}, ${ratio_x}, ${ratio_y})`; } @@ -137,7 +137,7 @@ export function scale_independent( * * @category Main */ -export function scale(ratio: number, rune: string): string { +export function scale(ratio: number, rune: Rune): string { throwIfNotRune(scale.name, rune); return scale_independent(ratio, ratio, rune); } @@ -151,7 +151,7 @@ export function scale(ratio: number, rune: string): string { * * @category Main */ -export function translate(x: number, y: number, rune: string): string { +export function translate(x: number, y: number, rune: Rune): Rune { throwIfNotRune(translate.name, rune); return `translated(${rune}, ${x}, ${y})`; } @@ -167,7 +167,7 @@ export function translate(x: number, y: number, rune: string): string { * * @category Main */ -export function rotate(rad: number, rune: string): string { +export function rotate(rad: number, rune: Rune): Rune { throwIfNotRune(rotate.name, rune); return `rotated(${rune}, ${rad})`; } @@ -185,7 +185,7 @@ export function rotate(rad: number, rune: string): string { * * @category Main */ -export function stack_frac(frac: number, rune1: string, rune2: string): string { +export function stack_frac(frac: number, rune1: Rune, rune2: Rune): Rune { throwIfNotRune(stack_frac.name, rune1); throwIfNotRune(stack_frac.name, rune2); @@ -203,8 +203,9 @@ export function stack_frac(frac: number, rune1: string, rune2: string): string { * * @category Main */ -export function stack(rune1: string, rune2: string): string { - throwIfNotRune(stack.name, rune1, rune2); +export function stack(rune1: Rune, rune2: Rune): Rune { + throwIfNotRune(stack.name, rune1); + throwIfNotRune(stack.name, rune2); return `stack(${rune1}, ${rune2})`; } @@ -217,7 +218,7 @@ export function stack(rune1: string, rune2: string): string { * * @category Main */ -export function stackn(n: number, rune: string): string { +export function stackn(n: number, rune: Rune): Rune { throwIfNotRune(stackn.name, rune); return `stackn(${n}, ${rune})`; @@ -232,7 +233,7 @@ export function stackn(n: number, rune: string): string { * * @category Main */ -export function quarter_turn_right(rune: string): string { +export function quarter_turn_right(rune: Rune): Rune { throwIfNotRune(quarter_turn_right.name, rune); return `quarter_turn_right(${rune})`; } @@ -246,7 +247,7 @@ export function quarter_turn_right(rune: string): string { * * @category Main */ -export function quarter_turn_left(rune: string): string { +export function quarter_turn_left(rune: Rune): Rune { throwIfNotRune(quarter_turn_left.name, rune); return `quarter_turn_left(${rune})`; } @@ -259,7 +260,7 @@ export function quarter_turn_left(rune: string): string { * * @category Main */ -export function turn_upside_down(rune: string): string { +export function turn_upside_down(rune: Rune): Rune { throwIfNotRune(turn_upside_down.name, rune); return `quarter_upside_down(${rune})`; } @@ -277,8 +278,9 @@ export function turn_upside_down(rune: string): string { * * @category Main */ -export function beside_frac(frac: number, rune1: string, rune2: string): string { - throwIfNotRune(beside_frac.name, rune1, rune2); +export function beside_frac(frac: number, rune1: Rune, rune2: Rune): Rune { + throwIfNotRune(beside_frac.name, rune1); + throwIfNotRune(beside_frac.name, rune2); return `beside_frac(${frac}, ${rune1}, ${rune2})`; } @@ -294,8 +296,9 @@ export function beside_frac(frac: number, rune1: string, rune2: string): string * * @category Main */ -export function beside(rune1: string, rune2: string): string { - throwIfNotRune(beside.name, rune1, rune2); +export function beside(rune1: Rune, rune2: Rune): Rune { + throwIfNotRune(beside.name, rune1); + throwIfNotRune(beside.name, rune2); return `stack(${rune1}, ${rune2})`; } @@ -308,7 +311,7 @@ export function beside(rune1: string, rune2: string): string { * * @category Main */ -export function flip_vert(rune: string): string { +export function flip_vert(rune: Rune): Rune { throwIfNotRune(flip_vert.name, rune); return `flip_vert(${rune})`; } @@ -322,7 +325,7 @@ export function flip_vert(rune: string): string { * * @category Main */ -export function flip_horiz(rune: string): string { +export function flip_horiz(rune: Rune): Rune { throwIfNotRune(flip_horiz.name, rune); return `flip_horiz(${rune})`; } @@ -336,7 +339,7 @@ export function flip_horiz(rune: string): string { * * @category Main */ -export function make_cross(rune: string): string { +export function make_cross(rune: Rune): Rune { throwIfNotRune(make_cross.name, rune); return stack( beside(quarter_turn_right(rune), turn_upside_down(rune)), @@ -356,9 +359,9 @@ export function make_cross(rune: string): string { */ export function repeat_pattern( n: number, - pattern: (a: string) => Rune, - initial: string -): string { + pattern: (a: Rune) => Rune, + initial: Rune +): Rune { if (n === 0) { return initial; } @@ -378,7 +381,7 @@ export function repeat_pattern( * * @category Main */ -export function overlay_frac(frac: number, rune1: string, rune2: string): string { +export function overlay_frac(frac: number, rune1: Rune, rune2: Rune): string { throwIfNotRune(overlay_frac.name, rune1); throwIfNotRune(overlay_frac.name, rune2); return `overlay_frac(${frac}, ${rune1}, ${rune2})`; @@ -392,7 +395,7 @@ export function overlay_frac(frac: number, rune1: string, rune2: string): string * * @category Main */ -export function overlay(rune1: string, rune2: string): string { +export function overlay(rune1: Rune, rune2: Rune): string { throwIfNotRune(overlay.name, rune1); throwIfNotRune(overlay.name, rune2); return `overlay(${rune1}, ${rune2})`; @@ -415,7 +418,7 @@ export function overlay(rune1: string, rune2: string): string { * * @category Color */ -export function color(rune: string, r: number, g: number, b: number): string { +export function color(rune: Rune, r: number, g: number, b: number): string { throwIfNotRune(color.name, rune); return `color(${rune}, ${r}, ${g}, ${b})`; } @@ -429,7 +432,7 @@ export function color(rune: string, r: number, g: number, b: number): string { * * @category Color */ -export function random_color(rune: string): string { +export function random_color(rune: Rune): Rune { throwIfNotRune(random_color.name, rune); return `random(${rune})`; } @@ -441,7 +444,7 @@ export function random_color(rune: string): string { * * @category Color */ -export function red(rune: string): string { +export function red(rune: Rune): Rune { throwIfNotRune(red.name, rune); return `red(${rune})`; } @@ -453,7 +456,7 @@ export function red(rune: string): string { * * @category Color */ -export function pink(rune: string): string { +export function pink(rune: Rune): Rune { throwIfNotRune(pink.name, rune); return `pink(${rune})`; } @@ -465,7 +468,7 @@ export function pink(rune: string): string { * * @category Color */ -export function purple(rune: string): string { +export function purple(rune: Rune): Rune { throwIfNotRune(purple.name, rune); return `purple(${rune})`; } @@ -477,7 +480,7 @@ export function purple(rune: string): string { * * @category Color */ -export function indigo(rune: string): string { +export function indigo(rune: Rune): Rune { throwIfNotRune(indigo.name, rune); return `indigo(${rune})`; } @@ -489,7 +492,7 @@ export function indigo(rune: string): string { * * @category Color */ -export function blue(rune: string): string { +export function blue(rune: Rune): Rune { throwIfNotRune(blue.name, rune); return `blue(${rune})`; } @@ -501,7 +504,7 @@ export function blue(rune: string): string { * * @category Color */ -export function green(rune: string): string { +export function green(rune: Rune): Rune { throwIfNotRune(green.name, rune); return `green(${rune})`; } @@ -513,7 +516,7 @@ export function green(rune: string): string { * * @category Color */ -export function yellow(rune: string): string { +export function yellow(rune: Rune): Rune { throwIfNotRune(yellow.name, rune); return `yellow(${rune})`; } @@ -525,7 +528,7 @@ export function yellow(rune: string): string { * * @category Color */ -export function orange(rune: string): string { +export function orange(rune: Rune): Rune { throwIfNotRune(orange.name, rune); return `orange(${rune})`; } @@ -537,7 +540,7 @@ export function orange(rune: string): string { * * @category Color */ -export function brown(rune: string): string { +export function brown(rune: Rune): Rune { throwIfNotRune(brown.name, rune); return `brown(${rune})`; } @@ -549,7 +552,7 @@ export function brown(rune: string): string { * * @category Color */ -export function black(rune: string): string { +export function black(rune: Rune): Rune { throwIfNotRune(black.name, rune); return `black(${rune})`; } @@ -561,7 +564,7 @@ export function black(rune: string): string { * * @category Color */ -export function white(rune: string): string { +export function white(rune: Rune): Rune { throwIfNotRune(white.name, rune); return `white(${rune})`; } @@ -577,7 +580,7 @@ export function white(rune: string): string { * * @category Main */ -export function show(rune: string): string { +export function show(rune: Rune): Rune { throwIfNotRune(show.name, rune); return rune; } @@ -590,7 +593,7 @@ export function show(rune: string): string { * * @category Main */ -export function anaglyph(rune: string): string { +export function anaglyph(rune: Rune): Rune { throwIfNotRune(anaglyph.name, rune); return rune; } @@ -603,7 +606,7 @@ export function anaglyph(rune: string): string { * * @category Main */ -export function hollusion(rune: string): string { +export function hollusion(rune: Rune): Rune { throwIfNotRune(hollusion.name, rune); return rune; } diff --git a/src/bundles/rune_in_words/src/runes_ops.ts b/src/bundles/rune_in_words/src/runes_ops.ts index e9df217d5a..0681426bc3 100644 --- a/src/bundles/rune_in_words/src/runes_ops.ts +++ b/src/bundles/rune_in_words/src/runes_ops.ts @@ -1,12 +1,14 @@ // ============================================================================= // Utility Functions + +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import type { Rune } from './rune'; + // ============================================================================= -export function throwIfNotRune(name: string, ...runes: any) { - runes.forEach((rune) => { - if (!(typeof rune === 'string')) { - throw Error(`${name} expects a rune (string) as argument.`); - } - }); +export function throwIfNotRune(name: string, rune: unknown): asserts rune is Rune { + if (typeof rune !== 'string') { + throw new InvalidParameterTypeError('Rune', rune, name); + } } // ============================================================================= @@ -55,7 +57,7 @@ export const colorPalette = [ '#795548' ]; -export function addColorFromHex(rune, hex) { +export function addColorFromHex(rune: Rune, hex: string) { throwIfNotRune('addColorFromHex', rune); return `color(${rune}, ${hex})`; } diff --git a/src/bundles/scrabble/package.json b/src/bundles/scrabble/package.json index 67e48609a1..882b14f461 100644 --- a/src/bundles/scrabble/package.json +++ b/src/bundles/scrabble/package.json @@ -16,8 +16,9 @@ "test": "buildtools test --project .", "tsc": "buildtools tsc .", "lint": "buildtools lint .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/scrabble/src/functions.ts b/src/bundles/scrabble/src/functions.ts index 4ec5162bd6..7315b45f26 100644 --- a/src/bundles/scrabble/src/functions.ts +++ b/src/bundles/scrabble/src/functions.ts @@ -1,12 +1,5 @@ import scrabble_words_raw from './words.json' with { type: 'json' }; -/** - * The `scrabble` Source Module provides the allowable - * words in Scrabble in a list and in an array, according to - * https://github.com/benjamincrom/scrabble/blob/master/scrabble/dictionary.json - * @module scrabble - */ - /** * `scrabble_words` is an array of strings, each representing * an allowed word in Scrabble. diff --git a/src/bundles/scrabble/src/index.ts b/src/bundles/scrabble/src/index.ts index 28147eda01..bbdf8e09e3 100644 --- a/src/bundles/scrabble/src/index.ts +++ b/src/bundles/scrabble/src/index.ts @@ -1,7 +1,10 @@ /** - * Scrabble words for Source Academy - * @author Martin Henz + * The `scrabble` Source Module provides the allowable + * words in Scrabble in a list and in an array, according to + * https://github.com/benjamincrom/scrabble/blob/master/scrabble/dictionary.json + * * @module scrabble + * @author Martin Henz */ export { scrabble_words, diff --git a/src/bundles/sound/package.json b/src/bundles/sound/package.json index 5ad2cfeb22..8a8ef5a105 100644 --- a/src/bundles/sound/package.json +++ b/src/bundles/sound/package.json @@ -1,10 +1,11 @@ { "name": "@sourceacademy/bundle-sound", - "version": "1.0.0", + "version": "1.1.0", "private": true, "dependencies": { "@sourceacademy/bundle-midi": "workspace:^", - "js-slang": "^1.0.85" + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -23,8 +24,9 @@ "lint": "buildtools lint .", "tsc": "buildtools tsc .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/sound/src/__tests__/recording.test.ts b/src/bundles/sound/src/__tests__/recording.test.ts index 874908b7a1..4da2baf43e 100644 --- a/src/bundles/sound/src/__tests__/recording.test.ts +++ b/src/bundles/sound/src/__tests__/recording.test.ts @@ -42,12 +42,14 @@ describe(funcs.init_record, () => { test('sets stream correctly when permission is accepted', async () => { expect(funcs.init_record()).toEqual('obtaining recording permission'); await expect.poll(() => funcs.globalVars.stream).toBe(mockStream); + expect(mockedGetUserMedia).toHaveBeenCalledOnce(); }); test('sets stream to false when permission is rejected', async () => { mockedGetUserMedia.mockRejectedValueOnce(''); expect(funcs.init_record()).toEqual('obtaining recording permission'); await expect.poll(() => funcs.globalVars.stream).toEqual(false); + expect(mockedGetUserMedia).toHaveBeenCalledOnce(); }); }); @@ -63,12 +65,12 @@ describe('Recording functions', () => { describe(funcs.record, () => { test('throws error if called without init_record', () => { - expect(() => funcs.record(0)).toThrowError('record: Call init_record(); to obtain permission to use microphone'); + expect(() => funcs.record(0)).toThrow('record: Call init_record(); to obtain permission to use microphone'); }); test('throws error if called concurrently with another sound', () => { - funcs.play_wave(() => 0, 10); - expect(() => funcs.record(1)).toThrowError('record: Cannot record while another sound is playing!'); + funcs.play_wave(_t => 0, 10); + expect(() => funcs.record(1)).toThrow('record: Cannot record while another sound is playing!'); }); test(`${funcs.record.name} works`, async () => { @@ -89,7 +91,7 @@ describe('Recording functions', () => { mockAudioContext.close(); // End the recording signal playing // Resolving the promise before processing is done throws an error - expect(soundPromise).toThrowError('recording still being processed'); + expect(soundPromise).toThrow('recording still being processed'); expect(stringify(soundPromise)).toEqual(''); const mockRecordedSound = funcs.silence_sound(0); @@ -101,12 +103,12 @@ describe('Recording functions', () => { describe(funcs.record_for, () => { test('throws error if called without init_record', () => { - expect(() => funcs.record_for(0, 0)).toThrowError('record_for: Call init_record(); to obtain permission to use microphone'); + expect(() => funcs.record_for(0, 0)).toThrow('record_for: Call init_record(); to obtain permission to use microphone'); }); test('throws error if called concurrently with another sound', () => { - funcs.play_wave(() => 0, 10); - expect(() => funcs.record_for(1, 1)).toThrowError('record_for: Cannot record while another sound is playing!'); + funcs.play_wave(_t => 0, 10); + expect(() => funcs.record_for(1, 1)).toThrow('record_for: Cannot record while another sound is playing!'); }); test(`${funcs.record_for.name} works`, async () => { diff --git a/src/bundles/sound/src/__tests__/sound.test.ts b/src/bundles/sound/src/__tests__/sound.test.ts index ea9af73b35..2001efeb4a 100644 --- a/src/bundles/sound/src/__tests__/sound.test.ts +++ b/src/bundles/sound/src/__tests__/sound.test.ts @@ -1,6 +1,7 @@ +import { stringify } from 'js-slang/dist/utils/stringify'; import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; import * as funcs from '../functions'; -import * as play_in_tab from '../play_in_tab'; +import { play_in_tab } from '../play_in_tab'; import type { Sound, Wave } from '../types'; import { mockAudioContext } from './utils'; @@ -8,17 +9,22 @@ vi.stubGlobal('AudioContext', function () { return mockAudioContext; }); describe(funcs.make_sound, () => { it('Should error gracefully when duration is negative', () => { - expect(() => funcs.make_sound(() => 0, -1)) - .toThrow('make_sound: Sound duration must be greater than or equal to 0'); + expect(() => funcs.make_sound(_t => 0, -1)) + .toThrow('make_sound: Expected number greater than 0 for duration, got -1.'); }); it('Should not error when duration is zero', () => { - expect(() => funcs.make_sound(() => 0, 0)).not.toThrow(); + expect(() => funcs.make_sound(_t => 0, 0)).not.toThrow(); }); it('Should error gracefully when wave is not a function', () => { expect(() => funcs.make_sound(true as any, 1)) - .toThrow('make_sound expects a wave, got true'); + .toThrow('make_sound: Expected Wave, got true'); + }); + + it('Should error if the provided function does not take exactly one parameter', () => { + expect(() => funcs.make_sound(((_t: number, _u: number) => 0) as any, 1)) + .toThrow('make_sound: Expected Wave, got (_t, _u) => 0.'); }); }); @@ -33,84 +39,94 @@ describe('Concurrent playback functions', () => { describe(funcs.play, () => { it('Should error gracefully when duration is negative', () => { - const sound: Sound = [() => 0, -1]; + const sound: Sound = [_t => 0, -1]; expect(() => funcs.play(sound)) - .toThrow('play: duration of sound is negative'); + .toThrow('play: Expected number greater than 0 for duration, got -1.'); }); it('Should not error when duration is zero', () => { - const sound = funcs.make_sound(() => 0, 0); + const sound = funcs.make_sound(_t => 0, 0); expect(() => funcs.play(sound)).not.toThrow(); }); it('Should throw error when given not a sound', () => { - expect(() => funcs.play(0 as any)).toThrow('play is expecting sound, but encountered 0'); + expect(() => funcs.play(0 as any)).toThrow('play: Expected sound, got 0.'); + }); + + it('Should throw error if sound returns non-number', () => { + expect(() => funcs.play(funcs.make_sound(t => t > 0.5 ? 1 : 'a' as any, 5))) + .toThrow('play: Provided Sound returned a non-numeric value "a".'); }); test('Concurrently playing two sounds should error', () => { const sound = funcs.silence_sound(10); expect(() => funcs.play(sound)).not.toThrow(); - expect(() => funcs.play(sound)).toThrowError('play: Previous sound still playing'); + expect(() => funcs.play(sound)).toThrow('play: Previous sound still playing'); }); }); describe(funcs.play_wave, () => { it('Should error gracefully when duration is negative', () => { - expect(() => funcs.play_wave(() => 0, -1)) - .toThrow('play_wave: Sound duration must be greater than or equal to 0'); + expect(() => funcs.play_wave(_t => 0, -1)) + .toThrow('play_wave: Expected number greater than 0 for duration, got -1.'); }); it('Should error gracefully when duration is not a number', () => { - expect(() => funcs.play_wave(() => 0, true as any)) - .toThrow('play_wave expects a number for duration, got true'); + expect(() => funcs.play_wave(_t => 0, true as any)) + .toThrow('play_wave: Expected number greater than 0 for duration, got true.'); }); it('Should error gracefully when wave is not a function', () => { expect(() => funcs.play_wave(true as any, 0)) - .toThrow('play_wave expects a wave, got true'); + .toThrow('play_wave: Expected Wave, got true'); }); test('Concurrently playing two sounds should error', () => { - const wave: Wave = () => 0; + const wave: Wave = _t => 0; expect(() => funcs.play_wave(wave, 10)).not.toThrow(); - expect(() => funcs.play_wave(wave, 10)).toThrowError('play: Previous sound still playing'); + expect(() => funcs.play_wave(wave, 10)).toThrow('play: Previous sound still playing'); }); }); describe(funcs.stop, () => { test('Calling stop without ever calling any playback functions should not throw an error', () => { - expect(funcs.stop).not.toThrowError(); + expect(funcs.stop).not.toThrow(); }); it('sets isPlaying to false', () => { funcs.globalVars.isPlaying = true; - funcs.stop(); + expect(funcs.stop).not.toThrow(); expect(funcs.globalVars.isPlaying).toEqual(false); }); }); }); -describe(play_in_tab.play_in_tab, () => { +describe(play_in_tab, () => { it('Should error gracefully when duration is negative', () => { - const sound = [() => 0, -1]; - expect(() => play_in_tab.play_in_tab(sound as any)) - .toThrow('play_in_tab: duration of sound is negative'); + const sound = [(_t: number) => 0, -1]; + expect(() => play_in_tab(sound as any)) + .toThrow('play_in_tab: Expected number greater than 0 for duration, got -1.'); }); it('Should not error when duration is zero', () => { - const sound = funcs.make_sound(() => 0, 0); - expect(() => play_in_tab.play_in_tab(sound)).not.toThrow(); + const sound = funcs.make_sound(_t => 0, 0); + expect(() => play_in_tab(sound)).not.toThrow(); + }); + + it('Should throw error if sound returns non-number', () => { + expect(() => play_in_tab(funcs.make_sound(t => t > 0.5 ? 1 : 'a' as any, 5))) + .toThrow('play_in_tab: Provided Sound returned a non-numeric value "a".'); }); it('Should throw error when given not a sound', () => { - expect(() => play_in_tab.play_in_tab(0 as any)).toThrow('play_in_tab is expecting sound, but encountered 0'); + expect(() => play_in_tab(0 as any)).toThrow('play_in_tab: Expected Sound, got 0.'); }); test('Multiple calls does not cause an error', () => { const sound = funcs.silence_sound(10); - expect(() => play_in_tab.play_in_tab(sound)).not.toThrow(); - expect(() => play_in_tab.play_in_tab(sound)).not.toThrow(); - expect(() => play_in_tab.play_in_tab(sound)).not.toThrow(); + expect(() => play_in_tab(sound)).not.toThrow(); + expect(() => play_in_tab(sound)).not.toThrow(); + expect(() => play_in_tab(sound)).not.toThrow(); }); }); @@ -127,8 +143,8 @@ function evaluateSound(sound: Sound) { describe(funcs.simultaneously, () => { it('works with sounds of the same duration', () => { - const sound0 = funcs.make_sound(() => 1, 10); - const sound1 = funcs.make_sound(() => 0, 10); + const sound0 = funcs.make_sound(_t => 1, 10); + const sound1 = funcs.make_sound(_t => 0, 10); const newSound = funcs.simultaneously([sound0, [sound1, null]]); const points = evaluateSound(newSound); @@ -141,8 +157,8 @@ describe(funcs.simultaneously, () => { }); it('works with sounds of different durations', () => { - const sound0 = funcs.make_sound(() => 1, 10); - const sound1 = funcs.make_sound(() => 2, 5); + const sound0 = funcs.make_sound(_t => 1, 10); + const sound1 = funcs.make_sound(_t => 2, 5); const newSound = funcs.simultaneously([sound0, [sound1, null]]); const points = evaluateSound(newSound); @@ -161,8 +177,8 @@ describe(funcs.simultaneously, () => { describe(funcs.consecutively, () => { it('works', () => { - const sound0 = funcs.make_sound(() => 1, 2); - const sound1 = funcs.make_sound(() => 2, 1); + const sound0 = funcs.make_sound(_t => 1, 2); + const sound1 = funcs.make_sound(_t => 2, 1); const newSound = funcs.consecutively([sound0, [sound1, null]]); const points = evaluateSound(newSound); @@ -175,3 +191,15 @@ describe(funcs.consecutively, () => { expect(points[2]).toEqual(2); }); }); + +describe('Sound transformers', () => { + describe(funcs.phase_mod, () => { + it('throws when given not a sound', () => { + expect(() => funcs.phase_mod(0, 1, 1)(0 as any)).toThrow('SoundTransformer: Expected Sound, got 0'); + }); + + test('returned transformer toReplString representation', () => { + expect(stringify(funcs.phase_mod(0, 1, 1))).toEqual(''); + }); + }); +}); diff --git a/src/bundles/sound/src/functions.ts b/src/bundles/sound/src/functions.ts index 70e7c4f7c4..9ea7d33ec1 100644 --- a/src/bundles/sound/src/functions.ts +++ b/src/bundles/sound/src/functions.ts @@ -1,4 +1,7 @@ import { midi_note_to_frequency } from '@sourceacademy/bundle-midi'; +import type { MIDINote } from '@sourceacademy/bundle-midi/types'; +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange, isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import { accumulate, head, @@ -6,10 +9,13 @@ import { is_pair, length, list, + map, pair, tail, - type List + type List, + type Pair } from 'js-slang/dist/stdlib/list'; +import { stringify } from 'js-slang/dist/utils/stringify'; import type { Sound, SoundProducer, @@ -88,9 +94,9 @@ function linear_decay(decay_period: number): (t: number) => number { */ function getAudioStream(func_name: string) { if (globalVars.stream === null) { - throw new Error(`${func_name}: Call init_record(); to obtain permission to use microphone`); + throw new GeneralRuntimeError(`${func_name}: Call init_record(); to obtain permission to use microphone`); } else if (globalVars.stream === false) { - throw new Error(`${func_name}: Permission has been denied.\n + throw new GeneralRuntimeError(`${func_name}: Permission has been denied.\n Re-start browser and call init_record();\n to obtain permission to use microphone.`); } @@ -187,12 +193,10 @@ export function init_record(): string { * @param buffer - pause before recording, in seconds */ export function record(buffer: number): () => SoundPromise { - if (typeof buffer !== 'number' || buffer < 0) { - throw new Error(`${record.name}: Expected a positive number for buffer, got ${buffer}`); - } + assertNumberWithinRange(buffer, record.name, 0, undefined, false); if (globalVars.isPlaying) { - throw new Error(`${record.name}: Cannot record while another sound is playing!`); + throw new GeneralRuntimeError(`${record.name}: Cannot record while another sound is playing!`); } const stream = getAudioStream(record.name); @@ -210,7 +214,7 @@ export function record(buffer: number): () => SoundPromise { play_recording_signal(); const promise = () => { if (globalVars.recordedSound === null) { - throw new Error('recording still being processed'); + throw new GeneralRuntimeError('recording still being processed'); } else { return globalVars.recordedSound; } @@ -218,7 +222,6 @@ export function record(buffer: number): () => SoundPromise { // TODO: Remove when ReplResult is properly implemented promise.toReplString = () => ''; - promise.toString = () => ''; return promise; }; } @@ -245,7 +248,7 @@ export function record(buffer: number): () => SoundPromise { */ export function record_for(duration: number, buffer: number): SoundPromise { if (globalVars.isPlaying) { - throw new Error(`${record_for.name}: Cannot record while another sound is playing!`); + throw new GeneralRuntimeError(`${record_for.name}: Cannot record while another sound is playing!`); } const stream = getAudioStream(record_for.name); @@ -268,15 +271,13 @@ export function record_for(duration: number, buffer: number): SoundPromise { const promise = () => { if (globalVars.recordedSound === null) { - throw new Error('recording still being processed'); + throw new GeneralRuntimeError('recording still being processed'); } else { return globalVars.recordedSound; } }; promise.toReplString = () => ''; - // TODO: Remove when ReplResult is properly implemented - promise.toString = () => ''; return promise; } @@ -284,23 +285,15 @@ export function record_for(duration: number, buffer: number): SoundPromise { * Throws an exception if duration is not a number or if * number is negative */ -function validateDuration(func_name: string, duration: unknown): asserts duration is number { - if (typeof duration !== 'number') { - throw new Error(`${func_name} expects a number for duration, got ${duration}`); - } - - if (duration < 0) { - throw new Error(`${func_name}: Sound duration must be greater than or equal to 0`); - } +export function validateDuration(func_name: string, duration: unknown): asserts duration is number { + assertNumberWithinRange(duration, func_name, 0, undefined, false, 'duration'); } /** * Throws an exception if wave is not a function */ function validateWave(func_name: string, wave: unknown): asserts wave is Wave { - if (typeof wave !== 'function') { - throw new Error(`${func_name} expects a wave, got ${wave}`); - } + assertFunctionOfLength(wave, 1, func_name, 'Wave'); } /** @@ -312,13 +305,16 @@ function validateWave(func_name: string, wave: unknown): asserts wave is Wave { * @param wave wave function of the Sound * @param duration duration of the Sound * @returns with wave as wave function and duration as duration - * @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); + * @example + * ``` + * const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); + * ``` */ export function make_sound(wave: Wave, duration: number): Sound { validateDuration(make_sound.name, duration); validateWave(make_sound.name, wave); - return pair((t: number) => (t >= duration ? 0 : wave(t)), duration); + return pair(t => (t >= duration ? 0 : wave(t)), duration); } /** @@ -326,7 +322,10 @@ export function make_sound(wave: Wave, duration: number): Sound { * * @param sound given Sound * @returns the wave function of the Sound - * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + * @example + * ``` + * get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + * ``` */ export function get_wave(sound: Sound): Wave { return head(sound); @@ -337,7 +336,10 @@ export function get_wave(sound: Sound): Wave { * * @param sound given Sound * @returns the duration of the Sound - * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + * @example + * ``` + * get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + * ``` */ export function get_duration(sound: Sound): number { return tail(sound); @@ -348,13 +350,16 @@ export function get_duration(sound: Sound): number { * * @param x input to be checked * @returns true if x is a Sound, false otherwise - * @example is_sound(make_sound(t => 0, 2)); // Returns true + * @example + * ``` + * is_sound(make_sound(t => 0, 2)); // Returns true + * ``` */ export function is_sound(x: unknown): x is Sound { return ( is_pair(x) - && typeof get_wave(x) === 'function' - && typeof get_duration(x) === 'number' + && isFunctionOfLength(head(x), 1) + && typeof tail(x) === 'number' ); } @@ -364,7 +369,10 @@ export function is_sound(x: unknown): x is Sound { * * @param wave the wave function to play, starting at 0 * @returns the resulting Sound - * @example play_wave(t => math_sin(t * 3000), 5); + * @example + * ``` + * play_wave(t => math_sin(t * 3000), 5); + * ``` */ export function play_wave(wave: Wave, duration: number): Sound { validateDuration(play_wave.name, duration); @@ -379,22 +387,23 @@ export function play_wave(wave: Wave, duration: number): Sound { * * @param sound the Sound to play * @returns the given Sound - * @example play(sine_sound(440, 5)); + * @example + * ``` + * play(sine_sound(440, 5)); + * ``` */ export function play(sound: Sound): Sound { // Type-check sound if (!is_sound(sound)) { - throw new Error(`${play.name} is expecting sound, but encountered ${sound}`); + throw new InvalidParameterTypeError('sound', sound, play.name); } else if (globalVars.isPlaying) { - throw new Error(`${play.name}: Previous sound still playing!`); + throw new GeneralRuntimeError(`${play.name}: Previous sound still playing!`); } const duration = get_duration(sound); - if (duration < 0) { - throw new Error(`${play.name}: duration of sound is negative`); - } else if (duration === 0) { - return sound; - } + validateDuration(play.name, duration); + + if (duration === 0) return sound; const audioplayer = getAudioContext(); @@ -407,12 +416,16 @@ export function play(sound: Sound): Sound { const channel = theBuffer.getChannelData(0); - let temp: number; let prev_value = 0; const wave = get_wave(sound); - for (let i = 0; i < channel.length; i += 1) { - temp = wave(i / FS); + for (let i = 0; i < channel.length; i++) { + const temp = wave(i / FS); + + if (typeof temp !== 'number') { + throw new GeneralRuntimeError(`${play.name}: Provided Sound returned a non-numeric value ${stringify(temp)}.`); + } + // clip amplitude if (temp > 1) { channel[i] = 1; @@ -460,7 +473,11 @@ export function stop(): void { * * @param duration the duration of the noise sound * @returns resulting noise Sound - * @example noise_sound(5); + * @example + * ``` + * noise_sound(5); + * ``` + * * @category Primitive */ export function noise_sound(duration: number): Sound { @@ -469,11 +486,15 @@ export function noise_sound(duration: number): Sound { } /** - * Makes a silence Sound with given duration + * Makes a silent Sound with given duration * * @param duration the duration of the silence Sound * @returns resulting silence Sound - * @example silence_sound(5); + * @example + * ``` + * silence_sound(5); + * ``` + * * @category Primitive */ export function silence_sound(duration: number): Sound { @@ -487,7 +508,11 @@ export function silence_sound(duration: number): Sound { * @param freq the frequency of the sine wave Sound * @param duration the duration of the sine wave Sound * @returns resulting sine wave Sound - * @example sine_sound(440, 5); + * @example + * ``` + * sine_sound(440, 5); + * ``` + * * @category Primitive */ export function sine_sound(freq: number, duration: number): Sound { @@ -501,7 +526,11 @@ export function sine_sound(freq: number, duration: number): Sound { * @param f the frequency of the square wave Sound * @param duration the duration of the square wave Sound * @returns resulting square wave Sound - * @example square_sound(440, 5); + * @example + * ``` + * square_sound(440, 5); + * ``` + * * @category Primitive */ export function square_sound(f: number, duration: number): Sound { @@ -525,7 +554,11 @@ export function square_sound(f: number, duration: number): Sound { * @param freq the frequency of the triangle wave Sound * @param duration the duration of the triangle wave Sound * @returns resulting triangle wave Sound - * @example triangle_sound(440, 5); + * @example + * ``` + * triangle_sound(440, 5); + * ``` + * * @category Primitive */ export function triangle_sound(freq: number, duration: number): Sound { @@ -551,7 +584,11 @@ export function triangle_sound(freq: number, duration: number): Sound { * @param freq the frequency of the sawtooth wave Sound * @param duration the duration of the sawtooth wave Sound * @returns resulting sawtooth wave Sound - * @example sawtooth_sound(440, 5); + * @example + * ``` + * sawtooth_sound(440, 5); + * ``` + * * @category Primitive */ export function sawtooth_sound(freq: number, duration: number): Sound { @@ -579,9 +616,13 @@ export function sawtooth_sound(freq: number, duration: number): Sound { * * @param list_of_sounds given list of Sounds * @returns the combined Sound - * @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); + * @example + * ``` + * const sound = consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); + * play(sound); + * ``` */ -export function consecutively(list_of_sounds: List): Sound { +export function consecutively(list_of_sounds: List): Sound { function consec_two(ss1: Sound, ss2: Sound) { const wave1 = get_wave(ss1); const wave2 = get_wave(ss2); @@ -602,9 +643,13 @@ export function consecutively(list_of_sounds: List): Sound { * * @param list_of_sounds given list of Sounds * @returns the combined Sound - * @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))) + * @example + * ``` + * const new_sound = simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))); + * play(new_sound); + * ``` */ -export function simultaneously(list_of_sounds: List): Sound { +export function simultaneously(list_of_sounds: List): Sound { function simul_two(ss1: Sound, ss2: Sound) { const wave1 = get_wave(ss1); const wave2 = get_wave(ss2); @@ -636,6 +681,23 @@ export function simultaneously(list_of_sounds: List): Sound { return make_sound(normalised_wave, highest_duration); } +/** + * Utility function for wrapping Sound transformers. Adds the toReplString representation + * and adds check for verifying that the given input is a Sound. + */ +function wrapSoundTransformer(transformer: SoundTransformer): SoundTransformer { + function wrapped(sound: Sound) { + if (!is_sound(sound)) { + throw new InvalidParameterTypeError('Sound', sound, 'SoundTransformer'); + } + + return transformer(sound); + } + + wrapped.toReplString = () => ''; + return wrapped; +} + /** * Returns an envelope: a function from Sound to Sound. * When the adsr envelope is applied to a Sound, it returns @@ -648,8 +710,11 @@ export function simultaneously(list_of_sounds: List): Sound { * @param decay_ratio proportion of Sound decay phase * @param sustain_level sustain level between 0 and 1 * @param release_ratio proportion of Sound in release phase - * @returns Envelope a function from Sound to Sound - * @example adsr(0.2, 0.3, 0.3, 0.1)(sound); + * @function + * @example + * ``` + * adsr(0.2, 0.3, 0.3, 0.1)(sound); + * ``` */ export function adsr( attack_ratio: number, @@ -657,12 +722,19 @@ export function adsr( sustain_level: number, release_ratio: number ): SoundTransformer { - return sound => { + assertNumberWithinRange(attack_ratio, adsr.name, undefined, undefined, false, 'attack_ratio'); + assertNumberWithinRange(decay_ratio, adsr.name, undefined, undefined, false, 'decay_ratio'); + assertNumberWithinRange(sustain_level, adsr.name, 0, undefined, false, 'sustain_level'); + assertNumberWithinRange(release_ratio, adsr.name, undefined, undefined, false, 'release_ratio'); + + return wrapSoundTransformer(sound => { const wave = get_wave(sound); const duration = get_duration(sound); + const attack_time = duration * attack_ratio; const decay_time = duration * decay_ratio; const release_time = duration * release_ratio; + return make_sound((x) => { if (x < attack_time) { return wave(x) * (x / attack_time); @@ -683,7 +755,7 @@ export function adsr( * linear_decay(release_time)(x - (duration - release_time)) ); }, duration); - }; + }); } /** @@ -699,27 +771,37 @@ export function adsr( * @param base_frequency frequency of the first harmonic * @param duration duration of the produced Sound, in seconds * @param envelopes – list of envelopes, which are functions from Sound to Sound - * @returns Sound resulting Sound - * @example stacking_adsr(sine_sound, 300, 5, list(adsr(0.1, 0.3, 0.2, 0.5), adsr(0.2, 0.5, 0.6, 0.1), adsr(0.3, 0.1, 0.7, 0.3))); + * @returns resulting Sound + * @example + * ``` + * const sound = stacking_adsr( + * sine_sound, + * 300, + * 5, + * list( + * adsr(0.1, 0.3, 0.2, 0.5), + * adsr(0.2, 0.5, 0.6, 0.1), + * adsr(0.3, 0.1, 0.7, 0.3) + * ) + * ); + * play(sound); + * ``` */ export function stacking_adsr( waveform: SoundProducer, base_frequency: number, duration: number, - envelopes: List + envelopes: List ): Sound { - function zip(lst: List, n: number) { + function zip(lst: List, n: number): List> { if (is_null(lst)) { return lst; } return pair(pair(n, head(lst)), zip(tail(lst), n + 1)); } - return simultaneously(accumulate( - (x: any, y: any) => pair(tail(x)(waveform(base_frequency * head(x), duration)), y), - null, - zip(envelopes, 1) - )); + const new_list = map(x => tail(x)(waveform(base_frequency * head(x), duration)), zip(envelopes, 1)); + return simultaneously(new_list); } /** @@ -733,21 +815,27 @@ export function stacking_adsr( * @param freq the frequency of the sine wave to be modulated * @param duration the duration of the output Sound * @param amount the amount of modulation to apply to the carrier sine wave - * @returns function which takes in a Sound and returns a Sound - * @example phase_mod(440, 5, 1)(sine_sound(220, 5)); + * @example + * ``` + * phase_mod(440, 5, 1)(sine_sound(220, 5)); + * ``` */ export function phase_mod( freq: number, duration: number, amount: number ): SoundTransformer { - return modulator => { + assertNumberWithinRange(freq, phase_mod.name, 0, undefined, false); + validateDuration(phase_mod.name, duration); + assertNumberWithinRange(amount, phase_mod.name, undefined, undefined, false); + + return wrapSoundTransformer(modulator => { const wave = get_wave(modulator); return make_sound( t => Math.sin(2 * Math.PI * t * freq + amount * wave(t)), duration ); - }; + }); } // Instruments @@ -758,10 +846,16 @@ export function phase_mod( * @param note MIDI note * @param duration duration in seconds * @returns Sound resulting bell Sound with given pitch and duration - * @example bell(40, 1); + * @example + * ``` + * bell(40, 1); + * ``` + * * @category Instrument */ -export function bell(note: number, duration: number): Sound { +export function bell(note: MIDINote, duration: number): Sound { + validateDuration(bell.name, duration); + return stacking_adsr( square_sound, midi_note_to_frequency(note), @@ -781,10 +875,15 @@ export function bell(note: number, duration: number): Sound { * @param note MIDI note * @param duration duration in seconds * @returns Sound resulting cello Sound with given pitch and duration - * @example cello(36, 5); + * @example + * ``` + * cello(36, 5); + * ``` * @category Instrument */ -export function cello(note: number, duration: number): Sound { +export function cello(note: MIDINote, duration: number): Sound { + validateDuration(cello.name, duration); + return stacking_adsr( square_sound, midi_note_to_frequency(note), @@ -799,11 +898,16 @@ export function cello(note: number, duration: number): Sound { * @param note MIDI note * @param duration duration in seconds * @returns Sound resulting piano Sound with given pitch and duration - * @example piano(48, 5); + * @example + * ``` + * piano(48, 5); + * ``` * @category Instrument * */ -export function piano(note: number, duration: number): Sound { +export function piano(note: MIDINote, duration: number): Sound { + validateDuration(piano.name, duration); + return stacking_adsr( triangle_sound, midi_note_to_frequency(note), @@ -818,10 +922,15 @@ export function piano(note: number, duration: number): Sound { * @param note MIDI note * @param duration duration in seconds * @returns Sound resulting trombone Sound with given pitch and duration - * @example trombone(60, 2); + * @example + * ``` + * trombone(60, 2); + * ``` * @category Instrument */ -export function trombone(note: number, duration: number): Sound { +export function trombone(note: MIDINote, duration: number): Sound { + validateDuration(trombone.name, duration); + return stacking_adsr( square_sound, midi_note_to_frequency(note), @@ -836,10 +945,15 @@ export function trombone(note: number, duration: number): Sound { * @param note MIDI note * @param duration duration in seconds * @returns Sound resulting violin Sound with given pitch and duration - * @example violin(53, 4); + * @example + * ``` + * violin(53, 4); + * ``` * @category Instrument */ -export function violin(note: number, duration: number): Sound { +export function violin(note: MIDINote, duration: number): Sound { + validateDuration(violin.name, duration); + return stacking_adsr( sawtooth_sound, midi_note_to_frequency(note), diff --git a/src/bundles/sound/src/index.ts b/src/bundles/sound/src/index.ts index 6ecf04d96b..6e99609502 100644 --- a/src/bundles/sound/src/index.ts +++ b/src/bundles/sound/src/index.ts @@ -13,7 +13,7 @@ * `(get_wave(sound))(get_duration(sound) + x) === 0` for any x >= 0. * * Two functions which combine Sounds, `consecutively` and `simultaneously` are given. - * Additionally, we provide sound transformation functions `adsr` and `phase_mod` + * Additionally, we provide sound transformation functions like `adsr` and `phase_mod` * which take in a Sound and return a Sound. * * Finally, the provided `play` function takes in a Sound and plays it using your diff --git a/src/bundles/sound/src/play_in_tab.ts b/src/bundles/sound/src/play_in_tab.ts index a55aa04fb1..87ed695ae8 100644 --- a/src/bundles/sound/src/play_in_tab.ts +++ b/src/bundles/sound/src/play_in_tab.ts @@ -1,5 +1,7 @@ +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; import context from 'js-slang/context'; -import { FS, get_duration, get_wave, is_sound } from './functions'; +import { stringify } from 'js-slang/dist/utils/stringify'; +import { FS, get_duration, get_wave, is_sound, validateDuration } from './functions'; import { RIFFWAVE } from './riffwave'; import type { AudioPlayed, Sound } from './types'; @@ -12,34 +14,37 @@ context.moduleContexts.sound.state = { audioPlayed }; * * @param sound the Sound to play * @returns the given Sound - * @example play_in_tab(sine_sound(440, 5)); + * @example + * ``` + * play_in_tab(sine_sound(440, 5)); + * ``` */ export function play_in_tab(sound: Sound): Sound { // Type-check sound if (!is_sound(sound)) { - throw new Error(`${play_in_tab.name} is expecting sound, but encountered ${sound}`); + throw new InvalidParameterTypeError('Sound', sound, play_in_tab.name); } const duration = get_duration(sound); - if (duration < 0) { - throw new Error(`${play_in_tab.name}: duration of sound is negative`); - } else if (duration === 0) { - return sound; - } + validateDuration(play_in_tab.name, duration); + if (duration === 0) return sound; // Create mono buffer const channel: number[] = []; const len = Math.ceil(FS * duration); - let temp: number; let prev_value = 0; const wave = get_wave(sound); for (let i = 0; i < len; i += 1) { - temp = wave(i / FS); + const temp = wave(i / FS); + + if (typeof temp !== 'number') { + throw new GeneralRuntimeError(`${play_in_tab.name}: Provided Sound returned a non-numeric value ${stringify(temp)}.`); + } + // clip amplitude - // channel[i] = temp > 1 ? 1 : temp < -1 ? -1 : temp; if (temp > 1) { channel[i] = 1; } else if (temp < -1) { @@ -61,13 +66,14 @@ export function play_in_tab(sound: Sound): Sound { channel[i] = Math.floor(channel[i] * 32767.999); } + // @ts-expect-error RIFFWAVE type definition missing const riffwave = new RIFFWAVE([]); riffwave.header.sampleRate = FS; riffwave.header.numChannels = 1; riffwave.header.bitsPerSample = 16; riffwave.Make(channel); - const soundToPlay = { + const soundToPlay: AudioPlayed = { toReplString: () => '', dataUri: riffwave.dataURI }; diff --git a/src/bundles/sound/src/riffwave.ts b/src/bundles/sound/src/riffwave.ts index 5186ee708d..57fdb6872f 100644 --- a/src/bundles/sound/src/riffwave.ts +++ b/src/bundles/sound/src/riffwave.ts @@ -19,6 +19,8 @@ /* v8 ignore start */ /* eslint-disable */ +// @ts-nocheck + var FastBase64 = { chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', encLookup: String, diff --git a/src/bundles/sound_matrix/package.json b/src/bundles/sound_matrix/package.json index 6f8b250731..b7fc326e6e 100644 --- a/src/bundles/sound_matrix/package.json +++ b/src/bundles/sound_matrix/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "js-slang": "^1.0.85" + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -19,8 +19,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/sound_matrix/src/functions.ts b/src/bundles/sound_matrix/src/functions.ts index 53b076e458..b7f6575997 100644 --- a/src/bundles/sound_matrix/src/functions.ts +++ b/src/bundles/sound_matrix/src/functions.ts @@ -10,8 +10,8 @@ */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { list_to_vector, vector_to_list } from './list'; -import type { List } from './types'; +import { GeneralRuntimeError } from 'js-slang/dist/errors/base'; +import { list_to_vector, vector_to_list, type List } from 'js-slang/dist/stdlib/list'; export const ToneMatrix = { initialise_matrix, @@ -212,7 +212,7 @@ function initialise_matrix($container: HTMLElement): void { ToneMatrix.initialise_matrix = initialise_matrix; // bind the click events to the matrix -function bind_events_to_rect(c) { +function bind_events_to_rect(c: HTMLCanvasElement) { c.addEventListener( 'click', (event) => { @@ -337,7 +337,7 @@ ToneMatrix.bindMatrixButtons = bindMatrixButtons; // return the current state of the matrix, represented by a list of lists of bits export function get_matrix(): List { if (!matrix) { - throw new Error('Please activate the tone matrix first by clicking on the tab!'); + throw new GeneralRuntimeError('Please activate the tone matrix first by clicking on the tab!'); } const matrix_list = matrix.slice(0); const result: List[] = []; @@ -367,12 +367,12 @@ ToneMatrix.clear_matrix = clear_matrix; const set_time_out_renamed = window.setTimeout; -export function set_timeout(f, t) { +export function set_timeout(f: () => void, t: number) { if (typeof f === 'function' && typeof t === 'number') { const timeoutObj = set_time_out_renamed(f, t); timeout_objects.push(timeoutObj); } else { - throw new Error('set_timeout(f, t) expects a function and a number respectively.'); + throw new GeneralRuntimeError('set_timeout(f, t) expects a function and a number respectively.'); } } diff --git a/src/bundles/sound_matrix/src/list.ts b/src/bundles/sound_matrix/src/list.ts deleted file mode 100644 index 3861875d0f..0000000000 --- a/src/bundles/sound_matrix/src/list.ts +++ /dev/null @@ -1,346 +0,0 @@ -// list.js: Supporting lists in the Scheme style, using pairs made -// up of two-element JavaScript array (vector) - -// Author: Martin Henz - -// Note: this library is used in the externalLibs of cadet-frontend. -// It is distinct from the LISTS library of Source §2, which contains -// primitive and predeclared functions from Chapter 2 of SICP JS. - -// array test works differently for Rhino and -// the Firefox environment (especially Web Console) -export function array_test(x): boolean { - if (Array.isArray === undefined) { - return x instanceof Array; - } else { - return Array.isArray(x); - } -} - -// pair constructs a pair using a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function pair(x, xs): [any, any] { - return [x, xs]; -} - -// is_pair returns true iff arg is a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_pair(x): boolean { - return array_test(x) && x.length === 2; -} - -// head returns the first component of the given pair, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function head(xs): any { - if (is_pair(xs)) { - return xs[0]; - } else { - throw new Error('head(xs) expects a pair as argument xs, but encountered ' + xs); - } -} - -// tail returns the second component of the given pair -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function tail(xs) { - if (is_pair(xs)) { - return xs[1]; - } else { - throw new Error('tail(xs) expects a pair as argument xs, but encountered ' + xs); - } -} - -// is_null returns true if arg is exactly null -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_null(xs) { - return xs === null; -} - -// is_list recurses down the list and checks that it ends with the empty list [] -// does not throw Value exceptions -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_list(xs) { - for (; ; xs = tail(xs)) { - if (is_null(xs)) { - return true; - } else if (!is_pair(xs)) { - return false; - } - } -} - -// list makes a list out of its arguments -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list(...args) { - let the_list: any = null; - for (let i = args.length - 1; i >= 0; i--) { - the_list = pair(args[i], the_list); - } - return the_list; -} - -// list_to_vector returns vector that contains the elements of the argument list -// in the given order. -// list_to_vector throws an exception if the argument is not a list -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list_to_vector(lst) { - const vector: any[] = []; - while (!is_null(lst)) { - vector.push(head(lst)); - lst = tail(lst); - } - return vector; -} - -// vector_to_list returns a list that contains the elements of the argument vector -// in the given order. -// vector_to_list throws an exception if the argument is not a vector -// LOW-LEVEL FUNCTION, NOT SOURCE -export function vector_to_list(vector) { - return list(...vector); -} - -// returns the length of a given argument list -// throws an exception if the argument is not a list -export function length(xs) { - let i = 0; - while (!is_null(xs)) { - i += 1; - xs = tail(xs); - } - return i; -} - -// map applies first arg f to the elements of the second argument, -// assumed to be a list. -// f is applied element-by-element: -// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] -// map throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the first -// argument is not a function. -// tslint:disable-next-line:ban-types -export function map(f, xs) { - return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); -} - -// build_list takes a non-negative integer n as first argument, -// and a function fun as second argument. -// build_list returns a list of n elements, that results from -// applying fun to the numbers from 0 to n-1. -// tslint:disable-next-line:ban-types -export function build_list(n, fun) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error('build_list(n, fun) expects a positive integer as ' - + 'argument n, but encountered ' - + n); - } - - // tslint:disable-next-line:ban-types - function build(i, alreadyBuilt) { - if (i < 0) { - return alreadyBuilt; - } else { - return build(i - 1, pair(fun(i), alreadyBuilt)); - } - } - - return build(n - 1, null); -} - -// for_each applies first arg fun to the elements of the list passed as -// second argument. fun is applied element-by-element: -// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). -// for_each returns true. -// for_each throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the -// first argument is not a function. -// tslint:disable-next-line:ban-types -export function for_each(fun, xs) { - if (!is_list(xs)) { - throw new Error('for_each expects a list as argument xs, but encountered ' + xs); - } - for (; !is_null(xs); xs = tail(xs)) { - fun(head(xs)); - } - return true; -} - -// reverse reverses the argument list -// reverse throws an exception if the argument is not a list. -export function reverse(xs) { - if (!is_list(xs)) { - throw new Error('reverse(xs) expects a list as argument xs, but encountered ' + xs); - } - let result: any = null; - for (; !is_null(xs); xs = tail(xs)) { - result = pair(head(xs), result); - } - return result; -} - -// append first argument list and second argument list. -// In the result, the [] at the end of the first argument list -// is replaced by the second argument list -// append throws an exception if the first argument is not a list -export function append(xs, ys) { - if (is_null(xs)) { - return ys; - } else { - return pair(head(xs), append(tail(xs), ys)); - } -} - -// member looks for a given first-argument element in a given -// second argument list. It returns the first postfix sublist -// that starts with the given element. It returns [] if the -// element does not occur in the list -export function member(v, xs) { - for (; !is_null(xs); xs = tail(xs)) { - if (head(xs) === v) { - return xs; - } - } - return null; -} - -// removes the first occurrence of a given first-argument element -// in a given second-argument list. Returns the original list -// if there is no occurrence. -export function remove(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return tail(xs); - } else { - return pair(head(xs), remove(v, tail(xs))); - } - } -} - -// Similar to remove. But removes all instances of v instead of just the first -export function remove_all(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return remove_all(v, tail(xs)); - } else { - return pair(head(xs), remove_all(v, tail(xs))); - } - } -} - -// for backwards-compatibility -// equal computes the structural equality -// over its arguments -export function equal(item1, item2) { - if (is_pair(item1) && is_pair(item2)) { - return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); - } else { - return item1 === item2; - } -} - -// assoc treats the second argument as an association, -// a list of (index,value) pairs. -// assoc returns the first (index,value) pair whose -// index equal (using structural equality) to the given -// first argument v. Returns false if there is no such -// pair -export function assoc(v, xs) { - if (is_null(xs)) { - return false; - } else if (equal(v, head(head(xs)))) { - return head(xs); - } else { - return assoc(v, tail(xs)); - } -} - -// filter returns the sublist of elements of given list xs -// for which the given predicate function returns true. -// tslint:disable-next-line:ban-types -export function filter(pred, xs) { - if (is_null(xs)) { - return xs; - } else { - if (pred(head(xs))) { - return pair(head(xs), filter(pred, tail(xs))); - } else { - return filter(pred, tail(xs)); - } - } -} - -// enumerates numbers starting from start, -// using a step size of 1, until the number -// exceeds end. -export function enum_list(start, end) { - if (typeof start !== 'number') { - throw new Error('enum_list(start, end) expects a number as argument start, but encountered ' - + start); - } - if (typeof end !== 'number') { - throw new Error('enum_list(start, end) expects a number as argument start, but encountered ' - + end); - } - if (start > end) { - return null; - } else { - return pair(start, enum_list(start + 1, end)); - } -} - -// Returns the item in list lst at index n (the first item is at position 0) -export function list_ref(xs, n) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error('list_ref(xs, n) expects a positive integer as argument n, but encountered ' - + n); - } - for (; n > 0; --n) { - xs = tail(xs); - } - return head(xs); -} - -// accumulate applies given operation op to elements of a list -// in a right-to-left order, first apply op to the last element -// and an initial element, resulting in r1, then to the -// second-last element and r1, resulting in r2, etc, and finally -// to the first element and r_n-1, where n is the length of the -// list. -// accumulate(op,zero,list(1,2,3)) results in -// op(1, op(2, op(3, zero))) -export function accumulate(op, initial, sequence) { - if (is_null(sequence)) { - return initial; - } else { - return op(head(sequence), accumulate(op, initial, tail(sequence))); - } -} - -// set_head(xs,x) changes the head of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_head(xs, x) { - if (is_pair(xs)) { - xs[0] = x; - return undefined; - } else { - throw new Error('set_head(xs,x) expects a pair as argument xs, but encountered ' + xs); - } -} - -// set_tail(xs,x) changes the tail of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_tail(xs, x) { - if (is_pair(xs)) { - xs[1] = x; - return undefined; - } else { - throw new Error('set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs); - } -} diff --git a/src/bundles/sound_matrix/src/types.ts b/src/bundles/sound_matrix/src/types.ts deleted file mode 100644 index 771c27b3a9..0000000000 --- a/src/bundles/sound_matrix/src/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Pair = [H, T]; -export type EmptyList = null; -export type NonEmptyList = Pair; -export type List = EmptyList | NonEmptyList; diff --git a/src/bundles/stereo_sound/package.json b/src/bundles/stereo_sound/package.json index 48b5de8241..127be43c31 100644 --- a/src/bundles/stereo_sound/package.json +++ b/src/bundles/stereo_sound/package.json @@ -8,7 +8,8 @@ }, "dependencies": { "@sourceacademy/bundle-midi": "workspace:^", - "js-slang": "^1.0.85" + "@sourceacademy/modules-lib": "workspace:^", + "js-slang": "^1.0.92" }, "type": "module", "exports": { @@ -20,8 +21,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/stereo_sound/src/__tests__/index.test.ts b/src/bundles/stereo_sound/src/__tests__/index.test.ts index 19d31e690e..ac90e03ff4 100644 --- a/src/bundles/stereo_sound/src/__tests__/index.test.ts +++ b/src/bundles/stereo_sound/src/__tests__/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import * as funcs from '../functions'; -import type { Sound } from '../types'; +import type { Sound, Wave } from '../types'; function evaluateSound(sound: Sound) { const leftWave = funcs.get_left_wave(sound); @@ -17,6 +17,36 @@ function evaluateSound(sound: Sound) { return output; } +describe(funcs.is_sound, () => { + const left: Wave = _t => 0; + const right: Wave = _t => 0; + + it('returns true for a stereo sound', () => { + const x: Sound = [[left, right], 1]; + expect(funcs.is_sound(x)).toEqual(true); + }); + + it('returns false when duration is not a number', () => { + const x = [[left, right], '1']; + expect(funcs.is_sound(x)).toEqual(false); + }); + + it('returns false when left wave is not a wave', () => { + const x = [[0, right], 1]; + expect(funcs.is_sound(x)).toEqual(false); + }); + + it('returns false when right wave is not a wave', () => { + const x = [[left, 0], 1]; + expect(funcs.is_sound(x)).toEqual(false); + }); + + it('returns false when waves are not a pair', () => { + const x = [0, 1]; + expect(funcs.is_sound(x)).toEqual(false); + }); +}); + describe(funcs.pan, () => { it('should mute the left channel when panned all the way right', () => { const sound = funcs.make_stereo_sound( @@ -129,7 +159,7 @@ describe(funcs.consecutively, () => { describe(funcs.make_sound, () => { it('Should error gracefully when duration is negative', () => { expect(() => funcs.make_sound(() => 0, -1)) - .toThrow('make_sound: Sound duration must be greater than or equal to 0'); + .toThrow('make_sound: Expected integer greater than 0, got -1.'); }); it('Should not error when duration is zero', () => { @@ -138,13 +168,13 @@ describe(funcs.make_sound, () => { it('Should error gracefully when wave is not a function', () => { expect(() => funcs.make_sound(true as any, 1)) - .toThrow('make_sound: wave must be a Wave, got true'); + .toThrow('make_sound: Expected Wave, got true.'); }); }); describe(funcs.play, () => { it('Should error gracefully when duration is negative', () => { - const sound: Sound = [[() => 0, () => 0], -1]; + const sound: Sound = [[_t => 0, _t => 0], -1]; expect(() => funcs.play(sound)) .toThrow('play: duration of sound is negative'); }); @@ -155,40 +185,40 @@ describe(funcs.play, () => { }); it('Should throw error when given not a sound', () => { - expect(() => funcs.play(0 as any)).toThrow('play is expecting sound, but encountered 0'); + expect(() => funcs.play(0 as any)).toThrow('play: Expected sound, got 0.'); }); }); describe(funcs.play_wave, () => { it('Should error gracefully when duration is negative', () => { expect(() => funcs.play_wave(() => 0, -1)) - .toThrow('play_wave: Sound duration must be greater than or equal to 0'); + .toThrow('play_wave: Expected integer greater than 0, got -1.'); }); it('Should error gracefully when duration is not a number', () => { expect(() => funcs.play_wave(() => 0, true as any)) - .toThrow('play_wave expects a number for duration, got true'); + .toThrow('play_wave: Expected integer greater than 0, got true.'); }); it('Should error gracefully when wave is not a function', () => { expect(() => funcs.play_wave(true as any, 0)) - .toThrow('play_wave: wave must be a Wave, got true'); + .toThrow('play_wave: Expected Wave, got true.'); }); }); describe(funcs.play_in_tab, () => { it('Should error gracefully when duration is negative', () => { - const sound: Sound = [[() => 0, () => 0], -1]; + const sound: Sound = [[_t => 0, _t => 0], -1]; expect(() => funcs.play_in_tab(sound)) .toThrow('play_in_tab: duration of sound is negative'); }); it('Should not error when duration is zero', () => { - const sound: Sound = [[() => 0, () => 0], 0]; + const sound: Sound = [[_t => 0, _t => 0], 0]; expect(() => funcs.play_in_tab(sound)).not.toThrow(); }); it('Should throw error when given not a sound', () => { - expect(() => funcs.play_in_tab(0 as any)).toThrow('play_in_tab is expecting sound, but encountered 0'); + expect(() => funcs.play_in_tab(0 as any)).toThrow('play_in_tab: Expected sound, got 0.'); }); }); diff --git a/src/bundles/stereo_sound/src/functions.ts b/src/bundles/stereo_sound/src/functions.ts index c8c193c3dd..32be6416b3 100644 --- a/src/bundles/stereo_sound/src/functions.ts +++ b/src/bundles/stereo_sound/src/functions.ts @@ -1,4 +1,6 @@ import { midi_note_to_frequency } from '@sourceacademy/bundle-midi'; +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertNumberWithinRange, isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import context from 'js-slang/context'; import { accumulate, @@ -7,9 +9,11 @@ import { is_pair, length, list, + map, pair, tail, - type List + type List, + type Pair } from 'js-slang/dist/stdlib/list'; import { RIFFWAVE } from './riffwave'; import type { @@ -67,9 +71,9 @@ let recorded_sound: Sound | undefined; // to record a sound function check_permission() { if (permission === undefined) { - throw new Error('Call init_record(); to obtain permission to use microphone'); + throw new GeneralRuntimeError('Call init_record(); to obtain permission to use microphone'); } else if (permission === false) { - throw new Error(`Permission has been denied.\n + throw new GeneralRuntimeError(`Permission has been denied.\n Re-start browser and call init_record();\n to obtain permission to use microphone.`); } // (permission === true): do nothing @@ -173,7 +177,7 @@ export function record(buffer: number): () => () => Sound { play_recording_signal(); return () => { if (recorded_sound === undefined) { - throw new Error('recording still being processed'); + throw new GeneralRuntimeError('recording still being processed'); } else { return recorded_sound; } @@ -209,28 +213,20 @@ export function record_for(duration: number, buffer: number): () => Sound { }, recording_signal_duration_ms + buffer * 1000); return () => { if (recorded_sound === undefined) { - throw new Error('recording still being processed'); + throw new GeneralRuntimeError('recording still being processed'); } else { return recorded_sound; } }; } -function validateDuration(func_name: string, duration: unknown): asserts duration is number { - if (typeof duration !== 'number') { - throw new Error(`${func_name} expects a number for duration, got ${duration}`); - } - - if (duration < 0) { - throw new Error(`${func_name}: Sound duration must be greater than or equal to 0`); - } +function validateDuration(func_name: string, duration: unknown, param_name?: string): asserts duration is number { + assertNumberWithinRange(duration, func_name, 0, undefined, true, param_name); } function validateWave(func_name: string, wave: unknown, lr?: 'left' | 'right'): asserts wave is Wave { - const direction = lr !== undefined ? `${lr}_` : ''; - if (typeof wave !== 'function') { - throw new Error(`${func_name}: ${direction}wave must be a Wave, got ${wave}`); + throw new InvalidParameterTypeError('Wave', wave, func_name, lr === undefined ? undefined : `${lr} wave`); } } @@ -339,12 +335,45 @@ export function get_duration(sound: Sound): number { * @example is_sound(make_sound(t => 0, 2)); // Returns true */ export function is_sound(x: unknown): x is Sound { - return ( - is_pair(x) - && typeof get_left_wave(x) === 'function' - && typeof get_right_wave(x) === 'function' - && typeof get_duration(x) === 'number' - ); + if (!is_pair(x)) return false; + + const waves = head(x); + if (!is_pair(waves)) return false; + + const left_wave = head(waves); + if (!isFunctionOfLength(left_wave, 1)) return false; + + const right_wave = tail(waves); + if (!isFunctionOfLength(right_wave, 1)) return false; + + const duration = tail(x); + return typeof duration === 'number'; +} + +function throwIfNotSound(obj: unknown, func_name: string, param_name?: string): asserts obj is Sound { + if (!is_pair(obj)) { + throw new InvalidParameterTypeError('sound', obj, func_name, param_name); + } + + const waves = head(obj); + if (!is_pair(waves)) { + throw new GeneralRuntimeError(`${func_name}: head of sound should be a pair of waves.`); + } + + const left_wave = head(waves); + if (!isFunctionOfLength(left_wave, 1)) { + throw new GeneralRuntimeError(`${func_name}: left wave is not a valid wave.`); + } + + const right_wave = tail(waves); + if (!isFunctionOfLength(right_wave, 1)) { + throw new GeneralRuntimeError(`${func_name}: right wave is not a valid wave.`); + } + + const duration = tail(obj); + if (typeof duration !== 'number') { + throw new GeneralRuntimeError(`${func_name}: Duration of sound is not a number!`); + } } /** @@ -393,17 +422,16 @@ export function play_waves( * @example play_in_tab(sine_sound(440, 5)); */ export function play_in_tab(sound: Sound): Sound { - // Type-check sound - if (!is_sound(sound)) { - throw new Error(`${play_in_tab.name} is expecting sound, but encountered ${sound}`); + throwIfNotSound(sound, play_in_tab.name); + + if (isPlaying) { // If a sound is already playing, terminate execution. - } else if (isPlaying) { - throw new Error(`${play_in_tab.name}: audio system still playing previous sound`); + throw new GeneralRuntimeError(`${play_in_tab.name}: audio system still playing previous sound`); } const duration = get_duration(sound); if (duration < 0) { - throw new Error(`${play_in_tab.name}: duration of sound is negative`); + throw new GeneralRuntimeError(`${play_in_tab.name}: duration of sound is negative`); } else if (duration === 0) { return sound; } @@ -469,6 +497,7 @@ export function play_in_tab(sound: Sound): Sound { channel[i] = Math.floor(channel[i] * 32767.999); } + // @ts-expect-error RIFFWAVE type definition missing const riffwave = new RIFFWAVE([]); riffwave.header.sampleRate = FS; riffwave.header.numChannels = 2; @@ -494,16 +523,16 @@ export function play_in_tab(sound: Sound): Sound { */ export function play(sound: Sound): Sound { // Type-check sound - if (!is_sound(sound)) { - throw new Error(`${play.name} is expecting sound, but encountered ${sound}`); + throwIfNotSound(sound, play.name); + + if (isPlaying) { // If a sound is already playing, terminate execution. - } else if (isPlaying) { - throw new Error(`${play.name}: audio system still playing previous sound`); + throw new GeneralRuntimeError(`${play.name}: audio system still playing previous sound`); } const duration = get_duration(sound); if (duration < 0) { - throw new Error(`${play.name}: duration of sound is negative`); + throw new GeneralRuntimeError(`${play.name}: duration of sound is negative`); } else if (duration === 0) { return sound; } @@ -570,6 +599,7 @@ export function play(sound: Sound): Sound { channel[i] = Math.floor(channel[i] * 32767.999); } + // @ts-expect-error RIFFWAVE type definition missing const riffwave = new RIFFWAVE([]); riffwave.header.sampleRate = FS; riffwave.header.numChannels = 2; @@ -789,7 +819,7 @@ export function sawtooth_sound(freq: number, duration: number): Sound { * @returns the combined Sound * @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); */ -export function consecutively(list_of_sounds: List): Sound { +export function consecutively(list_of_sounds: List): Sound { function stereo_cons_two(sound1: Sound, sound2: Sound) { const Lwave1 = get_left_wave(sound1); const Rwave1 = get_right_wave(sound1); @@ -815,7 +845,7 @@ export function consecutively(list_of_sounds: List): Sound { * @returns the combined Sound * @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))) */ -export function simultaneously(list_of_sounds: List): Sound { +export function simultaneously(list_of_sounds: List): Sound { function stereo_simul_two(sound1: Sound, sound2: Sound) { const Lwave1 = get_left_wave(sound1); const Rwave1 = get_right_wave(sound1); @@ -916,20 +946,17 @@ export function stacking_adsr( waveform: SoundProducer, base_frequency: number, duration: number, - envelopes: List + envelopes: List ): Sound { - function zip(lst: List, n: number) { + function zip(lst: List, n: number): List> { if (is_null(lst)) { return lst; } return pair(pair(n, head(lst)), zip(tail(lst), n + 1)); } - return simultaneously(accumulate( - (x: any, y: any) => pair(tail(x)(waveform(base_frequency * head(x), duration)), y), - null, - zip(envelopes, 1) - )); + const new_list = map(x => tail(x)(waveform(base_frequency * head(x), duration)), zip(envelopes, 1)); + return simultaneously(new_list); } /** diff --git a/src/bundles/stereo_sound/src/riffwave.ts b/src/bundles/stereo_sound/src/riffwave.ts index 5186ee708d..57fdb6872f 100644 --- a/src/bundles/stereo_sound/src/riffwave.ts +++ b/src/bundles/stereo_sound/src/riffwave.ts @@ -19,6 +19,8 @@ /* v8 ignore start */ /* eslint-disable */ +// @ts-nocheck + var FastBase64 = { chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', encLookup: String, diff --git a/src/bundles/unittest/package.json b/src/bundles/unittest/package.json index 3ca5adb386..e2dfefc0f3 100644 --- a/src/bundles/unittest/package.json +++ b/src/bundles/unittest/package.json @@ -1,10 +1,11 @@ { "name": "@sourceacademy/bundle-unittest", - "version": "1.0.0", + "version": "1.0.1", "private": true, "dependencies": { + "@sourceacademy/modules-lib": "workspace:^", "es-toolkit": "^1.44.0", - "js-slang": "^1.0.85" + "js-slang": "^1.0.92" }, "devDependencies": { "@sourceacademy/modules-buildtools": "workspace:^", @@ -20,8 +21,9 @@ "test": "buildtools test --project .", "tsc": "buildtools tsc .", "lint": "buildtools lint .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/unittest/src/__tests__/index.test.ts b/src/bundles/unittest/src/__tests__/index.test.ts index 4890a8332f..e324d2622d 100644 --- a/src/bundles/unittest/src/__tests__/index.test.ts +++ b/src/bundles/unittest/src/__tests__/index.test.ts @@ -4,16 +4,16 @@ import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import * as asserts from '../asserts'; import * as testing from '../functions'; import * as mocks from '../mocks'; -import { UnitestBundleInternalError } from '../types'; +import { UnittestBundleInternalError } from '../types'; vi.spyOn(performance, 'now').mockReturnValue(0); describe('Test \'it\' and \'describe\'', () => { beforeEach(() => { - testing.suiteResults.splice(0); + testing.topLevelSuiteResults.splice(0); }); - test('it and describe correctly set and resets the value of current test and suite', () => { + test('it() and describe() correctly set and resets the value of current test and suite', () => { expect(testing.currentTest).toBeNull(); expect(testing.currentSuite).toBeNull(); testing.describe('suite', () => { @@ -28,33 +28,33 @@ describe('Test \'it\' and \'describe\'', () => { }); test('it() throws an error when called without describe', () => { - expect(() => testing.it('desc', () => {})).toThrowError('it must be called from within a test suite!'); + expect(() => testing.it('desc', () => { })).toThrow('it must be called from within a test suite!'); }); test('it() throws an error even if it is called after describe', () => { - testing.describe('a test', () => {}); - expect(() => testing.it('desc', () => {})).toThrowError('it must be called from within a test suite!'); + testing.describe('a test', () => { }); + expect(() => testing.it('desc', () => { })).toThrow('it must be called from within a test suite!'); }); test('it() works fine from within a describe block', () => { expect(() => { testing.describe('desc', () => { - testing.it('desc', () => {}); + testing.it('desc', () => { }); }); }).not.toThrow(); }); test('it() correctly assigns results to the correct suite', () => { testing.describe('block1', () => { - testing.it('test1', () => {}); + testing.it('test1', () => { }); }); testing.describe('block2', () => { - testing.it('test2', () => {}); + testing.it('test2', () => { }); }); - expect(testing.suiteResults.length).toEqual(2); - const [result1, result2] = testing.suiteResults; + expect(testing.topLevelSuiteResults.length).toEqual(2); + const [result1, result2] = testing.topLevelSuiteResults; expect(result1).toMatchObject({ name: 'block1', results: [{ name: 'test1', passed: true }], @@ -75,17 +75,17 @@ describe('Test \'it\' and \'describe\'', () => { test('it() correctly assigns results to child suites', () => { testing.describe('block1', () => { testing.describe('block3', () => { - testing.it('test3', () => {}); + testing.it('test3', () => { }); }); - testing.it('test1', () => {}); + testing.it('test1', () => { }); }); testing.describe('block2', () => { - testing.it('test2', () => {}); + testing.it('test2', () => { }); }); - expect(testing.suiteResults.length).toEqual(2); - const [result1, result2] = testing.suiteResults; + expect(testing.topLevelSuiteResults.length).toEqual(2); + const [result1, result2] = testing.topLevelSuiteResults; // Verify Result 1 first expect(result1.results.length).toEqual(2); const [subResult1, subResult2] = result1.results; @@ -118,18 +118,41 @@ describe('Test \'it\' and \'describe\'', () => { test('it() throws when called within another it block', () => { const f = () => testing.describe('suite', () => { testing.it('test0', () => { - testing.it('test1', () => {}); + testing.it('test1', () => { }); }); }); - expect(f).toThrowError('it cannot be called from within another test!'); + expect(f).toThrow('it cannot be called from within another test!'); + }); + + test('it() and describe() throw when provided a non-nullary function', () => { + expect(() => testing.it('test name', 0 as any)).toThrow( + 'it: A test must be a nullary function!' + ); + + expect(() => testing.describe('test name', 0 as any)).toThrow( + 'describe: A test suite must be a nullary function!' + ); + }); + + test('internal errors are not handled', () => { + expect(() => testing.describe('suite', () => { + testing.test('test', () => { throw new UnittestBundleInternalError(''); }); + })).toThrow(UnittestBundleInternalError); }); }); describe('Test assertion functions', () => { - test('assert', () => { - expect(() => asserts.assert(() => true)).not.toThrow(); - expect(() => asserts.assert(() => false)).toThrow('Assert failed'); + describe(asserts.assert, () => { + it('works', () => { + expect(() => asserts.assert(() => true)).not.toThrow(); + expect(() => asserts.assert(() => false)).toThrow('Assert failed'); + }); + + it('will throw an error if not provided a nullary function', () => { + expect(() => asserts.assert(0 as any)).toThrow(`${asserts.assert.name} expects a nullary function that returns a boolean!`); + expect(() => asserts.assert(((x: any) => x === true) as any)).toThrow(`${asserts.assert.name} expects a nullary function that returns a boolean!`); + }); }); describe(asserts.assert_equals, () => { @@ -168,8 +191,8 @@ describe('Test assertion functions', () => { }); test('deep equality', () => { - const list0 = list(1, pair(2, 3), 4); - const list1 = list(1, pair(2, 3), 4); + const list0 = list(1, pair(2, 3), 4); + const list1 = list(1, pair(2, 3), 4); expect(() => asserts.assert_equals(list0, list1)).not.toThrow(); }); @@ -274,11 +297,11 @@ describe('Test assertion functions', () => { describe(asserts.assert_length, () => { it('throws when the received value isn\'t a list', () => { - expect(() => asserts.assert_length('string' as any, 0)).toThrow('First argument to assert_length must be a list.'); + expect(() => asserts.assert_length('string' as any, 0)).toThrow('First argument to assert_length must be a list or array.'); }); it('throws when the expected value isn\'t a number', () => { - expect(() => asserts.assert_length(list(), 'string' as any)).toThrow('Second argument to assert_length must be an integer.'); + expect(() => asserts.assert_length(list(), 'string' as any)).toThrow('assert_length: Expected integer for len, got "string".'); }); it('works for empty lists', () => { @@ -303,11 +326,11 @@ describe('Mocking functions', () => { expect(testFunc).toHaveBeenCalledOnce(); }); - test('mocked functions retain the stringify of the original function', () => { + test('mocked functions have nice string representation', () => { const fn = () => 2; const mocked = mocks.mock_function(fn); - expect(stringify(fn)).toEqual(stringify(mocked)); + expect(stringify(mocked)).toEqual(''); }); test('no calls', () => { @@ -373,21 +396,27 @@ describe('Mocking functions', () => { expect(mocks.get_ret_vals(fn)).toEqual(null); }); + describe(mocks.mock_function, () => { + it('throws when passed not a function', () => { + expect(() => mocks.mock_function(0 as any)).toThrow('mock_function: Expected function, got 0.'); + }); + }); + describe(mocks.get_arg_list, () => { it('throws when function isn\'t a mocked function', () => { - expect(() => mocks.get_arg_list((() => 0) as any)).toThrowError('get_arg_list expects a mocked function as argument'); + expect(() => mocks.get_arg_list((() => 0) as any)).toThrow('get_arg_list: Expected mocked function, got () => 0.'); }); }); describe(mocks.get_ret_vals, () => { it('throws when function isn\'t a mocked function', () => { - expect(() => mocks.get_ret_vals((() => 0) as any)).toThrowError('get_ret_vals expects a mocked function as argument'); + expect(() => mocks.get_ret_vals((() => 0) as any)).toThrowError('get_ret_vals: Expected mocked function, got () => 0.'); }); }); describe(mocks.clear_mock, () => { it('throws when function isn\'t a mocked function', () => { - expect(() => mocks.clear_mock((() => 0) as any)).toThrowError('clear_mock expects a mocked function as argument'); + expect(() => mocks.clear_mock((() => 0) as any)).toThrowError('clear_mock: Expected mocked function, got () => 0.'); }); it('works', () => { @@ -399,16 +428,4 @@ describe('Mocking functions', () => { expect(mocks.get_num_calls(fn)).toEqual(0); }); }); - - describe(mocks.mock_function, () => { - it('throws when passed not a function', () => { - expect(() => mocks.mock_function(0 as any)).toThrowError('mock_function expects a function as argument'); - }); - }); -}); - -test('internal errors are not handled', () => { - expect(() => { - throw new UnitestBundleInternalError(); - }).toThrow(); }); diff --git a/src/bundles/unittest/src/asserts.ts b/src/bundles/unittest/src/asserts.ts index a1d337271e..fcfe3aba59 100644 --- a/src/bundles/unittest/src/asserts.ts +++ b/src/bundles/unittest/src/asserts.ts @@ -1,6 +1,8 @@ +import { assertNumberWithinRange, isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import { isEqualWith } from 'es-toolkit'; import * as list from 'js-slang/dist/stdlib/list'; import { stringify } from 'js-slang/dist/utils/stringify'; +import { UnittestAssertionError, UnittestBundleInternalError } from './types'; /** * Asserts that a predicate returns true. @@ -8,16 +10,22 @@ import { stringify } from 'js-slang/dist/utils/stringify'; * @returns */ export function assert(pred: () => boolean) { + if (!isFunctionOfLength(pred, 0)) { + throw new UnittestBundleInternalError(`${assert.name} expects a nullary function that returns a boolean!`); + } + if (!pred()) { - throw new Error('Assert failed!'); + throw new UnittestAssertionError('Assert failed!'); } } -function equalityComparer(expected: any, received: any): boolean | undefined { +function equalityComparer(expected: unknown, received: unknown): boolean | undefined { if (typeof expected === 'number') { + if (typeof received !== 'number') return false; + // if either is a float, use approximate checking if (!Number.isInteger(expected) || !Number.isInteger(received)) { - return Math.abs(expected - received) <= 0.001; + return Math.abs(expected - received) <= 0.0001; } return expected === received; @@ -39,6 +47,7 @@ function equalityComparer(expected: any, received: any): boolean | undefined { return true; } + // TODO: Need to account for circular lists if (!isEqualWith(list.head(list0), list.head(list1), equalityComparer)) { return false; } @@ -56,7 +65,19 @@ function equalityComparer(expected: any, received: any): boolean | undefined { return true; } - // TODO: A comparison for streams/arrays? + if (Array.isArray(expected)) { + if (!Array.isArray(received) || received.length !== expected.length) return false; + + for (let i = 0; i < expected.length; i++) { + const expectedItem = expected[i]; + const receivedItem = received[i]; + + if (!isEqualWith(expectedItem, receivedItem, equalityComparer)) return false; + } + return true; + } + + // TODO: A comparison for streams? return undefined; } @@ -67,9 +88,9 @@ function equalityComparer(expected: any, received: any): boolean | undefined { * @param received The given value. * @returns */ -export function assert_equals(expected: any, received: any) { +export function assert_equals(expected: any, received: any): void { if (!isEqualWith(expected, received, equalityComparer)) { - throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); + throw new UnittestAssertionError(`Expected \`${expected}\`, got \`${received}\`!`); } } @@ -79,9 +100,9 @@ export function assert_equals(expected: any, received: any) { * @param received The given value. * @returns */ -export function assert_not_equals(expected: any, received: any) { +export function assert_not_equals(expected: any, received: any): void { if (!isEqualWith(expected, received, equalityComparer)) { - throw new Error(`Expected \`${expected}\` to not equal \`${received}\`!`); + throw new UnittestAssertionError(`Expected \`${expected}\` to not equal \`${received}\`!`); } } @@ -90,9 +111,9 @@ export function assert_not_equals(expected: any, received: any) { * @param xs The list or pair to assert. * @param toContain The element that `xs` is expected to contain. */ -export function assert_contains(xs: any, toContain: any) { +export function assert_contains(xs: any, toContain: any): void { const fail = () => { - throw new Error(`Expected \`${stringify(xs)}\` to contain \`${toContain}\`.`); + throw new UnittestAssertionError(`Expected \`${stringify(xs)}\` to contain \`${toContain}\`.`); }; function member(xs: list.List | list.Pair, item: any): boolean { @@ -109,28 +130,35 @@ export function assert_contains(xs: any, toContain: any) { isEqualWith(list.tail(xs), item, equalityComparer) ) return true; - if (list.is_pair(list.head(xs)) && member(list.head(xs), item)) { - return true; - } + const head_element = list.head(xs); + if (list.is_pair(head_element) && member(head_element, item)) return true; + + const tail_element = list.tail(xs); - return list.is_pair(list.tail(xs)) && member(list.tail(xs), item); + return list.is_pair(tail_element) && member(tail_element, item); } - throw new Error(`First argument to ${assert_contains.name} must be a list or a pair, got \`${stringify(xs)}\`.`); + throw new UnittestAssertionError(`First argument to ${assert_contains.name} must be a list or a pair, got \`${stringify(xs)}\`.`); } + if (!member(xs, toContain)) fail(); } /** - * Asserts that the given list has length `len`. - * @param xs The list to assert. - * @param len The expected length of the list. + * Asserts that the given item `xs` is either a list or array with length `len`. + * @param xs The list or array to assert. + * @param len The expected length of the list or array. */ -export function assert_length(xs: any, len: number) { - if (!list.is_list(xs)) throw new Error(`First argument to ${assert_length.name} must be a list.`); - if (!Number.isInteger(len)) throw new Error(`Second argument to ${assert_length.name} must be an integer.`); - - assert_equals(list.length(xs), len); +export function assert_length(xs: any, len: number): void { + assertNumberWithinRange(len, { func_name: assert_length.name, param_name: 'len', integer: true }); + + if (list.is_list(xs)) { + assert_equals(list.length(xs), len); + } else if (Array.isArray(xs)) { + assert_equals(xs.length, len); + } else { + throw new UnittestAssertionError(`First argument to ${assert_length.name} must be a list or array.`); + } } /** @@ -138,17 +166,17 @@ export function assert_length(xs: any, len: number) { * @param item The number to check * @param expected The value to check against */ -export function assert_greater(item: any, expected: number) { +export function assert_greater(item: any, expected: number): void { if (typeof expected !== 'number') { - throw new Error(`${assert_greater.name}: Expected value should be a number!`); + throw new UnittestAssertionError(`${assert_greater.name}: Expected value should be a number!`); } if (typeof item !== 'number') { - throw new Error(`Expected ${item} to be a number!`); + throw new UnittestAssertionError(`Expected ${item} to be a number!`); } if (item <= expected) { - throw new Error(`Expected ${item} to be greater than ${expected}`); + throw new UnittestAssertionError(`Expected ${item} to be greater than ${expected}`); } } @@ -157,16 +185,16 @@ export function assert_greater(item: any, expected: number) { * @param item The number to check * @param expected The value to check against */ -export function assert_greater_equals(item: any, expected: number) { +export function assert_greater_equals(item: any, expected: number): void { if (typeof expected !== 'number') { - throw new Error(`${assert_greater_equals.name}: Expected value should be a number!`); + throw new UnittestAssertionError(`${assert_greater_equals.name}: Expected value should be a number!`); } if (typeof item !== 'number') { - throw new Error(`Expected ${item} to be a number!`); + throw new UnittestAssertionError(`Expected ${item} to be a number!`); } if (item < expected) { - throw new Error(`Expected ${item} to be greater than or equal to ${expected}`); + throw new UnittestAssertionError(`Expected ${item} to be greater than or equal to ${expected}`); } } diff --git a/src/bundles/unittest/src/functions.ts b/src/bundles/unittest/src/functions.ts index 76c54e4b91..a2ae4f2d63 100644 --- a/src/bundles/unittest/src/functions.ts +++ b/src/bundles/unittest/src/functions.ts @@ -1,7 +1,8 @@ +import { isFunctionOfLength } from '@sourceacademy/modules-lib/utilities'; import context from 'js-slang/context'; import { - UnitestBundleInternalError, + UnittestBundleInternalError, type Suite, type SuiteResult, type Test, @@ -20,7 +21,7 @@ function getNewSuite(name?: string): Suite { * If describe was called multiple times from the root level, we need somewhere * to collect those Suite Results since none of them will have a parent suite */ -export const suiteResults: SuiteResult[] = []; +export const topLevelSuiteResults: SuiteResult[] = []; export let currentSuite: Suite | null = null; export let currentTest: string | null = null; @@ -32,16 +33,21 @@ function handleErr(err: any) { if (err.message) { return (err as Error).message; } + // eslint-disable-next-line @sourceacademy/throw-runtime-error throw err; } function runTest(name: string, funcName: string, func: Test) { + if (!isFunctionOfLength(func, 0)) { + throw new UnittestBundleInternalError(`${funcName}: A test must be a nullary function!`); + } + if (currentSuite === null) { - throw new UnitestBundleInternalError(`${funcName} must be called from within a test suite!`); + throw new UnittestBundleInternalError(`${funcName} must be called from within a test suite!`); } if (currentTest !== null) { - throw new UnitestBundleInternalError(`${funcName} cannot be called from within another test!`); + throw new UnittestBundleInternalError(`${funcName} cannot be called from within another test!`); } try { @@ -52,7 +58,7 @@ function runTest(name: string, funcName: string, func: Test) { passed: true, }); } catch (err) { - if (err instanceof UnitestBundleInternalError) { + if (err instanceof UnittestBundleInternalError) { throw err; } @@ -104,13 +110,21 @@ function determinePassCount(results: (TestResult | SuiteResult)[]): number { * @param func Function containing tests. */ export function describe(msg: string, func: TestSuite): void { - const oldSuite = currentSuite; + if (!isFunctionOfLength(func, 0)) { + throw new UnittestBundleInternalError(`${describe.name}: A test suite must be a nullary function!`); + } + + const parentSuite = currentSuite; const newSuite = getNewSuite(msg); currentSuite = newSuite; newSuite.startTime = performance.now(); - func(); - currentSuite = oldSuite; + + try { + func(); + } finally { + currentSuite = parentSuite; + } const passCount = determinePassCount(newSuite.results); const suiteResult: SuiteResult = { @@ -121,13 +135,13 @@ export function describe(msg: string, func: TestSuite): void { runtime: performance.now() - newSuite.startTime }; - if (oldSuite !== null) { - oldSuite.results.push(suiteResult); + if (parentSuite !== null) { + parentSuite.results.push(suiteResult); } else { - suiteResults.push(suiteResult); + topLevelSuiteResults.push(suiteResult); } } context.moduleContexts.unittest.state = { - suiteResults + suiteResults: topLevelSuiteResults }; diff --git a/src/bundles/unittest/src/mocks.ts b/src/bundles/unittest/src/mocks.ts index 7e927256f2..515bd3bcb1 100644 --- a/src/bundles/unittest/src/mocks.ts +++ b/src/bundles/unittest/src/mocks.ts @@ -1,4 +1,5 @@ -import { pair, vector_to_list, type List } from 'js-slang/dist/stdlib/list'; +import { InvalidCallbackError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { vector_to_list, type List } from 'js-slang/dist/stdlib/list'; /** * Symbol for identifying the mock properties. Should not be exposed to cadets. @@ -13,9 +14,9 @@ interface MockedFunction { }; } -function throwIfNotMockedFunction(obj: (...args: any[]) => any, func_name: string): asserts obj is MockedFunction { - if (!(mockSymbol in obj)) { - throw new Error(`${func_name} expects a mocked function as argument`); +function throwIfNotMockedFunction(obj: unknown, func_name: string, param_name?: string): asserts obj is MockedFunction { + if (typeof obj !== 'function' || !(mockSymbol in obj)) { + throw new InvalidCallbackError('mocked function', obj, func_name, param_name); } } @@ -25,19 +26,26 @@ function throwIfNotMockedFunction(obj: (...args: any[]) => any, func_name: strin * original value you passed in if you want the mocked function to be properly tracked. * @param fn Function to mock * @returns A mocked version of the given function. + * @example + * ``` + * const fn = mock_function(x => x + 1); + * fn(1); + * head(get_arg_list(fn)) === 1; // is true + * head(get_ret_vals(fn)) === 2; // is true + * ``` */ export function mock_function(fn: (...args: any[]) => any): MockedFunction { if (typeof fn !== 'function') { - throw new Error(`${mock_function.name} expects a function as argument`); + throw new InvalidParameterTypeError('function', fn, mock_function.name); } - const arglist: any[] = []; + const arglist: List[] = []; const retVals: any[] = []; // TODO: Check if some kind of function copying is required // js-slang has its own set of utils for doing this function func(...args: any[]) { - arglist.push(args); + arglist.push(vector_to_list(args)); const retVal = fn.apply(fn, args); if (retVal !== undefined) { retVals.push(retVal); @@ -46,8 +54,9 @@ export function mock_function(fn: (...args: any[]) => any): MockedFunction { return retVal; } + // @ts-expect-error This is fine func[mockSymbol] = { arglist, retVals }; - func.toString = () => fn.toString(); + func.toReplString = () => ''; return func; } @@ -70,11 +79,7 @@ export function get_num_calls(fn: MockedFunction) { export function get_arg_list(fn: MockedFunction) { throwIfNotMockedFunction(fn, get_arg_list.name); const { arglist } = fn[mockSymbol]; - - return arglist.reduceRight((res, args) => { - const argsAsList = vector_to_list(args); - return pair(argsAsList, res); - }, null); + return vector_to_list(arglist); } /** diff --git a/src/bundles/unittest/src/types.ts b/src/bundles/unittest/src/types.ts index 25d5fcd45c..c701d89599 100644 --- a/src/bundles/unittest/src/types.ts +++ b/src/bundles/unittest/src/types.ts @@ -1,3 +1,5 @@ +import { InternalRuntimeError, RuntimeSourceError } from 'js-slang/dist/errors/base'; + /** * Represents a function that when called, should either execute successfully * or throw an assertion error @@ -58,5 +60,17 @@ export interface UnittestModuleState { * These errors represent errors that shouldn't be handled as if they were thrown * by the assertion functions */ -export class UnitestBundleInternalError extends Error { -}; +export class UnittestBundleInternalError extends InternalRuntimeError { } + +/** + * Error thrown by the bundle's `assert` functions + */ +export class UnittestAssertionError extends RuntimeSourceError { + constructor( + private readonly assertion: string + ) { super(undefined); } + + public override explain(): string { + return this.assertion; + } +} diff --git a/src/bundles/unity_academy/package.json b/src/bundles/unity_academy/package.json index d752440626..632f467fd8 100644 --- a/src/bundles/unity_academy/package.json +++ b/src/bundles/unity_academy/package.json @@ -5,6 +5,7 @@ "dependencies": { "@blueprintjs/core": "^6.0.0", "@blueprintjs/icons": "^6.0.0", + "@sourceacademy/modules-lib": "workspace:^", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -24,8 +25,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/bundles/unity_academy/src/UnityAcademy.tsx b/src/bundles/unity_academy/src/UnityAcademy.tsx index e7fd00843d..31a0ad7ec4 100644 --- a/src/bundles/unity_academy/src/UnityAcademy.tsx +++ b/src/bundles/unity_academy/src/UnityAcademy.tsx @@ -7,6 +7,7 @@ import { Button } from '@blueprintjs/core'; import { Cross, Disable } from '@blueprintjs/icons'; +import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { Vector3, normalizeVector, pointDistance, zeroVector } from './UnityAcademyMaths'; @@ -18,12 +19,14 @@ type Transform = { scale: Vector3; }; -type StudentGameObject = { - startMethod: Function | null; - updateMethod: Function | null; - onCollisionEnterMethod: Function | null; - onCollisionStayMethod: Function | null; - onCollisionExitMethod: Function | null; +export type CollisionHandler = (self: GameObjectIdentifier, other: GameObjectIdentifier) => void; + +interface StudentGameObject { + startMethod: ((id: GameObjectIdentifier) => void) | null; + updateMethod: ((id: GameObjectIdentifier) => void) | null; + onCollisionEnterMethod: CollisionHandler | null; + onCollisionStayMethod: CollisionHandler | null; + onCollisionExitMethod: CollisionHandler | null; transform: Transform; rigidbody: RigidbodyData | null; audioSource: AudioSourceData | null; @@ -55,24 +58,25 @@ type AudioSourceData = { declare const createUnityInstance: Function; // This function comes from {BUILD_NAME}.loader.js in Unity Academy Application (For Example: ua-frontend-prod.loader.js) -export function getInstance(): UnityAcademyJsInteropContext { +export function getInstance(): UnityAcademyJsInteropContext | undefined { return (window as any).unityAcademyContext as UnityAcademyJsInteropContext; } type AudioClipInternalName = string; -export class AudioClipIdentifier { // A wrapper class to store identifier string and prevent users from using arbitrary string for idenfitier - audioClipInternalName: AudioClipInternalName; - constructor(audioClipInternalName: string) { - this.audioClipInternalName = audioClipInternalName; - } +/** + * A wrapper class to store identifier string and prevent users from using arbitrary string for idenfitier + */ +export class AudioClipIdentifier { + constructor( + readonly audioClipInternalName: string + ) {} } export class GameObjectIdentifier { // A wrapper class to store identifier string and prevent users from using arbitrary string for idenfitier - gameObjectIdentifier: string; - constructor(gameObjectIdentifier: string) { - this.gameObjectIdentifier = gameObjectIdentifier; - } + constructor( + readonly gameObjectIdentifier: string + ) {} } // Information of the special React component for this module: @@ -85,7 +89,7 @@ export class GameObjectIdentifier { // A wrapper class to store identifier strin // Also, the display space in the Tab area is relatively small for holding a game window. // Another reason of using separated React component is to let Unity Academy be able to fill the whole page to give students higher resolution graphics and more fancy visual effects. class UnityComponent extends React.Component { - render() { + override render() { const moduleInstance = getInstance(); return ( //
@@ -123,7 +127,7 @@ class UnityComponent extends React.Component { icon={} active={true} onClick={() => { - moduleInstance.setShowUnityComponent(0); + moduleInstance?.setShowUnityComponent(0); }}// Note: Here if I directly use "this.moduleInstance......" instead using this lambda function, the "this" reference will become undefined and lead to a runtime error when user clicks the "Run" button text="Hide Unity Academy Window" style={{ @@ -137,7 +141,7 @@ class UnityComponent extends React.Component { icon={} active={true} onClick={() => { - moduleInstance.terminate(); + moduleInstance?.terminate(); }} text="Terminate Unity Academy Instance" style={{ @@ -151,9 +155,8 @@ class UnityComponent extends React.Component { ); } - componentDidMount() { - getInstance() - .firstTimeLoadUnityApplication(); + override componentDidMount() { + getInstance()?.firstTimeLoadUnityApplication(); } } @@ -168,7 +171,7 @@ const UNITY_CONFIG = { productVersion: 'See \'About\' in the embedded frontend.' }; -class UnityAcademyJsInteropContext { +export class UnityAcademyJsInteropContext { // private unityConfig : any; public unityInstance: any; private unityContainerElement: HTMLElement | null; @@ -178,11 +181,18 @@ class UnityAcademyJsInteropContext { private studentActionQueue: any; // [get / clear by interop] private deltaTime = 0; // [set by interop] private input: InputData; // [set by interop] 0 = key idle, 1 = on key down, 2 = holding key, 3 = on key up - public gameObjectIdentifierWrapperClass: any; // [get by interop] For interop to create the class instance with the correct type when calling users' Start and Update functions. Only the object with this class type can pass checkGameObjectIdentifierParameter in functions.ts + + /** + * [get by interop] + * For interop to create the class instance with the correct type when calling users' Start and Update functions. + * Only the object with this class type can pass checkGameObjectIdentifierParameter in functions.ts + */ + public gameObjectIdentifierWrapperClass: new (...args: any[]) => GameObjectIdentifier; + private targetFrameRate: number; private unityInstanceState; // [set by interop] private guiData: any[]; // [get / clear by interop] - public dimensionMode; + public dimensionMode?: '2d' | '3d'; private isShowingUnityAcademy: boolean; // [get by interop] private latestUserAgreementVersion: string; private audioClipStorage: AudioClipInternalName[]; @@ -237,7 +247,7 @@ class UnityAcademyJsInteropContext { xhr.open('GET', jsonUrl, false); xhr.send(); if (xhr.status !== 200) { - throw new Error(`Unable to get prefab list. Error code = ${xhr.status}`); + throw new GeneralRuntimeError(`Unable to get prefab list. Error code = ${xhr.status}`); } this.prefabInfo = JSON.parse(xhr.responseText); } @@ -380,7 +390,7 @@ class UnityAcademyJsInteropContext { } } if (!prefabExists) { - throw new Error(`Unknown prefab name: '${prefabName}'. Please refer to this prefab list at [ ${UNITY_ACADEMY_BACKEND_URL}webgl_assetbundles/prefab_info.html ] for all available prefab names.`); + throw new GeneralRuntimeError(`Unknown prefab name: '${prefabName}'. Please refer to this prefab list at [ ${UNITY_ACADEMY_BACKEND_URL}webgl_assetbundles/prefab_info.html ] for all available prefab names.`); } const gameObjectIdentifier = `${prefabName}_${this.gameObjectIdentifierSerialCounter}`; this.gameObjectIdentifierSerialCounter++; @@ -449,17 +459,17 @@ class UnityAcademyJsInteropContext { getStudentGameObject(gameObjectIdentifier: GameObjectIdentifier): StudentGameObject { const retVal = this.studentGameObjectStorage[gameObjectIdentifier.gameObjectIdentifier]; if (retVal === undefined) { - throw new Error(`Could not find GameObject with identifier ${gameObjectIdentifier}`); + throw new GeneralRuntimeError(`Could not find GameObject with identifier ${gameObjectIdentifier}`); } return retVal; } - setStartInternal(gameObjectIdentifier: GameObjectIdentifier, startFunction: Function): void { + setStartInternal(gameObjectIdentifier: GameObjectIdentifier, startFunction: (id: GameObjectIdentifier) => void): void { const gameObject = this.getStudentGameObject(gameObjectIdentifier); gameObject.startMethod = startFunction; } - setUpdateInternal(gameObjectIdentifier: GameObjectIdentifier, updateFunction: Function): void { + setUpdateInternal(gameObjectIdentifier: GameObjectIdentifier, updateFunction: (id: GameObjectIdentifier) => void): void { const gameObject = this.getStudentGameObject(gameObjectIdentifier); gameObject.updateMethod = updateFunction; } @@ -567,7 +577,7 @@ class UnityAcademyJsInteropContext { console.log(`Applying rigidbody to GameObject ${gameObjectIdentifier.gameObjectIdentifier}`); const gameObject = this.getStudentGameObject(gameObjectIdentifier); if (gameObject.rigidbody !== null) { - throw new Error(`Trying to duplicately apply rigidbody on GameObject ${gameObjectIdentifier.gameObjectIdentifier}`); + throw new GeneralRuntimeError(`Trying to duplicately apply rigidbody on GameObject ${gameObjectIdentifier.gameObjectIdentifier}`); } gameObject.rigidbody = { velocity: zeroVector(), @@ -581,7 +591,7 @@ class UnityAcademyJsInteropContext { } private getRigidbody(gameObject: StudentGameObject): RigidbodyData { - if (gameObject.rigidbody === null) throw new Error('You must call apply_rigidbody on the game object before using this physics function!'); + if (gameObject.rigidbody === null) throw new GeneralRuntimeError('You must call apply_rigidbody on the game object before using this physics function!'); return gameObject.rigidbody; } @@ -625,17 +635,17 @@ class UnityAcademyJsInteropContext { this.dispatchStudentAction(`removeColliderComponents|${gameObjectIdentifier.gameObjectIdentifier}`); } - setOnCollisionEnterInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function) { + setOnCollisionEnterInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler) { const gameObject = this.getStudentGameObject(gameObjectIdentifier); gameObject.onCollisionEnterMethod = eventFunction; } - setOnCollisionStayInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function) { + setOnCollisionStayInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler) { const gameObject = this.getStudentGameObject(gameObjectIdentifier); gameObject.onCollisionStayMethod = eventFunction; } - setOnCollisionExitInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function) { + setOnCollisionExitInternal(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler) { const gameObject = this.getStudentGameObject(gameObjectIdentifier); gameObject.onCollisionExitMethod = eventFunction; } @@ -691,7 +701,7 @@ class UnityAcademyJsInteropContext { const gameObject = this.getStudentGameObject(gameObjectIdentifier); const retVal = gameObject.audioSource; if (retVal === null) { - throw new Error('The given GameObject is not a valid audio source.'); + throw new GeneralRuntimeError('The given GameObject is not a valid audio source.'); } return retVal; } @@ -735,11 +745,13 @@ class UnityAcademyJsInteropContext { } } -export function initializeModule(dimensionMode: string) { +export function initializeModule(dimensionMode: '2d' | '3d') { let instance = getInstance(); if (instance !== undefined) { if (!instance.isUnityInstanceReady()) { - throw new Error('Unity Academy Embedded Frontend is not ready to accept a new Source program now, please try again later. If you just successfully ran your code before but haven\'t open Unity Academy Embedded Frontend before running your code again, please try open the frontend first. If this error persists or you can not open Unity Academy Embedded Frontend, please try to refresh your browser\'s page.'); + throw new GeneralRuntimeError( + 'Unity Academy Embedded Frontend is not ready to accept a new Source program now, please try again later. If you just successfully ran your code before but haven\'t open Unity Academy Embedded Frontend before running your code again, please try open the frontend first. If this error persists or you can not open Unity Academy Embedded Frontend, please try to refresh your browser\'s page.' + ); } if (instance.unityInstance === null) { instance.reloadUnityAcademyInstanceAfterTermination(); diff --git a/src/bundles/unity_academy/src/UnityAcademyMaths.ts b/src/bundles/unity_academy/src/UnityAcademyMaths.ts index f5d93cc2d4..d598f58bad 100644 --- a/src/bundles/unity_academy/src/UnityAcademyMaths.ts +++ b/src/bundles/unity_academy/src/UnityAcademyMaths.ts @@ -4,34 +4,50 @@ * @author Wang Zihan */ +import { InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; + export class Vector3 { - x = 0; - y = 0; - z = 0; - constructor(x, y, z) { - this.x = x; - this.y = y; - this.z = z; - } + constructor( + public x: number, + public y: number, + public z: number + ) { } toString() { return `(${this.x}, ${this.y}, ${this.z})`; } -} -export function checkVector3Parameter(parameter: any): void { - if (typeof parameter !== 'object') { - throw new Error(`The given parameter is not a valid 3D vector! Wrong parameter type: ${typeof parameter}`); + equals(other: Vector3): boolean { + return Math.abs(this.x - other.x) < 0.00001 + && Math.abs(this.y - other.y) < 0.00001 + && Math.abs(this.z - other.z) < 0.00001; } - if (typeof parameter.x !== 'number' || typeof parameter.y !== 'number' || typeof parameter.z !== 'number') { - throw new Error('The given parameter is not a valid 3D vector!'); +} + +/** + * Type guard for validating that the provided parameter is indeed a {@link Vector3} + */ +export function checkVector3Parameter(parameter: unknown, func_name: string, param_name?: string): asserts parameter is Vector3 { + if (typeof parameter === 'object' && parameter !== null + && 'x' in parameter && typeof parameter.x === 'number' + && 'y' in parameter && typeof parameter.y === 'number' + && 'z' in parameter && typeof parameter.z === 'number' + ) { + return; } + throw new InvalidParameterTypeError('3D vector', parameter, func_name, param_name); } +/** + * Creates a new {@link Vector3}. + */ export function makeVector3D(x: number, y: number, z: number): Vector3 { return new Vector3(x, y, z); } +/** + * Scales the given vector by the given factor. + */ export function scaleVector(vector: Vector3, factor: number): Vector3 { return new Vector3(vector.x * factor, vector.y * factor, vector.z * factor); } @@ -49,13 +65,21 @@ export function dotProduct(vectorA: Vector3, vectorB: Vector3): number { } export function crossProduct(vectorA: Vector3, vectorB: Vector3): Vector3 { - return new Vector3(vectorA.y * vectorB.z - vectorB.y * vectorA.z, vectorB.x * vectorA.z - vectorA.x * vectorB.z, vectorA.x * vectorB.y - vectorB.x * vectorA.y); + return new Vector3( + vectorA.y * vectorB.z - vectorB.y * vectorA.z, + vectorB.x * vectorA.z - vectorA.x * vectorB.z, + vectorA.x * vectorB.y - vectorB.x * vectorA.y + ); } export function vectorMagnitude(vector: Vector3): number { return Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z); } +/** + * Returns a "normalized" version of the provided vector, i.e the vector that has the + * same direction as the original one but with a magnitude of 1. + */ export function normalizeVector(vector: Vector3): Vector3 { const magnitude = vectorMagnitude(vector); if (magnitude === 0) return new Vector3(0, 0, 0); // If the parameter is a zero vector, then return a new zero vector. diff --git a/src/bundles/unity_academy/src/__tests__/functions.test.ts b/src/bundles/unity_academy/src/__tests__/functions.test.ts new file mode 100644 index 0000000000..ee6cc61deb --- /dev/null +++ b/src/bundles/unity_academy/src/__tests__/functions.test.ts @@ -0,0 +1,671 @@ +import { describe, expect, it, vi } from 'vitest'; +import * as academy from '../UnityAcademy'; +import { Vector3 } from '../UnityAcademyMaths'; +import * as funcs from '../functions'; + +const mockedGetInstance = vi.spyOn(academy, 'getInstance'); +const testIdentifier = new academy.GameObjectIdentifier('test'); + +/** + * For the `testInstance` parameter: + * - Passing `undefined` simulates uninitialized instance + * - Passing `'destroyed'` simulates a destroyed object + * - Passing anything else is given as the return value to `getInstance`. + * + * If the instance is anything other than `undefined`, the `gameObjectIdentifierWrapperClass` + * is automatically set to {@link academy.GameObjectIdentifier} + * + * Game objects will always not be destroyed unless `testInstance` is `'destroyed'`. + */ +function testWithInstance | undefined>( + desc: string, + testInstance: T, + testFn: (instance: T) => void, + skipOrOnly?: 'only' | 'skip', +): void; +function testWithInstance( + desc: string, + testInstance: 'destroyed', + testFn: (instance: undefined) => void, + skipOrOnly?: 'only' | 'skip', +): void; +function testWithInstance | undefined>( + desc: string, + testInstance: T | 'destroyed', + testFn: (instance: T) => void, + skipOrOnly?: 'only' | 'skip', +) { + let fn: (desc: string, testFn: () => void) => void; + if (skipOrOnly === 'skip') { + fn = it.skip; + } else if (skipOrOnly === 'only') { + fn = it.only; + } else { + fn = it; + } + + let instance: any; + if (testInstance === 'destroyed') { + instance = { + gameObjectIdentifierWrapperClass: academy.GameObjectIdentifier, + getStudentGameObject: () => ({ isDestroyed: true } as any) + }; + } else { + if (testInstance === undefined) { + instance = testInstance; + } else { + instance = { + gameObjectIdentifierWrapperClass: academy.GameObjectIdentifier, + ...testInstance, + }; + + if (!instance.getStudentGameObject) { + instance.getStudentGameObject = () => ({ isDestroyed: false } as any); + } + } + } + + fn(desc, () => { + mockedGetInstance.mockReturnValue(instance); + try { + testFn(instance); + expect(mockedGetInstance).toHaveBeenCalled(); + } finally { + mockedGetInstance.mockClear(); + } + }); +}; + +describe(funcs.add_impulse_force, () => { + testWithInstance('throws when instance has not been initializede', undefined, () => { + expect(() => funcs.add_impulse_force( + new academy.GameObjectIdentifier('test'), + new Vector3(0, 0, 0) + )).toThrow( + 'add_impulse_force: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when identifier isn\'t valid', + {}, + () => { + expect(() => funcs.add_impulse_force(0 as any, new Vector3(0, 0, 0))).toThrow( + 'add_impulse_force: Expected GameObjectIdentifier, got 0.' + ); + } + ); + + testWithInstance( + 'works', + { addImpulseForceInternal: vi.fn() }, + ({ addImpulseForceInternal }) => { + expect(funcs.add_impulse_force( + testIdentifier, + new Vector3(0, 0, 0) + )).toBeUndefined(); + + expect(addImpulseForceInternal).toHaveBeenCalledOnce(); + } + ); +}); + +describe(funcs.add_vectors, () => { + it('throws when first parameter is not Vector3', () => { + expect(() => funcs.add_vectors(0 as any, new Vector3(0, 0, 0))) + .toThrow('add_vectors: Expected 3D vector for vectorA, got 0.'); + }); + + it('throws when second parameter is not Vector3', () => { + expect(() => funcs.add_vectors(new Vector3(0, 0, 0), 0 as any)) + .toThrow('add_vectors: Expected 3D vector for vectorB, got 0.'); + }); + + it('works', () => { + const lhs = new Vector3(1, 1, 1); + const rhs = new Vector3(1, 2, 3); + const expected = new Vector3(2, 3, 4); + expect(funcs.add_vectors(lhs, rhs).equals(expected)).toEqual(true); + }); +}); + +describe(funcs.apply_rigidbody, () => { + testWithInstance( + 'throws when instance has not been initialized', + undefined, + () => { + expect(() => funcs.apply_rigidbody(testIdentifier)) + .toThrow( + 'apply_rigidbody: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }, + ); + + testWithInstance( + 'throws when game object is destroyed', + 'destroyed', + () => { + expect(() => funcs.apply_rigidbody(testIdentifier)) + .toThrow('apply_rigidbody: Trying to use a GameObject that is already destroyed.'); + }, + ); + + testWithInstance( + 'works', + { applyRigidbodyInternal: vi.fn() }, + ({ applyRigidbodyInternal }) => { + expect(funcs.apply_rigidbody(testIdentifier)).toBeUndefined(); + expect(applyRigidbodyInternal).toHaveBeenCalledOnce(); + }, + ); +}); + +describe(funcs.assertIsValidKeyCode, () => { + it('throws when provided not a string', () => { + expect(() => funcs.assertIsValidKeyCode(0, 'foo')).toThrow('foo: Expected KeyCode, got 0.'); + }); + + it('throws when provided a string of length 2', () => { + expect(() => funcs.assertIsValidKeyCode('hi', 'foo')).toThrow('foo: Expected KeyCode, got "hi".'); + }); + + describe('doesn\'t throw for the button codes', () => { + it.each(funcs.BUTTON_KEY_CODES)('%s', (code) => { + expect(() => funcs.assertIsValidKeyCode(code, 'foo')).not.toThrow(); + }); + }); + + describe('doesn\'t throw for letters and numbers', () => { + let letters = 'abcdefghijklmnopqrstuvwxyz'; + + letters += letters.toUpperCase(); + letters += '0123456789'; + + it.each(letters.split(''))('%s', code => { + expect(() => funcs.assertIsValidKeyCode(code, 'foo')).not.toThrow(); + }); + }); +}); + +describe(funcs.change_audio_clip, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.change_audio_clip( + testIdentifier, + new academy.AudioClipIdentifier('test') + )).toThrow( + 'change_audio_clip: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when audioSrc is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.change_audio_clip( + 0 as any, + new academy.AudioClipIdentifier('test') + )).toThrow( + 'change_audio_clip: Expected GameObjectIdentifier, got 0.' + ); + } + ); + + testWithInstance( + 'throws when audioSrc is destroyed', + 'destroyed', + () => { + expect(() => funcs.change_audio_clip( + testIdentifier, + new academy.AudioClipIdentifier('test') + )).toThrow( + 'change_audio_clip: Trying to use a GameObject that is already destroyed.' + ); + } + ); + + testWithInstance( + 'works', + { setAudioSourceProp: vi.fn() }, + ({ setAudioSourceProp }) => { + const audioId = new academy.AudioClipIdentifier('test'); + + expect(funcs.change_audio_clip( + testIdentifier, + audioId + )).toBeUndefined(); + + expect(setAudioSourceProp) + .toHaveBeenCalledExactlyOnceWith( + 'audioClipIdentifier', + testIdentifier, + audioId, + ); + } + ); +}); + +describe(funcs.copy_position, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.copy_position( + testIdentifier, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_position: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when from is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_position( + 0 as any, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_position: Expected GameObjectIdentifier for from, got 0.' + ); + } + ); + + testWithInstance( + 'throws when to is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_position( + testIdentifier, + 0 as any, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_position: Expected GameObjectIdentifier for to, got 0.' + ); + } + ); + + testWithInstance( + 'works', + { copyTransformPropertiesInternal: vi.fn() }, + ({ copyTransformPropertiesInternal }) => { + const testVector = new Vector3(0, 0, 0); + expect(funcs.copy_position(testIdentifier, testIdentifier, testVector)).toBeUndefined(); + + expect(copyTransformPropertiesInternal).toHaveBeenCalledExactlyOnceWith( + 'position', + testIdentifier, + testIdentifier, + testVector + ); + } + ); +}); + +describe(funcs.copy_rotation, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.copy_rotation( + testIdentifier, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_rotation: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when from is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_rotation( + 0 as any, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_rotation: Expected GameObjectIdentifier for from, got 0.' + ); + } + ); + + testWithInstance( + 'throws when to is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_rotation( + testIdentifier, + 0 as any, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_rotation: Expected GameObjectIdentifier for to, got 0.' + ); + } + ); + + testWithInstance( + 'works', + { copyTransformPropertiesInternal: vi.fn() }, + ({ copyTransformPropertiesInternal }) => { + const testVector = new Vector3(0, 0, 0); + expect(funcs.copy_rotation(testIdentifier, testIdentifier, testVector)).toBeUndefined(); + + expect(copyTransformPropertiesInternal).toHaveBeenCalledExactlyOnceWith( + 'rotation', + testIdentifier, + testIdentifier, + testVector + ); + } + ); +}); + +describe(funcs.copy_scale, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.copy_scale( + testIdentifier, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_scale: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when from is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_scale( + 0 as any, + testIdentifier, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_scale: Expected GameObjectIdentifier for from, got 0.' + ); + } + ); + + testWithInstance( + 'throws when to is not a GameObjectIdentifier', + {}, + () => { + expect(() => funcs.copy_scale( + testIdentifier, + 0 as any, + new Vector3(0, 0, 0) + )).toThrow( + 'copy_scale: Expected GameObjectIdentifier for to, got 0.' + ); + } + ); + + testWithInstance( + 'works', + { copyTransformPropertiesInternal: vi.fn() }, + ({ copyTransformPropertiesInternal }) => { + const testVector = new Vector3(0, 0, 0); + expect(funcs.copy_scale(testIdentifier, testIdentifier, testVector)).toBeUndefined(); + + expect(copyTransformPropertiesInternal).toHaveBeenCalledExactlyOnceWith( + 'scale', + testIdentifier, + testIdentifier, + testVector + ); + } + ); +}); + +describe(funcs.cross, () => { + it('throws when first parameter is not Vector3', () => { + expect(() => funcs.cross(0 as any, new Vector3(0, 0, 0))) + .toThrow('cross: Expected 3D vector for vectorA, got 0.'); + }); + + it('throws when second parameter is not Vector3', () => { + expect(() => funcs.cross(new Vector3(0, 0, 0), 0 as any)) + .toThrow('cross: Expected 3D vector for vectorB, got 0.'); + }); + + it('works', () => { + const lhs = new Vector3(1, 1, 1); + const rhs = new Vector3(1, 2, 3); + const expected = new Vector3(1, -2, 1); + expect(funcs.cross(lhs, rhs).equals(expected)).toEqual(true); + }); +}); + +describe(funcs.debug_log, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.debug_log(0)).toThrow( + 'debug_log: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.debug_logerror, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.debug_logerror(0)).toThrow( + 'debug_logerror: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.debug_logwarning, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.debug_logwarning(0)).toThrow( + 'debug_logwarning: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.delta_time, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + mockedGetInstance.mockReturnValueOnce(undefined); + expect(() => funcs.delta_time()).toThrow( + 'delta_time: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.destroy, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.destroy( + new academy.GameObjectIdentifier('test') + )).toThrow( + 'destroy: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.dot, () => { + it('throws when first parameter is not Vector3', () => { + expect(() => funcs.dot(0 as any, new Vector3(0, 0, 0))) + .toThrow('dot: Expected 3D vector for vectorA, got 0.'); + }); + + it('throws when second parameter is not Vector3', () => { + expect(() => funcs.dot(new Vector3(0, 0, 0), 0 as any)) + .toThrow('dot: Expected 3D vector for vectorB, got 0.'); + }); + + it('works', () => { + const lhs = new Vector3(1, 1, 1); + const rhs = new Vector3(1, 2, 3); + expect(funcs.dot(lhs, rhs)).toEqual(6); + }); +}); + +describe(funcs.gameobject_distance, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.gameobject_distance( + new academy.GameObjectIdentifier('test'), + new academy.GameObjectIdentifier('test') + )).toThrow( + 'gameobject_distance: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_angular_velocity, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_angular_velocity( + new academy.GameObjectIdentifier('test') + )).toThrow( + 'get_angular_velocity: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_audio_play_progress, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_audio_play_progress( + new academy.GameObjectIdentifier('test') + )).toThrow( + 'get_audio_play_progress: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_custom_prop, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_custom_prop( + new academy.GameObjectIdentifier('test'), + 'prop' + )).toThrow( + 'get_custom_prop: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_key, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_key('0')).toThrow( + 'get_key: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_key_down, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_key_down('0')).toThrow( + 'get_key_down: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_key_up, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_key_up('0')).toThrow( + 'get_key_up: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_main_camera_following_target, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_main_camera_following_target()).toThrow( + 'get_main_camera_following_target: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); +}); + +describe(funcs.get_mass, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.get_mass(testIdentifier)).toThrow( + 'get_mass: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when identifier isn\'t valid', + {}, + () => { + expect(() => funcs.get_mass(0 as any)).toThrow( + 'get_mass: Expected GameObjectIdentifier, got 0.' + ); + } + ); + + testWithInstance( + 'works', + { getRigidbodyNumericalProp: vi.fn().mockReturnValue(10) }, + ({ getRigidbodyNumericalProp }) => { + expect(funcs.get_mass(testIdentifier)).toEqual(10); + + expect(getRigidbodyNumericalProp).toHaveBeenCalledExactlyOnceWith('mass', testIdentifier); + } + ); +}); + +describe(funcs.instantiate, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.instantiate('test')).toThrow( + 'instantiate: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when instance is a 2D instance', + { dimensionMode: '2d' }, + () => { + expect(() => funcs.instantiate('test')).toThrow( + 'instantiate: You are calling a "3D mode only" function in non-3d mode.' + ); + } + ); + + testWithInstance( + 'throws when prefab_name is not a string', + { dimensionMode: '3d' }, + () => { + expect(() => funcs.instantiate(0 as any)).toThrow('instantiate: Expected string, got 0.'); + } + ); + + testWithInstance( + 'works', + { + dimensionMode: '3d', + instantiateInternal: vi.fn().mockReturnValueOnce(testIdentifier) + }, + ({ instantiateInternal }) => { + expect(funcs.instantiate('test')).toBe(testIdentifier); + expect(instantiateInternal).toHaveBeenCalledExactlyOnceWith('test'); + } + ); +}); + +describe(funcs.instantiate_sprite, () => { + testWithInstance('throws when instance has not been initialized', undefined, () => { + expect(() => funcs.instantiate_sprite('test')).toThrow( + 'instantiate_sprite: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function' + ); + }); + + testWithInstance( + 'throws when instance is a 2D instance', + { dimensionMode: '3d' }, + () => { + expect(() => funcs.instantiate_sprite('test')).toThrow( + 'instantiate_sprite: You are calling a "2D mode only" function in non-2d mode.' + ); + } + ); + + testWithInstance( + 'throws when prefab_name is not a string', + { dimensionMode: '2d' }, + () => { + expect(() => funcs.instantiate_sprite(0 as any)).toThrow('instantiate_sprite: Expected string, got 0.'); + } + ); + + testWithInstance( + 'works', + { + dimensionMode: '2d', + instantiate2DSpriteUrlInternal: vi.fn().mockReturnValueOnce(testIdentifier) + }, + ({ instantiate2DSpriteUrlInternal }) => { + expect(funcs.instantiate_sprite('test')).toBe(testIdentifier); + expect(instantiate2DSpriteUrlInternal).toHaveBeenCalledExactlyOnceWith('test'); + } + ); +}); diff --git a/src/bundles/unity_academy/src/__tests__/maths.test.ts b/src/bundles/unity_academy/src/__tests__/maths.test.ts new file mode 100644 index 0000000000..7ae30522d2 --- /dev/null +++ b/src/bundles/unity_academy/src/__tests__/maths.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; +import * as maths from '../UnityAcademyMaths'; + +describe(maths.checkVector3Parameter, () => { + it('throws when provided object is not a Vector3', () => { + expect(() => maths.checkVector3Parameter(0, 'foo')) + .toThrow('foo: Expected 3D vector, got 0.'); + }); + + it('doesn\'t throw when provided object is a Vector3', () => { + expect(() => maths.checkVector3Parameter(new maths.Vector3(0, 0, 0), 'foo')).not.toThrow(); + }); +}); diff --git a/src/bundles/unity_academy/src/functions.ts b/src/bundles/unity_academy/src/functions.ts index 531cd7a5fb..04d0b0f6c5 100644 --- a/src/bundles/unity_academy/src/functions.ts +++ b/src/bundles/unity_academy/src/functions.ts @@ -4,7 +4,9 @@ * @author Wang Zihan */ -import { getInstance, initializeModule, type AudioClipIdentifier, type GameObjectIdentifier } from './UnityAcademy'; +import { GeneralRuntimeError, InvalidParameterTypeError } from '@sourceacademy/modules-lib/errors'; +import { assertFunctionOfLength, assertNumberWithinRange } from '@sourceacademy/modules-lib/utilities'; +import { getInstance, initializeModule, type AudioClipIdentifier, type CollisionHandler, type GameObjectIdentifier } from './UnityAcademy'; import { addVectors, checkVector3Parameter, @@ -44,42 +46,46 @@ export function init_unity_academy_3d(): void { initializeModule('3d'); } -function checkUnityAcademyExistence() { - if (getInstance() === undefined) { - throw new Error('Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function'); +function checkUnityAcademyExistence(func_name: string, mode?: '3D' | '2D') { + const instance = getInstance(); + + if (instance === undefined) { + throw new GeneralRuntimeError(`${func_name}: Unity module is not initialized, please call init_unity_academy_3d / init_unity_academy_2d first before calling this function`); } -} -function checkIs2DMode(): void { - if (getInstance().dimensionMode !== '2d') throw new Error('You are calling a "2D mode only" function in non-2d mode.'); -} + if (mode === '3D' && instance.dimensionMode !== '3d') { + throw new GeneralRuntimeError(`${func_name}: You are calling a "3D mode only" function in non-3d mode.`); + } else if (mode === '2D' && instance.dimensionMode !== '2d') { + throw new GeneralRuntimeError(`${func_name}: You are calling a "2D mode only" function in non-2d mode.`); + } -function checkIs3DMode(): void { - if (getInstance().dimensionMode !== '3d') throw new Error('You are calling a "3D mode only" function in non-3d mode.'); + return instance; } -function checkGameObjectIdentifierParameter(gameObjectIdentifier: any) { +function checkGameObjectIdentifierParameter( + gameObjectIdentifier: unknown, + func_name: string, + param_name?: string +): asserts gameObjectIdentifier is GameObjectIdentifier { // Here I can not just do "gameObjectIdentifier instanceof GameObjectIdentifier". // Because if I do that, when students re-run their code on the same Unity instance, (gameObjectIdentifier instanceof GameObjectIdentifier) will always evaluate to false // even when students provide the parameter with the correct type. - const instance = getInstance(); + const instance = getInstance()!; if (!(gameObjectIdentifier instanceof instance.gameObjectIdentifierWrapperClass)) { - throw new Error(`Type "${(typeof gameObjectIdentifier).toString()}" can not be used as game object identifier!`); + throw new InvalidParameterTypeError('GameObjectIdentifier', gameObjectIdentifier, func_name, param_name); } if (instance.getStudentGameObject(gameObjectIdentifier).isDestroyed) { - throw new Error('Trying to use a GameObject that is already destroyed.'); + throw new GeneralRuntimeError(`${func_name}: Trying to use a GameObject that is already destroyed.`); } } -function checkParameterType(parameter: any, expectedType: string, numberAllowInfinity = false) { - const actualType = typeof parameter; - if (actualType !== expectedType) { - throw new Error(`Wrong parameter type: expected ${expectedType}, but got ${actualType}`); - } - if (actualType.toString() === 'number') { - if (!numberAllowInfinity && (parameter === Infinity || parameter === -Infinity)) { - throw new Error('Wrong parameter type: expected a finite number, but got Infinity or -Infinity'); - } +function validateNumber(obj: unknown, func_name: string, param_name?: string, allowInfinity: boolean = false): asserts obj is number { + assertNumberWithinRange(obj, { + func_name, param_name + }); + + if (!allowInfinity && (obj === Infinity || obj === -Infinity)) { + throw new InvalidParameterTypeError('finite number', obj, func_name, param_name); } } @@ -92,8 +98,8 @@ function checkParameterType(parameter: any, expectedType: string, numberAllowInf * @category Common */ export function same_gameobject(first: GameObjectIdentifier, second: GameObjectIdentifier): boolean { - checkUnityAcademyExistence(); - const instance = getInstance(); + const instance = checkUnityAcademyExistence(same_gameobject.name); + if (!(first instanceof instance.gameObjectIdentifierWrapperClass) || !(second instanceof instance.gameObjectIdentifierWrapperClass)) { return false; } @@ -108,12 +114,12 @@ export function same_gameobject(first: GameObjectIdentifier, second: GameObjectI * @category Common * @category Outside Lifecycle */ -export function set_start(gameObjectIdentifier: GameObjectIdentifier, startFunction: Function): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(startFunction, 'function'); - getInstance() - .setStartInternal(gameObjectIdentifier, startFunction); +export function set_start(gameObjectIdentifier: GameObjectIdentifier, startFunction: (id: GameObjectIdentifier) => void): void { + const instance = checkUnityAcademyExistence(set_start.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_start.name); + assertFunctionOfLength(startFunction, 1, set_start.name); + + instance.setStartInternal(gameObjectIdentifier, startFunction); } /** @@ -125,12 +131,12 @@ export function set_start(gameObjectIdentifier: GameObjectIdentifier, startFunct * @category Common * @category Outside Lifecycle */ -export function set_update(gameObjectIdentifier: GameObjectIdentifier, updateFunction: Function): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(updateFunction, 'function'); - getInstance() - .setUpdateInternal(gameObjectIdentifier, updateFunction); +export function set_update(gameObjectIdentifier: GameObjectIdentifier, updateFunction: (id: GameObjectIdentifier) => void): void { + const instance = checkUnityAcademyExistence(set_update.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_update.name); + assertFunctionOfLength(updateFunction, 1, set_update.name); + + instance.setUpdateInternal(gameObjectIdentifier, updateFunction); } /** @@ -149,11 +155,13 @@ export function set_update(gameObjectIdentifier: GameObjectIdentifier, updateFun * @category Outside Lifecycle */ export function instantiate(prefab_name: string): GameObjectIdentifier { - checkUnityAcademyExistence(); - checkIs3DMode(); - checkParameterType(prefab_name, 'string'); - return getInstance() - .instantiateInternal(prefab_name); + const instance = checkUnityAcademyExistence(instantiate.name, '3D'); + + if (typeof prefab_name !== 'string') { + throw new InvalidParameterTypeError('string', prefab_name, instantiate.name); + } + + return instance.instantiateInternal(prefab_name); } /** @@ -172,11 +180,13 @@ export function instantiate(prefab_name: string): GameObjectIdentifier { * @category Outside Lifecycle */ export function instantiate_sprite(sourceImageUrl: string) { - checkUnityAcademyExistence(); - checkIs2DMode(); - checkParameterType(sourceImageUrl, 'string'); - return getInstance() - .instantiate2DSpriteUrlInternal(sourceImageUrl); + const instance = checkUnityAcademyExistence(instantiate_sprite.name, '2D'); + + if (typeof sourceImageUrl !== 'string') { + throw new InvalidParameterTypeError('string', sourceImageUrl, instantiate_sprite.name); + } + + return instance.instantiate2DSpriteUrlInternal(sourceImageUrl); } /** @@ -192,9 +202,8 @@ export function instantiate_sprite(sourceImageUrl: string) { * @category Outside Lifecycle */ export function instantiate_empty(): GameObjectIdentifier { - checkUnityAcademyExistence(); - return getInstance() - .instantiateEmptyGameObjectInternal(); + const instance = checkUnityAcademyExistence(instantiate_empty.name); + return instance.instantiateEmptyGameObjectInternal(); } /** @@ -217,9 +226,8 @@ export function instantiate_empty(): GameObjectIdentifier { * @category Common */ export function delta_time() { - checkUnityAcademyExistence(); - return getInstance() - .getDeltaTime(); + const instance = checkUnityAcademyExistence(delta_time.name); + return instance.getDeltaTime(); } /** @@ -233,10 +241,10 @@ export function delta_time() { * @category Common */ export function destroy(gameObjectIdentifier: GameObjectIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - getInstance() - .destroyGameObjectInternal(gameObjectIdentifier); + const instance = checkUnityAcademyExistence(destroy.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, destroy.name); + + instance.destroyGameObjectInternal(gameObjectIdentifier); } /** @@ -247,10 +255,9 @@ export function destroy(gameObjectIdentifier: GameObjectIdentifier): void { * @category Transform */ export function get_position(gameObjectIdentifier: GameObjectIdentifier): Vector3 { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getGameObjectTransformProp('position', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_position.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_position.name); + return instance.getGameObjectTransformProp('position', gameObjectIdentifier); } /** @@ -261,11 +268,11 @@ export function get_position(gameObjectIdentifier: GameObjectIdentifier): Vector * @category Transform */ export function set_position(gameObjectIdentifier: GameObjectIdentifier, position: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(position); - return getInstance() - .setGameObjectTransformProp('position', gameObjectIdentifier, position); + const instance = checkUnityAcademyExistence(set_position.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_position.name); + checkVector3Parameter(position, set_position.name); + + return instance.setGameObjectTransformProp('position', gameObjectIdentifier, position); } /** @@ -276,10 +283,10 @@ export function set_position(gameObjectIdentifier: GameObjectIdentifier, positio * @category Transform */ export function get_rotation_euler(gameObjectIdentifier: GameObjectIdentifier): Vector3 { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getGameObjectTransformProp('rotation', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_rotation_euler.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_rotation_euler.name); + + return instance.getGameObjectTransformProp('rotation', gameObjectIdentifier); } /** @@ -290,11 +297,11 @@ export function get_rotation_euler(gameObjectIdentifier: GameObjectIdentifier): * @category Transform */ export function set_rotation_euler(gameObjectIdentifier: GameObjectIdentifier, rotation: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(rotation); - return getInstance() - .setGameObjectTransformProp('rotation', gameObjectIdentifier, rotation); + const instance = checkUnityAcademyExistence(set_rotation_euler.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_rotation_euler.name); + checkVector3Parameter(rotation, set_rotation_euler.name); + + return instance.setGameObjectTransformProp('rotation', gameObjectIdentifier, rotation); } /** @@ -307,10 +314,10 @@ export function set_rotation_euler(gameObjectIdentifier: GameObjectIdentifier, r * @category Transform */ export function get_scale(gameObjectIdentifier: GameObjectIdentifier): Vector3 { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getGameObjectTransformProp('scale', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_scale.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_scale.name); + + return instance.getGameObjectTransformProp('scale', gameObjectIdentifier); } /** @@ -323,11 +330,11 @@ export function get_scale(gameObjectIdentifier: GameObjectIdentifier): Vector3 { * @category Transform */ export function set_scale(gameObjectIdentifier: GameObjectIdentifier, scale: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(scale); - return getInstance() - .setGameObjectTransformProp('scale', gameObjectIdentifier, scale); + const instance = checkUnityAcademyExistence(set_scale.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_scale.name); + checkVector3Parameter(scale, set_scale.name, 'scale'); + + return instance.setGameObjectTransformProp('scale', gameObjectIdentifier, scale); } /** @@ -339,11 +346,11 @@ export function set_scale(gameObjectIdentifier: GameObjectIdentifier, scale: Vec * @category Transform */ export function translate_world(gameObjectIdentifier: GameObjectIdentifier, deltaPosition: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(deltaPosition); - return getInstance() - .translateWorldInternal(gameObjectIdentifier, deltaPosition); + const instance = checkUnityAcademyExistence(translate_world.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, translate_world.name); + checkVector3Parameter(deltaPosition, translate_world.name, 'deltaPosition'); + + return instance.translateWorldInternal(gameObjectIdentifier, deltaPosition); } /** @@ -359,11 +366,11 @@ export function translate_world(gameObjectIdentifier: GameObjectIdentifier, delt * @category Transform */ export function translate_local(gameObjectIdentifier: GameObjectIdentifier, deltaPosition: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(deltaPosition); - return getInstance() - .translateLocalInternal(gameObjectIdentifier, deltaPosition); + const instance = checkUnityAcademyExistence(translate_local.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, translate_local.name); + checkVector3Parameter(deltaPosition, translate_local.name, 'deltaPosition'); + + return instance.translateLocalInternal(gameObjectIdentifier, deltaPosition); } /** @@ -375,11 +382,11 @@ export function translate_local(gameObjectIdentifier: GameObjectIdentifier, delt * @category Transform */ export function rotate_world(gameObjectIdentifier: GameObjectIdentifier, angles: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(angles); - return getInstance() - .rotateWorldInternal(gameObjectIdentifier, angles); + const instance = checkUnityAcademyExistence(rotate_world.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, rotate_world.name); + checkVector3Parameter(angles, rotate_world.name, 'angles'); + + return instance.rotateWorldInternal(gameObjectIdentifier, angles); } /** @@ -394,12 +401,12 @@ export function rotate_world(gameObjectIdentifier: GameObjectIdentifier, angles: * @category Transform */ export function copy_position(from: GameObjectIdentifier, to: GameObjectIdentifier, deltaPosition: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(from); - checkGameObjectIdentifierParameter(to); - checkVector3Parameter(deltaPosition); - return getInstance() - .copyTransformPropertiesInternal('position', from, to, deltaPosition); + const instance = checkUnityAcademyExistence(copy_position.name); + checkGameObjectIdentifierParameter(from, copy_position.name, 'from'); + checkGameObjectIdentifierParameter(to, copy_position.name, 'to'); + checkVector3Parameter(deltaPosition, copy_position.name, 'deltaPosition'); + + return instance.copyTransformPropertiesInternal('position', from, to, deltaPosition); } /** @@ -414,12 +421,12 @@ export function copy_position(from: GameObjectIdentifier, to: GameObjectIdentifi * @category Transform */ export function copy_rotation(from: GameObjectIdentifier, to: GameObjectIdentifier, deltaRotation: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(from); - checkGameObjectIdentifierParameter(to); - checkVector3Parameter(deltaRotation); - return getInstance() - .copyTransformPropertiesInternal('rotation', from, to, deltaRotation); + const instance = checkUnityAcademyExistence(copy_rotation.name); + checkGameObjectIdentifierParameter(from, copy_rotation.name, 'from'); + checkGameObjectIdentifierParameter(to, copy_rotation.name, 'to'); + checkVector3Parameter(deltaRotation, copy_rotation.name, 'deltaRotation'); + + return instance.copyTransformPropertiesInternal('rotation', from, to, deltaRotation); } /** @@ -434,12 +441,12 @@ export function copy_rotation(from: GameObjectIdentifier, to: GameObjectIdentifi * @category Transform */ export function copy_scale(from: GameObjectIdentifier, to: GameObjectIdentifier, deltaScale: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(from); - checkGameObjectIdentifierParameter(to); - checkVector3Parameter(deltaScale); - return getInstance() - .copyTransformPropertiesInternal('scale', from, to, deltaScale); + const instance = checkUnityAcademyExistence(copy_scale.name); + checkGameObjectIdentifierParameter(from, copy_scale.name, 'from'); + checkGameObjectIdentifierParameter(to, copy_scale.name, 'to'); + checkVector3Parameter(deltaScale, copy_scale.name, 'deltaScale'); + + return instance.copyTransformPropertiesInternal('scale', from, to, deltaScale); } /** @@ -455,11 +462,11 @@ export function copy_scale(from: GameObjectIdentifier, to: GameObjectIdentifier, * @category Transform */ export function look_at(gameObjectIdentifier: GameObjectIdentifier, position: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(position); - getInstance() - .lookAtPositionInternal(gameObjectIdentifier, position); + const instance = checkUnityAcademyExistence(look_at.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, look_at.name); + checkVector3Parameter(position, look_at.name, 'position'); + + instance.lookAtPositionInternal(gameObjectIdentifier, position); } /** @@ -472,23 +479,39 @@ export function look_at(gameObjectIdentifier: GameObjectIdentifier, position: Ve * @category Transform */ export function gameobject_distance(gameObjectIdentifier_A: GameObjectIdentifier, gameObjectIdentifier_B: GameObjectIdentifier): number { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier_A); - checkGameObjectIdentifierParameter(gameObjectIdentifier_B); - return getInstance() - .gameObjectDistanceInternal(gameObjectIdentifier_A, gameObjectIdentifier_B); -} - -function checkKeyCodeValidityAndToLowerCase(keyCode: string): string { - if (typeof keyCode !== 'string') throw new Error(`Key code must be a string! Given type: ${typeof keyCode}`); - if (keyCode === 'LeftMouseBtn' || keyCode === 'RightMouseBtn' || keyCode === 'MiddleMouseBtn' || keyCode === 'Space' || keyCode === 'LeftShift' || keyCode === 'RightShift') return keyCode; - keyCode = keyCode.toLowerCase(); - if (keyCode.length !== 1) throw new Error(`Key code must be either a string of length 1 or one among 'LeftMouseBtn', 'RightMouseBtn', 'MiddleMouseBtn', 'Space', 'LeftShift' or 'RightShift'! Given length: ${keyCode.length}`); - const char = keyCode.charAt(0); - if (!((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9'))) { - throw new Error(`Key code must be either a letter between A-Z or a-z or 0-9 or one among 'LeftMouseBtn', 'RightMouseBtn', 'MiddleMouseBtn', 'Space', 'LeftShift' or 'RightShift'! Given: ${keyCode}`); + const instance = checkUnityAcademyExistence(gameobject_distance.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier_A, gameobject_distance.name, 'gameObjectIdentifier_A'); + checkGameObjectIdentifierParameter(gameObjectIdentifier_B, gameobject_distance.name, 'gameObjectIdentifier_B'); + return instance.gameObjectDistanceInternal(gameObjectIdentifier_A, gameObjectIdentifier_B); +} + +export const BUTTON_KEY_CODES = [ + 'LeftMouseBtn', + 'RightMouseBtn', + 'MiddleMouseBtn', + 'Space', + 'LeftShift', + 'RightShift' +] as const; + +type ButtonKeyCodes = (typeof BUTTON_KEY_CODES)[number]; + +type CharKeyCodes = + | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' + | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' + | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' + | 'v' | 'w' | 'x' | 'y' | 'z' + | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; + +type KeyCode = ButtonKeyCodes | CharKeyCodes | Uppercase; + +export function assertIsValidKeyCode(obj: unknown, func_name: string, param_name?: string): asserts obj is KeyCode { + if (typeof obj === 'string') { + if (BUTTON_KEY_CODES.includes(obj as any)) return; + if (/^[a-zA-Z0-9]$/.test(obj)) return; } - return keyCode; + + throw new InvalidParameterTypeError('KeyCode', obj, func_name, param_name); } /** @@ -500,11 +523,11 @@ function checkKeyCodeValidityAndToLowerCase(keyCode: string): string { * @param keyCode The key to detact input for. * @category Input */ -export function get_key_down(keyCode: string): boolean { - checkUnityAcademyExistence(); - keyCode = checkKeyCodeValidityAndToLowerCase(keyCode); - return getInstance() - .getKeyState(keyCode) === 1; +export function get_key_down(keyCode: KeyCode): boolean { + const instance = checkUnityAcademyExistence(get_key_down.name); + assertIsValidKeyCode(keyCode, get_key_down.name); + + return instance.getKeyState(keyCode) === 1; } /** @@ -516,11 +539,11 @@ export function get_key_down(keyCode: string): boolean { * @param keyCode The key to detact input for. * @category Input */ -export function get_key(keyCode: string): boolean { - checkUnityAcademyExistence(); - keyCode = checkKeyCodeValidityAndToLowerCase(keyCode); - const keyState = getInstance() - .getKeyState(keyCode); +export function get_key(keyCode: KeyCode): boolean { + const instance = checkUnityAcademyExistence(get_key.name); + assertIsValidKeyCode(keyCode, get_key.name); + + const keyState = instance.getKeyState(keyCode); return keyState === 1 || keyState === 2 || keyState === 3; } @@ -533,11 +556,11 @@ export function get_key(keyCode: string): boolean { * @param keyCode The key to detact input for. * @category Input */ -export function get_key_up(keyCode: string): boolean { - checkUnityAcademyExistence(); - keyCode = checkKeyCodeValidityAndToLowerCase(keyCode); - return getInstance() - .getKeyState(keyCode) === 3; +export function get_key_up(keyCode: KeyCode): boolean { + const instance = checkUnityAcademyExistence(get_key_up.name); + assertIsValidKeyCode(keyCode, get_key_up.name); + + return instance.getKeyState(keyCode) === 3; } /** @@ -552,12 +575,14 @@ export function get_key_up(keyCode: string): boolean { * @category Common */ export function play_animator_state(gameObjectIdentifier: GameObjectIdentifier, animatorStateName: string): void { - checkUnityAcademyExistence(); - checkIs3DMode(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(animatorStateName, 'string'); - getInstance() - .playAnimatorStateInternal(gameObjectIdentifier, animatorStateName); + const instance = checkUnityAcademyExistence(play_animator_state.name, '3D'); + checkGameObjectIdentifierParameter(gameObjectIdentifier, play_animator_state.name); + + if (typeof animatorStateName !== 'string') { + throw new InvalidParameterTypeError('string', animatorStateName, play_animator_state.name, 'animatorStateName'); + } + + instance.playAnimatorStateInternal(gameObjectIdentifier, animatorStateName); } /** @@ -573,10 +598,9 @@ export function play_animator_state(gameObjectIdentifier: GameObjectIdentifier, * @category Physics - Rigidbody */ export function apply_rigidbody(gameObjectIdentifier: GameObjectIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - getInstance() - .applyRigidbodyInternal(gameObjectIdentifier); + const instance = checkUnityAcademyExistence(apply_rigidbody.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, apply_rigidbody.name); + instance.applyRigidbodyInternal(gameObjectIdentifier); } /** @@ -589,10 +613,9 @@ export function apply_rigidbody(gameObjectIdentifier: GameObjectIdentifier): voi * @category Physics - Rigidbody */ export function get_mass(gameObjectIdentifier: GameObjectIdentifier): number { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getRigidbodyNumericalProp('mass', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_mass.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_mass.name); + return instance.getRigidbodyNumericalProp('mass', gameObjectIdentifier); } /** @@ -605,11 +628,11 @@ export function get_mass(gameObjectIdentifier: GameObjectIdentifier): number { * @category Physics - Rigidbody */ export function set_mass(gameObjectIdentifier: GameObjectIdentifier, mass: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(mass, 'number'); - getInstance() - .setRigidbodyNumericalProp('mass', gameObjectIdentifier, mass); + const instance = checkUnityAcademyExistence(set_mass.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_mass.name); + + validateNumber(mass, set_mass.name, 'mass'); + instance.setRigidbodyNumericalProp('mass', gameObjectIdentifier, mass); } /** @@ -622,10 +645,9 @@ export function set_mass(gameObjectIdentifier: GameObjectIdentifier, mass: numbe * @category Physics - Rigidbody */ export function get_velocity(gameObjectIdentifier: GameObjectIdentifier): Vector3 { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getRigidbodyVelocityVector3Prop('velocity', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_velocity.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_velocity.name); + return instance.getRigidbodyVelocityVector3Prop('velocity', gameObjectIdentifier); } /** @@ -638,11 +660,11 @@ export function get_velocity(gameObjectIdentifier: GameObjectIdentifier): Vector * @category Physics - Rigidbody */ export function set_velocity(gameObjectIdentifier: GameObjectIdentifier, velocity: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(velocity); - getInstance() - .setRigidbodyVelocityVector3Prop('velocity', gameObjectIdentifier, velocity); + const instance = checkUnityAcademyExistence(set_velocity.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_velocity.name); + checkVector3Parameter(velocity, set_velocity.name, 'velocity'); + + instance.setRigidbodyVelocityVector3Prop('velocity', gameObjectIdentifier, velocity); } /** @@ -657,10 +679,9 @@ export function set_velocity(gameObjectIdentifier: GameObjectIdentifier, velocit * @category Physics - Rigidbody */ export function get_angular_velocity(gameObjectIdentifier: GameObjectIdentifier): Vector3 { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - return getInstance() - .getRigidbodyVelocityVector3Prop('angularVelocity', gameObjectIdentifier); + const instance = checkUnityAcademyExistence(get_angular_velocity.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_angular_velocity.name); + return instance.getRigidbodyVelocityVector3Prop('angularVelocity', gameObjectIdentifier); } /** @@ -675,11 +696,11 @@ export function get_angular_velocity(gameObjectIdentifier: GameObjectIdentifier) * @category Physics - Rigidbody */ export function set_angular_velocity(gameObjectIdentifier: GameObjectIdentifier, angularVelocity: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(angularVelocity); - getInstance() - .setRigidbodyVelocityVector3Prop('angularVelocity', gameObjectIdentifier, angularVelocity); + const instance = checkUnityAcademyExistence(set_angular_velocity.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_angular_velocity.name); + checkVector3Parameter(angularVelocity, set_angular_velocity.name, 'angularVelocity'); + + instance.setRigidbodyVelocityVector3Prop('angularVelocity', gameObjectIdentifier, angularVelocity); } /** @@ -694,11 +715,11 @@ export function set_angular_velocity(gameObjectIdentifier: GameObjectIdentifier, * @category Physics - Rigidbody */ export function set_drag(gameObjectIdentifier: GameObjectIdentifier, value: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(value, 'number'); - getInstance() - .setRigidbodyNumericalProp('drag', gameObjectIdentifier, value); + const instance = checkUnityAcademyExistence(set_drag.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_drag.name); + validateNumber(value, set_drag.name, 'value'); + + instance.setRigidbodyNumericalProp('drag', gameObjectIdentifier, value); } /** @@ -713,11 +734,11 @@ export function set_drag(gameObjectIdentifier: GameObjectIdentifier, value: numb * @category Physics - Rigidbody */ export function set_angular_drag(gameObjectIdentifier: GameObjectIdentifier, value: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(value, 'number'); - getInstance() - .setRigidbodyNumericalProp('angularDrag', gameObjectIdentifier, value); + const instance = checkUnityAcademyExistence(set_angular_drag.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_angular_drag.name); + validateNumber(value, set_angular_drag.name, 'value'); + + instance.setRigidbodyNumericalProp('angularDrag', gameObjectIdentifier, value); } /** @@ -730,11 +751,14 @@ export function set_angular_drag(gameObjectIdentifier: GameObjectIdentifier, val * @category Physics - Rigidbody */ export function set_use_gravity(gameObjectIdentifier: GameObjectIdentifier, useGravity: boolean): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(useGravity, 'boolean'); - getInstance() - .setUseGravityInternal(gameObjectIdentifier, useGravity); + const instance = checkUnityAcademyExistence(set_use_gravity.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_use_gravity.name); + + if (typeof useGravity !== 'boolean') { + throw new InvalidParameterTypeError('boolean', useGravity, set_use_gravity.name, 'useGravity'); + } + + instance.setUseGravityInternal(gameObjectIdentifier, useGravity); } /** @@ -747,11 +771,11 @@ export function set_use_gravity(gameObjectIdentifier: GameObjectIdentifier, useG * @category Physics - Rigidbody */ export function add_impulse_force(gameObjectIdentifier: GameObjectIdentifier, force: Vector3): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkVector3Parameter(force); - getInstance() - .addImpulseForceInternal(gameObjectIdentifier, force); + const instance = checkUnityAcademyExistence(add_impulse_force.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, add_impulse_force.name); + checkVector3Parameter(force, add_impulse_force.name, 'force'); + + instance.addImpulseForceInternal(gameObjectIdentifier, force); } /** @@ -765,10 +789,9 @@ export function add_impulse_force(gameObjectIdentifier: GameObjectIdentifier, fo * @category Physics - Collision */ export function remove_collider_components(gameObjectIdentifier: GameObjectIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - getInstance() - .removeColliderComponentsInternal(gameObjectIdentifier); + const instance = checkUnityAcademyExistence(remove_collider_components.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, remove_collider_components.name); + instance.removeColliderComponentsInternal(gameObjectIdentifier); } /** @@ -791,12 +814,12 @@ export function remove_collider_components(gameObjectIdentifier: GameObjectIdent * @category Physics - Collision * @category Outside Lifecycle */ -export function on_collision_enter(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(eventFunction, 'function'); - getInstance() - .setOnCollisionEnterInternal(gameObjectIdentifier, eventFunction); +export function on_collision_enter(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler): void { + const instance = checkUnityAcademyExistence(on_collision_enter.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, on_collision_enter.name); + assertFunctionOfLength(eventFunction, 2, on_collision_enter.name, 'eventFunction'); + + instance.setOnCollisionEnterInternal(gameObjectIdentifier, eventFunction); } /** @@ -819,12 +842,12 @@ export function on_collision_enter(gameObjectIdentifier: GameObjectIdentifier, e * @category Physics - Collision * @category Outside Lifecycle */ -export function on_collision_stay(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(eventFunction, 'function'); - getInstance() - .setOnCollisionStayInternal(gameObjectIdentifier, eventFunction); +export function on_collision_stay(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler): void { + const instance = checkUnityAcademyExistence(on_collision_stay.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, on_collision_stay.name); + assertFunctionOfLength(eventFunction, 2, on_collision_stay.name, 'eventFunction'); + + instance.setOnCollisionStayInternal(gameObjectIdentifier, eventFunction); } /** @@ -847,12 +870,12 @@ export function on_collision_stay(gameObjectIdentifier: GameObjectIdentifier, ev * @category Physics - Collision * @category Outside Lifecycle */ -export function on_collision_exit(gameObjectIdentifier: GameObjectIdentifier, eventFunction: Function): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(eventFunction, 'function'); - getInstance() - .setOnCollisionExitInternal(gameObjectIdentifier, eventFunction); +export function on_collision_exit(gameObjectIdentifier: GameObjectIdentifier, eventFunction: CollisionHandler): void { + const instance = checkUnityAcademyExistence(on_collision_exit.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, on_collision_exit.name); + assertFunctionOfLength(eventFunction, 2, on_collision_exit.name, 'eventFunction'); + + instance.setOnCollisionExitInternal(gameObjectIdentifier, eventFunction); } /** @@ -870,12 +893,16 @@ export function on_collision_exit(gameObjectIdentifier: GameObjectIdentifier, ev * @category Graphical User Interface */ export function gui_label(text: string, x: number, y: number): void { - checkUnityAcademyExistence(); - checkParameterType(text, 'string'); - checkParameterType(x, 'number'); - checkParameterType(y, 'number'); - getInstance() - .onGUI_Label(text, x, y); + const instance = checkUnityAcademyExistence(gui_label.name); + + if (typeof text !== 'string') { + throw new InvalidParameterTypeError('string', text, gui_label.name, 'text'); + } + + validateNumber(x, gui_label.name, 'x'); + validateNumber(y, gui_label.name, 'y'); + + instance.onGUI_Label(text, x, y); } /** @@ -917,16 +944,20 @@ export function gui_label(text: string, x: number, y: number): void { * @param onClick The function that will be called when user clicks the button on screen. * @category Graphical User Interface */ -export function gui_button(text: string, x: number, y: number, width: number, height: number, onClick: Function): void { - checkUnityAcademyExistence(); - checkParameterType(text, 'string'); - checkParameterType(x, 'number'); - checkParameterType(y, 'number'); - checkParameterType(width, 'number'); - checkParameterType(height, 'number'); - checkParameterType(onClick, 'function'); - getInstance() - .onGUI_Button(text, x, y, width, height, onClick); +export function gui_button(text: string, x: number, y: number, width: number, height: number, onClick: () => void): void { + const instance = checkUnityAcademyExistence(gui_button.name); + + if (typeof text !== 'string') { + throw new InvalidParameterTypeError('string', text, gui_button.name, 'text'); + } + + validateNumber(x, gui_button.name, 'x'); + validateNumber(y, gui_button.name, 'y'); + validateNumber(width, gui_button.name, 'width'); + validateNumber(height, gui_button.name, 'height'); + assertFunctionOfLength(onClick, 0, gui_button.name, 'onClick'); + + instance.onGUI_Button(text, x, y, width, height, onClick); } /** @@ -943,9 +974,8 @@ export function gui_button(text: string, x: number, y: number, width: number, he * @category Outside Lifecycle */ export function get_main_camera_following_target(): GameObjectIdentifier { - checkUnityAcademyExistence(); - return getInstance() - .getGameObjectIdentifierForPrimitiveGameObject('MainCameraFollowingTarget'); + const instance = checkUnityAcademyExistence(get_main_camera_following_target.name); + return instance.getGameObjectIdentifierForPrimitiveGameObject('MainCameraFollowingTarget'); } /** @@ -960,9 +990,8 @@ export function get_main_camera_following_target(): GameObjectIdentifier { * @category Outside Lifecycle */ export function request_for_main_camera_control(): GameObjectIdentifier { - checkUnityAcademyExistence(); - return getInstance() - .requestForMainCameraControlInternal(); + const instance = checkUnityAcademyExistence(request_for_main_camera_control.name); + return instance.requestForMainCameraControlInternal(); } /** @@ -975,11 +1004,14 @@ export function request_for_main_camera_control(): GameObjectIdentifier { * @category Common */ export function set_custom_prop(gameObjectIdentifier: GameObjectIdentifier, propName: string, value: any): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(propName, 'string'); - getInstance() - .setCustomPropertyInternal(gameObjectIdentifier, propName, value); + const instance = checkUnityAcademyExistence(set_custom_prop.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, set_custom_prop.name); + + if (typeof propName !== 'string') { + throw new InvalidParameterTypeError('string', propName, set_custom_prop.name, 'propName'); + } + + instance.setCustomPropertyInternal(gameObjectIdentifier, propName, value); } /** @@ -993,11 +1025,14 @@ export function set_custom_prop(gameObjectIdentifier: GameObjectIdentifier, prop * @category Common */ export function get_custom_prop(gameObjectIdentifier: GameObjectIdentifier, propName: string): any { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(gameObjectIdentifier); - checkParameterType(propName, 'string'); - return getInstance() - .getCustomPropertyInternal(gameObjectIdentifier, propName); + const instance = checkUnityAcademyExistence(get_custom_prop.name); + checkGameObjectIdentifierParameter(gameObjectIdentifier, get_custom_prop.name); + + if (typeof propName !== 'string') { + throw new InvalidParameterTypeError('string', propName, get_custom_prop.name, 'propName'); + } + + return instance.getCustomPropertyInternal(gameObjectIdentifier, propName); } /** @@ -1011,9 +1046,10 @@ export function get_custom_prop(gameObjectIdentifier: GameObjectIdentifier, prop * @category Maths */ export function vector3(x: number, y: number, z: number): Vector3 { - checkParameterType(x, 'number'); - checkParameterType(y, 'number'); - checkParameterType(z, 'number'); + validateNumber(x, vector3.name, 'x'); + validateNumber(y, vector3.name, 'y'); + validateNumber(z, vector3.name, 'z'); + return makeVector3D(x, y, z); } @@ -1026,7 +1062,7 @@ export function vector3(x: number, y: number, z: number): Vector3 { * @category Maths */ export function get_x(vector: Vector3): number { - checkVector3Parameter(vector); + checkVector3Parameter(vector, get_x.name); return vector.x; } @@ -1039,7 +1075,7 @@ export function get_x(vector: Vector3): number { * @category Maths */ export function get_y(vector: Vector3): number { - checkVector3Parameter(vector); + checkVector3Parameter(vector, get_y.name); return vector.y; } @@ -1052,7 +1088,7 @@ export function get_y(vector: Vector3): number { * @category Maths */ export function get_z(vector: Vector3): number { - checkVector3Parameter(vector); + checkVector3Parameter(vector, get_z.name); return vector.z; } @@ -1065,8 +1101,9 @@ export function get_z(vector: Vector3): number { * @category Maths */ export function scale_vector(vector: Vector3, factor: number): Vector3 { - checkVector3Parameter(vector); - checkParameterType(factor, 'number'); + checkVector3Parameter(vector, scale_vector.name); + validateNumber(factor, scale_vector.name, 'factor'); + return scaleVector(vector, factor); } @@ -1079,8 +1116,9 @@ export function scale_vector(vector: Vector3, factor: number): Vector3 { * @category Maths */ export function add_vectors(vectorA: Vector3, vectorB: Vector3): Vector3 { - checkVector3Parameter(vectorA); - checkVector3Parameter(vectorB); + checkVector3Parameter(vectorA, add_vectors.name, 'vectorA'); + checkVector3Parameter(vectorB, add_vectors.name, 'vectorB'); + return addVectors(vectorA, vectorB); } @@ -1093,8 +1131,9 @@ export function add_vectors(vectorA: Vector3, vectorB: Vector3): Vector3 { * @category Maths */ export function vector_difference(vectorA: Vector3, vectorB: Vector3): Vector3 { - checkVector3Parameter(vectorA); - checkVector3Parameter(vectorB); + checkVector3Parameter(vectorA, vector_difference.name, 'vectorA'); + checkVector3Parameter(vectorB, vector_difference.name, 'vectorB'); + return vectorDifference(vectorA, vectorB); } @@ -1107,8 +1146,9 @@ export function vector_difference(vectorA: Vector3, vectorB: Vector3): Vector3 { * @category Maths */ export function dot(vectorA: Vector3, vectorB: Vector3): number { - checkVector3Parameter(vectorA); - checkVector3Parameter(vectorB); + checkVector3Parameter(vectorA, dot.name, 'vectorA'); + checkVector3Parameter(vectorB, dot.name, 'vectorB'); + return dotProduct(vectorA, vectorB); } @@ -1121,8 +1161,9 @@ export function dot(vectorA: Vector3, vectorB: Vector3): number { * @category Maths */ export function cross(vectorA: Vector3, vectorB: Vector3): Vector3 { - checkVector3Parameter(vectorA); - checkVector3Parameter(vectorB); + checkVector3Parameter(vectorA, cross.name, 'vectorA'); + checkVector3Parameter(vectorB, cross.name, 'vectorB'); + return crossProduct(vectorA, vectorB); } @@ -1134,7 +1175,7 @@ export function cross(vectorA: Vector3, vectorB: Vector3): Vector3 { * @category Maths */ export function normalize(vector: Vector3): Vector3 { - checkVector3Parameter(vector); + checkVector3Parameter(vector, normalize.name); return normalizeVector(vector); } @@ -1146,7 +1187,7 @@ export function normalize(vector: Vector3): Vector3 { * @category Maths */ export function magnitude(vector: Vector3): number { - checkVector3Parameter(vector); + checkVector3Parameter(vector, magnitude.name); return vectorMagnitude(vector); } @@ -1171,8 +1212,8 @@ export function zero_vector(): Vector3 { * @category Maths */ export function point_distance(pointA: Vector3, pointB: Vector3): number { - checkVector3Parameter(pointA); - checkVector3Parameter(pointB); + checkVector3Parameter(pointA, point_distance.name, 'pointA'); + checkVector3Parameter(pointB, point_distance.name, 'pointB'); return pointDistance(pointA, pointB); } @@ -1184,10 +1225,12 @@ export function point_distance(pointA: Vector3, pointB: Vector3): number { * @category Outside Lifecycle */ export function load_audio_clip_mp3(audioUrl: string): AudioClipIdentifier { - checkUnityAcademyExistence(); - checkParameterType(audioUrl, 'string'); - return getInstance() - .loadAudioClipInternal(audioUrl, 'mp3'); + const instance = checkUnityAcademyExistence(load_audio_clip_mp3.name); + if (typeof audioUrl !== 'string') { + throw new InvalidParameterTypeError('string', audioUrl, load_audio_clip_mp3.name); + } + + return instance.loadAudioClipInternal(audioUrl, 'mp3'); } /** @@ -1198,10 +1241,13 @@ export function load_audio_clip_mp3(audioUrl: string): AudioClipIdentifier { * @category Outside Lifecycle */ export function load_audio_clip_ogg(audioUrl: string): AudioClipIdentifier { - checkUnityAcademyExistence(); - checkParameterType(audioUrl, 'string'); - return getInstance() - .loadAudioClipInternal(audioUrl, 'ogg'); + const instance = checkUnityAcademyExistence(load_audio_clip_ogg.name); + + if (typeof audioUrl !== 'string') { + throw new InvalidParameterTypeError('string', audioUrl, load_audio_clip_ogg.name); + } + + return instance.loadAudioClipInternal(audioUrl, 'ogg'); } /** @@ -1212,10 +1258,12 @@ export function load_audio_clip_ogg(audioUrl: string): AudioClipIdentifier { * @category Outside Lifecycle */ export function load_audio_clip_wav(audioUrl: string): AudioClipIdentifier { - checkUnityAcademyExistence(); - checkParameterType(audioUrl, 'string'); - return getInstance() - .loadAudioClipInternal(audioUrl, 'wav'); + const instance = checkUnityAcademyExistence(load_audio_clip_wav.name); + if (typeof audioUrl !== 'string') { + throw new InvalidParameterTypeError('string', audioUrl, load_audio_clip_wav.name); + } + + return instance.loadAudioClipInternal(audioUrl, 'wav'); } /** @@ -1233,9 +1281,8 @@ export function load_audio_clip_wav(audioUrl: string): AudioClipIdentifier { */ export function instantiate_audio_source(audioClip: AudioClipIdentifier): GameObjectIdentifier { // todo: check audio clip identifier type - checkUnityAcademyExistence(); - return getInstance() - .instantiateAudioSourceInternal(audioClip); + const instance = checkUnityAcademyExistence(instantiate_audio_source.name); + return instance.instantiateAudioSourceInternal(audioClip); } /** @@ -1247,10 +1294,9 @@ export function instantiate_audio_source(audioClip: AudioClipIdentifier): GameOb * @category Sound / Audio */ export function play_audio(audioSrc: GameObjectIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - getInstance() - .setAudioSourceProp('isPlaying', audioSrc, true); + const instance = checkUnityAcademyExistence(play_audio.name); + checkGameObjectIdentifierParameter(audioSrc, play_audio.name); + instance.setAudioSourceProp('isPlaying', audioSrc, true); } /** @@ -1262,10 +1308,9 @@ export function play_audio(audioSrc: GameObjectIdentifier): void { * @category Sound / Audio */ export function pause_audio(audioSrc: GameObjectIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - getInstance() - .setAudioSourceProp('isPlaying', audioSrc, false); + const instance = checkUnityAcademyExistence(pause_audio.name); + checkGameObjectIdentifierParameter(audioSrc, pause_audio.name); + instance.setAudioSourceProp('isPlaying', audioSrc, false); } /** @@ -1278,11 +1323,11 @@ export function pause_audio(audioSrc: GameObjectIdentifier): void { * @category Sound / Audio */ export function set_audio_play_speed(audioSrc: GameObjectIdentifier, speed: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - checkParameterType(speed, 'number'); - getInstance() - .setAudioSourceProp('playSpeed', audioSrc, speed); + const instance = checkUnityAcademyExistence(set_audio_play_speed.name); + checkGameObjectIdentifierParameter(audioSrc, set_audio_play_speed.name); + validateNumber(speed, set_audio_play_speed.name, 'speed'); + + instance.setAudioSourceProp('playSpeed', audioSrc, speed); } /** @@ -1295,10 +1340,9 @@ export function set_audio_play_speed(audioSrc: GameObjectIdentifier, speed: numb * @category Sound / Audio */ export function get_audio_play_progress(audioSrc: GameObjectIdentifier): number { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - return getInstance() - .getAudioSourceProp('playProgress', audioSrc); + const instance = checkUnityAcademyExistence(get_audio_play_progress.name); + checkGameObjectIdentifierParameter(audioSrc, get_audio_play_progress.name); + return instance.getAudioSourceProp('playProgress', audioSrc); } /** @@ -1311,11 +1355,11 @@ export function get_audio_play_progress(audioSrc: GameObjectIdentifier): number * @category Sound / Audio */ export function set_audio_play_progress(audioSrc: GameObjectIdentifier, progress: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - checkParameterType(progress, 'number'); - getInstance() - .setAudioSourceProp('playProgress', audioSrc, progress); + const instance = checkUnityAcademyExistence(set_audio_play_progress.name); + checkGameObjectIdentifierParameter(audioSrc, set_audio_play_progress.name); + validateNumber(progress, set_audio_play_progress.name, 'progresss'); + + instance.setAudioSourceProp('playProgress', audioSrc, progress); } /** @@ -1323,11 +1367,10 @@ export function set_audio_play_progress(audioSrc: GameObjectIdentifier, progress * @category Sound / Audio */ export function change_audio_clip(audioSrc: GameObjectIdentifier, newAudioClip: AudioClipIdentifier): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); + const instance = checkUnityAcademyExistence(change_audio_clip.name); + checkGameObjectIdentifierParameter(audioSrc, change_audio_clip.name); // todo: check audio clip identifier type - getInstance() - .setAudioSourceProp('audioClipIdentifier', audioSrc, newAudioClip); + instance.setAudioSourceProp('audioClipIdentifier', audioSrc, newAudioClip); } /** @@ -1335,11 +1378,14 @@ export function change_audio_clip(audioSrc: GameObjectIdentifier, newAudioClip: * @category Sound / Audio */ export function set_audio_looping(audioSrc: GameObjectIdentifier, looping: boolean): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - checkParameterType(looping, 'boolean'); - getInstance() - .setAudioSourceProp('isLooping', audioSrc, looping); + const instance = checkUnityAcademyExistence(set_audio_looping.name); + checkGameObjectIdentifierParameter(audioSrc, set_audio_looping.name); + + if (typeof looping !== 'boolean') { + throw new InvalidParameterTypeError('boolean', looping, set_audio_looping.name, 'looping'); + } + + instance.setAudioSourceProp('isLooping', audioSrc, looping); } /** @@ -1347,11 +1393,11 @@ export function set_audio_looping(audioSrc: GameObjectIdentifier, looping: boole * @category Sound / Audio */ export function set_audio_volume(audioSrc: GameObjectIdentifier, volume: number): void { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - checkParameterType(volume, 'number'); - getInstance() - .setAudioSourceProp('volume', audioSrc, volume); + const instance = checkUnityAcademyExistence(set_audio_volume.name); + checkGameObjectIdentifierParameter(audioSrc, set_audio_volume.name); + validateNumber(volume, set_audio_volume.name, 'volume'); + + instance.setAudioSourceProp('volume', audioSrc, volume); } /** @@ -1359,10 +1405,10 @@ export function set_audio_volume(audioSrc: GameObjectIdentifier, volume: number) * @category Sound / Audio */ export function is_audio_playing(audioSrc: GameObjectIdentifier): boolean { - checkUnityAcademyExistence(); - checkGameObjectIdentifierParameter(audioSrc); - return getInstance() - .getAudioSourceProp('isPlaying', audioSrc); + const instance = checkUnityAcademyExistence(is_audio_playing.name); + checkGameObjectIdentifierParameter(audioSrc, is_audio_playing.name); + + return instance.getAudioSourceProp('isPlaying', audioSrc); } /** @@ -1377,10 +1423,9 @@ export function is_audio_playing(audioSrc: GameObjectIdentifier): boolean { * @category Outside Lifecycle */ export function debug_log(content: any): void { - checkUnityAcademyExistence(); + const instance = checkUnityAcademyExistence(debug_log.name); const contentStr = content.toString(); - getInstance() - .studentLogger(contentStr, 'log'); + instance.studentLogger(contentStr, 'log'); } /** @@ -1395,10 +1440,9 @@ export function debug_log(content: any): void { * @category Outside Lifecycle */ export function debug_logwarning(content: any): void { - checkUnityAcademyExistence(); + const instance = checkUnityAcademyExistence(debug_logwarning.name); const contentStr = content.toString(); - getInstance() - .studentLogger(contentStr, 'warning'); + instance.studentLogger(contentStr, 'warning'); } /** @@ -1415,10 +1459,9 @@ export function debug_logwarning(content: any): void { * @category Outside Lifecycle */ export function debug_logerror(content: any): void { - checkUnityAcademyExistence(); + const instance = checkUnityAcademyExistence(debug_logerror.name); const contentStr = content.toString(); - getInstance() - .studentLogger(contentStr, 'error'); + instance.studentLogger(contentStr, 'error'); } /** @@ -1433,10 +1476,12 @@ export function debug_logerror(content: any): void { */ export function set_audio_listener_position(positionX: number, positionY: number, positionZ: number) { // todo: check audio clip identifier type - checkUnityAcademyExistence(); - checkParameterType(positionX, 'number'); - checkParameterType(positionY, 'number'); - checkParameterType(positionZ, 'number'); + checkUnityAcademyExistence(set_audio_listener_position.name); + + validateNumber(positionX, set_audio_listener_position.name, 'positionX'); + validateNumber(positionY, set_audio_listener_position.name, 'positionY'); + validateNumber(positionZ, set_audio_listener_position.name, 'positionZ'); + // TODO } @@ -1452,11 +1497,15 @@ export function set_audio_listener_position(positionX: number, positionY: number */ export function play_audio_clip_3d_sound(audioClip: AudioClipIdentifier, volume: number, loop: boolean, positionX: number, positionY: number, positionZ: number) { // todo: check audio clip identifier type - checkUnityAcademyExistence(); - checkParameterType(volume, 'number'); - checkParameterType(loop, 'boolean'); - checkParameterType(positionX, 'number'); - checkParameterType(positionY, 'number'); - checkParameterType(positionZ, 'number'); + checkUnityAcademyExistence(play_audio_clip_3d_sound.name); + validateNumber(volume, play_audio_clip_3d_sound.name, 'volume'); + + if (typeof loop !== 'boolean') { + throw new InvalidParameterTypeError('boolean', loop, play_audio_clip_3d_sound.name, 'loop'); + } + + validateNumber(positionX, play_audio_clip_3d_sound.name, 'positionX'); + validateNumber(positionY, play_audio_clip_3d_sound.name, 'positionY'); + validateNumber(positionZ, play_audio_clip_3d_sound.name, 'positionZ'); // TODO } diff --git a/src/bundles/unity_academy/tsconfig.json b/src/bundles/unity_academy/tsconfig.json index b138a203d5..1000218ea9 100644 --- a/src/bundles/unity_academy/tsconfig.json +++ b/src/bundles/unity_academy/tsconfig.json @@ -6,7 +6,8 @@ "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "noImplicitAny": false }, "typedocOptions": { "name": "unity_academy" diff --git a/src/bundles/wasm/package.json b/src/bundles/wasm/package.json index d096735578..5bc8c65ca3 100644 --- a/src/bundles/wasm/package.json +++ b/src/bundles/wasm/package.json @@ -20,8 +20,9 @@ "build": "buildtools build bundle .", "lint": "buildtools lint .", "test": "buildtools test --project .", - "postinstall": "buildtools compile", - "serve": "yarn buildtools serve" + "postinstall": "yarn compile", + "serve": "yarn buildtools serve", + "compile": "buildtools compile" }, "scripts-info": { "build": "Compiles the given bundle to the output directory", diff --git a/src/tabs/ArcadeTwod/index.tsx b/src/tabs/ArcadeTwod/index.tsx index 9ac9913ce9..c97c90fd11 100644 --- a/src/tabs/ArcadeTwod/index.tsx +++ b/src/tabs/ArcadeTwod/index.tsx @@ -1,5 +1,5 @@ -import { Button, ButtonGroup } from '@blueprintjs/core'; -import { IconNames, Pause, Play } from '@blueprintjs/icons'; +import { ButtonGroup } from '@blueprintjs/core'; +import PlayButton from '@sourceacademy/modules-lib/tabs/PlayButton'; import { defineTab } from '@sourceacademy/modules-lib/tabs/utils'; import type { DebuggerContext } from '@sourceacademy/modules-lib/types'; import Phaser from 'phaser'; @@ -47,12 +47,15 @@ function A2dUiButtons(props: UiProps) { }; return - - +
{ this.pixNFlix = debuggerContext.result.value; } - public componentDidMount() { + public override componentDidMount() { if (this.isPixNFlix()) { this.setupVideoService(); window.addEventListener('beforeunload', this.pixNFlix.deinit); } } - public componentWillUnmount() { + public override componentWillUnmount() { if (this.isPixNFlix()) { this.closeVideo(); window.removeEventListener('beforeunload', this.pixNFlix.deinit); @@ -269,7 +268,7 @@ class PixNFlix extends React.Component { ); } - public render() { + public override render() { const { mode, width, height, FPS, volume, hasAudio } = this.state; const displayOptions = mode === VideoMode.Still || mode === VideoMode.Video; const videoIsActive = mode === VideoMode.Video; @@ -288,14 +287,14 @@ class PixNFlix extends React.Component {
); @@ -164,12 +157,9 @@ export default defineTab({ toSpawn() { return getInstance() !== undefined; }, - body() { return ; }, - label: 'Unity Academy', - - iconName: IconNames.CUBE + iconName: 'cube' }); diff --git a/src/tabs/UnityAcademy/package.json b/src/tabs/UnityAcademy/package.json index 0bd3a66515..44ca53f587 100644 --- a/src/tabs/UnityAcademy/package.json +++ b/src/tabs/UnityAcademy/package.json @@ -4,7 +4,6 @@ "private": true, "dependencies": { "@blueprintjs/core": "^6.0.0", - "@blueprintjs/icons": "^6.0.0", "@sourceacademy/bundle-unity_academy": "workspace:^", "@sourceacademy/modules-lib": "workspace:^", "react": "^19.0.0", diff --git a/src/tsconfig.json b/src/tsconfig.json index db70362810..2fcac36283 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,5 +1,4 @@ // Deployment tsconfig - { "compilerOptions": { /* Allow JavaScript files to be imported inside your project, instead of just .ts and .tsx files. */ @@ -8,18 +7,21 @@ "allowSyntheticDefaultImports": true, /* See https://www.typescriptlang.org/tsconfig#esModuleInterop */ "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, /* See https://www.typescriptlang.org/tsconfig#lib */ - "lib": ["es6", "dom", "es2016", "ESNext", "scripthost"], + "lib": [ + "es6", + "dom", + "es2016", + "ESNext", + "scripthost" + ], /* Sets the module system for the program. See the Modules reference page for more information. */ "module": "esnext", /* Specify the module resolution strategy: 'node' (Node.js) or 'classic' (used in TypeScript before the release of 1.6). */ "moduleResolution": "bundler", "skipLibCheck": true, - "noEmit": true, - /* Allows importing modules with a ‘.json’ extension, which is a common practice in node projects. */ "resolveJsonModule": true, /* Enables the generation of sourcemap files. These files allow debuggers and other tools to display the original TypeScript source code when actually working with the emitted JavaScript files. */ @@ -28,16 +30,15 @@ "strict": true, /* The target setting changes which JS features are downleveled and which are left intact. */ "target": "es6", - /* In some cases where no type annotations are present, TypeScript will fall back to a type of any for a variable when it cannot infer the type. */ - /* *** TEMPORARILY ADDED UNTIL ALL MODULES HAVE BEEN REFACTORED!!!!!!!!!!! *** */ - "noImplicitAny": false, + "noImplicitOverride": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - /* Ensure that imports used only as types get removed */ "verbatimModuleSyntax": true, "isolatedModules": true }, /* Specifies an array of filenames or patterns that should be skipped when resolving include. */ - "exclude": ["**/dist"] + "exclude": [ + "**/dist" + ] } diff --git a/vitest.config.ts b/vitest.config.ts index 6612e3a15d..ab91a85489 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ './src/bundles/*', './src/tabs/*' ], + testTimeout: 20_000, include: ['**/__tests__/**/*.test.{ts,tsx}'], exclude: ['**/dist'], reporters: testReporters, diff --git a/yarn.config.cjs b/yarn.config.cjs index 0b1278cee0..66620b6da1 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -1,5 +1,7 @@ // @ts-check +const fs = require('fs/promises'); +const pathlib = require('path'); const { defineConfig } = require('@yarnpkg/types'); const { name } = require('./package.json'); @@ -23,12 +25,19 @@ module.exports = defineConfig({ } } + // Load the Node version that the repository is supposed to use + const nodeVersionFile = pathlib.join(__dirname, '.node-version'); + const nodeVersion = (await fs.readFile(nodeVersionFile, 'utf-8')).trim(); + const [rootWorkspace] = Yarn.workspaces({ ident: name }); // There should not be any resolutions value for js-slang, // which might be present if you linked js-slang to a local copy rootWorkspace.set('resolutions.js-slang', undefined); + // Runtime version should match the one specified + rootWorkspace.set('devEngines.runtime.version', `^${nodeVersion}`); + // Make sure that if the dependency is defined in the root workspace // that all child workspaces use the same version of that dependency for (const workspaceDep of Yarn.dependencies({ workspace: rootWorkspace })) { diff --git a/yarn.lock b/yarn.lock index 9aa33c79aa..a44201b2f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,8 +6,8 @@ __metadata: cacheKey: 10c0 "@actions/artifact@npm:^6.0.0": - version: 6.2.1 - resolution: "@actions/artifact@npm:6.2.1" + version: 6.1.0 + resolution: "@actions/artifact@npm:6.1.0" dependencies: "@actions/core": "npm:^3.0.0" "@actions/github": "npm:^9.0.0" @@ -23,17 +23,17 @@ __metadata: archiver: "npm:^7.0.1" jwt-decode: "npm:^4.0.0" unzip-stream: "npm:^0.3.1" - checksum: 10c0/f019ece6b566da5a256a683009989b06ddc2dbbd1ad83a4af3b6ae75671bb32a237b40f89ec3a0b04fdf1ccc6681cff09a11978fbb7d052395e6be3b41e59312 + checksum: 10c0/e9857ee757d3ab105f3725dd6775c21f133d7f164ab48a0041b51cb5053147c1db0012cfa193d31bde1ef459f163a569213d21097744fdd604e009dc0374ecfc languageName: node linkType: hard "@actions/core@npm:^3.0.0": - version: 3.0.1 - resolution: "@actions/core@npm:3.0.1" + version: 3.0.0 + resolution: "@actions/core@npm:3.0.0" dependencies: "@actions/exec": "npm:^3.0.0" "@actions/http-client": "npm:^4.0.0" - checksum: 10c0/c1b86364e923e8b1bdcd338943d453c114b2cd8eb9507e07b7614679ea15ddf938b3bb75484aaf363bc3aa55ba926b9514ec08d79811a991f75c732a76c4d854 + checksum: 10c0/ef204ca270011308c3cdbf7da702c0b8220775ea28aec52ddba696443f11afad6e725f5534110f2ef014382ab807b695bb4dcbf307683a0fc6927b3817871f6c languageName: node linkType: hard @@ -292,27 +292,29 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^5.1.5": - version: 5.1.10 - resolution: "@asamuzakjp/css-color@npm:5.1.10" +"@asamuzakjp/css-color@npm:^5.0.1": + version: 5.0.1 + resolution: "@asamuzakjp/css-color@npm:5.0.1" dependencies: "@csstools/css-calc": "npm:^3.1.1" "@csstools/css-color-parser": "npm:^4.0.2" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" - checksum: 10c0/b85c1e941463c908812c69409b73436aa967ffcd62c551f9ee03e9d0e3b14491e80231a935e1267037148cecce3d33d79e09f3176d4f987a2888ea079d9489d7 + lru-cache: "npm:^11.2.6" + checksum: 10c0/3e8d74a3b7f3005a325cb8e7f3da1aa32aeac4cd9ce387826dc25b16eaab4dc0e4a6faded8ccc1895959141f4a4a70e8bc38723347b89667b7b224990d16683c languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^7.0.6": - version: 7.0.9 - resolution: "@asamuzakjp/dom-selector@npm:7.0.9" +"@asamuzakjp/dom-selector@npm:^7.0.2": + version: 7.0.3 + resolution: "@asamuzakjp/dom-selector@npm:7.0.3" dependencies: "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" css-tree: "npm:^3.2.1" is-potential-custom-element-name: "npm:^1.0.1" - checksum: 10c0/f19f8ba0b3d94325f68384d4f97f5021d1bbcafc8218a793ef66646981e0865d3b47d0bb36fb12196eaf32e0ec8efcb182d98c89db09d2daa5c9995e47bb0fc1 + lru-cache: "npm:^11.2.7" + checksum: 10c0/c64b06a23479970ded4f38bec34069e98f4062b4ecb798b81b1fc37b5472ec6110d5bd9d8a267bfc431503f6ee3080c2e94eb3d99e30aaa1b6d9f83fbd2744fa languageName: node linkType: hard @@ -687,7 +689,18 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.19.4, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.19.4, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/333b2aa761264b91577a74bee86141ef733f9f9f6d4fc52548e4847dc35dfbf821f58c46832c637bfa761a6d9909d6a68f7d1ed59e17e4ffbb958dc510c17b62 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.2": version: 7.29.2 resolution: "@babel/parser@npm:7.29.2" dependencies: @@ -984,21 +997,21 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/colors@npm:^5.1.16": - version: 5.1.16 - resolution: "@blueprintjs/colors@npm:5.1.16" +"@blueprintjs/colors@npm:^5.1.14": + version: 5.1.14 + resolution: "@blueprintjs/colors@npm:5.1.14" dependencies: tslib: "npm:~2.6.2" - checksum: 10c0/e6019c658d48240b69a855d0757d2097282458d60459f26be59458c7b5b3e170bc4d62e7c0b67893f7e1b7bcf46ea6fcd7e4f38e1483f2762aa865080de7b650 + checksum: 10c0/c4820ad88b6ce221c41b045df12d06c0ff56b6dad12f7e887c26706475f31aacb70a6c7854c98a6470aea0307745e892ada528f2c7b9d44296e3c2631aa541cd languageName: node linkType: hard "@blueprintjs/core@npm:^6.0.0": - version: 6.12.1 - resolution: "@blueprintjs/core@npm:6.12.1" + version: 6.9.1 + resolution: "@blueprintjs/core@npm:6.9.1" dependencies: - "@blueprintjs/colors": "npm:^5.1.16" - "@blueprintjs/icons": "npm:^6.9.1" + "@blueprintjs/colors": "npm:^5.1.14" + "@blueprintjs/icons": "npm:^6.6.0" "@floating-ui/react": "npm:^0.27.13" "@popperjs/core": "npm:^2.11.8" classnames: "npm:^2.3.1" @@ -1006,6 +1019,7 @@ __metadata: react-popper: "npm:^2.3.0" react-transition-group: "npm:^4.4.5" tslib: "npm:~2.6.2" + use-sync-external-store: "npm:^1.2.0" peerDependencies: "@types/react": 18 react: 18 @@ -1016,11 +1030,11 @@ __metadata: bin: upgrade-blueprint-2.0.0-rename: scripts/upgrade-blueprint-2.0.0-rename.sh upgrade-blueprint-3.0.0-rename: scripts/upgrade-blueprint-3.0.0-rename.sh - checksum: 10c0/1496303f8b5d2117785cee27f4a6d6425d50f45ac35839e4aeb8e3469162e97e3c218ceb5b020ff91a7e0012fb60b8e936f4cd0a43ad6ac7a7b50d2898ab94e3 + checksum: 10c0/1e51002bac715bd24365284d9765f45bb6d403edc581d7555236ee5b50707753929dcc4ad93ccb9f116170fbda1f9322d101230e3dd88438b08280c48eb002c2 languageName: node linkType: hard -"@blueprintjs/icons@npm:^6.0.0": +"@blueprintjs/icons@npm:^6.0.0, @blueprintjs/icons@npm:^6.6.0": version: 6.9.0 resolution: "@blueprintjs/icons@npm:6.9.0" dependencies: @@ -1038,24 +1052,6 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/icons@npm:^6.9.1": - version: 6.9.1 - resolution: "@blueprintjs/icons@npm:6.9.1" - dependencies: - change-case: "npm:^4.1.2" - classnames: "npm:^2.3.1" - tslib: "npm:~2.6.2" - peerDependencies: - "@types/react": 18 - react: 18 - react-dom: 18 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10c0/5958a5168065adddf46590a8d5862f45a26715ee983af14cd9f82277e45131b75d9949f7c5c0876bc3836ddf9ed43fdc8d97530f6c9579498485ea9d8fde9030 - languageName: node - linkType: hard - "@box2d/core@npm:^0.10.0": version: 0.10.0 resolution: "@box2d/core@npm:0.10.0" @@ -1115,43 +1111,45 @@ __metadata: languageName: node linkType: hard -"@chevrotain/cst-dts-gen@npm:12.0.0": - version: 12.0.0 - resolution: "@chevrotain/cst-dts-gen@npm:12.0.0" +"@chevrotain/cst-dts-gen@npm:11.1.2": + version: 11.1.2 + resolution: "@chevrotain/cst-dts-gen@npm:11.1.2" dependencies: - "@chevrotain/gast": "npm:12.0.0" - "@chevrotain/types": "npm:12.0.0" - checksum: 10c0/bc79319baafbbf77b5d58539d1456e57ede160266a47d4a7bb75716d288dfef7fe2646224f4e2fb296afc0ca1cd57d28e469f82738e9f08e022c292aff2047db + "@chevrotain/gast": "npm:11.1.2" + "@chevrotain/types": "npm:11.1.2" + lodash-es: "npm:4.17.23" + checksum: 10c0/372a9573a404a1d717c92875024588a53d1eb078f12d2cd1d79d9c2c888c9b429eb62bbc85501b8ff168c14b22a675da99d97bb39b0a774e9fee000dc60fd8ff languageName: node linkType: hard -"@chevrotain/gast@npm:12.0.0": - version: 12.0.0 - resolution: "@chevrotain/gast@npm:12.0.0" +"@chevrotain/gast@npm:11.1.2": + version: 11.1.2 + resolution: "@chevrotain/gast@npm:11.1.2" dependencies: - "@chevrotain/types": "npm:12.0.0" - checksum: 10c0/699f8757b910ee577b9cd955f2bde794ffae1c1faad57ac8040a06c5d1c42b56109f43d32f0b69eb1b56b9c189a32fa3b2b1da5d86ed4fa7c76dc9717a7f3a6b + "@chevrotain/types": "npm:11.1.2" + lodash-es: "npm:4.17.23" + checksum: 10c0/540bfc9270d752f398b29efe9c89bb907d2984a47db4308a943e50c1cbd261ee13ecbef15c0e07808cf476d835bc36e65854db0bd214c277296cc14013eca8f4 languageName: node linkType: hard -"@chevrotain/regexp-to-ast@npm:12.0.0, @chevrotain/regexp-to-ast@npm:~12.0.0": - version: 12.0.0 - resolution: "@chevrotain/regexp-to-ast@npm:12.0.0" - checksum: 10c0/5cfc68b0cb63cb88281b40660f618555d70feb52b61af1e492862468fa1a0e80c61e84e3ba459431126c889e53f99a8aba4044297e7490a579a67bbe66d5d45e +"@chevrotain/regexp-to-ast@npm:11.1.2": + version: 11.1.2 + resolution: "@chevrotain/regexp-to-ast@npm:11.1.2" + checksum: 10c0/645f02ac94cb33e04c10547b197762c6936c7b7668966240684795ce131cb515a76b63e0cc500e787d669f1a36bd2f902ec518f27843ec857ecf9043c717527e languageName: node linkType: hard -"@chevrotain/types@npm:12.0.0": - version: 12.0.0 - resolution: "@chevrotain/types@npm:12.0.0" - checksum: 10c0/8f62dbc49117f6347f35f6fb5dcd5a94ce974b3c8503d4e90c1af7b24523992da1e82afb3a4ab0f450b22adfebbd39c7621c26a3734a639daaf15f0e661f43dc +"@chevrotain/types@npm:11.1.2": + version: 11.1.2 + resolution: "@chevrotain/types@npm:11.1.2" + checksum: 10c0/c0c4679a3d407df34e18d5adfa7ac599b4a2bfddbf68da6e43678b9b3e16ab911de7766b37b9fc466261c3dead3db1b620e2e344f800fa9f0f381720475eda8f languageName: node linkType: hard -"@chevrotain/utils@npm:12.0.0": - version: 12.0.0 - resolution: "@chevrotain/utils@npm:12.0.0" - checksum: 10c0/7bbbbe5256411bbea374a9b2aabe4fb1a69303bcdcfa613aba205d309c24967eb1f567a41a7a6a772ea23e9f1f10dbb533aad6de6218b7f7c14d8395a8ec1473 +"@chevrotain/utils@npm:11.1.2": + version: 11.1.2 + resolution: "@chevrotain/utils@npm:11.1.2" + checksum: 10c0/72989e7051781b9084252486712844c55e3b454318c7da4a5f6ded28dcd3947ba2882773a6bf09b7e744599e8c8025df8d3de0d487121734e6edb66999450438 languageName: node linkType: hard @@ -1167,15 +1165,6 @@ __metadata: languageName: node linkType: hard -"@commander-js/extra-typings@npm:^12.0.1": - version: 12.1.0 - resolution: "@commander-js/extra-typings@npm:12.1.0" - peerDependencies: - commander: ~12.1.0 - checksum: 10c0/5d29eaa724b577e2a52a393ad54992924d2559931b8e493ab892477b7a4e878e475c6bf771260f8585d835f7d8e17ae4a2656c191e9595d210ae0b48291c0b3d - languageName: node - linkType: hard - "@commander-js/extra-typings@npm:^14.0.0": version: 14.0.0 resolution: "@commander-js/extra-typings@npm:14.0.0" @@ -1185,62 +1174,62 @@ __metadata: languageName: node linkType: hard -"@cspell/cspell-bundled-dicts@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-bundled-dicts@npm:9.8.0" +"@cspell/cspell-bundled-dicts@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-bundled-dicts@npm:9.7.0" dependencies: "@cspell/dict-ada": "npm:^4.1.1" "@cspell/dict-al": "npm:^1.1.1" "@cspell/dict-aws": "npm:^4.0.17" "@cspell/dict-bash": "npm:^4.2.2" - "@cspell/dict-companies": "npm:^3.2.11" + "@cspell/dict-companies": "npm:^3.2.10" "@cspell/dict-cpp": "npm:^7.0.2" "@cspell/dict-cryptocurrencies": "npm:^5.0.5" "@cspell/dict-csharp": "npm:^4.0.8" - "@cspell/dict-css": "npm:^4.1.1" + "@cspell/dict-css": "npm:^4.0.19" "@cspell/dict-dart": "npm:^2.3.2" "@cspell/dict-data-science": "npm:^2.0.13" "@cspell/dict-django": "npm:^4.1.6" "@cspell/dict-docker": "npm:^1.1.17" - "@cspell/dict-dotnet": "npm:^5.0.13" + "@cspell/dict-dotnet": "npm:^5.0.12" "@cspell/dict-elixir": "npm:^4.0.8" "@cspell/dict-en-common-misspellings": "npm:^2.1.12" - "@cspell/dict-en-gb-mit": "npm:^3.1.22" - "@cspell/dict-en_us": "npm:^4.4.33" - "@cspell/dict-filetypes": "npm:^3.0.18" + "@cspell/dict-en-gb-mit": "npm:^3.1.18" + "@cspell/dict-en_us": "npm:^4.4.29" + "@cspell/dict-filetypes": "npm:^3.0.15" "@cspell/dict-flutter": "npm:^1.1.1" - "@cspell/dict-fonts": "npm:^4.0.6" + "@cspell/dict-fonts": "npm:^4.0.5" "@cspell/dict-fsharp": "npm:^1.1.1" - "@cspell/dict-fullstack": "npm:^3.2.9" + "@cspell/dict-fullstack": "npm:^3.2.8" "@cspell/dict-gaming-terms": "npm:^1.1.2" "@cspell/dict-git": "npm:^3.1.0" "@cspell/dict-golang": "npm:^6.0.26" "@cspell/dict-google": "npm:^1.0.9" "@cspell/dict-haskell": "npm:^4.0.6" - "@cspell/dict-html": "npm:^4.0.15" + "@cspell/dict-html": "npm:^4.0.14" "@cspell/dict-html-symbol-entities": "npm:^4.0.5" "@cspell/dict-java": "npm:^5.0.12" "@cspell/dict-julia": "npm:^1.1.1" "@cspell/dict-k8s": "npm:^1.0.12" "@cspell/dict-kotlin": "npm:^1.1.1" - "@cspell/dict-latex": "npm:^5.1.0" + "@cspell/dict-latex": "npm:^5.0.0" "@cspell/dict-lorem-ipsum": "npm:^4.0.5" "@cspell/dict-lua": "npm:^4.0.8" "@cspell/dict-makefile": "npm:^1.0.5" - "@cspell/dict-markdown": "npm:^2.0.16" + "@cspell/dict-markdown": "npm:^2.0.14" "@cspell/dict-monkeyc": "npm:^1.0.12" "@cspell/dict-node": "npm:^5.0.9" - "@cspell/dict-npm": "npm:^5.2.38" + "@cspell/dict-npm": "npm:^5.2.34" "@cspell/dict-php": "npm:^4.1.1" "@cspell/dict-powershell": "npm:^5.0.15" - "@cspell/dict-public-licenses": "npm:^2.0.16" - "@cspell/dict-python": "npm:^4.2.26" + "@cspell/dict-public-licenses": "npm:^2.0.15" + "@cspell/dict-python": "npm:^4.2.25" "@cspell/dict-r": "npm:^2.1.1" - "@cspell/dict-ruby": "npm:^5.1.1" + "@cspell/dict-ruby": "npm:^5.1.0" "@cspell/dict-rust": "npm:^4.1.2" "@cspell/dict-scala": "npm:^5.0.9" "@cspell/dict-shell": "npm:^1.1.2" - "@cspell/dict-software-terms": "npm:^5.2.2" + "@cspell/dict-software-terms": "npm:^5.1.21" "@cspell/dict-sql": "npm:^2.2.1" "@cspell/dict-svelte": "npm:^1.0.7" "@cspell/dict-swift": "npm:^2.0.6" @@ -1248,62 +1237,62 @@ __metadata: "@cspell/dict-typescript": "npm:^3.2.3" "@cspell/dict-vue": "npm:^3.0.5" "@cspell/dict-zig": "npm:^1.0.0" - checksum: 10c0/1f1dc8004511105db5b7065c049e4132955011dac6e7991ab2fa2e0cc78634c80d42913876beea7c924699aa3fec071f7058cb1144fe40f0a9794ebbf28e0df4 + checksum: 10c0/dc55309e91a0e464de134dfe3ebcef8f9d016b274d4974d127a90d259b8ef45537818208bfb71fb9587edbddac353b9acc537e5330fbf93b9ec9efb38d022b3c languageName: node linkType: hard -"@cspell/cspell-json-reporter@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-json-reporter@npm:9.8.0" +"@cspell/cspell-json-reporter@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-json-reporter@npm:9.7.0" dependencies: - "@cspell/cspell-types": "npm:9.8.0" - checksum: 10c0/1759a9b52de971e0b6993554d20e4a32304a8267334b79133c7e1ac403e9c5f32ba0024968a71c94be0956d7c2fce3d8255f384f0629d3f37134ef53f8ff1b3a + "@cspell/cspell-types": "npm:9.7.0" + checksum: 10c0/bba3021cc5415fe2ffb78c37e6044af217c9babd0af0394e93c4aed0d8551455b737e03bce1683fbedee21e25f5b30ab6ec51701206943ef41af5fbe5b89f60d languageName: node linkType: hard -"@cspell/cspell-performance-monitor@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-performance-monitor@npm:9.8.0" - checksum: 10c0/be8ca2ace9a5ea2a3226ae744a8256dc9e0540d8b2dfaadfc5895a8e0cc920912a2b431379b4c51393e7482a07c5729ddccc034bba22251bb62e759b7af98d02 +"@cspell/cspell-performance-monitor@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-performance-monitor@npm:9.7.0" + checksum: 10c0/1919b3a3894484c7fe9962d223b5d905ea08cbdc9209b2777c992643e0ad831e2cd4eb2de22f2fc363e1be20d44d7dc31048f7cbfdc6cd99a675530bca61cdd6 languageName: node linkType: hard -"@cspell/cspell-pipe@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-pipe@npm:9.8.0" - checksum: 10c0/796c34f1a571953d86cbeca63675312bdc9c42c499aaa02b42dfdbd05440e8f5462d0a1ddcb10234c25152117afc9b66e9f5f67670fc065603a2daa0f72b09be +"@cspell/cspell-pipe@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-pipe@npm:9.7.0" + checksum: 10c0/7ec47493be7a2787dc0d9c44c0a9b37b909630fc57726acd088183b9d496a3f5067b2659c856a95230630d99bce6f90531bf4db8204a3a5e07037128f23bf809 languageName: node linkType: hard -"@cspell/cspell-resolver@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-resolver@npm:9.8.0" +"@cspell/cspell-resolver@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-resolver@npm:9.7.0" dependencies: global-directory: "npm:^5.0.0" - checksum: 10c0/b95a6c6f8921a33d2877e8e0beba6baddefe4fa75b67f26e5211477f787c2f846822e127c0e82570de710595246af3af4eb67998bdcde81735b723ecf02c1cf8 + checksum: 10c0/d03c6f4d1ea532adc684cfe023af2a5d7f5b917f53372d0a5404b0eb09a634511e8637bba695f70e481a410838d978a1e51aea504b476461b0a226ce3b9751f9 languageName: node linkType: hard -"@cspell/cspell-service-bus@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-service-bus@npm:9.8.0" - checksum: 10c0/23d8fdfd0f25d741119ea550a39a5c865ac80ed6e9be6c69691a93158e519bd1445a4e9fb9730f70158a5025016ab860a44e6cedae41a420b90c08cb233adf83 +"@cspell/cspell-service-bus@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-service-bus@npm:9.7.0" + checksum: 10c0/47e5ced1124b142c61edf3d6f52a8e7734b970580d9f715da7c505e368ae5c0e6eaa8a013dbeb3106f851cc5c3aa76e8e0f4c4b1d390feaad48f2bd013047874 languageName: node linkType: hard -"@cspell/cspell-types@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-types@npm:9.8.0" - checksum: 10c0/bd7b509398cc7bcc1333c90e85fa9e9726aa8b7cc6a061d00bf143bc806a5a1e170d2ecd380772c02566978cb9f859170e8c7a7bcb194e460f8d4d6c446b2d8d +"@cspell/cspell-types@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-types@npm:9.7.0" + checksum: 10c0/63af2d2d38e044fcc31f14eea9385d718c8d67aab9b46b2c566a83c1be62c99a313b858edd5a77fb6f21d1b7cd7ec8fd1324aba62905d7519b212d51ccbae158 languageName: node linkType: hard -"@cspell/cspell-worker@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/cspell-worker@npm:9.8.0" +"@cspell/cspell-worker@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/cspell-worker@npm:9.7.0" dependencies: - cspell-lib: "npm:9.8.0" - checksum: 10c0/231359c459eb60d7687c73ddd51b5655626cf08c244da5c3817df41996cced97f47d2ab27c8b23d24bcb76248928ec280875bc6afd566464e80711d0424bdb5e + cspell-lib: "npm:9.7.0" + checksum: 10c0/86f5b23089979b828dacee2679063267b030ce342874a0c2cec9566fa6243260cd91c831f0eb38956acfd5d4c346d7cb8d2a449b044552d22237ccb4476c6b7e languageName: node linkType: hard @@ -1337,10 +1326,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-companies@npm:^3.2.11": - version: 3.2.11 - resolution: "@cspell/dict-companies@npm:3.2.11" - checksum: 10c0/9aa941967bbc48e171cb862b250d274fdc4234449a96f146b4814b0927897af827a056adb244ab110ca9a4ea5837819a4031eef2c7de0f0791a4d30a408b16a6 +"@cspell/dict-companies@npm:^3.2.10": + version: 3.2.10 + resolution: "@cspell/dict-companies@npm:3.2.10" + checksum: 10c0/56ffda78e90a417fb470d3296d17fa74c2b86f1f73de121b12ca9510f81663eea7c20923fe4409c9159eb20b9b36ff4cf4b6cbb1b4fab48da404ebe1ee855f7b languageName: node linkType: hard @@ -1365,10 +1354,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-css@npm:^4.1.1": - version: 4.1.1 - resolution: "@cspell/dict-css@npm:4.1.1" - checksum: 10c0/979058aeaf695664255326b09d7fddbea57cb187484ae45e4741b6f6b92650b0ef9ce52d32651ba1927a6c2af3098ffa87edcad9f6f552e2c90c7c553ce2aac1 +"@cspell/dict-css@npm:^4.0.19": + version: 4.0.19 + resolution: "@cspell/dict-css@npm:4.0.19" + checksum: 10c0/e0ba38ec536ce8a9b88a4afb197b9467622bd6519a84e71435e9f0d8d90d12d94f6e83d5e504337a95f6ce99ee398c920c6367c6252c6c01c794cba61b621bde languageName: node linkType: hard @@ -1400,10 +1389,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-dotnet@npm:^5.0.13": - version: 5.0.13 - resolution: "@cspell/dict-dotnet@npm:5.0.13" - checksum: 10c0/b34792ea2b1258f4e215487c4ff61de2fb3c9c6e0381fec03c4fb8132f2decd2b7b73a6450c507e8a3211e616282a3ace94e7d99363503e0efa4ef2cb6f2fcca +"@cspell/dict-dotnet@npm:^5.0.12": + version: 5.0.12 + resolution: "@cspell/dict-dotnet@npm:5.0.12" + checksum: 10c0/19edcc32fdaff42de7ca993a3a682897e1e92fcd127be79ed6612685ede92d52232bbe9273712eb21c2cfc7daf11f0bcabedbea5d4fe5d3990cfbc65da0914e2 languageName: node linkType: hard @@ -1421,24 +1410,24 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-en-gb-mit@npm:^3.1.22": - version: 3.1.22 - resolution: "@cspell/dict-en-gb-mit@npm:3.1.22" - checksum: 10c0/78501fafeae62b966579c10de1f4fc24dedd57f83bdcafc72e314c9b781490858423890932b974f370dc8da2943cc8fdae435a289b13a397d8aa7986aa391d07 +"@cspell/dict-en-gb-mit@npm:^3.1.18": + version: 3.1.19 + resolution: "@cspell/dict-en-gb-mit@npm:3.1.19" + checksum: 10c0/f3d9cadaa86a758ea0d85e42859e0ba0a7b632aa53b63cf95caa049cac5c6144db37bf6c3e5511da76a3640d4c4aad28a6f0b3eb9424b0d04e82ccd8e92e9a43 languageName: node linkType: hard -"@cspell/dict-en_us@npm:^4.4.33": - version: 4.4.33 - resolution: "@cspell/dict-en_us@npm:4.4.33" - checksum: 10c0/c2b226f6879a58cfeecfa209116a241aabb9482b7fe56cac115b46bfefd557ec4f06efa33ae96c4d6e883cae70b5fdde83063315504c1dc4e4fb7916a46c2045 +"@cspell/dict-en_us@npm:^4.4.29": + version: 4.4.30 + resolution: "@cspell/dict-en_us@npm:4.4.30" + checksum: 10c0/7048beb2614559ee5fe9fd64b3c3c13f4f25d2792943f16277a6b50f4a6e868af4195d5b31c19a38a017feb2a46dd5a0a3d02e1e5e51f7bbcc077e095764b1b7 languageName: node linkType: hard -"@cspell/dict-filetypes@npm:^3.0.18": - version: 3.0.18 - resolution: "@cspell/dict-filetypes@npm:3.0.18" - checksum: 10c0/b7a223eacef51770ed844b48b64d92b05b41a0a2ecbb6856ba8758fe8e444ca5f4252ecc511ac00ec1d12c1b12aef1198865f612cceaaf6d304c92b049a739cb +"@cspell/dict-filetypes@npm:^3.0.15": + version: 3.0.15 + resolution: "@cspell/dict-filetypes@npm:3.0.15" + checksum: 10c0/2bf7c592fbe4755dfff8375fbe422b0ac6c0daebc71d4641141611520aeb67e043e9016075b7855513306b594980a6b55af2069e10848256493fcb39a34d0725 languageName: node linkType: hard @@ -1449,10 +1438,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-fonts@npm:^4.0.6": - version: 4.0.6 - resolution: "@cspell/dict-fonts@npm:4.0.6" - checksum: 10c0/73095a5bb3ec6ca24c7f01298b8344646005c0c05857b24ae106d7f795acf0b7107f4aaa677224c899d7aad7d0383f9f82dddd11a6b4cf3b26e3e5166b222674 +"@cspell/dict-fonts@npm:^4.0.5": + version: 4.0.5 + resolution: "@cspell/dict-fonts@npm:4.0.5" + checksum: 10c0/5ed1886fabe245e1abeb7da2b0d6a534ddf204666ecd5d7d153a422dfbb93854456025b1894bc61e7c21915116d06b177e3012bc7a969307c8797baac2380f6f languageName: node linkType: hard @@ -1463,10 +1452,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-fullstack@npm:^3.2.9": - version: 3.2.9 - resolution: "@cspell/dict-fullstack@npm:3.2.9" - checksum: 10c0/a13d08099d1048797fe37d2a654846ff5086193bd29d57b62423ebc74f6c08c9f3b52c49f08b73d6bd09cdb393b70351f85f151893a20a5f8c858e474dd42e75 +"@cspell/dict-fullstack@npm:^3.2.8": + version: 3.2.8 + resolution: "@cspell/dict-fullstack@npm:3.2.8" + checksum: 10c0/90a469b899574bee9fff390e4264cc72468847b4c53fb2bc5991874e7b65c8d949c693615953d836a8b7cba69c5690163d722c07126c0ca3bd798197a86c64e6 languageName: node linkType: hard @@ -1512,10 +1501,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-html@npm:^4.0.15": - version: 4.0.15 - resolution: "@cspell/dict-html@npm:4.0.15" - checksum: 10c0/0812ae7f11ea2160ab4df8039b0f5af023c102d8806dc6ea9b8a90f96cc564b00dad167c3eb1a6685a244980ac203cc168438b352c84918a215147ef632aca10 +"@cspell/dict-html@npm:^4.0.14": + version: 4.0.14 + resolution: "@cspell/dict-html@npm:4.0.14" + checksum: 10c0/8dedb8a20f7bc53db4b933ae118ee0ab654b176648b2451d335ca8bd266f84ce8deb52989aa51a52f872730262e113b73874b88320b115ab2e993876a3f24cb1 languageName: node linkType: hard @@ -1547,10 +1536,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-latex@npm:^5.1.0": - version: 5.1.0 - resolution: "@cspell/dict-latex@npm:5.1.0" - checksum: 10c0/e806722c0ff1581a069245cb297b954f8e24fb6e1942f2547b0fee7783fc9b59d08fe2d2c7ddf3f7f9eef60d783ac9a4290f37956b9723b13e21c9422d7962b0 +"@cspell/dict-latex@npm:^5.0.0": + version: 5.0.0 + resolution: "@cspell/dict-latex@npm:5.0.0" + checksum: 10c0/02d0d3d4a14eba96de6f4f6b51b3f2d1ac1d2de7b1069e3ae6631bdbc53982caead563abb2a568d436fa016bf99aea36d0e6e2c3b9a83c1e9fe9ccbc923d5da7 languageName: node linkType: hard @@ -1575,15 +1564,15 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-markdown@npm:^2.0.16": - version: 2.0.16 - resolution: "@cspell/dict-markdown@npm:2.0.16" +"@cspell/dict-markdown@npm:^2.0.14": + version: 2.0.14 + resolution: "@cspell/dict-markdown@npm:2.0.14" peerDependencies: - "@cspell/dict-css": ^4.1.1 - "@cspell/dict-html": ^4.0.15 + "@cspell/dict-css": ^4.0.19 + "@cspell/dict-html": ^4.0.14 "@cspell/dict-html-symbol-entities": ^4.0.5 "@cspell/dict-typescript": ^3.2.3 - checksum: 10c0/563414ae9d6b0a12ba89c54ec62ada59c1fbc0b7199a85d607d9aae22e6446f2fb1757a737b0d631843989888b611bf39eebf79eef1a43e37e0584181274248c + checksum: 10c0/2198375545579fe4aac7b2a53ae4125bee212fa489be5d19193b227308c3e5c687bfc140111c6263bf2d5a7963eb07f2e4b6c737b2de733fc4e0cf3b2123ece2 languageName: node linkType: hard @@ -1601,10 +1590,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-npm@npm:^5.2.38": - version: 5.2.38 - resolution: "@cspell/dict-npm@npm:5.2.38" - checksum: 10c0/6eeeb9a0fd114fedaf7b8599f899484b20acd4e67a008056833b5791d59098c023023ac7afcbe5f35e7863ff6f64dad5012fbfaa8edb8695775d8f5635d53395 +"@cspell/dict-npm@npm:^5.2.34": + version: 5.2.36 + resolution: "@cspell/dict-npm@npm:5.2.36" + checksum: 10c0/151d9995a82b3f8df750c9f6cc88a62adab3b910aceefd3aee6e979571fa194b6d26fc4acc557c8da9b10d3a5f0509c10b41c75c65e510e449bcca6e7edb4319 languageName: node linkType: hard @@ -1622,19 +1611,19 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-public-licenses@npm:^2.0.16": - version: 2.0.16 - resolution: "@cspell/dict-public-licenses@npm:2.0.16" - checksum: 10c0/473a29eb6fa8cf0d64fffcac0a686c492777dca9a0d6be4c890bcb0e98cb2f01a4afbbfcb88e903a5895593567ec6f2646097f07b0453b689fd70272088aa2a0 +"@cspell/dict-public-licenses@npm:^2.0.15": + version: 2.0.15 + resolution: "@cspell/dict-public-licenses@npm:2.0.15" + checksum: 10c0/ff3f159b00668f0714c0bc8c2492fb72aaedce5464baf0a4c60dd4a3348f9a3d15f7b63e4ed0f6e7124ba552701c14f2d10769cc1a4d593c82c460fbb3bbf84a languageName: node linkType: hard -"@cspell/dict-python@npm:^4.2.26": - version: 4.2.26 - resolution: "@cspell/dict-python@npm:4.2.26" +"@cspell/dict-python@npm:^4.2.25": + version: 4.2.25 + resolution: "@cspell/dict-python@npm:4.2.25" dependencies: "@cspell/dict-data-science": "npm:^2.0.13" - checksum: 10c0/3773c7856b47648f5f54c92cf5660f121fbafc98ecca5d6ab6767e2a8b297598b0c51e43f404faac9eef7a72adbf8c49312aea3d16399cee14a11746a2277e09 + checksum: 10c0/dcab0aac0075f76b0360fd07d61b4421f543e72a98a482494f9a3decb650d475f4508918c85ef8985eeabac11431bf7f67b496491d58a33cf41c25a6016e84a9 languageName: node linkType: hard @@ -1645,10 +1634,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-ruby@npm:^5.1.1": - version: 5.1.1 - resolution: "@cspell/dict-ruby@npm:5.1.1" - checksum: 10c0/ec23c736a4e5588c8c55a44b5c31eb7238a199ac4f2a84fd9aa6558a80f6416c42d7eaa7337e30590b66bbaac5523b6d64519f7e33eadc4cf1d878f20bb86fc0 +"@cspell/dict-ruby@npm:^5.1.0": + version: 5.1.0 + resolution: "@cspell/dict-ruby@npm:5.1.0" + checksum: 10c0/d9ca8a8d72869b37b201fd5d17d1e7d0094185dde559861b899256b7dd55e80aedba430ac2ca393d0db479c284cc89cd2eec8f9e56e5601ebd715f0463c75b7d languageName: node linkType: hard @@ -1673,10 +1662,10 @@ __metadata: languageName: node linkType: hard -"@cspell/dict-software-terms@npm:^5.2.2": - version: 5.2.2 - resolution: "@cspell/dict-software-terms@npm:5.2.2" - checksum: 10c0/eca6c5ee91a21c76b9d735c5777521287c896bd03e448c8512b61b75e926a269aef5e03dd0ea3cd2b8291ea56e6f140742f4a4826045603fffdeaba228272557 +"@cspell/dict-software-terms@npm:^5.1.21": + version: 5.1.24 + resolution: "@cspell/dict-software-terms@npm:5.1.24" + checksum: 10c0/de082990f9eefeaec8625e0885fcffd814c8fd8c69533f058625daf24a1d1f2282510a08eb155f2323ef66a93eab4521243f6856ced938622706971eee5a04d6 languageName: node linkType: hard @@ -1729,41 +1718,41 @@ __metadata: languageName: node linkType: hard -"@cspell/dynamic-import@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/dynamic-import@npm:9.8.0" +"@cspell/dynamic-import@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/dynamic-import@npm:9.7.0" dependencies: - "@cspell/url": "npm:9.8.0" + "@cspell/url": "npm:9.7.0" import-meta-resolve: "npm:^4.2.0" - checksum: 10c0/257fda196cee869cc041aed0206b0c754e42dba0bbb9a0ac207b09ed38fefe5a11879b4d87b055d69b65e1431ed92e3d13897293fc4a1efb8318c68ce633572c + checksum: 10c0/30f718b789e580fedb414fd65522a69ec7d96af42d063cfcb6b527b737313b4dc30e1dc0c5578ba202ed761073364c935cf24d6e8babdb192e602f101dab44db languageName: node linkType: hard -"@cspell/filetypes@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/filetypes@npm:9.8.0" - checksum: 10c0/15104dc46721a33ec9af722cca35d3efbb33502d15f2fe03145ddc19335115aff1e8c15208ec844d56257fb8f3bb298497ffd26b0ecdcadd2f6dce912f794f83 +"@cspell/filetypes@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/filetypes@npm:9.7.0" + checksum: 10c0/128b954c56864e4a2c5bae6a325beef6939739ce3ab020172f7be5a1bec5efe25277ad024fe0fb4a8f1becdf58579f620113aff04b59dd695916ce9aeabff4c3 languageName: node linkType: hard -"@cspell/rpc@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/rpc@npm:9.8.0" - checksum: 10c0/c79c6c5d4696dd275e11b8dab494e5b83be163e089b7bc562e270c7a376070947e2d5b2206b92d4a4a8f39020c76ac324f4eab641319fada0561f951f7880598 +"@cspell/rpc@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/rpc@npm:9.7.0" + checksum: 10c0/09424bcea49b9267b5f79551a51a9c242318ff7304fca4b0898177256d1372ecd39587f7c8bf8438f05ee31b3d2176a3d9125cb26dd8928a4752523433f12291 languageName: node linkType: hard -"@cspell/strong-weak-map@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/strong-weak-map@npm:9.8.0" - checksum: 10c0/f267d079e600536b016b9ac6c94e40af9a11b2850ae03224b68b2abc6cc6fc34782a2ac1b3623183b74bc40fad331d13f9e7dc7bf85c08941846779710bd0bd0 +"@cspell/strong-weak-map@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/strong-weak-map@npm:9.7.0" + checksum: 10c0/a1e3e4d228dc41a5ed59f698f932ac16ab1d6e9dad0a3eef9f73f310d391dfd2287ded7c3a4a10c53f0461c3765fe442d4a6a091c8a0606f36bd440c264e8a2d languageName: node linkType: hard -"@cspell/url@npm:9.8.0": - version: 9.8.0 - resolution: "@cspell/url@npm:9.8.0" - checksum: 10c0/c914602b72433b69c66cd802e1e2482e55ea82e2149f3342f9c5d6761b8cc5d23663c24496a93d36da7b20df3f19fde81029f77c9751301d9470d02778d1eba6 +"@cspell/url@npm:9.7.0": + version: 9.7.0 + resolution: "@cspell/url@npm:9.7.0" + checksum: 10c0/c11dff3b3a8a78c7022e8e6d88abbd49a0f9d29ffbd185008f224286f2fdc8490a3d35c8ea07fc0273b422256eb10186881d0bcb9c51dee641f09b1355984b88 languageName: node linkType: hard @@ -1882,7 +1871,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:1.9.2, @emnapi/core@npm:^1.4.3": +"@emnapi/core@npm:1.9.2": version: 1.9.2 resolution: "@emnapi/core@npm:1.9.2" dependencies: @@ -1892,7 +1881,17 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:1.9.2, @emnapi/runtime@npm:^1.4.3": +"@emnapi/core@npm:^1.4.3": + version: 1.8.1 + resolution: "@emnapi/core@npm:1.8.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.1.0" + tslib: "npm:^2.4.0" + checksum: 10c0/2c242f4b49779bac403e1cbcc98edacdb1c8ad36562408ba9a20663824669e930bc8493be46a2522d9dc946b8d96cd7073970bae914928c7671b5221c85b432e + languageName: node + linkType: hard + +"@emnapi/runtime@npm:1.9.2": version: 1.9.2 resolution: "@emnapi/runtime@npm:1.9.2" dependencies: @@ -1901,6 +1900,24 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.4.3": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/f4929d75e37aafb24da77d2f58816761fe3f826aad2e37fa6d4421dac9060cbd5098eea1ac3c9ecc4526b89deb58153852fa432f87021dc57863f2ff726d713f + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:1.1.0": + version: 1.1.0 + resolution: "@emnapi/wasi-threads@npm:1.1.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/e6d54bf2b1e64cdd83d2916411e44e579b6ae35d5def0dea61a3c452d9921373044dff32a8b8473ae60c80692bdc39323e98b96a3f3d87ba6886b24dd0ef7ca1 + languageName: node + linkType: hard + "@emnapi/wasi-threads@npm:1.2.1": version: 1.2.1 resolution: "@emnapi/wasi-threads@npm:1.2.1" @@ -1924,16 +1941,16 @@ __metadata: languageName: node linkType: hard -"@es-joy/jsdoccomment@npm:~0.86.0": - version: 0.86.0 - resolution: "@es-joy/jsdoccomment@npm:0.86.0" +"@es-joy/jsdoccomment@npm:~0.84.0": + version: 0.84.0 + resolution: "@es-joy/jsdoccomment@npm:0.84.0" dependencies: "@types/estree": "npm:^1.0.8" - "@typescript-eslint/types": "npm:^8.58.0" - comment-parser: "npm:1.4.6" + "@typescript-eslint/types": "npm:^8.54.0" + comment-parser: "npm:1.4.5" esquery: "npm:^1.7.0" - jsdoc-type-pratt-parser: "npm:~7.2.0" - checksum: 10c0/309f56912eba0100e7721ae00f6161fbe0c6acd00c6bb81177821851c5a56e397d2ab660d4493ac8c675eedba3be3c813bdc43e54f17b5fa866dbba980f337ab + jsdoc-type-pratt-parser: "npm:~7.1.1" + checksum: 10c0/b5562c176dde36cd2956bb115b79229d2253b27d6d7e52820eb55c509f75a72048ae8ea8d57193b33be42728c1aa7a5ee20937b4967175291cb4ae60fdda318d languageName: node linkType: hard @@ -2305,14 +2322,14 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.2": - version: 0.21.2 - resolution: "@eslint/config-array@npm:0.21.2" +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" dependencies: "@eslint/object-schema": "npm:^2.1.7" debug: "npm:^4.3.1" - minimatch: "npm:^3.1.5" - checksum: 10c0/89dfe815d18456177c0a1f238daf4593107fd20298b3598e0103054360d3b8d09d967defd8318f031185d68df1f95cfa68becf1390a9c5c6887665f1475142e3 + minimatch: "npm:^3.1.2" + checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c languageName: node linkType: hard @@ -2343,24 +2360,31 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^3.3.5": - version: 3.3.5 - resolution: "@eslint/eslintrc@npm:3.3.5" +"@eslint/eslintrc@npm:^3.3.1": + version: 3.3.1 + resolution: "@eslint/eslintrc@npm:3.3.1" dependencies: - ajv: "npm:^6.14.0" + ajv: "npm:^6.12.4" debug: "npm:^4.3.2" espree: "npm:^10.0.1" globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.1" - minimatch: "npm:^3.1.5" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10c0/9fb9f1ca65e46d6173966e3aaa5bd353e3a65d7f1f582bebf77f578fab7d7960a399fac1ecfb1e7d52bd61f5cefd6531087ca52a3a3c388f2e1b4f1ebd3da8b7 + checksum: 10c0/b0e63f3bc5cce4555f791a4e487bf999173fcf27c65e1ab6e7d63634d8a43b33c3693e79f192cbff486d7df1be8ebb2bd2edc6e70ddd486cbfa84a359a3e3b41 languageName: node linkType: hard -"@eslint/js@npm:9.39.4, @eslint/js@npm:^9.35.0": +"@eslint/js@npm:9.39.3": + version: 9.39.3 + resolution: "@eslint/js@npm:9.39.3" + checksum: 10c0/df1c70d6681c8daf4a3c86dfac159fcd98a73c4620c4fbe2be6caab1f30a34c7de0ad88ab0e81162376d2cde1a2eed1c32eff5f917ca369870930a51f8e818f1 + languageName: node + linkType: hard + +"@eslint/js@npm:^9.35.0": version: 9.39.4 resolution: "@eslint/js@npm:9.39.4" checksum: 10c0/5aa7dea2cbc5decf7f5e3b0c6f86a084ccee0f792d288ca8e839f8bc1b64e03e227068968e49b26096e6f71fd857ab6e42691d1b993826b9a3883f1bdd7a0e46 @@ -2423,6 +2447,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.1.0": + version: 1.7.5 + resolution: "@floating-ui/core@npm:1.7.5" + dependencies: + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.7.4": version: 1.7.4 resolution: "@floating-ui/core@npm:1.7.4" @@ -2442,6 +2475,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:~1.1.1": + version: 1.1.1 + resolution: "@floating-ui/dom@npm:1.1.1" + dependencies: + "@floating-ui/core": "npm:^1.1.0" + checksum: 10c0/2e508c834fed7567fc496ee946d092056487958330fd5260768536282eb19e990087ddbfe60a39cfe7ed87f8ec0741cc11fa33505c478238d78515a27dd69b6d + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^2.1.7": version: 2.1.7 resolution: "@floating-ui/react-dom@npm:2.1.7" @@ -2468,10 +2510,23 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.10": - version: 0.2.10 - resolution: "@floating-ui/utils@npm:0.2.10" - checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4 +"@floating-ui/utils@npm:^0.2.10, @floating-ui/utils@npm:^0.2.11": + version: 0.2.11 + resolution: "@floating-ui/utils@npm:0.2.11" + checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3 + languageName: node + linkType: hard + +"@gerrit0/mini-shiki@npm:^3.17.0": + version: 3.21.0 + resolution: "@gerrit0/mini-shiki@npm:3.21.0" + dependencies: + "@shikijs/engine-oniguruma": "npm:^3.21.0" + "@shikijs/langs": "npm:^3.21.0" + "@shikijs/themes": "npm:^3.21.0" + "@shikijs/types": "npm:^3.21.0" + "@shikijs/vscode-textmate": "npm:^10.0.2" + checksum: 10c0/4045d19854abfa4515381a04af07096c1de07471b029ee090375652d0199ed3fed6165a22bd9f8e8250c609124d8c05f5d4604eb6de87cf13513aa89cfb8d14e languageName: node linkType: hard @@ -2560,7 +2615,7 @@ __metadata: languageName: node linkType: hard -"@iconify/utils@npm:^3.0.2, @iconify/utils@npm:^3.1.0": +"@iconify/utils@npm:^3.0.1, @iconify/utils@npm:^3.1.0": version: 3.1.0 resolution: "@iconify/utils@npm:3.1.0" dependencies: @@ -2657,13 +2712,6 @@ __metadata: languageName: node linkType: hard -"@joeychenofficial/alt-ergo-modified@npm:^2.4.0": - version: 2.4.0 - resolution: "@joeychenofficial/alt-ergo-modified@npm:2.4.0" - checksum: 10c0/b6587dac0a76331602984ed61e482c2f7af2920d7069c873024bbc7575809ef440c7b91a7fb9a144b54abe98dd1d38a942e57f4476a0736a661c79bd66c176fa - languageName: node - linkType: hard - "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.12 resolution: "@jridgewell/gen-mapping@npm:0.3.12" @@ -2767,12 +2815,12 @@ __metadata: languageName: node linkType: hard -"@mermaid-js/parser@npm:^1.1.0": - version: 1.1.0 - resolution: "@mermaid-js/parser@npm:1.1.0" +"@mermaid-js/parser@npm:^1.0.0": + version: 1.0.0 + resolution: "@mermaid-js/parser@npm:1.0.0" dependencies: langium: "npm:^4.0.0" - checksum: 10c0/449e594a1f9ff1e24da7eb66b7d85694f1c48bdf1d27e798e0e182659e907fd3438a69bf30754807a8a8e286ab3173fa1b8e43ee102be31e30ee7632b9bc1d03 + checksum: 10c0/e311a0981d31984614eee25101c695e950cb1197befc51296d8735390433de59dc6e4a48c3cce1e23a1279b6baf60f9f724d529b741c0c86cdcb09afe3e5e046 languageName: node linkType: hard @@ -2799,13 +2847,6 @@ __metadata: languageName: node linkType: hard -"@nodable/entities@npm:^2.1.0": - version: 2.1.0 - resolution: "@nodable/entities@npm:2.1.0" - checksum: 10c0/5a4cba2b61a5b6c726328b18b1de6d033cae4a658a118644bf31e0bcbda126ea7b69385043dc556cf1ed859b9ca220e82b81b5e5c48ef1b519fb8ec104575dee - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3651,15 +3692,16 @@ __metadata: languageName: node linkType: hard -"@shikijs/core@npm:3.23.0": - version: 3.23.0 - resolution: "@shikijs/core@npm:3.23.0" +"@shikijs/core@npm:4.0.2": + version: 4.0.2 + resolution: "@shikijs/core@npm:4.0.2" dependencies: - "@shikijs/types": "npm:3.23.0" + "@shikijs/primitive": "npm:4.0.2" + "@shikijs/types": "npm:4.0.2" "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" hast-util-to-html: "npm:^9.0.5" - checksum: 10c0/c595f1f2ec09cab102d2b3e03a3c64bfaace5fe52760cfbcced961d3e16571aa646c7e6f85b3d2d0242efd2c832ce4d150b5f1b7332982c17cfbe72f32bd0850 + checksum: 10c0/0a8c56d50b2f67334e5e128b89f1e85844f60ae1dfbd4171e6e92242398678f6eaf91cd02f8418ac4abf4d81774e0ec9a83274a2e3f609c410e577520eadb0f5 languageName: node linkType: hard @@ -3674,17 +3716,6 @@ __metadata: languageName: node linkType: hard -"@shikijs/engine-javascript@npm:3.23.0": - version: 3.23.0 - resolution: "@shikijs/engine-javascript@npm:3.23.0" - dependencies: - "@shikijs/types": "npm:3.23.0" - "@shikijs/vscode-textmate": "npm:^10.0.2" - oniguruma-to-es: "npm:^4.3.4" - checksum: 10c0/884ebb7f66312c9f43e71fb33a3ac0e52f925fc6932de9f68f1bf171019c011c988a4bc0217212589985b1e1bc49452ed67eacbf3d74200b4a3725f11fd8ad98 - languageName: node - linkType: hard - "@shikijs/engine-oniguruma@npm:2.5.0": version: 2.5.0 resolution: "@shikijs/engine-oniguruma@npm:2.5.0" @@ -3695,7 +3726,17 @@ __metadata: languageName: node linkType: hard -"@shikijs/engine-oniguruma@npm:3.23.0, @shikijs/engine-oniguruma@npm:^3.23.0": +"@shikijs/engine-oniguruma@npm:^3.21.0": + version: 3.22.0 + resolution: "@shikijs/engine-oniguruma@npm:3.22.0" + dependencies: + "@shikijs/types": "npm:3.22.0" + "@shikijs/vscode-textmate": "npm:^10.0.2" + checksum: 10c0/21007cc1f2c714f37a53e163e1d604e6696d310f9e252970a828fe5450e4daa9f1f369b7ceffd1cb9cde348d9ca17e8a4d14180749ac052c74d104cab86834ea + languageName: node + linkType: hard + +"@shikijs/engine-oniguruma@npm:^3.23.0": version: 3.23.0 resolution: "@shikijs/engine-oniguruma@npm:3.23.0" dependencies: @@ -3714,7 +3755,16 @@ __metadata: languageName: node linkType: hard -"@shikijs/langs@npm:3.23.0, @shikijs/langs@npm:^3.23.0": +"@shikijs/langs@npm:^3.21.0": + version: 3.22.0 + resolution: "@shikijs/langs@npm:3.22.0" + dependencies: + "@shikijs/types": "npm:3.22.0" + checksum: 10c0/68bb7b10a4b8d78540d0518b80b4c57e42ac232e84a5f74a91d6335de80af730008cf269b4c3da46a2fd3c4a59cd427ab1e6f5934c884335f9f648f8c0c0a912 + languageName: node + linkType: hard + +"@shikijs/langs@npm:^3.23.0": version: 3.23.0 resolution: "@shikijs/langs@npm:3.23.0" dependencies: @@ -3723,7 +3773,18 @@ __metadata: languageName: node linkType: hard -"@shikijs/themes@npm:2.5.0": +"@shikijs/primitive@npm:4.0.2": + version: 4.0.2 + resolution: "@shikijs/primitive@npm:4.0.2" + dependencies: + "@shikijs/types": "npm:4.0.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.4" + checksum: 10c0/7173967ba705ccf3b72eaff8d7937f914f230446a92fead0341f672dc5f9309906b24728ce5b8d1ef4aae8f64eb12f40ea19b3570d7609cb3b8b45f4c19d2144 + languageName: node + linkType: hard + +"@shikijs/themes@npm:2.5.0, @shikijs/themes@npm:^2.1.0": version: 2.5.0 resolution: "@shikijs/themes@npm:2.5.0" dependencies: @@ -3732,7 +3793,16 @@ __metadata: languageName: node linkType: hard -"@shikijs/themes@npm:3.23.0, @shikijs/themes@npm:^3.23.0": +"@shikijs/themes@npm:^3.21.0": + version: 3.22.0 + resolution: "@shikijs/themes@npm:3.22.0" + dependencies: + "@shikijs/types": "npm:3.22.0" + checksum: 10c0/f662648e346e0133d84dee058f24db6434eb7e511ffe8e34e9632f1168d46b219fbddcca245166f98200b13549fc3256baf8d2a0df7c23e856c9933c0bd444f9 + languageName: node + linkType: hard + +"@shikijs/themes@npm:^3.23.0": version: 3.23.0 resolution: "@shikijs/themes@npm:3.23.0" dependencies: @@ -3751,6 +3821,19 @@ __metadata: languageName: node linkType: hard +"@shikijs/twoslash@npm:": + version: 4.0.2 + resolution: "@shikijs/twoslash@npm:4.0.2" + dependencies: + "@shikijs/core": "npm:4.0.2" + "@shikijs/types": "npm:4.0.2" + twoslash: "npm:^0.3.6" + peerDependencies: + typescript: ">=5.5.0" + checksum: 10c0/a3bec3f3b8596cb2d189a1001ff3a6721b961deca0b321e0b9892381e0bb0ecb5032d265b4e82da9b82fe78d2c8efa0e1d940e38134cb66cbaa098909094ecd9 + languageName: node + linkType: hard + "@shikijs/types@npm:2.5.0, @shikijs/types@npm:^2.1.0": version: 2.5.0 resolution: "@shikijs/types@npm:2.5.0" @@ -3761,6 +3844,16 @@ __metadata: languageName: node linkType: hard +"@shikijs/types@npm:3.22.0, @shikijs/types@npm:^3.21.0": + version: 3.22.0 + resolution: "@shikijs/types@npm:3.22.0" + dependencies: + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.4" + checksum: 10c0/68e5bb1827609fc026cba5a88442f41dd948f68fc4f23de0912ef2498944116471b543a5f40ab4ff2c2056399873c755fe717185fd4f8c928002fba934bd3a7b + languageName: node + linkType: hard + "@shikijs/types@npm:3.23.0, @shikijs/types@npm:^3.23.0": version: 3.23.0 resolution: "@shikijs/types@npm:3.23.0" @@ -3771,6 +3864,33 @@ __metadata: languageName: node linkType: hard +"@shikijs/types@npm:4.0.2": + version: 4.0.2 + resolution: "@shikijs/types@npm:4.0.2" + dependencies: + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.4" + checksum: 10c0/9df16cf9988fef0e8ac6e438bc90805ccea707700d169e8e16ab87617d14fb2eeac2a7ead310aa88398c04d1dde95f877c1b695567578e40e6845bce2f65fc73 + languageName: node + linkType: hard + +"@shikijs/vitepress-twoslash@npm:^2.1.0": + version: 2.5.0 + resolution: "@shikijs/vitepress-twoslash@npm:2.5.0" + dependencies: + "@shikijs/twoslash": "npm:" + floating-vue: "npm:^5.2.2" + mdast-util-from-markdown: "npm:^2.0.2" + mdast-util-gfm: "npm:^3.1.0" + mdast-util-to-hast: "npm:^13.2.0" + shiki: "npm:2.5.0" + twoslash: "npm:^0.2.12" + twoslash-vue: "npm:^0.2.12" + vue: "npm:^3.5.13" + checksum: 10c0/1d5193e0732ba272fa581c467b6903957cd40752cd65550ee70fc2aed3cbdda1db6e851207a13aa021e760f687bf6039d0e2ac1a7b160316806d12153f8f599a + languageName: node + linkType: hard + "@shikijs/vscode-textmate@npm:^10.0.2": version: 10.0.2 resolution: "@shikijs/vscode-textmate@npm:10.0.2" @@ -3868,7 +3988,8 @@ __metadata: resolution: "@sourceacademy/bundle-binary_tree@workspace:src/bundles/binary_tree" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + "@sourceacademy/modules-lib": "workspace:^" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -3878,6 +3999,7 @@ __metadata: resolution: "@sourceacademy/bundle-communication@workspace:src/bundles/communication" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@types/uniqid": "npm:^5.3.4" mqtt: "npm:^4.3.7" os: "npm:^0.1.2" @@ -3891,6 +4013,8 @@ __metadata: resolution: "@sourceacademy/bundle-copy_gc@workspace:src/bundles/copy_gc" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" + es-toolkit: "npm:^1.44.0" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -3904,7 +4028,7 @@ __metadata: "@jscad/stl-serializer": "npm:2.1.11" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" save-file: "npm:^2.3.1" typescript: "npm:^6.0.2" languageName: unknown @@ -3918,7 +4042,7 @@ __metadata: "@sourceacademy/modules-lib": "workspace:^" es-toolkit: "npm:^1.44.0" gl-matrix: "npm:^3.3.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -3928,17 +4052,18 @@ __metadata: resolution: "@sourceacademy/bundle-game@workspace:src/bundles/game" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" phaser: "npm:^3.54.0" typescript: "npm:^6.0.2" languageName: unknown linkType: soft -"@sourceacademy/bundle-mark_sweep@workspace:src/bundles/mark_sweep": +"@sourceacademy/bundle-mark_sweep@workspace:^, @sourceacademy/bundle-mark_sweep@workspace:src/bundles/mark_sweep": version: 0.0.0-use.local resolution: "@sourceacademy/bundle-mark_sweep@workspace:src/bundles/mark_sweep" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + es-toolkit: "npm:^1.44.0" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -3948,7 +4073,8 @@ __metadata: resolution: "@sourceacademy/bundle-midi@workspace:src/bundles/midi" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + "@sourceacademy/modules-lib": "workspace:^" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -3958,6 +4084,7 @@ __metadata: resolution: "@sourceacademy/bundle-nbody@workspace:src/bundles/nbody" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@types/plotly.js": "npm:^2.35.4" "@types/three": "npm:^0.183.0" nbody: "npm:^0.2.0" @@ -3995,13 +4122,14 @@ __metadata: resolution: "@sourceacademy/bundle-pix_n_flix@workspace:src/bundles/pix_n_flix" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" - "@vitest/browser-playwright": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" playwright: "npm:^1.55.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" languageName: unknown linkType: soft @@ -4013,18 +4141,20 @@ __metadata: "@sourceacademy/bundle-curve": "workspace:^" "@sourceacademy/bundle-sound": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@types/plotly.js": "npm:^3.0.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" plotly.js-dist: "npm:^3.0.0" typescript: "npm:^6.0.2" languageName: unknown linkType: soft -"@sourceacademy/bundle-repeat@workspace:src/bundles/repeat": +"@sourceacademy/bundle-repeat@workspace:^, @sourceacademy/bundle-repeat@workspace:src/bundles/repeat": version: 0.0.0-use.local resolution: "@sourceacademy/bundle-repeat@workspace:src/bundles/repeat" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4034,7 +4164,8 @@ __metadata: resolution: "@sourceacademy/bundle-repl@workspace:src/bundles/repl" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + "@sourceacademy/modules-lib": "workspace:^" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4048,7 +4179,7 @@ __metadata: "@sourceacademy/modules-lib": "workspace:^" "@types/three": "npm:^0.183.0" es-toolkit: "npm:^1.44.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" three: "npm:^0.183.0" typescript: "npm:^6.0.2" languageName: unknown @@ -4058,11 +4189,12 @@ __metadata: version: 0.0.0-use.local resolution: "@sourceacademy/bundle-rune@workspace:src/bundles/rune" dependencies: + "@sourceacademy/bundle-repeat": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" es-toolkit: "npm:^1.44.0" gl-matrix: "npm:^3.3.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4072,6 +4204,7 @@ __metadata: resolution: "@sourceacademy/bundle-rune_in_words@workspace:src/bundles/rune_in_words" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4092,7 +4225,7 @@ __metadata: "@sourceacademy/bundle-midi": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4102,7 +4235,7 @@ __metadata: resolution: "@sourceacademy/bundle-sound_matrix@workspace:src/bundles/sound_matrix" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4113,7 +4246,8 @@ __metadata: dependencies: "@sourceacademy/bundle-midi": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" - js-slang: "npm:^1.0.85" + "@sourceacademy/modules-lib": "workspace:^" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4123,8 +4257,9 @@ __metadata: resolution: "@sourceacademy/bundle-unittest@workspace:src/bundles/unittest" dependencies: "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" es-toolkit: "npm:^1.44.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" languageName: unknown linkType: soft @@ -4136,6 +4271,7 @@ __metadata: "@blueprintjs/core": "npm:^6.0.0" "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" "@types/react-dom": "npm:^19.0.0" react: "npm:^19.0.0" @@ -4161,10 +4297,11 @@ __metadata: dependencies: "@eslint/markdown": "npm:^7.5.1" "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@sourceacademy/modules-repotools": "workspace:^" "@stylistic/eslint-plugin": "npm:^5.10.0" - "@typescript-eslint/rule-tester": "npm:^8.58.0" - "@typescript-eslint/utils": "npm:^8.58.0" + "@typescript-eslint/rule-tester": "npm:^8.58.2" + "@typescript-eslint/utils": "npm:^8.58.2" "@vitest/eslint-plugin": "npm:^1.6.14" eslint: "npm:^9.35.0" eslint-plugin-import: "npm:^2.32.0" @@ -4172,8 +4309,9 @@ __metadata: eslint-plugin-react: "npm:^7.37.4" eslint-plugin-react-hooks: "npm:^7.0.1" globals: "npm:^17.0.0" + js-slang: "npm:^1.0.92" typescript: "npm:^6.0.2" - typescript-eslint: "npm:^8.58.0" + typescript-eslint: "npm:^8.58.2" peerDependencies: "@eslint/markdown": ^7.0.0 "@stylistic/eslint-plugin": ^5.10.0 @@ -4192,12 +4330,12 @@ __metadata: version: 0.0.0-use.local resolution: "@sourceacademy/markdown-plugin-directory-tree@workspace:lib/markdown-tree" dependencies: + "@shikijs/themes": "npm:^2.1.0" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-repotools": "workspace:^" "@types/markdown-it": "npm:^14.1.2" es-toolkit: "npm:^1.44.0" - shiki: "npm:^3.15.0" - tm-themes: "npm:^1.10.12" + shiki: "npm:^2.1.0" typescript: "npm:^6.0.2" yaml: "npm:^2.8.0" peerDependencies: @@ -4216,8 +4354,8 @@ __metadata: "@types/http-server": "npm:^0.12.4" "@types/node": "npm:^24.0.0" "@vitejs/plugin-react": "npm:^6.0.1" - "@vitest/browser-playwright": "npm:4.1.4" - "@vitest/coverage-v8": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" + "@vitest/coverage-v8": "npm:4.1.5" acorn: "npm:^8.8.1" acorn-typescript: "npm:^1.4.13" astring: "npm:^1.8.6" @@ -4231,7 +4369,7 @@ __metadata: typedoc: "npm:^0.28.18" typescript: "npm:^6.0.2" vite: "npm:^8.0.5" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" bin: buildtools: ./dist/index.js languageName: unknown @@ -4242,19 +4380,19 @@ __metadata: resolution: "@sourceacademy/modules-devserver@workspace:devserver" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@commander-js/extra-typings": "npm:^14.0.0" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" "@types/react-dom": "npm:^19.0.0" "@vitejs/plugin-react": "npm:^6.0.1" - "@vitest/browser-playwright": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" ace-builds: "npm:^1.25.1" classnames: "npm:^2.3.1" commander: "npm:^14.0.0" + es-toolkit: "npm:^1.44.0" eslint: "npm:^9.35.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" playwright: "npm:^1.55.1" re-resizable: "npm:^6.9.11" react: "npm:^19.0.0" @@ -4264,10 +4402,8 @@ __metadata: typescript: "npm:^6.0.2" vite: "npm:^8.0.5" vite-plugin-node-polyfills: "npm:^0.26.0" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" - peerDependencies: - es-toolkit: ^1.44.0 bin: devserver: ./bin.js languageName: unknown @@ -4277,16 +4413,21 @@ __metadata: version: 0.0.0-use.local resolution: "@sourceacademy/modules-docserver@workspace:docs" dependencies: + "@shikijs/vitepress-twoslash": "npm:^2.1.0" + "@sourceacademy/bundle-rune": "workspace:^" + "@sourceacademy/bundle-sound": "workspace:^" "@sourceacademy/lint-plugin": "workspace:^" "@sourceacademy/markdown-plugin-directory-tree": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" + "@types/node": "npm:^24.0.0" cspell: "npm:^9.2.1" + js-slang: "npm:^1.0.92" mermaid: "npm:^11.10.0" typescript: "npm:^6.0.2" - vitepress: "npm:^1.6.3" - vitepress-plugin-group-icons: "npm:^1.6.5" + vitepress: "npm:^1.6.4" + vitepress-plugin-group-icons: "npm:^1.7.5" vitepress-plugin-mermaid: "npm:^2.0.17" - vitepress-sidebar: "npm:^1.31.1" + vitepress-sidebar: "npm:^1.33.1" languageName: unknown linkType: soft @@ -4302,7 +4443,7 @@ __metadata: es-toolkit: "npm:^1.44.0" snyk-nodejs-lockfile-parser: "npm:^2.4.2" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" languageName: unknown linkType: soft @@ -4311,15 +4452,14 @@ __metadata: resolution: "@sourceacademy/modules-lib@workspace:lib/modules-lib" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/modules-buildtools": "workspace:^" "@types/react": "npm:^19.0.0" "@types/react-dom": "npm:^19.0.0" "@vitejs/plugin-react": "npm:^6.0.1" - "@vitest/browser-playwright": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" es-toolkit: "npm:^1.44.0" eslint: "npm:^9.35.0" - js-slang: "npm:^1.0.85" + js-slang: "npm:^1.0.92" playwright: "npm:^1.55.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" @@ -4328,7 +4468,7 @@ __metadata: typedoc-plugin-markdown: "npm:^4.7.0" typedoc-plugin-rename-defaults: "npm:^0.7.3" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" languageName: unknown linkType: soft @@ -4340,15 +4480,15 @@ __metadata: "@commander-js/extra-typings": "npm:^14.0.0" "@types/node": "npm:^24.0.0" "@vitejs/plugin-react": "npm:^6.0.1" - "@vitest/browser-playwright": "npm:4.1.4" - "@vitest/coverage-v8": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" + "@vitest/coverage-v8": "npm:4.1.5" chalk: "npm:^5.0.1" commander: "npm:^14.0.0" es-toolkit: "npm:^1.44.0" esbuild: "npm:^0.28.0" jsonschema: "npm:^1.5.0" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" peerDependencies: "@vitejs/plugin-react": "*" @@ -4364,14 +4504,16 @@ __metadata: "@eslint/markdown": "npm:^7.5.1" "@sourceacademy/lint-plugin": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" + "@sourceacademy/modules-lib": "workspace:^" "@sourceacademy/modules-repotools": "workspace:^" "@sourceacademy/vitest-reporter": "workspace:^" "@stylistic/eslint-plugin": "npm:^5.10.0" "@types/node": "npm:^24.0.0" "@types/react": "npm:^19.0.0" "@types/react-dom": "npm:^19.0.0" - "@vitest/coverage-v8": "npm:4.1.4" + "@vitest/coverage-v8": "npm:4.1.5" "@vitest/eslint-plugin": "npm:^1.6.14" + "@vitest/ui": "npm:4.1.5" "@yarnpkg/types": "npm:^4.0.1" esbuild: "npm:^0.28.0" eslint: "npm:^9.35.0" @@ -4388,14 +4530,14 @@ __metadata: react: "npm:^19.0.0" react-dom: "npm:^19.0.0" typescript: "npm:^6.0.2" - typescript-eslint: "npm:^8.58.0" - vitest: "npm:4.1.4" + typescript-eslint: "npm:^8.58.2" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" peerDependencies: "@blueprintjs/core": ^6.0.0 "@blueprintjs/icons": ^6.0.0 es-toolkit: ^1.44.0 - js-slang: ^1.0.85 + js-slang: ^1.0.92 react: ^19.0.0 react-dom: ^19.0.0 languageName: unknown @@ -4406,7 +4548,6 @@ __metadata: resolution: "@sourceacademy/tab-ArcadeTwod@workspace:src/tabs/ArcadeTwod" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" @@ -4439,7 +4580,6 @@ __metadata: resolution: "@sourceacademy/tab-Csg@workspace:src/tabs/Csg" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-csg": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" @@ -4456,19 +4596,18 @@ __metadata: resolution: "@sourceacademy/tab-Curve@workspace:src/tabs/Curve" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-curve": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" "@types/react-dom": "npm:^19.0.0" - "@vitest/browser-playwright": "npm:4.1.4" - "@vitest/coverage-v8": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" + "@vitest/coverage-v8": "npm:4.1.5" playwright: "npm:^1.55.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" languageName: unknown linkType: soft @@ -4491,6 +4630,7 @@ __metadata: resolution: "@sourceacademy/tab-MarkSweep@workspace:src/tabs/MarkSweep" dependencies: "@blueprintjs/core": "npm:^6.0.0" + "@sourceacademy/bundle-mark_sweep": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" @@ -4505,7 +4645,6 @@ __metadata: resolution: "@sourceacademy/tab-Nbody@workspace:src/tabs/Nbody" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-nbody": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" @@ -4541,7 +4680,6 @@ __metadata: resolution: "@sourceacademy/tab-Physics2D@workspace:src/tabs/Physics2D" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@box2d/debug-draw": "npm:^0.10.0" "@sourceacademy/bundle-physics_2d": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" @@ -4558,7 +4696,6 @@ __metadata: resolution: "@sourceacademy/tab-Pixnflix@workspace:src/tabs/Pixnflix" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-pix_n_flix": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" @@ -4601,13 +4738,12 @@ __metadata: resolution: "@sourceacademy/tab-Repl@workspace:src/tabs/Repl" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-repl": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" - "@types/react-dom": "npm:^19.0.0" ace-builds: "npm:^1.25.1" + es-toolkit: "npm:^1.44.0" react: "npm:^19.0.0" react-ace: "npm:^14.0.0" react-dom: "npm:^19.0.0" @@ -4639,13 +4775,13 @@ __metadata: "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" "@types/react": "npm:^19.0.0" - "@vitest/browser-playwright": "npm:4.1.4" + "@vitest/browser-playwright": "npm:4.1.5" es-toolkit: "npm:^1.44.0" playwright: "npm:^1.55.1" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" vitest-browser-react: "npm:^2.1.0" languageName: unknown linkType: soft @@ -4715,7 +4851,6 @@ __metadata: resolution: "@sourceacademy/tab-UnityAcademy@workspace:src/tabs/UnityAcademy" dependencies: "@blueprintjs/core": "npm:^6.0.0" - "@blueprintjs/icons": "npm:^6.0.0" "@sourceacademy/bundle-unity_academy": "workspace:^" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-lib": "workspace:^" @@ -4733,11 +4868,11 @@ __metadata: dependencies: "@types/istanbul-lib-report": "npm:^3.0.3" "@types/node": "npm:^24.0.0" - "@vitest/coverage-v8": "npm:4.1.4" + "@vitest/coverage-v8": "npm:4.1.5" esbuild: "npm:^0.28.0" istanbul-lib-report: "npm:^3.0.1" typescript: "npm:^6.0.2" - vitest: "npm:4.1.4" + vitest: "npm:4.1.5" languageName: unknown linkType: soft @@ -5194,7 +5329,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": +"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -5338,20 +5473,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.6.0 - resolution: "@types/node@npm:25.6.0" + version: 25.3.3 + resolution: "@types/node@npm:25.3.3" dependencies: - undici-types: "npm:~7.19.0" - checksum: 10c0/d2d2015630ff098a201407f55f5077a20270ae4f465c739b40865cd9933b91b9c5d2b85568eadaf3db0801b91e267333ca7eb39f007428b173d1cdab4b339ac5 + undici-types: "npm:~7.18.0" + checksum: 10c0/63e1d3816a9f4a706ab5d588d18cb98aa824b97748ff585537d327528e9438f58f69f45c7762e7cd3a1ab32c1619f551aabe8075d13172f9273cf10f6d83ab91 languageName: node linkType: hard "@types/node@npm:^22.0.0": - version: 22.19.17 - resolution: "@types/node@npm:22.19.17" + version: 22.19.10 + resolution: "@types/node@npm:22.19.10" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/b66c484c0a9f6d88b1ef360b0f487717234ee1a482cb2551ff73d9f3c43a42a777daf4c8a5eee970960728f8fe1f3877d3d8c6ffabcbca74cb401a59db700fa4 + checksum: 10c0/fb1484f68c259abb73b018e6a521dba2ffe7846128d3aacc7fba5672f354ddb364cb4c81b5a0e686cf58ea0067df4d68d4159f668c864238a0c3fe7fee603341 languageName: node linkType: hard @@ -5365,16 +5500,16 @@ __metadata: linkType: hard "@types/plotly.js@npm:^2.35.4": - version: 2.35.14 - resolution: "@types/plotly.js@npm:2.35.14" - checksum: 10c0/219fe2b49dfc47ba63dec63c2f6144de092da48065e2885c5d9276f992a760f49fb6733677f0f9b0255a2122cc3544a71671ae8f786dea04e34fc0668779864b + version: 2.35.13 + resolution: "@types/plotly.js@npm:2.35.13" + checksum: 10c0/2fefdca78184462869ea91637f7ba730651b5bf82f8971c5a2445544696c538e4bbcd750e1bf96050736403af4ccf81c6d85504454bb41fd61b5432864117518 languageName: node linkType: hard "@types/plotly.js@npm:^3.0.0": - version: 3.0.10 - resolution: "@types/plotly.js@npm:3.0.10" - checksum: 10c0/72f33ce44df74b53bcde5c0e503d5bf89424bed89791aed030da33c82d787d1d49c40748b764d3c65715a6b28b0c3bc78761a99f576e40fcd2078a9764d19192 + version: 3.0.9 + resolution: "@types/plotly.js@npm:3.0.9" + checksum: 10c0/8f5aa3cc96f6351e978d491356407e69cd417f6fc64df458cb90a5e382515b37e61dff7fe7416ed7d8ad2f3cdbbe1d7c1629d184bcf33e00111c39b3dce0b28e languageName: node linkType: hard @@ -5555,7 +5690,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/rule-tester@npm:^8.58.0": +"@typescript-eslint/rule-tester@npm:^8.58.2": version: 8.58.2 resolution: "@typescript-eslint/rule-tester@npm:8.58.2" dependencies: @@ -5617,20 +5752,27 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.56.0": +"@typescript-eslint/types@npm:8.58.0": version: 8.58.0 resolution: "@typescript-eslint/types@npm:8.58.0" checksum: 10c0/f2fe1321758a04591c20d77caba956ae76b77cff0b976a0224b37077d80b1ebd826874d15ec79c3a3b7d57ee5679e5d10756db1b082bde3d51addbd3a8431d38 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.58.2, @typescript-eslint/types@npm:^8.58.0, @typescript-eslint/types@npm:^8.58.2": +"@typescript-eslint/types@npm:8.58.2, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.58.2": version: 8.58.2 resolution: "@typescript-eslint/types@npm:8.58.2" checksum: 10c0/6707c1a2ec921b9ae441b35d9cb4e0af11673a67e332a366e3033f1d558ff5db4f39021872c207fb361841670e9ffcc4981f19eb21e4495a3a031d02015637a7 languageName: node linkType: hard +"@typescript-eslint/types@npm:^8.54.0": + version: 8.56.1 + resolution: "@typescript-eslint/types@npm:8.56.1" + checksum: 10c0/e5a0318abddf0c4f98da3039cb10b3c0601c8601f7a9f7043630f0d622dabfe83a4cd833545ad3531fc846e46ca2874377277b392c2490dffec279d9242d827b + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.58.2": version: 8.58.2 resolution: "@typescript-eslint/typescript-estree@npm:8.58.2" @@ -5650,7 +5792,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.58.2, @typescript-eslint/utils@npm:^8.58.0": +"@typescript-eslint/utils@npm:8.58.2, @typescript-eslint/utils@npm:^8.58.0, @typescript-eslint/utils@npm:^8.58.2": version: 8.58.2 resolution: "@typescript-eslint/utils@npm:8.58.2" dependencies: @@ -5685,14 +5827,14 @@ __metadata: languageName: node linkType: hard -"@typescript/vfs@npm:^1.5.2": - version: 1.6.1 - resolution: "@typescript/vfs@npm:1.6.1" +"@typescript/vfs@npm:^1.5.2, @typescript/vfs@npm:^1.6.0, @typescript/vfs@npm:^1.6.2": + version: 1.6.4 + resolution: "@typescript/vfs@npm:1.6.4" dependencies: - debug: "npm:^4.1.1" + debug: "npm:^4.4.3" peerDependencies: typescript: "*" - checksum: 10c0/3878686aff4bf26813dad9242aa8e01c5c9734f4d37f31035f93e9c8b850f15ec6a4480f04cf3a3a1cbf78a4e796ae1be5d6c54f7f7c91556eafee913a8d0da4 + checksum: 10c0/acb9de42f23fda3f75e3d7900ba106ef323a2f4e7cf0e4a94dbb457e26d353ca63bb35193ed1a32fc8733f3ae59099612b290b06bd6221694861beb9bfb62f62 languageName: node linkType: hard @@ -5849,21 +5991,6 @@ __metadata: languageName: node linkType: hard -"@upsetjs/venn.js@npm:^2.0.0": - version: 2.0.0 - resolution: "@upsetjs/venn.js@npm:2.0.0" - dependencies: - d3-selection: "npm:^3.0.0" - d3-transition: "npm:^3.0.1" - dependenciesMeta: - d3-selection: - optional: true - d3-transition: - optional: true - checksum: 10c0/b12014d94708ab4df7f5a4b6205c6f23ff235cca2ffe91df3314862b109b826e52f9020c2a2f7527d3712d21c578d6db9cdb60ce46a528739cc18e58d111f724 - languageName: node - linkType: hard - "@vitejs/plugin-react@npm:^6.0.1": version: 6.0.1 resolution: "@vitejs/plugin-react@npm:6.0.1" @@ -5892,47 +6019,47 @@ __metadata: languageName: node linkType: hard -"@vitest/browser-playwright@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/browser-playwright@npm:4.1.4" +"@vitest/browser-playwright@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/browser-playwright@npm:4.1.5" dependencies: - "@vitest/browser": "npm:4.1.4" - "@vitest/mocker": "npm:4.1.4" + "@vitest/browser": "npm:4.1.5" + "@vitest/mocker": "npm:4.1.5" tinyrainbow: "npm:^3.1.0" peerDependencies: playwright: "*" - vitest: 4.1.4 + vitest: 4.1.5 peerDependenciesMeta: playwright: optional: false - checksum: 10c0/3f2d29631de796d041d43334552b13c2adf3e5ab04b9a60ba28e666b3b4a6628cc57e7a8b4949f4ed4a536a796908079297d9d33b4e90f5b13e2abb45f419c22 + checksum: 10c0/47b0ecc13757e638f7765cb4b3172e817a25249b00bc4e9462f4228b6336c0b2f7bb692ae636373f55c8e9b35d18eaecb03abd5f15b0c42a8351da9a62f23d9f languageName: node linkType: hard -"@vitest/browser@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/browser@npm:4.1.4" +"@vitest/browser@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/browser@npm:4.1.5" dependencies: "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/mocker": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" magic-string: "npm:^0.30.21" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" tinyrainbow: "npm:^3.1.0" ws: "npm:^8.19.0" peerDependencies: - vitest: 4.1.4 - checksum: 10c0/c219c685d5befc2372d7cc80ef4ff78c9e57cb4a1bb74ec25d76596c9ad130d2bf67bc05d7ed1a6a8e0da763cd1c523eb81c95cd623c20f163509d0b36dae1c7 + vitest: 4.1.5 + checksum: 10c0/ea95d100853dd7a1ea9f1b036edfe441688bf5873742341ebf169ab2e32041ab6e21e5f2df918c3c4b9f110265457cdce0c0afa83407617e460a83979ae48e44 languageName: node linkType: hard -"@vitest/coverage-v8@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/coverage-v8@npm:4.1.4" +"@vitest/coverage-v8@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/coverage-v8@npm:4.1.5" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.1.4" + "@vitest/utils": "npm:4.1.5" ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -5942,18 +6069,18 @@ __metadata: std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.1.4 - vitest: 4.1.4 + "@vitest/browser": 4.1.5 + vitest: 4.1.5 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10c0/e128a70b15eeee55ad201b9f2a9d88f3a2ccd630c1518ec8ab1d80a6e7b557d23d426244ce09748c8553a53725137d92696bd1be3bd9349863dd375749988a4a + checksum: 10c0/71bf669cc1714611855caef5e89b4f3e405e410bdb34e4b2f6fbc9dc5e50dd9e09e73068c1750f6bfa03f0cd9209a2b6e03665c3bdbd34e0adff1ca65c482b7b languageName: node linkType: hard "@vitest/eslint-plugin@npm:^1.6.14": - version: 1.6.16 - resolution: "@vitest/eslint-plugin@npm:1.6.16" + version: 1.6.14 + resolution: "@vitest/eslint-plugin@npm:1.6.14" dependencies: "@typescript-eslint/scope-manager": "npm:^8.58.0" "@typescript-eslint/utils": "npm:^8.58.0" @@ -5969,29 +6096,29 @@ __metadata: optional: true vitest: optional: true - checksum: 10c0/b2871ce12b831ea1e1ff8cf01d978d0746aaeac951830880ad966ad2783de4fbe835a1c74c44f5bccca73ca6007174cd0434c2725269cfafdcc6fbb43d4da495 + checksum: 10c0/4721f554cab1c8bfa6d0df7b350e54223ecfd285ba3f51be5db2dafaa6147b26400dd80840114019bd80f584fed6e30180ba70e4b044dbfa7b909b283d241d8e languageName: node linkType: hard -"@vitest/expect@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/expect@npm:4.1.4" +"@vitest/expect@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/expect@npm:4.1.5" dependencies: "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/spy": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" chai: "npm:^6.2.2" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/99b53a931366ddc985f26528495ec991fa2ce64018b00a56f989c322553045c5adf17e091eb7a12d786246712f84d36fc88e9d26c852538ff4dd5a6f9cf98715 + checksum: 10c0/5184682304db471aa20024c1154210ad3d6d590afb61646201ce1a15297259f9a35f92f8fad4435bc8a82135e307ddd27c8495f72417d72d9aa139eb281d9e06 languageName: node linkType: hard -"@vitest/mocker@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/mocker@npm:4.1.4" +"@vitest/mocker@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/mocker@npm:4.1.5" dependencies: - "@vitest/spy": "npm:4.1.4" + "@vitest/spy": "npm:4.1.5" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -6002,56 +6129,89 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/da61ee63743da4bc45df0488c994e284e7059a4005149195744705945d19aeb267c801b1f7d85e71b40f547ff2d5a195175c5d51e8455179c794ce67a019de87 + checksum: 10c0/bcfe97700476130933c7ea33fa670c8d2768a81de5325ce407f901e55c2f66cabbb88a7b6cffb46ddf33dff7d8fc209d769fb298f568e310fbeead9b36f6fdb9 languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/pretty-format@npm:4.1.4" +"@vitest/pretty-format@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/pretty-format@npm:4.1.5" dependencies: tinyrainbow: "npm:^3.1.0" - checksum: 10c0/14a25c5acd02b1d18f9fab01d884658edb9137008d01025273617fb000e36391e4fda1513e94a257f5e611fb09041a0c042d145a90d359c9e810c0044b12763e + checksum: 10c0/42b5e9b75e87c0a884d36bee364e2d07ee45e96f413377737a74993e077d90c3a12aa36743855aee5e4e28b78fae20e3e6de5eef8d5344b9aba2bc1e1d5537a1 languageName: node linkType: hard -"@vitest/runner@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/runner@npm:4.1.4" +"@vitest/runner@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/runner@npm:4.1.5" dependencies: - "@vitest/utils": "npm:4.1.4" + "@vitest/utils": "npm:4.1.5" pathe: "npm:^2.0.3" - checksum: 10c0/a942ecf2e50e4c380f0d269f87272353dc40fe354357e1ecd0c6568fd37202bb86e33db676f4ad6cc5f1ab30937bba0b278d987729b21a0f22e9827f7f577da2 + checksum: 10c0/6a03b313a121155f6dd9e32eeb103c0e12440f586bc4ba1f0d77444e44c6df4652a44443718552037463115635b8378e11f35902d90ce1326f77743219fca056 languageName: node linkType: hard -"@vitest/snapshot@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/snapshot@npm:4.1.4" +"@vitest/snapshot@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/snapshot@npm:4.1.5" dependencies: - "@vitest/pretty-format": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/pretty-format": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/9221df7c097665a204c811184ac2f3b89638ecd115344e703e9c4361dabd2ba80be4710ed20d127817d34227a74f21b90725deaecd4632954b492ad258d4913f + checksum: 10c0/e11bf50d06702331290750a40eaef86078c108df3cd9a52bb1be7b84250048790163f36827525be6a383a4bb1994fc35e6d0c24239a41688b0bb68a1d15d172f + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/spy@npm:4.1.5" + checksum: 10c0/fda6b1ee0a2fec1a152d8041aba7a79744c3876863b244d1ed406d02b36e8ccc997edb2e3963d1027d728d3dc5a33813e11bef53a0a14fc7de4de5e721d0f591 languageName: node linkType: hard -"@vitest/spy@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/spy@npm:4.1.4" - checksum: 10c0/1036591947668845e45515d5b66b2095071609c243d2c987d650c71d0a27418e5de75a8b1ad44b7f45c5d97e71176640f0f49da94b32fb3d11e87cdd009bed26 +"@vitest/ui@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/ui@npm:4.1.5" + dependencies: + "@vitest/utils": "npm:4.1.5" + fflate: "npm:^0.8.2" + flatted: "npm:^3.4.2" + pathe: "npm:^2.0.3" + sirv: "npm:^3.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + vitest: 4.1.5 + checksum: 10c0/2109480d08516abe3350994ebfcb9adc8a2eb4bb1839c48a336d6ff3378eb49a520ad6490f177e60034ed4fcf579206d9ab14995e951cbf6d4c7c792c46fe202 languageName: node linkType: hard -"@vitest/utils@npm:4.1.4": - version: 4.1.4 - resolution: "@vitest/utils@npm:4.1.4" +"@vitest/utils@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/utils@npm:4.1.5" dependencies: - "@vitest/pretty-format": "npm:4.1.4" + "@vitest/pretty-format": "npm:4.1.5" convert-source-map: "npm:^2.0.0" tinyrainbow: "npm:^3.1.0" - checksum: 10c0/7f81db08e5a8db1e83a37a8d64db011ae3a08b5bcc9aa220a6da428385acb75b11c77b169ab7a9f753529cc25ec11406cff6099b92711fda6291f844fb840a4e + checksum: 10c0/72409717e68018e5fe42fa173cc4eff6def8c35bd52013f86ddb414cd28d73fcc425ac62968e01a52371b3fd5a7a775536283d2f1d64432753f628712a6a4908 + languageName: node + linkType: hard + +"@volar/language-core@npm:~2.4.8": + version: 2.4.28 + resolution: "@volar/language-core@npm:2.4.28" + dependencies: + "@volar/source-map": "npm:2.4.28" + checksum: 10c0/d41f7327fed7fa5301fbf2d8f96753d645a976b21dbbeb869794a780aa6523d1e6bf258242bc3d8ccd37f8e8b98a04fea9574e6f63badc585a8a3c2e068c4a86 + languageName: node + linkType: hard + +"@volar/source-map@npm:2.4.28": + version: 2.4.28 + resolution: "@volar/source-map@npm:2.4.28" + checksum: 10c0/24b0b02c7f66febe47f0bfda4a5ed4beaf949041eddc6325c7478b900faeb071795b696d97a4f326dde47217d06e40b67129300bc544f054772c5cb84c2f254e languageName: node linkType: hard @@ -6068,6 +6228,19 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-core@npm:3.5.32": + version: 3.5.32 + resolution: "@vue/compiler-core@npm:3.5.32" + dependencies: + "@babel/parser": "npm:^7.29.2" + "@vue/shared": "npm:3.5.32" + entities: "npm:^7.0.1" + estree-walker: "npm:^2.0.2" + source-map-js: "npm:^1.2.1" + checksum: 10c0/46133e028609bfe5be21ebf0857b260825cb1aa715e854dab7e2dd26916d9a5b75fbf3dd2add320255b8161973f4db2dc056a653a1bb67296cc81dd0dc5028ff + languageName: node + linkType: hard + "@vue/compiler-dom@npm:3.5.17": version: 3.5.17 resolution: "@vue/compiler-dom@npm:3.5.17" @@ -6078,6 +6251,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-dom@npm:^3.5.0": + version: 3.5.32 + resolution: "@vue/compiler-dom@npm:3.5.32" + dependencies: + "@vue/compiler-core": "npm:3.5.32" + "@vue/shared": "npm:3.5.32" + checksum: 10c0/d061e950240e48ce09500005f58ffe5b09f8627f5c999ecf7f52b9525d527e25fc5f7527e66ce67acbe5464a395ba91ad27a5791223f5d1c7064af3b234eefd4 + languageName: node + linkType: hard + "@vue/compiler-sfc@npm:3.5.17": version: 3.5.17 resolution: "@vue/compiler-sfc@npm:3.5.17" @@ -6105,6 +6288,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-vue2@npm:^2.7.16": + version: 2.7.16 + resolution: "@vue/compiler-vue2@npm:2.7.16" + dependencies: + de-indent: "npm:^1.0.2" + he: "npm:^1.2.0" + checksum: 10c0/c76c3fad770b9a7da40b314116cc9da173da20e5fd68785c8ed8dd8a87d02f239545fa296e16552e040ec86b47bfb18283b39447b250c2e76e479bd6ae475bb3 + languageName: node + linkType: hard + "@vue/devtools-api@npm:^7.7.0": version: 7.7.7 resolution: "@vue/devtools-api@npm:7.7.7" @@ -6138,6 +6331,27 @@ __metadata: languageName: node linkType: hard +"@vue/language-core@npm:~2.1.6": + version: 2.1.10 + resolution: "@vue/language-core@npm:2.1.10" + dependencies: + "@volar/language-core": "npm:~2.4.8" + "@vue/compiler-dom": "npm:^3.5.0" + "@vue/compiler-vue2": "npm:^2.7.16" + "@vue/shared": "npm:^3.5.0" + alien-signals: "npm:^0.2.0" + minimatch: "npm:^9.0.3" + muggle-string: "npm:^0.4.1" + path-browserify: "npm:^1.0.1" + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/9257f1fcbb84749f806cf0926ccc6d5f40bdee51ec3febbd7f72586ddf52db0b11bb8c24dc24b1b3ada8b34d80865b10a0a183c8033b028daab9f77326e44fb6 + languageName: node + linkType: hard + "@vue/reactivity@npm:3.5.17": version: 3.5.17 resolution: "@vue/reactivity@npm:3.5.17" @@ -6181,13 +6395,20 @@ __metadata: languageName: node linkType: hard -"@vue/shared@npm:3.5.17, @vue/shared@npm:^3.5.13": +"@vue/shared@npm:3.5.17": version: 3.5.17 resolution: "@vue/shared@npm:3.5.17" checksum: 10c0/915d8f80d863826531cf6ddefeb52455cbffcbca4d14717472b7765b3142d2ad9900dfce351e90a22e1fe9e2f8fca588421de6e751e1c816ab9e1fdefa3e8a0d languageName: node linkType: hard +"@vue/shared@npm:3.5.32, @vue/shared@npm:^3.5.0, @vue/shared@npm:^3.5.13": + version: 3.5.32 + resolution: "@vue/shared@npm:3.5.32" + checksum: 10c0/5b070d2cc7bd6db8109f3b8826a2ed67f4e9806ad1290d7a9af606afeaa071051c63c00096e4bf5094ae79673e21c637ed637efbe0cf73f6e1b5a3e32eaa73ed + languageName: node + linkType: hard + "@vueuse/core@npm:12.8.2, @vueuse/core@npm:^12.4.0": version: 12.8.2 resolution: "@vueuse/core@npm:12.8.2" @@ -6458,7 +6679,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.5.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2": +"acorn@npm:^8.0.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.5.0, acorn@npm:^8.8.1, acorn@npm:^8.8.2": version: 8.16.0 resolution: "acorn@npm:8.16.0" bin: @@ -6484,7 +6705,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.6, ajv@npm:^6.14.0": +"ajv@npm:^6.12.4, ajv@npm:^6.12.6": version: 6.14.0 resolution: "ajv@npm:6.14.0" dependencies: @@ -6517,6 +6738,13 @@ __metadata: languageName: node linkType: hard +"alien-signals@npm:^0.2.0": + version: 0.2.2 + resolution: "alien-signals@npm:0.2.2" + checksum: 10c0/47adce909e0a12cdd78ed982d82ae2f9b93c7e8e315d57e49b6f9e2734db2c1ec1e2173365d044202b1a8c4085c87161a4311934547cdfacf1ba85b28961fdb6 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -7073,12 +7301,12 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" +"brace-expansion@npm:^2.0.1, brace-expansion@npm:^2.0.2": + version: 2.1.0 + resolution: "brace-expansion@npm:2.1.0" dependencies: balanced-match: "npm:^1.0.0" - checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + checksum: 10c0/439cedf3e23d7993b37919f1d6fdc653ec21a42437ec3e7460bea9ca8b17edf7a24a633273c31d61aa4335877cf29a443f1871814131c87997a1e6223e1f1502 languageName: node linkType: hard @@ -7540,27 +7768,28 @@ __metadata: languageName: node linkType: hard -"chevrotain-allstar@npm:~0.4.1": - version: 0.4.1 - resolution: "chevrotain-allstar@npm:0.4.1" +"chevrotain-allstar@npm:~0.3.1": + version: 0.3.1 + resolution: "chevrotain-allstar@npm:0.3.1" dependencies: lodash-es: "npm:^4.17.21" peerDependencies: - chevrotain: ^12.0.0 - checksum: 10c0/a97b3b71950fe74f3dade0d3b8cc1b996ca5c41a711adaec9e706e554d8f80cb3bf6f0fef7f848cee24bcde1867fb5af9890da9363db54ddefec812f6accb29f + chevrotain: ^11.0.0 + checksum: 10c0/5cadedffd3114eb06b15fd3939bb1aa6c75412dbd737fe302b52c5c24334f9cb01cee8edc1d1067d98ba80dddf971f1d0e94b387de51423fc6cf3c5d8b7ef27a languageName: node linkType: hard -"chevrotain@npm:~12.0.0": - version: 12.0.0 - resolution: "chevrotain@npm:12.0.0" +"chevrotain@npm:~11.1.1": + version: 11.1.2 + resolution: "chevrotain@npm:11.1.2" dependencies: - "@chevrotain/cst-dts-gen": "npm:12.0.0" - "@chevrotain/gast": "npm:12.0.0" - "@chevrotain/regexp-to-ast": "npm:12.0.0" - "@chevrotain/types": "npm:12.0.0" - "@chevrotain/utils": "npm:12.0.0" - checksum: 10c0/c7bf530f59baae21cc7535406addfbb9ea373d89bb4ea1b874d89e7debd232c9bdf60aa5af236fcc0e12a68ea8f88a6638ffbccaadb829a7b300f98d1429c38d + "@chevrotain/cst-dts-gen": "npm:11.1.2" + "@chevrotain/gast": "npm:11.1.2" + "@chevrotain/regexp-to-ast": "npm:11.1.2" + "@chevrotain/types": "npm:11.1.2" + "@chevrotain/utils": "npm:11.1.2" + lodash-es: "npm:4.17.23" + checksum: 10c0/7f0b5780035c582d4c620c81e1fbb58c9f41a69f1c7efdae96819c7bc0928ddb4f046bb8239e71539f383b3b8ce460bd11f44b5fb5107e1d45a0cc91bd6a4198 languageName: node linkType: hard @@ -7714,13 +7943,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.0.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - "commander@npm:^14.0.0, commander@npm:^14.0.3": version: 14.0.3 resolution: "commander@npm:14.0.3" @@ -7735,20 +7957,21 @@ __metadata: languageName: node linkType: hard -"comment-json@npm:^4.6.2": - version: 4.6.2 - resolution: "comment-json@npm:4.6.2" +"comment-json@npm:^4.5.1": + version: 4.5.1 + resolution: "comment-json@npm:4.5.1" dependencies: array-timsort: "npm:^1.0.3" + core-util-is: "npm:^1.0.3" esprima: "npm:^4.0.1" - checksum: 10c0/8965ec6c40612aa0cc66d4324ff5819cf205c997f3a84dd82dffe4e6398449e37bbc5765184bc9149e95d15994f0c2740cee82284828fa1c0f733a669022d3dd + checksum: 10c0/aea59becb413fef2d21ec8f3d58b0dd024c47901c5f77c8436b19cc17f9ead0841b2f40d7a87a9b4061b8c048cd10c3c502e512eb8756ffc9aa58915ba5e4482 languageName: node linkType: hard -"comment-parser@npm:1.4.6": - version: 1.4.6 - resolution: "comment-parser@npm:1.4.6" - checksum: 10c0/10837626fc1cb84531564a5ec145f5818b3830393c09744ebfea4105319824e277bdb60ffcf38f44e165e002909fda835b21e20d032a8f8d068834aaef8af0ca +"comment-parser@npm:1.4.5": + version: 1.4.5 + resolution: "comment-parser@npm:1.4.5" + checksum: 10c0/6a6a74697c79927e3bd42bde9608a471f1a9d4995affbc22fa3364cc42b4017f82ef477431a1558b0b6bef959f9bb6964c01c1bbfc06a58ba1730dec9c423b44 languageName: node linkType: hard @@ -7863,7 +8086,7 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:~1.0.0": +"core-util-is@npm:^1.0.3, core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 @@ -8002,97 +8225,97 @@ __metadata: languageName: node linkType: hard -"cspell-config-lib@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-config-lib@npm:9.8.0" +"cspell-config-lib@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-config-lib@npm:9.7.0" dependencies: - "@cspell/cspell-types": "npm:9.8.0" - comment-json: "npm:^4.6.2" - smol-toml: "npm:^1.6.1" - yaml: "npm:^2.8.3" - checksum: 10c0/45d429df87672c1db585597396f70c1c0edd2ca0cfe74a44201b75248cb4d858fb22a5be831106b7f0fdd03a69cd85bb67aadd41af4837a9877ae5475d2081bb + "@cspell/cspell-types": "npm:9.7.0" + comment-json: "npm:^4.5.1" + smol-toml: "npm:^1.6.0" + yaml: "npm:^2.8.2" + checksum: 10c0/dcd7bc89d811c2276fad77be79cd04326aa5ab54bd6658fcd9e776f2bab943a5417969967548a2b7d3d24ef8f82f0873b6e16769f468430536d9ec842e0a0562 languageName: node linkType: hard -"cspell-dictionary@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-dictionary@npm:9.8.0" +"cspell-dictionary@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-dictionary@npm:9.7.0" dependencies: - "@cspell/cspell-performance-monitor": "npm:9.8.0" - "@cspell/cspell-pipe": "npm:9.8.0" - "@cspell/cspell-types": "npm:9.8.0" - cspell-trie-lib: "npm:9.8.0" + "@cspell/cspell-performance-monitor": "npm:9.7.0" + "@cspell/cspell-pipe": "npm:9.7.0" + "@cspell/cspell-types": "npm:9.7.0" + cspell-trie-lib: "npm:9.7.0" fast-equals: "npm:^6.0.0" - checksum: 10c0/eda488577e23be3d47bc4afb0f0a78cd882211eb827bf947f942be8474934c1c9359f98b72342138bb2b52e6ff2285f44e1e3824692796d35588e13533422703 + checksum: 10c0/2d38712ddfed5a7a563534b43497de750bc172a03abb48e13178c112fd5d6042e7ff219125c250fff8ed62233b8ae9b818bfb16522242b68ab208b59f1cfa8ac languageName: node linkType: hard -"cspell-gitignore@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-gitignore@npm:9.8.0" +"cspell-gitignore@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-gitignore@npm:9.7.0" dependencies: - "@cspell/url": "npm:9.8.0" - cspell-glob: "npm:9.8.0" - cspell-io: "npm:9.8.0" + "@cspell/url": "npm:9.7.0" + cspell-glob: "npm:9.7.0" + cspell-io: "npm:9.7.0" bin: cspell-gitignore: bin.mjs - checksum: 10c0/d01e7cbaf7950b07670c9daec73b26ca3f171dabd554373572f1ea64bebaf57d5f1753fc4744bfed3335a4965f3c56232fd8189e68339eff7b545304ed7a0dd3 + checksum: 10c0/cf21999afd4c5305fa7e7cbb38d42b5dadc4663a2a944d5912200e518ca1ece137726b9d9ab6d792ae9648a24d8808936476b7ce6258d458f4e5ccab9faa4b8b languageName: node linkType: hard -"cspell-glob@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-glob@npm:9.8.0" +"cspell-glob@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-glob@npm:9.7.0" dependencies: - "@cspell/url": "npm:9.8.0" - picomatch: "npm:^4.0.4" - checksum: 10c0/09b7ce77be7d76863e20e72650c02c44a771d5abf3381335d7f7dbcc250173458866deee756c1e431674117efc76d6cbbe0a49a7eecac5bb1cc9705acbdb2d26 + "@cspell/url": "npm:9.7.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/2628051c5f98f3cba19dbc3f59cbdfbc59113042d391602c38e5af0376ea864c35554f55b5a3198915c56e46686fc3c602f6dddcdf9f94a83aff829cee2b4cc9 languageName: node linkType: hard -"cspell-grammar@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-grammar@npm:9.8.0" +"cspell-grammar@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-grammar@npm:9.7.0" dependencies: - "@cspell/cspell-pipe": "npm:9.8.0" - "@cspell/cspell-types": "npm:9.8.0" + "@cspell/cspell-pipe": "npm:9.7.0" + "@cspell/cspell-types": "npm:9.7.0" bin: cspell-grammar: bin.mjs - checksum: 10c0/1259a9127790e71f27ecb551e1ae5cc30eb90af13900daf18c652de09e998798f883e9f72680a7e42d4d5007aa5ec326af3a35d35245cb1b5a2e2014a772ba1d + checksum: 10c0/f13536425d8740eaf259097e001129a81bb7b5705931658a70ab9c3e9eb7884ef8cb97544ad56e2e2b4f46a4af3c61cbecdabdd0bde987b77f6afb0ea49ef3e8 languageName: node linkType: hard -"cspell-io@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-io@npm:9.8.0" +"cspell-io@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-io@npm:9.7.0" dependencies: - "@cspell/cspell-service-bus": "npm:9.8.0" - "@cspell/url": "npm:9.8.0" - checksum: 10c0/24530cddb81c9c4371930138b3d7d6537342adeda6d35c1ca30b54703571f360f3311af40bc5f4bde37c5c2e9ce8108caa59dafa92397404622ebf89c3c1e67b + "@cspell/cspell-service-bus": "npm:9.7.0" + "@cspell/url": "npm:9.7.0" + checksum: 10c0/9adba4ec5b7eca1a6ff7edb1ad7ffb3eb3f1c322e63dfcc0cc81d6a2944c590cbe994f8fd6b2a565ac810cfb6847ba384137fef13a8fd21dd2f840d365b99ea1 languageName: node linkType: hard -"cspell-lib@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-lib@npm:9.8.0" +"cspell-lib@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-lib@npm:9.7.0" dependencies: - "@cspell/cspell-bundled-dicts": "npm:9.8.0" - "@cspell/cspell-performance-monitor": "npm:9.8.0" - "@cspell/cspell-pipe": "npm:9.8.0" - "@cspell/cspell-resolver": "npm:9.8.0" - "@cspell/cspell-types": "npm:9.8.0" - "@cspell/dynamic-import": "npm:9.8.0" - "@cspell/filetypes": "npm:9.8.0" - "@cspell/rpc": "npm:9.8.0" - "@cspell/strong-weak-map": "npm:9.8.0" - "@cspell/url": "npm:9.8.0" + "@cspell/cspell-bundled-dicts": "npm:9.7.0" + "@cspell/cspell-performance-monitor": "npm:9.7.0" + "@cspell/cspell-pipe": "npm:9.7.0" + "@cspell/cspell-resolver": "npm:9.7.0" + "@cspell/cspell-types": "npm:9.7.0" + "@cspell/dynamic-import": "npm:9.7.0" + "@cspell/filetypes": "npm:9.7.0" + "@cspell/rpc": "npm:9.7.0" + "@cspell/strong-weak-map": "npm:9.7.0" + "@cspell/url": "npm:9.7.0" clear-module: "npm:^4.1.2" - cspell-config-lib: "npm:9.8.0" - cspell-dictionary: "npm:9.8.0" - cspell-glob: "npm:9.8.0" - cspell-grammar: "npm:9.8.0" - cspell-io: "npm:9.8.0" - cspell-trie-lib: "npm:9.8.0" + cspell-config-lib: "npm:9.7.0" + cspell-dictionary: "npm:9.7.0" + cspell-glob: "npm:9.7.0" + cspell-grammar: "npm:9.7.0" + cspell-io: "npm:9.7.0" + cspell-trie-lib: "npm:9.7.0" env-paths: "npm:^4.0.0" gensequence: "npm:^8.0.8" import-fresh: "npm:^3.3.1" @@ -8100,48 +8323,48 @@ __metadata: vscode-languageserver-textdocument: "npm:^1.0.12" vscode-uri: "npm:^3.1.0" xdg-basedir: "npm:^5.1.0" - checksum: 10c0/7f2d56dd3c9be3fe10bbacc2bda6dcd831f252ed2ecb2c8b1915b55f89f9f920400e248fd402421e1b2f2e812d60789df5d1570418cc8134ebdd419bb5ffec51 + checksum: 10c0/c0b3b233e68537a88ca72339bca2a7af9e7e3d74837b007fd6bd719efadca2a611a7670baf41f23781dcfa478ad0f0a21c8a8e14bf8857fe456fb16b6f5a78af languageName: node linkType: hard -"cspell-trie-lib@npm:9.8.0": - version: 9.8.0 - resolution: "cspell-trie-lib@npm:9.8.0" +"cspell-trie-lib@npm:9.7.0": + version: 9.7.0 + resolution: "cspell-trie-lib@npm:9.7.0" peerDependencies: - "@cspell/cspell-types": 9.8.0 - checksum: 10c0/2d9a94281a7ae2d5896713aafa650ad75a5f24e8ba18b2c2710b6626753454aae452218b1339e33c33064a7f122baca3d72070ece617112358e2da6884f46e4b + "@cspell/cspell-types": 9.7.0 + checksum: 10c0/6f70dc3c646d0f01d2bff2a6d300c3f998bf37b94cf149cf09eb27b1a72503111c4cc079f1edc2078951571c7ffd69c87dd8ba6644f54f1588e398e66251a406 languageName: node linkType: hard "cspell@npm:^9.2.1": - version: 9.8.0 - resolution: "cspell@npm:9.8.0" - dependencies: - "@cspell/cspell-json-reporter": "npm:9.8.0" - "@cspell/cspell-performance-monitor": "npm:9.8.0" - "@cspell/cspell-pipe": "npm:9.8.0" - "@cspell/cspell-types": "npm:9.8.0" - "@cspell/cspell-worker": "npm:9.8.0" - "@cspell/dynamic-import": "npm:9.8.0" - "@cspell/url": "npm:9.8.0" + version: 9.7.0 + resolution: "cspell@npm:9.7.0" + dependencies: + "@cspell/cspell-json-reporter": "npm:9.7.0" + "@cspell/cspell-performance-monitor": "npm:9.7.0" + "@cspell/cspell-pipe": "npm:9.7.0" + "@cspell/cspell-types": "npm:9.7.0" + "@cspell/cspell-worker": "npm:9.7.0" + "@cspell/dynamic-import": "npm:9.7.0" + "@cspell/url": "npm:9.7.0" ansi-regex: "npm:^6.2.2" chalk: "npm:^5.6.2" chalk-template: "npm:^1.1.2" commander: "npm:^14.0.3" - cspell-config-lib: "npm:9.8.0" - cspell-dictionary: "npm:9.8.0" - cspell-gitignore: "npm:9.8.0" - cspell-glob: "npm:9.8.0" - cspell-io: "npm:9.8.0" - cspell-lib: "npm:9.8.0" + cspell-config-lib: "npm:9.7.0" + cspell-dictionary: "npm:9.7.0" + cspell-gitignore: "npm:9.7.0" + cspell-glob: "npm:9.7.0" + cspell-io: "npm:9.7.0" + cspell-lib: "npm:9.7.0" fast-json-stable-stringify: "npm:^2.1.0" - flatted: "npm:^3.4.2" + flatted: "npm:^3.3.3" semver: "npm:^7.7.4" tinyglobby: "npm:^0.2.15" bin: cspell: bin.mjs cspell-esm: bin.mjs - checksum: 10c0/9fa1f04a6f526e69912561123a6bd836cf84a61af02e4a94161f2bae59cb0b7cec2821f13e09b0dfc84b81102e4dfc97eac00d89a9a8c6b8dbedec93bdae4ae6 + checksum: 10c0/acd41e322fbd60f3c2232c430a35dee1d88f57c87fb9700cb76bc8c66598e4eeaf37d580749568f4b6fb2a66d9ff132c2a09bbc5d34cf2d2175a0dcaebc122d9 languageName: node linkType: hard @@ -8184,20 +8407,13 @@ __metadata: languageName: node linkType: hard -"cytoscape@npm:^3.23.0": +"cytoscape@npm:^3.23.0, cytoscape@npm:^3.29.3": version: 3.32.0 resolution: "cytoscape@npm:3.32.0" checksum: 10c0/21cb0d2e79ebe137c7218e96edc2fb1c9000faae4f58c6a3c1899d9689c447c91feff94e5de649f227ced66f8c6a092b838de3fff3d8b57366156900f5df6d71 languageName: node linkType: hard -"cytoscape@npm:^3.33.1": - version: 3.33.2 - resolution: "cytoscape@npm:3.33.2" - checksum: 10c0/2f3c480df7792419e526348cec4e97237399221080016cd96c17f0686be5245eba4651517cbf5639a6b8b186178a0e438b8fbd7f306384acf3c74db4f4c3ccb3 - languageName: node - linkType: hard - "d3-array@npm:1 - 2": version: 2.12.1 resolution: "d3-array@npm:2.12.1" @@ -8435,7 +8651,7 @@ __metadata: languageName: node linkType: hard -"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": +"d3-selection@npm:2 - 3, d3-selection@npm:3": version: 3.0.0 resolution: "d3-selection@npm:3.0.0" checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b @@ -8485,7 +8701,7 @@ __metadata: languageName: node linkType: hard -"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1": +"d3-transition@npm:2 - 3, d3-transition@npm:3": version: 3.0.1 resolution: "d3-transition@npm:3.0.1" dependencies: @@ -8551,13 +8767,13 @@ __metadata: languageName: node linkType: hard -"dagre-d3-es@npm:7.0.14": - version: 7.0.14 - resolution: "dagre-d3-es@npm:7.0.14" +"dagre-d3-es@npm:7.0.13": + version: 7.0.13 + resolution: "dagre-d3-es@npm:7.0.13" dependencies: d3: "npm:^7.9.0" lodash-es: "npm:^4.17.21" - checksum: 10c0/0dc91fc79300eb0a4eab5a48a76c2baf3ce439c389d19e2f015729bb57dafd75e1e9a4c2880daf016e81ee45caca7b21745c13b23b6cd2a786ce84767e88323e + checksum: 10c0/4eca80dbbad4075311e3853930f99486024785b54210541796d4216140d91744738ee51125e2692c3532af148fbc2e690171750583916ed2ad553150abb198c7 languageName: node linkType: hard @@ -8604,10 +8820,17 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.19": - version: 1.11.20 - resolution: "dayjs@npm:1.11.20" - checksum: 10c0/8af525e2aa100c8db9923d706c42b2b2d30579faf89456619413a5c10916efc92c2b166e193c27c02eb3174b30aa440ee1e7b72b0a2876b3da651d204db848a0 +"dayjs@npm:^1.11.18": + version: 1.11.19 + resolution: "dayjs@npm:1.11.19" + checksum: 10c0/7d8a6074a343f821f81ea284d700bd34ea6c7abbe8d93bce7aba818948957c1b7f56131702e5e890a5622cdfc05dcebe8aed0b8313bdc6838a594d7846b0b000 + languageName: node + linkType: hard + +"de-indent@npm:^1.0.2": + version: 1.0.2 + resolution: "de-indent@npm:1.0.2" + checksum: 10c0/7058ce58abd6dfc123dd204e36be3797abd419b59482a634605420f47ae97639d0c183ec5d1b904f308a01033f473673897afc2bd59bc620ebf1658763ef4291 languageName: node linkType: hard @@ -8858,15 +9081,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.3.1": - version: 3.4.0 - resolution: "dompurify@npm:3.4.0" +"dompurify@npm:^3.2.5": + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10c0/5593ac44ee20b9aa521c2120884effc98927fb9128c548183c75e79e0a04357c62ee913a049a267c8f396cb8c9d520ecf72562826c5524c46d4fe03c12063638 + checksum: 10c0/c8f8e5b0879a0d93c84a2e5e78649a47d0c057ed0f7850ca3d573d2cca64b84fb1ff85bd4b20980ade69c4e5b80ae73011340f1c2ff375c7ef98bb8268e1d13a languageName: node linkType: hard @@ -9015,6 +9238,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^7.0.1": + version: 7.0.1 + resolution: "entities@npm:7.0.1" + checksum: 10c0/b4fb9937bb47ecb00aaaceb9db9cdd1cc0b0fb649c0e843d05cf5dbbd2e9d2df8f98721d8b1b286445689c72af7b54a7242fc2d63ef7c9739037a8c73363e7ca + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -9196,14 +9426,14 @@ __metadata: linkType: hard "es-toolkit@npm:^1.39.7, es-toolkit@npm:^1.44.0": - version: 1.45.1 - resolution: "es-toolkit@npm:1.45.1" + version: 1.44.0 + resolution: "es-toolkit@npm:1.44.0" dependenciesMeta: "@trivago/prettier-plugin-sort-imports@4.3.0": unplugged: true prettier-plugin-sort-re-exports@0.0.1: unplugged: true - checksum: 10c0/b19180c778af8fe2fb450e8e05a5793166c91e0aa66b87d9fcfcc5618bd33e6ceec9c103e074458d32b2044972dc4fc63631b3b615834fde261917e9561f6f59 + checksum: 10c0/b80ff52ddc85ba26914cda57c9d4e46379ccc38c60dc097ef0d065cc0b20f95a16cf8d537969eea600b51c6687b5900a6cce67489db16d5ccc14d47597a29c34 languageName: node linkType: hard @@ -9515,16 +9745,16 @@ __metadata: linkType: hard "eslint-plugin-jsdoc@npm:^62.0.0": - version: 62.9.0 - resolution: "eslint-plugin-jsdoc@npm:62.9.0" + version: 62.7.1 + resolution: "eslint-plugin-jsdoc@npm:62.7.1" dependencies: - "@es-joy/jsdoccomment": "npm:~0.86.0" + "@es-joy/jsdoccomment": "npm:~0.84.0" "@es-joy/resolve.exports": "npm:1.2.0" are-docs-informative: "npm:^0.0.2" - comment-parser: "npm:1.4.6" + comment-parser: "npm:1.4.5" debug: "npm:^4.4.3" escape-string-regexp: "npm:^4.0.0" - espree: "npm:^11.2.0" + espree: "npm:^11.1.0" esquery: "npm:^1.7.0" html-entities: "npm:^2.6.0" object-deep-merge: "npm:^2.0.0" @@ -9534,7 +9764,7 @@ __metadata: to-valid-identifier: "npm:^1.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - checksum: 10c0/c3a9abbe3a5dacf585dba8953f155910f9d69341d432cb0edd1fcb3ed9762e642a95f8b4aac24603a2319d5d892a2fba875b55c50367e8dd2330c4caefb115c0 + checksum: 10c0/949c1f11ed86ddac0903ffe65e5e30d36766badea2e42ceeaf85168ec6f540f94a9974896600410ce1fcbbf34809b39236cc9614411c15d05b93be166c21ec3c languageName: node linkType: hard @@ -9650,30 +9880,23 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^5.0.1": - version: 5.0.1 - resolution: "eslint-visitor-keys@npm:5.0.1" - checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 - languageName: node - linkType: hard - "eslint@npm:^9.35.0": - version: 9.39.4 - resolution: "eslint@npm:9.39.4" + version: 9.39.3 + resolution: "eslint@npm:9.39.3" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.2" + "@eslint/config-array": "npm:^0.21.1" "@eslint/config-helpers": "npm:^0.4.2" "@eslint/core": "npm:^0.17.0" - "@eslint/eslintrc": "npm:^3.3.5" - "@eslint/js": "npm:9.39.4" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:9.39.3" "@eslint/plugin-kit": "npm:^0.4.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" - ajv: "npm:^6.14.0" + ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" @@ -9692,7 +9915,7 @@ __metadata: is-glob: "npm:^4.0.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.5" + minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" peerDependencies: @@ -9702,7 +9925,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/1955067c2d991f0c84f4c4abfafe31bb47fa3b717a7fd3e43fe1e511c6f859d7700cbca969f85661dc4c130f7aeced5e5444884314198a54428f5e5141db9337 + checksum: 10c0/5e5dbf84d4f604f5d2d7a58c5c3fcdde30a01b8973ff3caeca8b2bacc16066717cedb4385ce52db1a2746d0b621770d4d4227cc7f44982b0b03818be2c31538d languageName: node linkType: hard @@ -9717,14 +9940,14 @@ __metadata: languageName: node linkType: hard -"espree@npm:^11.2.0": - version: 11.2.0 - resolution: "espree@npm:11.2.0" +"espree@npm:^11.1.0": + version: 11.1.0 + resolution: "espree@npm:11.1.0" dependencies: - acorn: "npm:^8.16.0" + acorn: "npm:^8.15.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^5.0.1" - checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81 + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/32228d12896f5aa09f59fad8bf5df228d73310e436c21389876cdd21513b620c087d24b40646cdcff848540d11b078653db0e37ea67ac9c7012a12595d86630c languageName: node linkType: hard @@ -9999,26 +10222,22 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.1.5": - version: 1.1.5 - resolution: "fast-xml-builder@npm:1.1.5" - dependencies: - path-expression-matcher: "npm:^1.1.3" - checksum: 10c0/b814ba5559cb3140de46d2846045607ab4d4c0bfc312a49d22c91efb9f7cd7004971314841e5823eeb467a5bf403e3ade8371b7912200e111df027d42ae51715 +"fast-xml-builder@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-xml-builder@npm:1.0.0" + checksum: 10c0/2631fda265c81e8008884d08944eeed4e284430116faa5b8b7a43a3602af367223b7bf01c933215c9ad2358b8666e45041bc038d64877156a2f88821841b3014 languageName: node linkType: hard "fast-xml-parser@npm:^5.0.7": - version: 5.7.1 - resolution: "fast-xml-parser@npm:5.7.1" + version: 5.4.1 + resolution: "fast-xml-parser@npm:5.4.1" dependencies: - "@nodable/entities": "npm:^2.1.0" - fast-xml-builder: "npm:^1.1.5" - path-expression-matcher: "npm:^1.5.0" - strnum: "npm:^2.2.3" + fast-xml-builder: "npm:^1.0.0" + strnum: "npm:^2.1.2" bin: fxparser: src/cli/cli.js - checksum: 10c0/b8b54e33060da5fc5ce26fdc73c4728f18415f9be9a774f1406b03265a5b411b742c39dba0127c3f0f31fad5b3ee11f51be79aa16df160f69fd5e4b902bfbb85 + checksum: 10c0/8c696438a0c64135faf93ea6a93879208d649b7c9a3293d30d6eb750dc7f766fd083c0df5a82786b60809c3ead64fad155f28dbed25efea91017aaf9f64c91e5 languageName: node linkType: hard @@ -10061,7 +10280,7 @@ __metadata: languageName: node linkType: hard -"fflate@npm:~0.8.2": +"fflate@npm:^0.8.2, fflate@npm:~0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" checksum: 10c0/03448d630c0a583abea594835a9fdb2aaf7d67787055a761515bf4ed862913cfd693b4c4ffd5c3f3b355a70cf1e19033e9ae5aedcca103188aaff91b8bd6e293 @@ -10135,7 +10354,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9, flatted@npm:^3.4.2": +"flatted@npm:^3.2.9, flatted@npm:^3.3.3, flatted@npm:^3.4.2": version: 3.4.2 resolution: "flatted@npm:3.4.2" checksum: 10c0/a65b67aae7172d6cdf63691be7de6c5cd5adbdfdfe2e9da1a09b617c9512ed794037741ee53d93114276bff3f93cd3b0d97d54f9b316e1e4885dde6e9ffdf7ed @@ -10151,6 +10370,22 @@ __metadata: languageName: node linkType: hard +"floating-vue@npm:^5.2.2": + version: 5.2.2 + resolution: "floating-vue@npm:5.2.2" + dependencies: + "@floating-ui/dom": "npm:~1.1.1" + vue-resize: "npm:^2.0.0-alpha.1" + peerDependencies: + "@nuxt/kit": ^3.2.0 + vue: ^3.2.0 + peerDependenciesMeta: + "@nuxt/kit": + optional: true + checksum: 10c0/f765c4ef62c4bdbc6b38167c472d2badabdd3ace4484819532b099f1a027bcba6dba1916414cca14f6c375e33f399bd42111db3cbf69a3803f44bc3dd88e6761 + languageName: node + linkType: hard + "focus-trap@npm:^7.6.4": version: 7.6.5 resolution: "focus-trap@npm:7.6.5" @@ -10161,12 +10396,12 @@ __metadata: linkType: hard "follow-redirects@npm:^1.0.0": - version: 1.16.0 - resolution: "follow-redirects@npm:1.16.0" + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" peerDependenciesMeta: debug: optional: true - checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181 + checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f languageName: node linkType: hard @@ -10412,9 +10647,9 @@ __metadata: linkType: hard "gl-matrix@npm:^3.3.0": - version: 3.4.4 - resolution: "gl-matrix@npm:3.4.4" - checksum: 10c0/9aa022ffac0d158212ad0cd29939864ad919ac31cd5dc5a5d35e9d66bb62679ddf152ff7b2173ded20131045e40572b87f31b26a920be2a7583a1516b13b5b4b + version: 3.4.3 + resolution: "gl-matrix@npm:3.4.3" + checksum: 10c0/c8ee6e2ce2d089b4ba4ae13ec9d4cb99bf2abe5f68f0cb08d94bbd8bafbec13aacc7230b86539ce5ca01b79226ea8c3194f971f5ca0c81838bc5e4e619dc398e languageName: node linkType: hard @@ -10506,9 +10741,9 @@ __metadata: linkType: hard "globals@npm:^17.0.0": - version: 17.5.0 - resolution: "globals@npm:17.5.0" - checksum: 10c0/92828102ed2f5637907725f0478038bed02fc83e9fc89300bb753639ba7c022b6c02576fc772117302b431b204591db1f2fa909d26f3f0a9852cc856a941df3f + version: 17.4.0 + resolution: "globals@npm:17.4.0" + checksum: 10c0/2be9e8c2b9035836f13d420b22f0247a328db82967d3bebfc01126d888ed609305f06c05895914e969653af5c6ba35fd7a0920f3e6c869afa60666c810630feb languageName: node linkType: hard @@ -10979,10 +11214,10 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^5.1.5": - version: 5.1.5 - resolution: "immutable@npm:5.1.5" - checksum: 10c0/8017ece1578e3c5939ba3305176aee059def1b8a90c7fa2a347ef583ebbd38cbe77ce1bbd786a5fab57e2da00bbcb0493b92e4332cdc4e1fe5cfb09a4688df31 +"immutable@npm:^5.0.2": + version: 5.1.2 + resolution: "immutable@npm:5.1.2" + checksum: 10c0/da5af92d2c70323c1f9a0e418832c9eef441feadaf6a295a4e07764bd2400c85186872e016071d9253549d58d364160d55dca8dcdf59fd4a6a06c6756fe61657 languageName: node linkType: hard @@ -11779,27 +12014,25 @@ __metadata: languageName: node linkType: hard -"js-slang@npm:^1.0.85": - version: 1.0.85 - resolution: "js-slang@npm:1.0.85" +"js-slang@npm:^1.0.92": + version: 1.0.92 + resolution: "js-slang@npm:1.0.92" dependencies: "@babel/parser": "npm:^7.19.4" - "@commander-js/extra-typings": "npm:^12.0.1" - "@joeychenofficial/alt-ergo-modified": "npm:^2.4.0" + "@commander-js/extra-typings": "npm:^14.0.0" "@ts-morph/bootstrap": "npm:^0.18.0" - "@types/estree": "npm:^1.0.5" acorn: "npm:^8.8.2" acorn-class-fields: "npm:^1.0.0" acorn-loose: "npm:^8.0.0" acorn-walk: "npm:^8.0.0" astring: "npm:^1.4.3" - commander: "npm:^12.0.0" + commander: "npm:^14.0.0" js-base64: "npm:^3.7.5" lodash: "npm:^4.17.21" source-map: "npm:0.7.6" bin: js-slang: dist/repl/index.js - checksum: 10c0/8a206d6052e5b23a27bda4aef66675b4b7f9bebde936c789a182948478e26fa7aee6289effd0d867725a939e14674ee59fd218e1bec1fd1dc597b6bf7cbbaf42 + checksum: 10c0/faea8ab62fa1faecf68725becfb1184a13d8163bbc18d21a14c24f709fdd657ed5b7bbc9280765ca520d554261e96b1b1591abb16406ea8aa7ddff51501ad84c languageName: node linkType: hard @@ -11829,7 +12062,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:^4.1.0": version: 4.1.1 resolution: "js-yaml@npm:4.1.1" dependencies: @@ -11847,19 +12080,19 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:~7.2.0": - version: 7.2.0 - resolution: "jsdoc-type-pratt-parser@npm:7.2.0" - checksum: 10c0/efe7e87583adba264234d445b47c5bfdb98c81d5c8ce8ea4c5ebcf4e249cc152cf6ecb0ec3e040a7359f391899556858fc8a22f9deb2373885cdd8ee6e221b29 +"jsdoc-type-pratt-parser@npm:~7.1.1": + version: 7.1.1 + resolution: "jsdoc-type-pratt-parser@npm:7.1.1" + checksum: 10c0/5a5216a75962b3a8a3a1e7e09a19b31b5a373c06c726a00b081480daee00196250d4acc8dfbecc0a7846d439a5bcf4a326df6348b879cf95f60c62ce5818dadb languageName: node linkType: hard "jsdom@npm:^29.0.0": - version: 29.0.2 - resolution: "jsdom@npm:29.0.2" + version: 29.0.0 + resolution: "jsdom@npm:29.0.0" dependencies: - "@asamuzakjp/css-color": "npm:^5.1.5" - "@asamuzakjp/dom-selector": "npm:^7.0.6" + "@asamuzakjp/css-color": "npm:^5.0.1" + "@asamuzakjp/dom-selector": "npm:^7.0.2" "@bramus/specificity": "npm:^2.4.2" "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1" "@exodus/bytes": "npm:^1.15.0" @@ -11873,7 +12106,7 @@ __metadata: saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" tough-cookie: "npm:^6.0.1" - undici: "npm:^7.24.5" + undici: "npm:^7.24.3" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^8.0.1" whatwg-mimetype: "npm:^5.0.0" @@ -11884,7 +12117,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10c0/a325324117932de83d13f00c74ff91a5f6b4bbbf11f45e7e31189fea06d007f764aac286dad80f643430c81b618b9fb20eac1ef03f120cec4c68df271e7547e2 + checksum: 10c0/8bbb4f89bfb7013a729bf50419fa5878086b52068a5d3836b51b99be5faf7cf9f871818954d468ce1f2f1c6b86d16af6ed5167f4084f7df7de517b104b451454 languageName: node linkType: hard @@ -11982,14 +12215,14 @@ __metadata: languageName: node linkType: hard -"katex@npm:^0.16.25": - version: 0.16.45 - resolution: "katex@npm:0.16.45" +"katex@npm:^0.16.22": + version: 0.16.25 + resolution: "katex@npm:0.16.25" dependencies: commander: "npm:^8.3.0" bin: katex: cli.js - checksum: 10c0/f715eb9a73daff68ac14dcc8c2f47f02f5fc756a74077d22479e64e06872b2b44f0d81f1c91a98a9873722a08841388a869a0b9ddb96b224362eb8054ddf533a + checksum: 10c0/5bb4b1cd914b76d5efb01ee054c1a221ac723be1e38fb260264c6e253036943d301c1741cbf64f840689c6b3942bce21a6da6637de846a428e4c661dc8ee46ab languageName: node linkType: hard @@ -12035,16 +12268,15 @@ __metadata: linkType: hard "langium@npm:^4.0.0": - version: 4.2.2 - resolution: "langium@npm:4.2.2" + version: 4.2.1 + resolution: "langium@npm:4.2.1" dependencies: - "@chevrotain/regexp-to-ast": "npm:~12.0.0" - chevrotain: "npm:~12.0.0" - chevrotain-allstar: "npm:~0.4.1" + chevrotain: "npm:~11.1.1" + chevrotain-allstar: "npm:~0.3.1" vscode-languageserver: "npm:~9.0.1" vscode-languageserver-textdocument: "npm:~1.0.11" vscode-uri: "npm:~3.1.0" - checksum: 10c0/4758dfcf211847830a8d41b118e39998b15435d8017364cc0855115ff84a300602465d62d54874b655e3cbfaaae9c9e3f6895252cb93455801bd885adc7199a5 + checksum: 10c0/19ddf79cc3c435ec70f8eb50de255571711db7cea89d171cf80bc97e7ed73d4d0bb6b4215899df8369fa6d0e17f442f30af4ec2e9657041bf2f93be1310ba50a languageName: node linkType: hard @@ -12259,6 +12491,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:4.17.23": + version: 4.17.23 + resolution: "lodash-es@npm:4.17.23" + checksum: 10c0/3150fb6660c14c7a6b5f23bd11597d884b140c0e862a17fdb415aaa5ef7741523182904a6b7929f04e5f60a11edb5a79499eb448734381c99ffb3c4734beeddd + languageName: node + linkType: hard + "lodash-es@npm:^4.17.21, lodash-es@npm:^4.17.23": version: 4.18.1 resolution: "lodash-es@npm:4.18.1" @@ -12414,9 +12653,9 @@ __metadata: linkType: hard "lodash@npm:^4.17.15, lodash@npm:^4.17.21": - version: 4.18.1 - resolution: "lodash@npm:4.18.1" - checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c languageName: node linkType: hard @@ -12461,7 +12700,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.7": +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.6, lru-cache@npm:^11.2.7": version: 11.2.7 resolution: "lru-cache@npm:11.2.7" checksum: 10c0/549cdb59488baa617135fc12159cafb1a97f91079f35093bb3bcad72e849fc64ace636d244212c181dfdf1a99bbfa90757ff303f98561958ee4d0f885d9bd5f7 @@ -12582,7 +12821,7 @@ __metadata: languageName: node linkType: hard -"markdown-it@npm:^14.1.1": +"markdown-it@npm:^14.1.0, markdown-it@npm:^14.1.1": version: 14.1.1 resolution: "markdown-it@npm:14.1.1" dependencies: @@ -12605,7 +12844,7 @@ __metadata: languageName: node linkType: hard -"marked@npm:^16.3.0": +"marked@npm:^16.2.1": version: 16.4.2 resolution: "marked@npm:16.4.2" bin: @@ -12826,7 +13065,7 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-hast@npm:^13.0.0": +"mdast-util-to-hast@npm:^13.0.0, mdast-util-to-hast@npm:^13.2.0": version: 13.2.1 resolution: "mdast-util-to-hast@npm:13.2.1" dependencies: @@ -12908,31 +13147,30 @@ __metadata: linkType: hard "mermaid@npm:^11.10.0": - version: 11.14.0 - resolution: "mermaid@npm:11.14.0" + version: 11.12.3 + resolution: "mermaid@npm:11.12.3" dependencies: "@braintree/sanitize-url": "npm:^7.1.1" - "@iconify/utils": "npm:^3.0.2" - "@mermaid-js/parser": "npm:^1.1.0" + "@iconify/utils": "npm:^3.0.1" + "@mermaid-js/parser": "npm:^1.0.0" "@types/d3": "npm:^7.4.3" - "@upsetjs/venn.js": "npm:^2.0.0" - cytoscape: "npm:^3.33.1" + cytoscape: "npm:^3.29.3" cytoscape-cose-bilkent: "npm:^4.1.0" cytoscape-fcose: "npm:^2.2.0" d3: "npm:^7.9.0" d3-sankey: "npm:^0.12.3" - dagre-d3-es: "npm:7.0.14" - dayjs: "npm:^1.11.19" - dompurify: "npm:^3.3.1" - katex: "npm:^0.16.25" + dagre-d3-es: "npm:7.0.13" + dayjs: "npm:^1.11.18" + dompurify: "npm:^3.2.5" + katex: "npm:^0.16.22" khroma: "npm:^2.1.0" lodash-es: "npm:^4.17.23" - marked: "npm:^16.3.0" + marked: "npm:^16.2.1" roughjs: "npm:^4.6.6" stylis: "npm:^4.3.6" ts-dedent: "npm:^2.2.0" uuid: "npm:^11.1.0" - checksum: 10c0/2075b72d4496418ec28aa7e7d8d1b4ddcd408209940fad1efa3dc5fe98b465f38d7b5d998dc0ba3c56de639df5cc73c6395e04a9e7b18a451e2633b57a74c019 + checksum: 10c0/a50e0e79fa913d8d0c88a8927d14b07c8236ebb595aade815152b2668ed4e3e5204884c776739ee0b473df2277a4431cf21ed98cc2a7d81995d0f5a94af93058 languageName: node linkType: hard @@ -13488,7 +13726,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.5": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.5 resolution: "minimatch@npm:3.1.5" dependencies: @@ -13506,7 +13744,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -13515,6 +13753,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.3": + version: 9.0.9 + resolution: "minimatch@npm:9.0.9" + dependencies: + brace-expansion: "npm:^2.0.2" + checksum: 10c0/0b6a58530dbb00361745aa6c8cffaba4c90f551afe7c734830bd95fd88ebf469dd7355a027824ea1d09e37181cfeb0a797fb17df60c15ac174303ac110eb7e86 + languageName: node + linkType: hard + "minimist@npm:^1.1.0, minimist@npm:^1.1.1, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -13741,6 +13988,13 @@ __metadata: languageName: node linkType: hard +"muggle-string@npm:^0.4.1": + version: 0.4.1 + resolution: "muggle-string@npm:0.4.1" + checksum: 10c0/e914b63e24cd23f97e18376ec47e4ba3aa24365e4776212b666add2e47bb158003212980d732c49abf3719568900af7861873844a6e2d3a7ca7e86952c0e99e9 + languageName: node + linkType: hard + "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -14182,13 +14436,6 @@ __metadata: languageName: node linkType: hard -"oniguruma-parser@npm:^0.12.1": - version: 0.12.1 - resolution: "oniguruma-parser@npm:0.12.1" - checksum: 10c0/b843ea54cda833efb19f856314afcbd43e903ece3de489ab78c527ddec84859208052557daa9fad4bdba89ebdd15b0cc250de86b3daf8c7cbe37bac5a6a185d3 - languageName: node - linkType: hard - "oniguruma-to-es@npm:^3.1.0": version: 3.1.1 resolution: "oniguruma-to-es@npm:3.1.1" @@ -14200,17 +14447,6 @@ __metadata: languageName: node linkType: hard -"oniguruma-to-es@npm:^4.3.4": - version: 4.3.4 - resolution: "oniguruma-to-es@npm:4.3.4" - dependencies: - oniguruma-parser: "npm:^0.12.1" - regex: "npm:^6.0.1" - regex-recursion: "npm:^6.0.2" - checksum: 10c0/fb58459f50db71c2c4785205636186bfbb125b094c4275512a8f41f123ed3fbf61f37c455f4360ef14a56c693981aecd7da3ae2c05614a222e872c4643b463fc - languageName: node - linkType: hard - "opener@npm:^1.5.1": version: 1.5.2 resolution: "opener@npm:1.5.2" @@ -14510,20 +14746,6 @@ __metadata: languageName: node linkType: hard -"path-expression-matcher@npm:^1.1.3": - version: 1.1.3 - resolution: "path-expression-matcher@npm:1.1.3" - checksum: 10c0/45c01471bc62c5f38d069418aec831763e6f45bb85f9520b08de441e6cd14f84b3098ecb66255e819c2af21102abcd2b45550dc1285996717ce9292802df2bc5 - languageName: node - linkType: hard - -"path-expression-matcher@npm:^1.5.0": - version: 1.5.0 - resolution: "path-expression-matcher@npm:1.5.0" - checksum: 10c0/646cb5bc66cd7d809a52288336f3ac1e6223f156fd8e912936e490e590f7f93e8056d4fd25fcbcc7da61bb698fa520112cb050372a3f65e7b79bd4afa0f77610 - languageName: node - linkType: hard - "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -14617,9 +14839,9 @@ __metadata: linkType: hard "picomatch@npm:^2.0.4, picomatch@npm:^2.3.1": - version: 2.3.2 - resolution: "picomatch@npm:2.3.2" - checksum: 10c0/a554d1709e59be97d1acb9eaedbbc700a5c03dbd4579807baed95100b00420bc729335440ef15004ae2378984e2487a7c1cebd743cfdb72b6fa9ab69223c0d61 + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be languageName: node linkType: hard @@ -14657,27 +14879,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.59.1": - version: 1.59.1 - resolution: "playwright-core@npm:1.59.1" +"playwright-core@npm:1.58.2": + version: 1.58.2 + resolution: "playwright-core@npm:1.58.2" bin: playwright-core: cli.js - checksum: 10c0/d41a74d9681ce3beb3d5239e9ed577710b4ad099a6ca2476219c6599d51e9cb4b80bd72ed82c528da6a5d929c18ae3b872cf02bb83f78fa1c2cb9199c501abee + checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b languageName: node linkType: hard "playwright@npm:^1.55.1": - version: 1.59.1 - resolution: "playwright@npm:1.59.1" + version: 1.58.2 + resolution: "playwright@npm:1.58.2" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.59.1" + playwright-core: "npm:1.58.2" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/dfe38396e616e5c4f98825ce90037bb96e477c5a2bd9258a24854f8ce72a8a41427b19098863866f85aa0216e70287dd537c4438d761aca93995e31ae099c533 + checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727 languageName: node linkType: hard @@ -14689,9 +14911,9 @@ __metadata: linkType: hard "plotly.js-dist@npm:^3.0.0": - version: 3.5.0 - resolution: "plotly.js-dist@npm:3.5.0" - checksum: 10c0/c9e8380a74256abbd1283eaa4f7efb48edcf0a4ceaf9e6dcae7ed0af1495af5a6a0fc1c00946024d8ca44bc9c5c9b42fab4b5b52722577adb09cfcef9b7250f3 + version: 3.3.1 + resolution: "plotly.js-dist@npm:3.3.1" + checksum: 10c0/8332667bc104a7fa13465f2462cb322c2b445ec4f3e2c522214508bd5eeca6b6471153c9f805e7f8228528a9faf6749fbfbc4f2e4612499397aeff8e928330c4 languageName: node linkType: hard @@ -14744,13 +14966,13 @@ __metadata: linkType: hard "postcss@npm:^8.4.43, postcss@npm:^8.5.6, postcss@npm:^8.5.8": - version: 8.5.13 - resolution: "postcss@npm:8.5.13" + version: 8.5.8 + resolution: "postcss@npm:8.5.8" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/3aa7c8cbdfbfd99b34406a433cef56d164dd135fc9cb9e63d487cc363291f877a55ec7b8ff6ec15348c17c2d98a43be46bfad671e6340403041a3e79f70c2f2f + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c languageName: node linkType: hard @@ -15660,19 +15882,19 @@ __metadata: linkType: hard "sass@npm:^1.85.0": - version: 1.99.0 - resolution: "sass@npm:1.99.0" + version: 1.97.3 + resolution: "sass@npm:1.97.3" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" - immutable: "npm:^5.1.5" + immutable: "npm:^5.0.2" source-map-js: "npm:>=0.6.2 <2.0.0" dependenciesMeta: "@parcel/watcher": optional: true bin: sass: sass.js - checksum: 10c0/83c54a8c6decb79fff50dd9500d7932cf1cb7c5d9be4bc42bd3d537402c37bbee062aea6efdbdf9fb0b8697b18177d60c72bf101872336b93b1c27a2dc3621e1 + checksum: 10c0/67f6b5d220f20c1c23a8b16dda5fd1c5d119ad5caf8195b185d553b5b239fb188a3787f04fc00171c62515f2c4e5e0eb5ad4992a80f8543428556883c1240ba3 languageName: node linkType: hard @@ -15862,7 +16084,7 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^2.1.0": +"shiki@npm:2.5.0, shiki@npm:^2.1.0": version: 2.5.0 resolution: "shiki@npm:2.5.0" dependencies: @@ -15878,22 +16100,6 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^3.15.0": - version: 3.23.0 - resolution: "shiki@npm:3.23.0" - dependencies: - "@shikijs/core": "npm:3.23.0" - "@shikijs/engine-javascript": "npm:3.23.0" - "@shikijs/engine-oniguruma": "npm:3.23.0" - "@shikijs/langs": "npm:3.23.0" - "@shikijs/themes": "npm:3.23.0" - "@shikijs/types": "npm:3.23.0" - "@shikijs/vscode-textmate": "npm:^10.0.2" - "@types/hast": "npm:^3.0.4" - checksum: 10c0/b06a3eddac4bd0a838f9bd79bea70b0a01195570cb11d70fd2ff7ab0a42c33a8c4980ee52174173aae0476cc152b4c1a68c081a82d94ee340cb3ef9d772ae4ba - languageName: node - linkType: hard - "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0" @@ -15995,10 +16201,10 @@ __metadata: languageName: node linkType: hard -"smol-toml@npm:^1.6.1": - version: 1.6.1 - resolution: "smol-toml@npm:1.6.1" - checksum: 10c0/511a78722f99c7616fdb46af708de3d7e81434b5a3d58061166da73f28bfc6cae4f0cd04683f60515b9c490cd10152fce72287c960b337419c0299cc1f0f2a22 +"smol-toml@npm:^1.6.0": + version: 1.6.0 + resolution: "smol-toml@npm:1.6.0" + checksum: 10c0/baf33bb6cd914d481329e31998a12829cd126541458ba400791212c80f1245d5b27dac04a56a52c02b287d2a494f1628c05fc19643286b258b2e0bb9fe67747c languageName: node linkType: hard @@ -16061,8 +16267,8 @@ __metadata: linkType: hard "snyk-nodejs-lockfile-parser@npm:^2.4.2": - version: 2.7.0 - resolution: "snyk-nodejs-lockfile-parser@npm:2.7.0" + version: 2.5.0 + resolution: "snyk-nodejs-lockfile-parser@npm:2.5.0" dependencies: "@snyk/dep-graph": "npm:^2.12.0" "@snyk/error-catalog-nodejs-public": "npm:^5.16.0" @@ -16084,7 +16290,7 @@ __metadata: uuid: "npm:^8.3.0" bin: parse-nodejs-lockfile: bin/index.js - checksum: 10c0/3cc278810d8b3bef245d6e6ad2354fa10649328929a6c7a979ae9783bedaa904bf0d1f30e77d96e4d47e568b93f73825f62436f544b1e036db17a07f02fb7842 + checksum: 10c0/9cc7d03322d76dd66df9372f542b3d980a0278e458a9402d29c4320e2054dab3821c59b0ac6c43b85fbaa0d216353e2b411b4c61efe8f60bf551c5c481a86518 languageName: node linkType: hard @@ -16553,10 +16759,10 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^2.2.3": - version: 2.2.3 - resolution: "strnum@npm:2.2.3" - checksum: 10c0/1ee78101f1cd73a5b32f63cfd0be501bd246801a002f5987efef903a49e9297d1b63574e302ab3c06ee5e715c524d6cbdfef010e372ec1ea848e0179836cc208 +"strnum@npm:^2.1.2": + version: 2.1.2 + resolution: "strnum@npm:2.1.2" + checksum: 10c0/4e04753b793540d79cd13b2c3e59e298440477bae2b853ab78d548138385193b37d766d95b63b7046475d68d44fb1fca692f0a3f72b03f4168af076c7b246df9 languageName: node linkType: hard @@ -16753,13 +16959,6 @@ __metadata: languageName: node linkType: hard -"tm-themes@npm:^1.10.12": - version: 1.12.2 - resolution: "tm-themes@npm:1.12.2" - checksum: 10c0/1acdd9d355c4509291b1e063256524b3e69a447ec7eb393de8a19b7b5746bb7b89b81cbd9ef545314d4345719aebde9841f6e79326765989ef3758abf53dc3ae - languageName: node - linkType: hard - "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -16955,6 +17154,57 @@ __metadata: languageName: node linkType: hard +"twoslash-protocol@npm:0.2.12": + version: 0.2.12 + resolution: "twoslash-protocol@npm:0.2.12" + checksum: 10c0/9a32d31a7fcdd9722627981b0bb20c43b7257f85cb6e455c5c60cb2aba10adc28ff45beb4367f80d241b2cfa19ab9572ff88b66f77f59038f64713460fdb34ba + languageName: node + linkType: hard + +"twoslash-protocol@npm:0.3.6": + version: 0.3.6 + resolution: "twoslash-protocol@npm:0.3.6" + checksum: 10c0/3316ad1eb0ccd83c8664582f9718c9fa0fb034ae44c68f374a49e91911585769b8994fda9532c16719d5a3c7407bf8e24fdab11a59d3f41ba41217ef67a1ffcf + languageName: node + linkType: hard + +"twoslash-vue@npm:^0.2.12": + version: 0.2.12 + resolution: "twoslash-vue@npm:0.2.12" + dependencies: + "@vue/language-core": "npm:~2.1.6" + twoslash: "npm:0.2.12" + twoslash-protocol: "npm:0.2.12" + peerDependencies: + typescript: "*" + checksum: 10c0/2f069439a503e848ba6eba69f7e79d4c44be598a73fcd4999ca5ba1e58fa20b8409b5b302113ed806e356fd83e9edf2532847164c3e7e56539d0b56fa081feee + languageName: node + linkType: hard + +"twoslash@npm:0.2.12, twoslash@npm:^0.2.12": + version: 0.2.12 + resolution: "twoslash@npm:0.2.12" + dependencies: + "@typescript/vfs": "npm:^1.6.0" + twoslash-protocol: "npm:0.2.12" + peerDependencies: + typescript: "*" + checksum: 10c0/1476da54614c91f4ec061f3fceef180f7b83a167e5446ba6f63c36fb821750028e056b88c7345fa914a30e05268ba7f5d8567e57ded6ecd8abf39777514abfbb + languageName: node + linkType: hard + +"twoslash@npm:^0.3.6": + version: 0.3.6 + resolution: "twoslash@npm:0.3.6" + dependencies: + "@typescript/vfs": "npm:^1.6.2" + twoslash-protocol: "npm:0.3.6" + peerDependencies: + typescript: ^5.5.0 + checksum: 10c0/b0c95590e3e87a05f35c4d613087934e16074098fabb9c9096c01a02522ab3be29301bc04007e5803c2586ec33ed5212f955280af7cd0acafccdb7d32a2a2088 + languageName: node + linkType: hard + "typanion@npm:^3.8.0": version: 3.14.0 resolution: "typanion@npm:3.14.0" @@ -17059,11 +17309,11 @@ __metadata: linkType: hard "typedoc-plugin-markdown@npm:^4.7.0": - version: 4.11.0 - resolution: "typedoc-plugin-markdown@npm:4.11.0" + version: 4.10.0 + resolution: "typedoc-plugin-markdown@npm:4.10.0" peerDependencies: typedoc: 0.28.x - checksum: 10c0/03374acfd0b5bd5af13198c043ffc31324b097647f5ffd92959647a9a277f0ece665331ff6a650ddaefdf9ff33928e138c5483e7cddbac830eb77e69b6067803 + checksum: 10c0/20c7bc8ef68bd90053649ce223d02d4aceefed675c09efb1740c7791fbc37c10a1e25d14647605484a198f0695312eb21119015616d91c73fe1d63df5e4fb061 languageName: node linkType: hard @@ -17078,7 +17328,7 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:^0.28.18, typedoc@npm:^0.28.9": +"typedoc@npm:^0.28.18": version: 0.28.19 resolution: "typedoc@npm:0.28.19" dependencies: @@ -17095,7 +17345,24 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.58.0": +"typedoc@npm:^0.28.9": + version: 0.28.16 + resolution: "typedoc@npm:0.28.16" + dependencies: + "@gerrit0/mini-shiki": "npm:^3.17.0" + lunr: "npm:^2.3.9" + markdown-it: "npm:^14.1.0" + minimatch: "npm:^9.0.5" + yaml: "npm:^2.8.1" + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x + bin: + typedoc: bin/typedoc + checksum: 10c0/ae444913068088e88be6319a017a3a18f69cbd91dbb5b959fbdd0cf87d1a2a07f3a0d4ab29c957a83dd72808ff35bdd6ceec3ad1803fa412ddceffb78fa60ebb + languageName: node + linkType: hard + +"typescript-eslint@npm:^8.58.2": version: 8.58.2 resolution: "typescript-eslint@npm:8.58.2" dependencies: @@ -17230,10 +17497,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.19.0": - version: 7.19.2 - resolution: "undici-types@npm:7.19.2" - checksum: 10c0/7159f10546f9f6c47d36776bb1bbf8671e87c1e587a6fee84ae1f111ae8de4f914efa8ca0dfcd224f4f4a9dfc3f6028f627ccb5ddaccf82d7fd54671b89fac3e +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d languageName: node linkType: hard @@ -17244,10 +17511,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:^7.24.5": - version: 7.25.0 - resolution: "undici@npm:7.25.0" - checksum: 10c0/02a0b45dc14eb91bc488948750232450fe52f27a6b08086d6ac6736bb47908d600fe3a96d346f12eab24729c782e5c2f693bc8e8eca6696d4e4c09b1ed4cb4ec +"undici@npm:^7.24.3": + version: 7.24.4 + resolution: "undici@npm:7.24.4" + checksum: 10c0/cb302e81fadb7f0b7946ab77595715c0961b46a025ccecae79ba599432d0bc8d1e3da4dfe7ff66bc74f115c1b8ff0f099bc4e9bf313db4562da23995872c6d17 languageName: node linkType: hard @@ -17566,6 +17833,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.5.0 + resolution: "use-sync-external-store@npm:1.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/1b8663515c0be34fa653feb724fdcce3984037c78dd4a18f68b2c8be55cc1a1084c578d5b75f158d41b5ddffc2bf5600766d1af3c19c8e329bb20af2ec6f52f4 + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1" @@ -17796,7 +18072,7 @@ __metadata: languageName: node linkType: hard -"vitepress-plugin-group-icons@npm:^1.6.5": +"vitepress-plugin-group-icons@npm:^1.7.5": version: 1.7.5 resolution: "vitepress-plugin-group-icons@npm:1.7.5" dependencies: @@ -17827,7 +18103,7 @@ __metadata: languageName: node linkType: hard -"vitepress-sidebar@npm:^1.31.1": +"vitepress-sidebar@npm:^1.33.1": version: 1.33.1 resolution: "vitepress-sidebar@npm:1.33.1" dependencies: @@ -17838,7 +18114,7 @@ __metadata: languageName: node linkType: hard -"vitepress@npm:^1.6.3": +"vitepress@npm:^1.6.4": version: 1.6.4 resolution: "vitepress@npm:1.6.4" dependencies: @@ -17875,8 +18151,8 @@ __metadata: linkType: hard "vitest-browser-react@npm:^2.1.0": - version: 2.2.0 - resolution: "vitest-browser-react@npm:2.2.0" + version: 2.1.0 + resolution: "vitest-browser-react@npm:2.1.0" peerDependencies: "@types/react": ^18.0.0 || ^19.0.0 "@types/react-dom": ^18.0.0 || ^19.0.0 @@ -17888,21 +18164,21 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/d2e582e564cf7f65f19a5a9c36b0b136e84fc6dabd42566703d79e0b220094a5a88a9197a42b2c4779f38977d79c8f3306387cd7edd2ef8e57790b921a759975 + checksum: 10c0/739575a803ea048dc6a81a2027abf619529fb715ce2f3fc9b00c3bba315b042bd45c9b62bef9c3c4a48a6879e8952ee4fac30bc0378aa125eee6fdfb28e6b56b languageName: node linkType: hard -"vitest@npm:4.1.4": - version: 4.1.4 - resolution: "vitest@npm:4.1.4" +"vitest@npm:4.1.5": + version: 4.1.5 + resolution: "vitest@npm:4.1.5" dependencies: - "@vitest/expect": "npm:4.1.4" - "@vitest/mocker": "npm:4.1.4" - "@vitest/pretty-format": "npm:4.1.4" - "@vitest/runner": "npm:4.1.4" - "@vitest/snapshot": "npm:4.1.4" - "@vitest/spy": "npm:4.1.4" - "@vitest/utils": "npm:4.1.4" + "@vitest/expect": "npm:4.1.5" + "@vitest/mocker": "npm:4.1.5" + "@vitest/pretty-format": "npm:4.1.5" + "@vitest/runner": "npm:4.1.5" + "@vitest/snapshot": "npm:4.1.5" + "@vitest/spy": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -17920,12 +18196,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.4 - "@vitest/browser-preview": 4.1.4 - "@vitest/browser-webdriverio": 4.1.4 - "@vitest/coverage-istanbul": 4.1.4 - "@vitest/coverage-v8": 4.1.4 - "@vitest/ui": 4.1.4 + "@vitest/browser-playwright": 4.1.5 + "@vitest/browser-preview": 4.1.5 + "@vitest/browser-webdriverio": 4.1.5 + "@vitest/coverage-istanbul": 4.1.5 + "@vitest/coverage-v8": 4.1.5 + "@vitest/ui": 4.1.5 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -17956,7 +18232,7 @@ __metadata: optional: false bin: vitest: vitest.mjs - checksum: 10c0/a85288778cf6a6f0222aaac547fc84f917565ba78d1e32df4693226ec93aa8675f549b246b70913e9f1d80a87830b39843f9bd96b39d270e599ff4f71def6260 + checksum: 10c0/196eaf5e95b45a3f6d3001a2408d7dc6f146c29c873ed4e42e1ad4c9327122934fb3793a12b6ce3b7c25d355e738b20123acc0894ce30358c3370b15f4bd0865 languageName: node linkType: hard @@ -18016,6 +18292,15 @@ __metadata: languageName: node linkType: hard +"vue-resize@npm:^2.0.0-alpha.1": + version: 2.0.0-alpha.1 + resolution: "vue-resize@npm:2.0.0-alpha.1" + peerDependencies: + vue: ^3.0.0 + checksum: 10c0/47691bd15565d28d5ea83bcad07086f5b35584518d91b816b748d041ce4eee40a889a4e5ad6514d7bb65af642d36823a4b7e442d1e63f195f770d3d646848954 + languageName: node + linkType: hard + "vue@npm:^3.5.13": version: 3.5.17 resolution: "vue@npm:3.5.17" @@ -18365,7 +18650,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.8.0, yaml@npm:^2.8.1, yaml@npm:^2.8.3": +"yaml@npm:^2.0.0, yaml@npm:^2.8.0, yaml@npm:^2.8.1, yaml@npm:^2.8.2, yaml@npm:^2.8.3": version: 2.8.3 resolution: "yaml@npm:2.8.3" bin: