diff --git a/.github/actions/src/__tests__/lockfiles.test.ts b/.github/actions/src/__tests__/lockfiles.test.ts new file mode 100644 index 0000000000..6b5ae77e5d --- /dev/null +++ b/.github/actions/src/__tests__/lockfiles.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it, vi } from 'vitest'; +import * as utils from '../lockfiles.js'; + +vi.mock(import('../gitRoot.js'), () => ({ + gitRoot: 'root' +})); + +describe(utils.extractPackageName, () => { + it('works with packages that start with @', () => { + expect(utils.extractPackageName('@sourceacademy/tab-Rune@workspace:^')) + .toEqual('@sourceacademy/tab-Rune'); + }); + + it('works with regular package names', () => { + expect(utils.extractPackageName('lodash@npm:^4.17.20')) + .toEqual('lodash'); + }); + + it('throws on invalid package name', () => { + expect(() => utils.extractPackageName('something weird')) + .toThrowError('Invalid package name: something weird'); + }); +}); diff --git a/.github/actions/src/__tests__/sample_why.txt b/.github/actions/src/__tests__/sample_why.txt new file mode 100644 index 0000000000..dde2fbfdd6 --- /dev/null +++ b/.github/actions/src/__tests__/sample_why.txt @@ -0,0 +1,13 @@ +{"value":"@sourceacademy/bundle-unittest@workspace:src/bundles/unittest","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/markdown-plugin-directory-tree@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:lib/markdown-tree","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/markdown-plugin-directory-tree@workspace:lib/markdown-tree","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-github-actions@workspace:.github/actions","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-repotools@virtual:1448f4d06c0cf02cc986bce0139b31c0f3c95af1573e69673bc11ad2928a2fc505818a40d3f1caf168c35ce00c20238d52dfdba1940c59c81fd5df1b08ba49d6#workspace:lib/repotools","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-repotools@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:lib/repotools","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules-repotools@workspace:lib/repotools","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"@sourceacademy/modules@workspace:.","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"archiver-utils@npm:5.0.2","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.15"}}} +{"value":"js-slang@npm:1.0.85","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} +{"value":"source-academy-wabt@npm:1.1.3","children":{"lodash@npm:4.17.21":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"}}} diff --git a/.github/actions/src/info/__tests__/index.test.ts b/.github/actions/src/info/__tests__/index.test.ts index a68c04b57e..0effab3c01 100644 --- a/.github/actions/src/info/__tests__/index.test.ts +++ b/.github/actions/src/info/__tests__/index.test.ts @@ -4,7 +4,7 @@ import pathlib from 'path'; import * as core from '@actions/core'; import { describe, expect, test, vi } from 'vitest'; import * as git from '../../commons.js'; -import * as lockfiles from '../../lockfiles/index.js'; +import * as lockfiles from '../../lockfiles.js'; import { getAllPackages, getRawPackages, main } from '../index.js'; const mockedCheckChanges = vi.spyOn(git, 'checkDirForChanges'); diff --git a/.github/actions/src/info/index.ts b/.github/actions/src/info/index.ts index 16dcb67a4c..d84751e6b1 100644 --- a/.github/actions/src/info/index.ts +++ b/.github/actions/src/info/index.ts @@ -6,7 +6,7 @@ import type { SummaryTableRow } from '@actions/core/lib/summary.js'; import packageJson from '../../../../package.json' with { type: 'json' }; import { checkDirForChanges, type PackageRecord, type RawPackageRecord } from '../commons.js'; import { gitRoot } from '../gitRoot.js'; -import { getPackagesWithResolutionChanges, hasLockFileChanged } from '../lockfiles/index.js'; +import { getPackagesWithResolutionChanges, hasLockFileChanged } from '../lockfiles.js'; import { topoSortPackages } from './sorter.js'; const packageNameRE = /^@sourceacademy\/(.+?)-(.+)$/u; @@ -16,7 +16,7 @@ const packageNameRE = /^@sourceacademy\/(.+?)-(.+)$/u; * an unprocessed format */ export async function getRawPackages(gitRoot: string, maxDepth?: number) { - let packagesWithResolutionChanges: Set | null = null; + let packagesWithResolutionChanges: string[] | null = null; // If there are lock file changes we need to set hasChanges to true for // that package even if that package's directory has no changes @@ -43,7 +43,7 @@ export async function getRawPackages(gitRoot: string, maxDepth?: number) { output[packageJson.name] = { directory: currentDir, - hasChanges: packagesWithResolutionChanges?.has(packageJson.name) ?? hasChanges, + hasChanges: hasChanges || !!packagesWithResolutionChanges?.includes(packageJson.name), package: packageJson }; } catch (error) { diff --git a/.github/actions/src/lockfiles.ts b/.github/actions/src/lockfiles.ts new file mode 100644 index 0000000000..028cbabed5 --- /dev/null +++ b/.github/actions/src/lockfiles.ts @@ -0,0 +1,173 @@ +import fs from 'fs/promises'; +import pathlib from 'path'; +import * as core from '@actions/core'; +import { getExecOutput } from '@actions/exec'; +import memoize from 'lodash/memoize.js'; +import { extractPkgsFromYarnLockV2 } from 'snyk-nodejs-lockfile-parser'; +import { gitRoot } from './gitRoot.js'; + +const packageNameRE = /^(.+)@.+$/; + +/** + * Lockfile specifications come in the form of package_name@resolution, but + * we only want the package name. This function extracts that package name, + * accounting for the fact that package names might start with '@' + */ +export function extractPackageName(raw: string) { + const match = packageNameRE.exec(raw); + if (!match) { + throw new Error(`Invalid package name: ${raw}`); + } + + return match[1]; +} + +/** + * Parses and lockfile's contents and extracts all the different dependencies and + * versions + */ +function processLockFileText(contents: string) { + const lockFile = extractPkgsFromYarnLockV2(contents); + const mappings = new Set(); + for (const [pkgSpecifier, { resolution }] of Object.entries(lockFile)) { + mappings.add(`${pkgSpecifier} -> ${resolution}`); + } + return mappings; +} + +/** + * Retrieves the contents of the lockfile in the repo + */ +async function getCurrentLockFile() { + const lockFilePath = pathlib.join(gitRoot, 'yarn.lock'); + const contents = await fs.readFile(lockFilePath, 'utf-8'); + return processLockFileText(contents); +} + +/** + * Retrieves the contents of the lockfile on the master branch + */ +async function getMasterLockFile() { + const { stdout, stderr, exitCode } = await getExecOutput( + 'git', + [ + '--no-pager', + 'show', + 'origin/master:yarn.lock' + ], + { silent: true } + ); + + if (exitCode !== 0) { + core.error(stderr); + throw new Error('git show exited with non-zero error-code'); + } + + return processLockFileText(stdout); +} + +interface ResolutionSpec { pkgSpecifier: string, pkgName: string } + +/** + * Parsed output entry returned by `yarn why` + */ +interface YarnWhyOutput { + value: string; + children: { + [locator: string]: { + locator: string; + descriptor: string; + }; + }; +} + +/** + * Run `yarn why ` to see why a package is included + * Don't use recursive (-R) since we want to build the graph ourselves + * @function + */ +const runYarnWhy = memoize(async (pkgName: string) => { + // Memoize the call so that we don't need to call yarn why multiple times for each package + const { stdout: output, exitCode, stderr } = await getExecOutput('yarn', ['why', pkgName, '--json'], { silent: true }); + if (exitCode !== 0) { + core.error(stderr); + throw new Error(`yarn why for ${pkgName} failed!`); + } + + return output.split('\n').reduce((res, each) => { + each = each.trim(); + if (each === '') return res; + + const pkg = JSON.parse(each) as YarnWhyOutput; + return [...res, pkg]; + }, []); +}) + +/** + * Determines the names of the packages that have changed versions + */ +export async function getPackagesWithResolutionChanges() { + const [currentLockFileMappings, masterLockFileMappings] = await Promise.all([ + getCurrentLockFile(), + getMasterLockFile() + ]); + + const changes = new Set(masterLockFileMappings); + for (const edge of currentLockFileMappings) { + changes.delete(edge); + } + + const frontier: ResolutionSpec[] = []; + const changedDeps = new Set(); + for (const edge of changes) { + const [pkgSpecifier] = edge.split(' -> '); + changedDeps.add(pkgSpecifier); + frontier.push({ + pkgSpecifier, + pkgName: extractPackageName(pkgSpecifier) + }); + } + + while (frontier.length > 0) { + const { pkgName, pkgSpecifier } = frontier.shift()!; + + const reasons = await runYarnWhy(pkgName); + reasons.forEach(pkg => { + if (changedDeps.has(pkg.value)) { + // If we've already added this pkg specifier, don't need to explore it again + return; + } + + const childrenSpecifiers = Object.values(pkg.children).map(({ descriptor }) => descriptor); + if (!childrenSpecifiers.includes(pkgSpecifier)) return; + + frontier.push({ pkgSpecifier: pkg.value, pkgName: extractPackageName(pkg.value) }); + changedDeps.add(pkg.value); + }); + } + + core.info('=== Summary of dirty monorepo packages ===\n'); + const pkgsToRebuild = [...changedDeps].filter(pkgSpecifier => pkgSpecifier.includes('@workspace:')); + for (const pkgName of pkgsToRebuild) { + core.info(`- ${pkgName}`); + } + + return pkgsToRebuild.map(extractPackageName); +} + +/** + * Returns `true` if there are changes present in the given directory relative to + * the master branch\ + * Used to determine, particularly for libraries, if running tests and tsc are necessary + */ +export const hasLockFileChanged = memoize(async () => { + const { exitCode } = await getExecOutput( + 'git --no-pager diff --quiet origin/master -- yarn.lock', + [], + { + failOnStdErr: false, + ignoreReturnCode: true + } + ); + return exitCode !== 0; +}); diff --git a/.github/actions/src/lockfiles/__tests__/lockfiles.test.ts b/.github/actions/src/lockfiles/__tests__/lockfiles.test.ts deleted file mode 100644 index 3af81be9ee..0000000000 --- a/.github/actions/src/lockfiles/__tests__/lockfiles.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type fs from 'fs/promises'; -import type pathlib from 'path'; -import * as exec from '@actions/exec'; -import { describe, expect, it, vi } from 'vitest'; -import * as utils from '../utils.js'; - -vi.mock(import('../../gitRoot.js'), () => ({ - gitRoot: 'root' -})); - -const mockedExec = vi.spyOn(exec, 'getExecOutput'); - -describe(utils.extractPackageName, () => { - it('works with packages that start with @', () => { - expect(utils.extractPackageName('@sourceacademy/tab-Rune@workspace:^')) - .toEqual('@sourceacademy/tab-Rune'); - }); - - it('works with regular package names', () => { - expect(utils.extractPackageName('lodash@npm:^4.17.20')) - .toEqual('lodash'); - }); - - it('throws an error on an invalid package name', () => { - expect(() => utils.extractPackageName('something weird')) - .toThrowError('Invalid package name: something weird'); - }); -}); - -describe(utils.getPackageReason, async () => { - const { join }: typeof pathlib = await vi.importActual('path'); - const { readFile }: typeof fs = await vi.importActual('fs/promises'); - - const textPath = join(import.meta.dirname, 'sample_why.txt'); - const sampleText = await readFile(textPath, 'utf-8'); - - it('works', async () => { - mockedExec.mockResolvedValueOnce({ - stdout: sampleText, - stderr: '', - exitCode: 0 - }); - - const retValue = await utils.getPackageReason('lodash'); - expect(retValue).toMatchInlineSnapshot(` - [ - "@sourceacademy/bundle-ar", - "@sourceacademy/bundle-arcade_2d", - "@sourceacademy/bundle-binary_tree", - "@sourceacademy/bundle-communication", - "@sourceacademy/bundle-copy_gc", - "@sourceacademy/bundle-csg", - "@sourceacademy/bundle-curve", - "@sourceacademy/bundle-game", - "@sourceacademy/bundle-mark_sweep", - "@sourceacademy/bundle-midi", - "@sourceacademy/bundle-nbody", - "@sourceacademy/bundle-painter", - "@sourceacademy/bundle-physics_2d", - "@sourceacademy/bundle-pix_n_flix", - "@sourceacademy/bundle-plotly", - "@sourceacademy/bundle-remote_execution", - "@sourceacademy/bundle-repeat", - "@sourceacademy/bundle-repl", - "@sourceacademy/bundle-robot_simulation", - "@sourceacademy/bundle-rune", - "@sourceacademy/bundle-rune_in_words", - "@sourceacademy/bundle-scrabble", - "@sourceacademy/bundle-sound", - "@sourceacademy/bundle-sound_matrix", - "@sourceacademy/bundle-stereo_sound", - "@sourceacademy/bundle-unittest", - "@sourceacademy/bundle-unity_academy", - "@sourceacademy/bundle-wasm", - "@sourceacademy/lint-plugin", - "@sourceacademy/markdown-plugin-directory-tree", - "@sourceacademy/modules-buildtools", - "@sourceacademy/modules-devserver", - "@sourceacademy/modules-docserver", - "@sourceacademy/modules-github-actions", - "@sourceacademy/modules-lib", - "@sourceacademy/modules-repotools", - "@sourceacademy/modules", - "@sourceacademy/tab-ArcadeTwod", - "@sourceacademy/tab-AugmentedReality", - "@sourceacademy/tab-CopyGc", - "@sourceacademy/tab-Csg", - "@sourceacademy/tab-Curve", - "@sourceacademy/tab-Game", - "@sourceacademy/tab-MarkSweep", - "@sourceacademy/tab-Nbody", - "@sourceacademy/tab-Painter", - "@sourceacademy/tab-Physics2D", - "@sourceacademy/tab-Pixnflix", - "@sourceacademy/tab-Plotly", - "@sourceacademy/tab-Repeat", - "@sourceacademy/tab-Repl", - "@sourceacademy/tab-RobotSimulation", - "@sourceacademy/tab-Rune", - "@sourceacademy/tab-Sound", - "@sourceacademy/tab-SoundMatrix", - "@sourceacademy/tab-StereoSound", - "@sourceacademy/tab-Unittest", - "@sourceacademy/tab-UnityAcademy", - ] - `); - expect(mockedExec).toHaveBeenCalledOnce(); - }); -}); diff --git a/.github/actions/src/lockfiles/__tests__/sample_why.txt b/.github/actions/src/lockfiles/__tests__/sample_why.txt deleted file mode 100644 index 240cec0056..0000000000 --- a/.github/actions/src/lockfiles/__tests__/sample_why.txt +++ /dev/null @@ -1,58 +0,0 @@ -{"value":"@sourceacademy/bundle-ar@workspace:src/bundles/ar","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-arcade_2d@workspace:src/bundles/arcade_2d","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-binary_tree@workspace:src/bundles/binary_tree","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}}}}}} -{"value":"@sourceacademy/bundle-communication@workspace:src/bundles/communication","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-copy_gc@workspace:src/bundles/copy_gc","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-csg@workspace:src/bundles/csg","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-curve@workspace:src/bundles/curve","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-game@workspace:src/bundles/game","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-mark_sweep@workspace:src/bundles/mark_sweep","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-midi@workspace:src/bundles/midi","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-nbody@workspace:src/bundles/nbody","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-painter@workspace:src/bundles/painter","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-physics_2d@workspace:src/bundles/physics_2d","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-pix_n_flix@workspace:src/bundles/pix_n_flix","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-plotly@workspace:src/bundles/plotly","children":{"@sourceacademy/bundle-curve@workspace:src/bundles/curve":{"value":{"locator":"@sourceacademy/bundle-curve@workspace:src/bundles/curve","descriptor":"@sourceacademy/bundle-curve@workspace:^"},"children":{}},"@sourceacademy/bundle-sound@workspace:src/bundles/sound":{"value":{"locator":"@sourceacademy/bundle-sound@workspace:src/bundles/sound","descriptor":"@sourceacademy/bundle-sound@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-remote_execution@workspace:src/bundles/remote_execution","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-repeat@workspace:src/bundles/repeat","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-repl@workspace:src/bundles/repl","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-robot_simulation@workspace:src/bundles/robot_simulation","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-rune@workspace:src/bundles/rune","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-rune_in_words@workspace:src/bundles/rune_in_words","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-scrabble@workspace:src/bundles/scrabble","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-sound@workspace:src/bundles/sound","children":{"@sourceacademy/bundle-midi@workspace:src/bundles/midi":{"value":{"locator":"@sourceacademy/bundle-midi@workspace:src/bundles/midi","descriptor":"@sourceacademy/bundle-midi@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-sound_matrix@workspace:src/bundles/sound_matrix","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-stereo_sound@workspace:src/bundles/stereo_sound","children":{"@sourceacademy/bundle-midi@workspace:src/bundles/midi":{"value":{"locator":"@sourceacademy/bundle-midi@workspace:src/bundles/midi","descriptor":"@sourceacademy/bundle-midi@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/bundle-unittest@workspace:src/bundles/unittest","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}},"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}}}} -{"value":"@sourceacademy/bundle-unity_academy@workspace:src/bundles/unity_academy","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/bundle-wasm@workspace:src/bundles/wasm","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"source-academy-wabt@npm:1.1.3":{"value":{"locator":"source-academy-wabt@npm:1.1.3","descriptor":"source-academy-wabt@npm:^1.0.4"},"children":{"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}}}}}} -{"value":"@sourceacademy/lint-plugin@workspace:lib/lintplugin","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools":{"value":{"locator":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools","descriptor":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/markdown-plugin-directory-tree@workspace:lib/markdown-tree","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}},"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools":{"value":{"locator":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools","descriptor":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","children":{"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}},"@sourceacademy/modules-repotools@virtual:1448f4d06c0cf02cc986bce0139b31c0f3c95af1573e69673bc11ad2928a2fc505818a40d3f1caf168c35ce00c20238d52dfdba1940c59c81fd5df1b08ba49d6#workspace:lib/repotools":{"value":{"locator":"@sourceacademy/modules-repotools@virtual:1448f4d06c0cf02cc986bce0139b31c0f3c95af1573e69673bc11ad2928a2fc505818a40d3f1caf168c35ce00c20238d52dfdba1940c59c81fd5df1b08ba49d6#workspace:lib/repotools","descriptor":"@sourceacademy/modules-repotools@virtual:1448f4d06c0cf02cc986bce0139b31c0f3c95af1573e69673bc11ad2928a2fc505818a40d3f1caf168c35ce00c20238d52dfdba1940c59c81fd5df1b08ba49d6#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/modules-devserver@workspace:devserver","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/modules-docserver@workspace:docs","children":{"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}},"@sourceacademy/lint-plugin@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:lib/lintplugin":{"value":{"locator":"@sourceacademy/lint-plugin@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:lib/lintplugin","descriptor":"@sourceacademy/lint-plugin@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:^"},"children":{}},"@sourceacademy/markdown-plugin-directory-tree@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:lib/markdown-tree":{"value":{"locator":"@sourceacademy/markdown-plugin-directory-tree@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:lib/markdown-tree","descriptor":"@sourceacademy/markdown-plugin-directory-tree@virtual:f0562030a653a107496800a80ececfadfd737a6a260bbd2572ca4e42de1d6b97749197bb1c41d0d71c9f18ab009789560666041994678fe1aba63902e9fb15bd#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/modules-github-actions@workspace:.github/actions","children":{"@actions/artifact@npm:2.3.2":{"value":{"locator":"@actions/artifact@npm:2.3.2","descriptor":"@actions/artifact@npm:^2.3.2"},"children":{"archiver@npm:7.0.1":{"value":{"locator":"archiver@npm:7.0.1","descriptor":"archiver@npm:^7.0.1"},"children":{"archiver-utils@npm:5.0.2":{"value":{"locator":"archiver-utils@npm:5.0.2","descriptor":"archiver-utils@npm:^5.0.2"},"children":{"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.15"},"children":{}}}},"zip-stream@npm:6.0.1":{"value":{"locator":"zip-stream@npm:6.0.1","descriptor":"zip-stream@npm:^6.0.1"},"children":{"archiver-utils@npm:5.0.2":{"value":{"locator":"archiver-utils@npm:5.0.2","descriptor":"archiver-utils@npm:^5.0.0"},"children":{}}}}}}}},"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}},"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools":{"value":{"locator":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:lib/repotools","descriptor":"@sourceacademy/modules-repotools@virtual:420fa510eaf06edbb20ddb72fbcd454e937e7e266b8c6b7a55ab314e0562f2688175809f2b44c78563a0467da21b6d34688fe0020ecef5e669997fe518ee56e1#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/modules-lib@workspace:lib/modules-lib","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}}}} -{"value":"@sourceacademy/modules-repotools@workspace:lib/repotools","children":{"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}}}} -{"value":"@sourceacademy/modules@workspace:.","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"js-slang@npm:1.0.85":{"value":{"locator":"js-slang@npm:1.0.85","descriptor":"js-slang@npm:^1.0.81"},"children":{}},"lodash@npm:4.17.21":{"value":{"locator":"lodash@npm:4.17.21","descriptor":"lodash@npm:^4.17.21"},"children":{}},"@sourceacademy/lint-plugin@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:lib/lintplugin":{"value":{"locator":"@sourceacademy/lint-plugin@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:lib/lintplugin","descriptor":"@sourceacademy/lint-plugin@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:^"},"children":{}},"@sourceacademy/modules-repotools@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:lib/repotools":{"value":{"locator":"@sourceacademy/modules-repotools@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:lib/repotools","descriptor":"@sourceacademy/modules-repotools@virtual:72a684de9912e8b42da7a581a5478d78ad73e0a505830e61e9b2fbc990fcc10719f71ef5373c26313c28b9295f1381b0d95a4d0ab2ae6e33671c2ba25a5c1dc2#workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-ArcadeTwod@workspace:src/tabs/ArcadeTwod","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-AugmentedReality@workspace:src/tabs/AugmentedReality","children":{"@sourceacademy/bundle-ar@workspace:src/bundles/ar":{"value":{"locator":"@sourceacademy/bundle-ar@workspace:src/bundles/ar","descriptor":"@sourceacademy/bundle-ar@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-CopyGc@workspace:src/tabs/CopyGc","children":{"@sourceacademy/bundle-copy_gc@workspace:src/bundles/copy_gc":{"value":{"locator":"@sourceacademy/bundle-copy_gc@workspace:src/bundles/copy_gc","descriptor":"@sourceacademy/bundle-copy_gc@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Csg@workspace:src/tabs/Csg","children":{"@sourceacademy/bundle-csg@workspace:src/bundles/csg":{"value":{"locator":"@sourceacademy/bundle-csg@workspace:src/bundles/csg","descriptor":"@sourceacademy/bundle-csg@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Curve@workspace:src/tabs/Curve","children":{"@sourceacademy/bundle-curve@workspace:src/bundles/curve":{"value":{"locator":"@sourceacademy/bundle-curve@workspace:src/bundles/curve","descriptor":"@sourceacademy/bundle-curve@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Game@workspace:src/tabs/Game","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-MarkSweep@workspace:src/tabs/MarkSweep","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Nbody@workspace:src/tabs/Nbody","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Painter@workspace:src/tabs/Painter","children":{"@sourceacademy/bundle-painter@workspace:src/bundles/painter":{"value":{"locator":"@sourceacademy/bundle-painter@workspace:src/bundles/painter","descriptor":"@sourceacademy/bundle-painter@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Physics2D@workspace:src/tabs/Physics2D","children":{"@sourceacademy/bundle-physics_2d@workspace:src/bundles/physics_2d":{"value":{"locator":"@sourceacademy/bundle-physics_2d@workspace:src/bundles/physics_2d","descriptor":"@sourceacademy/bundle-physics_2d@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Pixnflix@workspace:src/tabs/Pixnflix","children":{"@sourceacademy/bundle-pix_n_flix@workspace:src/bundles/pix_n_flix":{"value":{"locator":"@sourceacademy/bundle-pix_n_flix@workspace:src/bundles/pix_n_flix","descriptor":"@sourceacademy/bundle-pix_n_flix@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Plotly@workspace:src/tabs/Plotly","children":{"@sourceacademy/bundle-plotly@workspace:src/bundles/plotly":{"value":{"locator":"@sourceacademy/bundle-plotly@workspace:src/bundles/plotly","descriptor":"@sourceacademy/bundle-plotly@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Repeat@workspace:src/tabs/Repeat","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Repl@workspace:src/tabs/Repl","children":{"@sourceacademy/bundle-repl@workspace:src/bundles/repl":{"value":{"locator":"@sourceacademy/bundle-repl@workspace:src/bundles/repl","descriptor":"@sourceacademy/bundle-repl@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-RobotSimulation@workspace:src/tabs/RobotSimulation","children":{"@sourceacademy/bundle-robot_simulation@workspace:src/bundles/robot_simulation":{"value":{"locator":"@sourceacademy/bundle-robot_simulation@workspace:src/bundles/robot_simulation","descriptor":"@sourceacademy/bundle-robot_simulation@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Rune@workspace:src/tabs/Rune","children":{"@sourceacademy/bundle-rune@workspace:src/bundles/rune":{"value":{"locator":"@sourceacademy/bundle-rune@workspace:src/bundles/rune","descriptor":"@sourceacademy/bundle-rune@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Sound@workspace:src/tabs/Sound","children":{"@sourceacademy/bundle-sound@workspace:src/bundles/sound":{"value":{"locator":"@sourceacademy/bundle-sound@workspace:src/bundles/sound","descriptor":"@sourceacademy/bundle-sound@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-SoundMatrix@workspace:src/tabs/SoundMatrix","children":{"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-StereoSound@workspace:src/tabs/StereoSound","children":{"@sourceacademy/bundle-stereo_sound@workspace:src/bundles/stereo_sound":{"value":{"locator":"@sourceacademy/bundle-stereo_sound@workspace:src/bundles/stereo_sound","descriptor":"@sourceacademy/bundle-stereo_sound@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-Unittest@workspace:src/tabs/Unittest","children":{"@sourceacademy/bundle-unittest@workspace:src/bundles/unittest":{"value":{"locator":"@sourceacademy/bundle-unittest@workspace:src/bundles/unittest","descriptor":"@sourceacademy/bundle-unittest@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} -{"value":"@sourceacademy/tab-UnityAcademy@workspace:src/tabs/UnityAcademy","children":{"@sourceacademy/bundle-unity_academy@workspace:src/bundles/unity_academy":{"value":{"locator":"@sourceacademy/bundle-unity_academy@workspace:src/bundles/unity_academy","descriptor":"@sourceacademy/bundle-unity_academy@workspace:^"},"children":{}},"@sourceacademy/modules-buildtools@workspace:lib/buildtools":{"value":{"locator":"@sourceacademy/modules-buildtools@workspace:lib/buildtools","descriptor":"@sourceacademy/modules-buildtools@workspace:^"},"children":{}},"@sourceacademy/modules-lib@workspace:lib/modules-lib":{"value":{"locator":"@sourceacademy/modules-lib@workspace:lib/modules-lib","descriptor":"@sourceacademy/modules-lib@workspace:^"},"children":{}}}} diff --git a/.github/actions/src/lockfiles/index.ts b/.github/actions/src/lockfiles/index.ts deleted file mode 100644 index cddb5feae5..0000000000 --- a/.github/actions/src/lockfiles/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getExecOutput } from '@actions/exec'; -import memoize from 'lodash/memoize.js'; -import { getPackageDiffs, getPackageReason } from './utils.js'; - -/** - * Returns `true` if there are changes present in the given directory relative to - * the master branch\ - * Used to determine, particularly for libraries, if running tests and tsc are necessary - */ -export const hasLockFileChanged = memoize(async () => { - const { exitCode } = await getExecOutput( - 'git --no-pager diff --quiet origin/master -- yarn.lock', - [], - { - failOnStdErr: false, - ignoreReturnCode: true - } - ); - return exitCode !== 0; -}); - -/** - * Gets a list of packages that have had some dependency of theirs - * change according to the lockfile but not their package.json - */ -export async function getPackagesWithResolutionChanges() { - const packages = [...await getPackageDiffs()]; - const workspaces = await Promise.all(packages.map(getPackageReason)); - return new Set(workspaces.flat()); -} diff --git a/.github/actions/src/lockfiles/utils.ts b/.github/actions/src/lockfiles/utils.ts deleted file mode 100644 index 8a160287c3..0000000000 --- a/.github/actions/src/lockfiles/utils.ts +++ /dev/null @@ -1,130 +0,0 @@ -import fs from 'fs/promises'; -import pathlib from 'path'; -import * as core from '@actions/core'; -import { getExecOutput } from '@actions/exec'; -import { extractPkgsFromYarnLockV2 } from 'snyk-nodejs-lockfile-parser'; -import { gitRoot } from '../gitRoot.js'; - -const packageNameRE = /^(.+)@.+$/; - -/** - * Lockfile specifications come in the form of package_name@resolution, but - * we only want the package name. This function extracts that package name, - * accounting for the fact that package names might start with '@' - */ -export function extractPackageName(raw: string) { - const match = packageNameRE.exec(raw); - if (!match) { - throw new Error(`Invalid package name: ${raw}`); - } - - return match[1]; -} - -/** - * Using `yarn why`, determine which repository package is the reason why the - * given package is present in the lockfile. - */ -export async function getPackageReason(pkg: string) { - const { stdout, stderr, exitCode } = await getExecOutput('yarn why', [pkg, '-R', '--json'], { silent: true }); - if (exitCode !== 0) { - core.error(stderr); - throw new Error(`yarn why for '${pkg}' exited with non-zero exit code!`); - } - - return stdout.trim().split('\n') - .filter(l => l.trim().length > 0) - .map(each => { - const entry = JSON.parse(each).value; - return extractPackageName(entry); - }); -} - -/** - * Determines the names of the packages that have changed versions - */ -export async function getPackageDiffs() { - const [currentLockFile, masterLockFile] = await Promise.all([ - getCurrentLockFile(), - getMasterLockFile() - ]); - - const packages = new Set(); - - for (const [depName, versions] of Object.entries(currentLockFile)) { - if (packages.has(depName)) continue; - - if (!(depName in masterLockFile)) { - core.info(`${depName} in current lockfile, not in master lock file`); - continue; - } - - let needToAdd = false; - for (const version of versions) { - if (!masterLockFile[depName].has(version)) { - core.info(`${depName} has ${version} in current lockfile but not in master`); - needToAdd = true; - } - } - - for (const version of masterLockFile[depName]) { - if (!versions.has(version)) { - core.info(`${depName} has ${version} in master lockfile but not in current`); - needToAdd = true; - } - } - - if (needToAdd) packages.add(depName); - } - - return packages; -} - -/** - * Retrieves the contents of the lockfile in the repo - */ -export async function getCurrentLockFile() { - const lockFilePath = pathlib.join(gitRoot, 'yarn.lock'); - const contents = await fs.readFile(lockFilePath, 'utf-8'); - return processLockFileText(contents); -} - -/** - * Retrieves the contents of the lockfile on the master branch - */ -export async function getMasterLockFile() { - const { stdout, stderr, exitCode } = await getExecOutput( - 'git', - [ - '--no-pager', - 'show', - 'origin/master:yarn.lock' - ], - { silent: true } - ); - - if (exitCode !== 0) { - core.error(stderr); - throw new Error('git show exited with non-zero error-code'); - } - - return processLockFileText(stdout); -} - -/** - * Parses and lockfile's contents and extracts all the different dependencies and - * versions - */ -export function processLockFileText(contents: string) { - const lockFile = extractPkgsFromYarnLockV2(contents); - return Object.entries(lockFile).reduce>>((res, [key, { version }]) => { - const newKey = extractPackageName(key); - - if (!(newKey in res)) { - res[newKey] = new Set(); - } - - res[newKey].add(version); - return res; - }, {}); -} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 785b7ef138..f93eed1385 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -149,7 +149,7 @@ jobs: uses: ./.github/actions/src/init with: package-name: ${{ matrix.bundleInfo.name }} - playwright: ${{ matrix.bundleInfo.needsPlaywright }} + playwright: ${{ matrix.bundleInfo.needsPlaywright && matrix.bundleInfo.changes }} - name: Build Bundle run: | diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 6ae20e6a79..074bb36a24 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,6 +1,6 @@ // Vitepress config import pathlib from 'path'; -import { directoryTreePlugin } from '@sourceacademy/markdown-plugin-directory-tree'; +import { directoryTreePlugin, dirtreeTransformer, grammar as dirtreetm } from '@sourceacademy/markdown-plugin-directory-tree'; import { defineConfig, type UserConfig } from 'vitepress'; import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'; import { withMermaid } from 'vitepress-plugin-mermaid'; @@ -20,9 +20,8 @@ const vitepressOptions: UserConfig = { md.use(groupIconMdPlugin); md.use(directoryTreePlugin); }, - languageAlias: { - dirtree: 'yml' - } + codeTransformers: [dirtreeTransformer()], + languages: [dirtreetm] }, outDir: pathlib.join(import.meta.dirname, '..', '..', 'build', 'devdocs'), srcDir: 'src', diff --git a/docs/src/modules/1-getting-started/1-overview.md b/docs/src/modules/1-getting-started/1-overview.md index 173043be5d..17f72a77bb 100644 --- a/docs/src/modules/1-getting-started/1-overview.md +++ b/docs/src/modules/1-getting-started/1-overview.md @@ -3,6 +3,54 @@ This page contains information regarding the overview of the Source Modules system. If you want to skip this overview, navigate to the bottom of the page where the **next page** button is located. +## Repository Structure + +```dirtree +name: modules +path: ../../../.. +children: +- name: .github + children: + - name: actions + comment: Source code for custom Github Actions needed by the repo + + - name: workflows + comment: Directory containing Github workflow configurations + +- name: build + comment: Output directory for bundles and tabs + +- name: devserver + comment: Source files for devserver + +- name: docs + comment: Source files for modules documentation + +- name: lib + comment: Common libraries for use throughout the repository + +- name: src + children: + - name: bundles + comment: Directories containing bundles + - name: tabs + comment: Directories containing tabs + - name: java + comment: Files for java-slang + +- name: eslint.config.js + comment: ESLint configuration for the entire repository + +- name: tsconfig.json + comment: tsconfig for vitest configuration files + +- name: vitest.config.ts + comment: Root Vitest configuration for the entire repository + +- name: yarn.config.cjs + comment: Yarn constraints file +``` + ## Terminology The module system imitates ESM Javascript, allowing the use of `import` statements to import external code into Source programs: @@ -58,3 +106,4 @@ The project was suggested by Professor Martin Henz who first proposed to include > The Source Academy frontend will fetch the module from the backend, which of course will cache it. Our modules can still be written in JavaScript and loaded in the frontend in whatever way works (e.g. `eval`). > > In the future, we can allow for a more flexible scheme where users can import Source programs using the same syntax. + diff --git a/docs/src/modules/1-getting-started/3-git.md b/docs/src/modules/1-getting-started/3-git.md index 5438965386..948dea5b39 100644 --- a/docs/src/modules/1-getting-started/3-git.md +++ b/docs/src/modules/1-getting-started/3-git.md @@ -41,6 +41,10 @@ Make sure that the branch is named appropriately. For example, if you were creat When you're ready for your work to be incorporated into the `master` branch, [open](https://github.com/source-academy/modules/compare) a pull request with the modules repository to merge your changes into `master` branch. Make sure that your branch is up-to-date with the `master` branch (by performing a `git merge` from the `master` branch to your own branch). +When you open a PR in Github, Github will automatically scaffold the PR for you with some helpful headers and checklists. Do provide an informative description +of what your PR intends to achieve as well as mark off the relevant checklist items. You can refer to other PRs (both open as well as ones that have been closed) +for further examples of what to write. + This will automatically trigger the repository's workflows, which will verify that your changes don't break any of the existing code. If the workflow fails, check the summary and determine where the errors lie, then fix your code as necessary. diff --git a/docs/src/modules/1-getting-started/4-cheat.md b/docs/src/modules/1-getting-started/4-cheat.md index e8d9ff8ac2..bcbd611957 100644 --- a/docs/src/modules/1-getting-started/4-cheat.md +++ b/docs/src/modules/1-getting-started/4-cheat.md @@ -34,7 +34,7 @@ All commands have a `-h` or `--help` option that can be used to get more informa yarn add -D <package> - Adds a package to the current bundle or tab that will only be used during runtime
+ Adds a package to the current bundle or tab that will is not required at runtime
diff --git a/docs/src/modules/1-getting-started/5-faq.md b/docs/src/modules/1-getting-started/5-faq.md index 6075ceadd0..5e2dd6e818 100644 --- a/docs/src/modules/1-getting-started/5-faq.md +++ b/docs/src/modules/1-getting-started/5-faq.md @@ -43,17 +43,6 @@ As described in the paragraphs above, this return value of the `init()` function ## Set Up and Configuration -### How do we use our own local version of the js-slang interpreter with the modules? - -> I have made some code changes to js-slang library and I want to test them out with my own local modules. - -To use your local `js-slang` interpreter with your local modules, you will need to follow the steps below. - -1. Serve your modules on a local server, done by transpiling your modules into JavaScript (`yarn build`) and then serving them as static assets (`yarn serve`). The default url for the local server is `http://localhost:8022`. Note that `yarn serve` serves the contents of the `build` folder. -2. Ensure that your local version of `js-slang` is linked to your local `cadet-frontend`. This is achieved by `yarn link` at the local `js-slang` root folder and `yarn link js-slang` at the local `cadet-frontend` root folder. -3. Ensure that your `cadet-frontend` environment variable `REACT_APP_MODULE_BACKEND_URL` is set to the address of your locally served modules server (from step 1). Again, the default url for the local server is `http://localhost:8022`. -4. Start your `cadet-frontend` web server locally to test your module. - ### Is it possible to be using modules served from more than one location simultaneously? > I want to use my own modules served from `http://localhost:8022` and the official modules from `https://source-academy.github.io/modules` at the same time. Is it going to be possible? @@ -96,13 +85,3 @@ Currently, two options are supported. * [Inline styles](https://www.w3schools.com/react/react_css.asp), where styles are added as an object to the react component's `style` prop * CSS inside JavaScript (eg. [Styled Components](https://styled-components.com/)) - -## Interaction with `js-slang` - -### How can we switch to the Source interpreter rather than the Source transpiler? - -> I am modifying `js-slang`'s Source interpreter and need to change the mode of evaluation from the Source transpiler (default) to the Source interpreter. - -Include `"enable verbose";` as the first line of your program. Other functions of this setting can be found [here](https://github.com/source-academy/js-slang/blob/master/README.md#error-messages). - -Note that the Source Academy frontend also switches to the interpreter as soon as a breakpoint is set. An example of a system that requires that the interpreter is run instead of the transpiler is the environment model visualizer. diff --git a/docs/src/modules/2-bundle/1-overview/1-overview.md b/docs/src/modules/2-bundle/1-overview/1-overview.md index cec1013c5d..c6f56b4ac3 100644 --- a/docs/src/modules/2-bundle/1-overview/1-overview.md +++ b/docs/src/modules/2-bundle/1-overview/1-overview.md @@ -74,7 +74,7 @@ Note that `curve/functions.ts` exports both `createDrawFunction` and `make_point export { make_point } from './functions'; ``` -However, only `make_point` is exported at the bundle's entry point however `createDrawFunction` is not, so cadets will not be able to access it, identical to how ES modules behave. +However, only `make_point` is exported at the bundle's entry point while `createDrawFunction` is not, so cadets will not be able to access it, identical to how ES modules behave. ```js // User's Source program diff --git a/docs/src/modules/2-bundle/2-creating/2-creating.md b/docs/src/modules/2-bundle/2-creating/2-creating.md index 3d511fba96..3bb0d0a516 100644 --- a/docs/src/modules/2-bundle/2-creating/2-creating.md +++ b/docs/src/modules/2-bundle/2-creating/2-creating.md @@ -42,7 +42,7 @@ The command should have creates a new folder `src/bundles/new_bundle` with all t ![](./new_bundle.png) -From there you can edit your bundle as necessary +From there, you can edit your bundle as necessary. ## Naming your Bundle @@ -52,4 +52,4 @@ The name of your bundle is what Source users will import from to actually use yo import { func } from 'new_bundle'; ``` -Your bundle's name should be concise and give be somewhat descriptive of what functionalities your bundle provides. +Your bundle's name should be concise and be somewhat descriptive of what functionalities your bundle provides. diff --git a/docs/src/modules/5-advanced/linting.md b/docs/src/modules/5-advanced/linting.md index 19a52899be..e13e74b08d 100644 --- a/docs/src/modules/5-advanced/linting.md +++ b/docs/src/modules/5-advanced/linting.md @@ -41,7 +41,7 @@ const TextBox = require('Textbox').default; ## Ignoring Files By default, ESLint has been configured to not lint files in specific directories or matching specific patterns. You can see the ignore patterns in `eslint.config.js` under the section labelled "Global Ignores". Please -note that if any of your code files matche these ignore patterns, they will not be properly linted by ESLint. +note that if any of your code files matches these ignore patterns, they will not be properly linted by ESLint. ## Integration with Git Hooks diff --git a/docs/src/modules/5-advanced/misc.md b/docs/src/modules/5-advanced/misc.md index 33432532fe..e8c4763409 100644 --- a/docs/src/modules/5-advanced/misc.md +++ b/docs/src/modules/5-advanced/misc.md @@ -19,7 +19,7 @@ export function bar() { return 1; } Now consider a tab that wants to display something to the user depending on what function the user passed to the tab: -```tsx [tab.tsx] {9,11} +```tsx [tab.tsx] {8,10} import { foo, bar } from '@sourceacademy/modules-bundle0'; interface Props { diff --git a/docs/src/repotools/6-docserver/2-dirtree.md b/docs/src/repotools/6-docserver/2-dirtree.md index df9da762b6..a59b2a2626 100644 --- a/docs/src/repotools/6-docserver/2-dirtree.md +++ b/docs/src/repotools/6-docserver/2-dirtree.md @@ -3,11 +3,11 @@ The idea behind this plugin was inspired by [this](https://tree.nathanfriend.com/?) ASCII tree generator. ::: details Creating a Markdown-It Plugin + The documentation for developers looking to create a Markdown-It plugin are woefully inadequate. Thus, most of this plugin was written based on the implementations of other plugins, such as the Vitepress Mermaid plugin and Vitepress Code snippet plugin. It basically works by detecting when a code block has been assigned the `dirtree` language, parses the code block's content as YAML and converts that object into the directory structure that can then be rendered as text. -Using the `languageAlias` markdown configuration, we can get Vitepress to highlight and colour the rendered directory trees. ::: To create your own directory tree, use the `dirtree` language with your markdown code block: @@ -60,7 +60,7 @@ children: - name: item2 comment: Notice how all comments are children: - - name: item 3 + - name: item3 comment: aligned at the end - this_item_has_a_very_long_name ``` @@ -92,3 +92,15 @@ It would also check that `docs/root/item1` exists and that is is a directory (si If the check fails a warning will be printed to the console stating which files it wasn't able to locate. Path validation is optional; Not providing a path means that no validation is performed, so `dirtree`s can reflect arbitrary directory structures. + +## Highlighting + +Each level of nesting is given its own colour retrieved from the Github set of colours. Code highlighting in Vitepress is powered by [`shiki`](https://shiki.style). +After the initial YML gets parsed and transformed into the raw uncoloured directory tree, the rendered code block gets passed to a code transformer, which parses +the directory tree according to its own Textmate Grammar. Then, colours are applied to each line. + +The grammar and code transformer are also exported from the Markdown Tree plugin and are applied in the Vitepress configuration. Refer to the +`codeTransformers` and `languages` under the `markdown` set of options. + +Do note that directory trees are intended to reflect actual structures within an actual file system. Therefore, if you use names for items that aren't valid +as file names you might get incorrect highlighting. diff --git a/lib/__test_mocks__/bundles/tsconfig.json b/lib/__test_mocks__/bundles/tsconfig.json index 799c68db64..20300b7544 100644 --- a/lib/__test_mocks__/bundles/tsconfig.json +++ b/lib/__test_mocks__/bundles/tsconfig.json @@ -1,43 +1,5 @@ { - "compilerOptions": { - /* Allow JavaScript files to be imported inside your project, instead of just .ts and .tsx files. */ - "allowJs": false, - /* When set to true, allowSyntheticDefaultImports allows you to write an import like "import React from "react";" */ - "allowSyntheticDefaultImports": true, - /* See https://www.typescriptlang.org/tsconfig#esModuleInterop */ - "esModuleInterop": true, - /* Controls how JSX constructs are emitted in JavaScript files. This only affects output of JS files that started in .tsx files. */ - "jsx": "react-jsx", - /* See https://www.typescriptlang.org/tsconfig#lib */ - "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": "node", - /* Allows importing modules with a ‘.json’ extension, which is a common practice in node projects. */ - "resolveJsonModule": true, - /* The longest common path of all non-declaration input files. */ - "rootDir": "./", - /* 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. */ - "sourceMap": false, - /* Skip running typescript on declaration files. This option is needed due to a known bug in react-ace */ - "skipLibCheck": true, - /* The strict flag enables a wide range of type checking behavior that results in stronger guarantees of program correctness. */ - "strict": true, - "forceConsistentCasingInFileNames": 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, - "verbatimModuleSyntax": true, - "paths": { - "js-slang/context": ["./context.d.ts"] - }, - "ignoreDeprecations": "5.0" - }, - /* Specifies an array of filenames or patterns to include in the program. These filenames are resolved relative to the directory containing the tsconfig.json file. */ - "include": ["."], - /* Specifies an array of filenames or patterns that should be skipped when resolving include. */ - "exclude": ["jest.config.js"] + "extends": [ + "../../../src/bundles/tsconfig.json" + ] } diff --git a/lib/__test_mocks__/tabs/tab0/src/index.tsx b/lib/__test_mocks__/tabs/tab0/src/index.tsx index cb9aa00ba0..68a5527d6a 100644 --- a/lib/__test_mocks__/tabs/tab0/src/index.tsx +++ b/lib/__test_mocks__/tabs/tab0/src/index.tsx @@ -2,7 +2,7 @@ import { defineTab } from '@sourceacademy/modules-lib/tabs/utils'; export default defineTab({ toSpawn: () => true, - body: context => context, + body: () =>

Nothing

, label: 'SomeTab', - icon: 'SomeIcon' + iconName: 'add' }); diff --git a/lib/__test_mocks__/tabs/tab1/index.tsx b/lib/__test_mocks__/tabs/tab1/index.tsx index cb9aa00ba0..68a5527d6a 100644 --- a/lib/__test_mocks__/tabs/tab1/index.tsx +++ b/lib/__test_mocks__/tabs/tab1/index.tsx @@ -2,7 +2,7 @@ import { defineTab } from '@sourceacademy/modules-lib/tabs/utils'; export default defineTab({ toSpawn: () => true, - body: context => context, + body: () =>

Nothing

, label: 'SomeTab', - icon: 'SomeIcon' + iconName: 'add' }); diff --git a/lib/__test_mocks__/tabs/tsconfig.json b/lib/__test_mocks__/tabs/tsconfig.json index 5de3cfada1..d545f675aa 100644 --- a/lib/__test_mocks__/tabs/tsconfig.json +++ b/lib/__test_mocks__/tabs/tsconfig.json @@ -1,4 +1,7 @@ { + "extends": [ + "../../../src/tabs/tsconfig.json" + ], "compilerOptions": { "noEmit": true } diff --git a/lib/buildtools/package.json b/lib/buildtools/package.json index 15d991d8be..eed2979723 100644 --- a/lib/buildtools/package.json +++ b/lib/buildtools/package.json @@ -9,7 +9,7 @@ "@types/http-server": "^0.12.4", "@types/lodash": "^4.14.198", "@types/node": "^22.15.30", - "typescript": "^5.8.2" + "typescript": "^5.9.3" }, "exports": null, "bin": { @@ -30,7 +30,6 @@ "http-server": "^14.1.1", "jsdom": "^26.1.0", "lodash": "^4.17.21", - "typedoc": "^0.28.9", "vite": "^7.1.11", "vitest": "^4.0.4" }, diff --git a/lib/buildtools/src/build/__tests__/all.test.ts b/lib/buildtools/src/build/__tests__/all.test.ts deleted file mode 100644 index 3be1260d24..0000000000 --- a/lib/buildtools/src/build/__tests__/all.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import pathlib from 'path'; -import { afterEach, describe, expect, test, vi } from 'vitest'; -import { testMocksDir } from '../../__tests__/fixtures.js'; -import * as docs from '../../build/docs/index.js'; -import * as modules from '../../build/modules/index.js'; -import { getCommandRunner } from '../../commands/__tests__/testingUtils.js'; -import { getBuildAllCommand } from '../../commands/build.js'; -import * as lint from '../../prebuild/lint.js'; -import * as tsc from '../../prebuild/tsc.js'; -import * as all from '../all.js'; - -vi.spyOn(all, 'buildAll'); -const mockedBuildBundle = vi.spyOn(modules, 'buildBundle'); -const mockedBuildTab = vi.spyOn(modules, 'buildTab'); -const mockedBuildSingleBundleDocs = vi.spyOn(docs, 'buildSingleBundleDocs'); - -const mockedRunTsc = vi.spyOn(tsc, 'runTsc').mockResolvedValue({ - severity: 'success', - input: {} as any, - results: [], -}); - -const mockedRunEslint = vi.spyOn(lint, 'runEslint').mockResolvedValue({ - severity: 'success', - formatted: '', - input: {} as any -}); - -describe('Test the buildAll command', () => { - const runCommand = getCommandRunner(getBuildAllCommand); - - afterEach(() => { - expect(all.buildAll).toHaveBeenCalledTimes(1); - }); - - describe('Test command with a bundle', () => { - const bundlePath = pathlib.join(testMocksDir, 'bundles', 'test0'); - - test('Regular execution for a bundle', async () => { - mockedBuildBundle.mockResolvedValueOnce({ - severity: 'success', - type: 'bundle', - path: '/build/bundles', - input: {} as any - }); - - mockedBuildSingleBundleDocs.mockResolvedValueOnce({ - type: 'docs', - severity: 'success', - path: '/build/jsons', - input: {} as any, - }); - - await expect(runCommand(bundlePath)).commandSuccess(); - }); - - test('Regular execution for a bundle with --tsc', async () => { - mockedBuildBundle.mockResolvedValueOnce({ - severity: 'success', - type: 'bundle', - path: '/build/bundles', - input: {} as any - }); - - mockedBuildSingleBundleDocs.mockResolvedValueOnce({ - type: 'docs', - severity: 'success', - path: '/build/jsons', - input: {} as any, - }); - - await expect(runCommand(bundlePath, '--tsc')).commandSuccess(); - expect(tsc.runTsc).toHaveBeenCalledTimes(1); - }); - - test('Regular execution for a bundle with --lint', async () => { - mockedBuildBundle.mockResolvedValueOnce({ - severity: 'success', - type: 'bundle', - path: '/build/bundles', - input: {} as any - }); - - mockedBuildSingleBundleDocs.mockResolvedValueOnce({ - type: 'docs', - severity: 'success', - path: '/build/jsons', - input: {} as any, - }); - - await expect(runCommand(bundlePath, '--lint')).commandSuccess(); - expect(lint.runEslint).toHaveBeenCalledTimes(1); - }); - - test('Lint error should avoid building bundle and json', async () => { - mockedRunEslint.mockResolvedValueOnce({ - severity: 'error', - input: {} as any, - formatted: '' - }); - - await expect(runCommand(bundlePath, '--lint')).commandExit(); - - expect(lint.runEslint).toHaveBeenCalledTimes(1); - expect(modules.buildBundle).not.toHaveBeenCalled(); - expect(docs.buildSingleBundleDocs).not.toHaveBeenCalled(); - }); - - test('Tsc error should avoid building bundle and json', async () => { - mockedRunTsc.mockResolvedValueOnce({ - severity: 'error', - input: {} as any, - results: [] - }); - - await expect(runCommand(bundlePath, '--tsc')).commandExit(); - - expect(tsc.runTsc).toHaveBeenCalledTimes(1); - expect(modules.buildBundle).not.toHaveBeenCalled(); - expect(docs.buildSingleBundleDocs).not.toHaveBeenCalled(); - }); - - test('Bundle error doesn\'t affect building json', async () => { - mockedBuildBundle.mockResolvedValueOnce({ - severity: 'error', - type: 'bundle', - input: {} as any, - errors: [] - }); - - mockedBuildSingleBundleDocs.mockResolvedValueOnce({ - type: 'docs', - severity: 'success', - path: '/build/jsons', - input: {} as any, - }); - - await expect(runCommand(bundlePath)).commandExit(); - - expect(modules.buildBundle).toHaveBeenCalledTimes(1); - expect(docs.buildSingleBundleDocs).toHaveBeenCalledTimes(1); - }); - - test('JSON error doesn\'t affect building bundle', async () => { - mockedBuildBundle.mockResolvedValueOnce({ - severity: 'success', - type: 'bundle', - path: '/build/bundles', - input: {} as any - }); - - mockedBuildSingleBundleDocs.mockResolvedValueOnce({ - type: 'docs', - severity: 'error', - errors: [], - input: {} as any, - }); - - await expect(runCommand(bundlePath)).commandExit(); - - expect(modules.buildBundle).toHaveBeenCalledTimes(1); - expect(docs.buildSingleBundleDocs).toHaveBeenCalledTimes(1); - }); - }); - - describe('Test command with a tab', () => { - const tabPath = pathlib.join(testMocksDir, 'tabs', 'tab0'); - - test('Regular execution for a tab', async () => { - mockedBuildTab.mockResolvedValueOnce({ - type: 'tab', - severity: 'success', - input: {} as any, - path: '/build/tabs', - }); - - await expect(runCommand(tabPath)).commandSuccess(); - }); - - test('Regular execution for a tab with --tsc', async () => { - mockedBuildTab.mockResolvedValueOnce({ - type: 'tab', - severity: 'success', - input: {} as any, - path: '/build/tabs', - }); - - await expect(runCommand(tabPath, '--tsc')).commandSuccess(); - expect(tsc.runTsc).toHaveBeenCalledTimes(1); - }); - - test('Regular execution for a tab with --lint', async () => { - mockedBuildTab.mockResolvedValueOnce({ - type: 'tab', - severity: 'success', - input: {} as any, - path: '/build/tabs', - }); - - await expect(runCommand(tabPath, '--lint')).commandSuccess(); - expect(lint.runEslint).toHaveBeenCalledTimes(1); - }); - - test('Lint error should avoid building tab', async () => { - mockedRunEslint.mockResolvedValueOnce({ - severity: 'error', - input: {} as any, - formatted: '' - }); - - await expect(runCommand(tabPath, '--lint')).commandExit(); - - expect(lint.runEslint).toHaveBeenCalledTimes(1); - expect(modules.buildTab).not.toHaveBeenCalled(); - }); - - test('Tsc error should avoid building tab', async () => { - mockedRunTsc.mockResolvedValueOnce({ - severity: 'error', - input: {} as any, - results: [] - }); - - await expect(runCommand(tabPath, '--tsc')).commandExit(); - - expect(tsc.runTsc).toHaveBeenCalledTimes(1); - expect(modules.buildTab).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/lib/buildtools/src/build/all.ts b/lib/buildtools/src/build/all.ts deleted file mode 100644 index bf9bc0add5..0000000000 --- a/lib/buildtools/src/build/all.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { BuildResult, InputAsset, Severity } from '@sourceacademy/modules-repotools/types'; -import { compareSeverity } from '@sourceacademy/modules-repotools/utils'; -import type { LogLevel } from 'typedoc'; -import type { PrebuildOptions } from '../prebuild/index.js'; -import { runEslint, type LintResult } from '../prebuild/lint.js'; -import { runTsc, type TscResult } from '../prebuild/tsc.js'; -import { buildSingleBundleDocs } from './docs/index.js'; -import { buildBundle, buildTab } from './modules/index.js'; - -interface BuildAllPrebuildError { - severity: 'error'; - - tsc: TscResult | undefined; - lint: LintResult | undefined; -} - -interface BuildAllBundleResult { - severity: Severity; - results: BuildResult; - docs: BuildResult; - - tsc: TscResult | undefined; - lint: LintResult | undefined; -} - -interface BuildAllTabResult { - severity: Severity; - results: BuildResult; - - tsc: TscResult | undefined; - lint: LintResult | undefined; -} - -export type BuildAllResult = BuildAllPrebuildError | BuildAllBundleResult | BuildAllTabResult; - -/** - * For a bundle, builds both the bundle itself and its JSON documentation\ - * For a tab, build just the tab - */ -export async function buildAll(input: InputAsset, prebuild: PrebuildOptions, outDir: string, logLevel: LogLevel): Promise { - const [tscResult, lintResult] = await Promise.all([ - prebuild.tsc ? runTsc(input, true) : Promise.resolve(undefined), - prebuild.lint ? runEslint(input) : Promise.resolve(undefined) - ]); - - if (tscResult?.severity === 'error' || lintResult?.severity === 'error') { - return { - severity: 'error', - lint: lintResult, - tsc: tscResult - }; - } - - if (input.type === 'bundle') { - const [bundleResult, docsResult] = await Promise.all([ - buildBundle(outDir, input, false), - buildSingleBundleDocs(input, outDir, logLevel) - ]); - - return { - severity: compareSeverity(bundleResult.severity, docsResult.severity), - results: bundleResult, - docs: docsResult, - lint: lintResult, - tsc: tscResult, - }; - } else { - const tabResult = await buildTab(outDir, input, false); - return { - severity: tabResult.severity, - results: tabResult, - lint: lintResult, - tsc: tscResult, - }; - } -} diff --git a/lib/buildtools/src/build/formatter.ts b/lib/buildtools/src/build/formatter.ts deleted file mode 100644 index 2fe6f02643..0000000000 --- a/lib/buildtools/src/build/formatter.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { BuildResult, ResultTypeWithWarn } from '@sourceacademy/modules-repotools/types'; -import chalk from 'chalk'; -import { formatLintResult, type LintResult } from '../prebuild/lint.js'; -import { formatTscResult, type TscResult } from '../prebuild/tsc.js'; - -interface ResultsObject { - tsc?: TscResult; - lint?: LintResult; - docs?: BuildResult; - results?: BuildResult; -} - -/** - * Formats a single result object into a string - */ -export function formatResult(result: ResultTypeWithWarn, type?: 'docs' | 'bundle' | 'tab' | 'html') { - if (result.severity === 'error') { - return result.errors.join('\n'); - } - - const typeStr = type ?? 'output'; - const successStr = chalk.greenBright(`${typeStr} written to ${result.path}`); - - if (result.severity === 'warn') { - return [ - ...result.warnings, - successStr - ].join('\n'); - } - - return successStr; -} - -/** - * Formats a larger result object, particularly one with prebuild results too. - */ -export function formatResultObject(results: ResultsObject): string { - const args: string[] = []; - - if (results.tsc) { - args.push(formatTscResult(results.tsc)); - } - - if (results.lint) { - args.push(formatLintResult(results.lint)); - } - - if (results.docs) { - args.push(formatResult(results.docs, 'docs')); - } - - if (results.results !== undefined) { - args.push(formatResult(results.results, results.results.type)); - } - - return args.join('\n'); -} diff --git a/lib/buildtools/src/commands/__tests__/build.test.ts b/lib/buildtools/src/commands/__tests__/build.test.ts index 40d40c2271..4f93c6a00b 100644 --- a/lib/buildtools/src/commands/__tests__/build.test.ts +++ b/lib/buildtools/src/commands/__tests__/build.test.ts @@ -7,7 +7,7 @@ import { testMocksDir } from '../../__tests__/fixtures.js'; import * as json from '../../build/docs/json.js'; import * as modules from '../../build/modules/index.js'; import * as lintRunner from '../../prebuild/lint.js'; -import * as tscRunner from '../../prebuild/tsc.js'; +import * as tscRunner from '../../../../repotools/src/tsc/index.js'; import * as commands from '../build.js'; import { getCommandRunner } from './testingUtils.js'; @@ -131,7 +131,7 @@ function testBuildCommand>( if (typeof prebuild === 'boolean') { tsc = lint = prebuild; } else { - ;({ tsc, lint } = prebuild); + ; ({ tsc, lint } = prebuild); } if (lint) { diff --git a/lib/buildtools/src/commands/__tests__/commandUtils.test.ts b/lib/buildtools/src/commands/__tests__/commandUtils.test.ts index 652bfaf967..6810700c0e 100644 --- a/lib/buildtools/src/commands/__tests__/commandUtils.test.ts +++ b/lib/buildtools/src/commands/__tests__/commandUtils.test.ts @@ -43,7 +43,7 @@ describe('Log Level option', () => { return new Promise((resolve, reject) => { new Command() .exitOverride(reject) - .configureOutput({ writeErr: () => { } }) + .configureOutput({ writeErr: () => {} }) .addOption(logLevelOption) .action(({ logLevel }) => resolve(logLevel)) .parse(args, { from: 'user' }); diff --git a/lib/buildtools/src/commands/__tests__/testing.test.ts b/lib/buildtools/src/commands/__tests__/testing.test.ts index 4d6f89b7ac..d6e8e87163 100644 --- a/lib/buildtools/src/commands/__tests__/testing.test.ts +++ b/lib/buildtools/src/commands/__tests__/testing.test.ts @@ -140,7 +140,7 @@ describe('Test regular test command', () => { describe('Test silent option', () => { const runCommand = (...args: string[]) => new Promise( (resolve, reject) => { - const command = new Command() + new Command() .exitOverride(reject) .addOption(silentOption) .configureOutput({ writeErr: () => { } }) diff --git a/lib/buildtools/src/commands/build.ts b/lib/buildtools/src/commands/build.ts index a57fb4eebb..3651796b3f 100644 --- a/lib/buildtools/src/commands/build.ts +++ b/lib/buildtools/src/commands/build.ts @@ -2,11 +2,8 @@ import { Command } from '@commander-js/extra-typings'; import { bundlesDir, outDir } from '@sourceacademy/modules-repotools/getGitRoot'; import { resolveAllBundles, resolveEitherBundleOrTab, resolveSingleBundle, resolveSingleTab } from '@sourceacademy/modules-repotools/manifest'; import chalk from 'chalk'; -import { buildAll } from '../build/all.js'; -import { buildHtml, buildSingleBundleDocs } from '../build/docs/index.js'; -import { formatResult, formatResultObject } from '../build/formatter.js'; -import { buildBundle, buildTab } from '../build/modules/index.js'; -import { buildManifest } from '../build/modules/manifest.js'; +import * as builders from '@sourceacademy/modules-repotools/build'; +import { formatResult, formatResultObject } from '../formatter.js'; import { runBuilderWithPrebuild } from '../prebuild/index.js'; import * as cmdUtils from './commandUtils.js'; @@ -33,12 +30,12 @@ export const getBuildBundleCommand = () => new Command('bundle') console.warn(chalk.yellowBright('--tsc was specified with --watch, ignoring...')); } - await buildBundle(outDir, result.bundle, true); + await builders.buildBundle(outDir, result.bundle, true); return; } - const results = await runBuilderWithPrebuild(buildBundle, opts, result.bundle, outDir, 'bundle', false); - console.log(formatResultObject(results)); + const results = await runBuilderWithPrebuild(builders.buildBundle, opts, result.bundle, outDir, 'bundle', false); + console.log(formatResultObject({ prebuild: results })); cmdUtils.processResult(results, opts.ci); }); @@ -61,12 +58,12 @@ export const getBuildTabCommand = () => new Command('tab') console.warn(chalk.yellowBright('--tsc was specified with --watch, ignoring...')); } - await buildTab(outDir, tab, true); + await builders.buildTab(outDir, tab, true); return; } - const results = await runBuilderWithPrebuild(buildTab, opts, tab, outDir, 'tab', false); - console.log(formatResultObject(results)); + const results = await runBuilderWithPrebuild(builders.buildTab, opts, tab, outDir, 'tab', false); + console.log(formatResultObject({ prebuild: results })); cmdUtils.processResult(results, opts.ci); }); @@ -80,8 +77,8 @@ export const getBuildDocsCommand = () => new Command('docs') if (manifestResult === undefined) { cmdUtils.logCommandErrorAndExit(`No bundle found at ${directory}!`); } else if (manifestResult.severity === 'success') { - const docResult = await buildSingleBundleDocs(manifestResult.bundle, outDir, logLevel); - console.log(formatResultObject({ docs: docResult })); + const docResult = await builders.buildSingleBundleDocs(manifestResult.bundle, outDir, logLevel); + console.log(formatResultObject({ prebuild: { docs: docResult } })); cmdUtils.processResult({ results: docResult }, ci); } else { cmdUtils.logCommandErrorAndExit(manifestResult); @@ -98,7 +95,7 @@ export const getBuildAllCommand = () => new Command('all') .action(async (directory, opts) => { const resolvedResult = await resolveEitherBundleOrTab(directory); if (resolvedResult.severity === 'error') { - if (resolvedResult.errors.length === 0) { + if (resolvedResult.asset === undefined) { cmdUtils.logCommandErrorAndExit(`Could not locate tab/bundle at ${directory}`); } else { const errStr = resolvedResult.errors.join('\n'); @@ -106,8 +103,8 @@ export const getBuildAllCommand = () => new Command('all') } } - const result = await buildAll(resolvedResult.asset, opts, outDir, opts.logLevel); - console.log(formatResultObject(result)); + const result = await builders.buildAll(resolvedResult.asset, opts, outDir, opts.logLevel); + console.log(formatResultObject({ prebuild: result })); cmdUtils.processResult(result, opts.ci); }); @@ -126,7 +123,7 @@ export const getManifestCommand = () => new Command('manifest') cmdUtils.logCommandErrorAndExit(resolveResult); } - const manifestResult = await buildManifest(resolveResult.bundles, outDir); + const manifestResult = await builders.buildManifest(resolveResult.bundles, outDir); console.log(formatResult(manifestResult)); }); @@ -139,7 +136,7 @@ export const getBuildHtmlCommand = () => new Command('html') cmdUtils.logCommandErrorAndExit(resolveResult); } - const htmlResult = await buildHtml(resolveResult.bundles, outDir, logLevel); + const htmlResult = await builders.buildHtml(resolveResult.bundles, outDir, logLevel); console.log(formatResult(htmlResult, 'html')); cmdUtils.processResult(htmlResult, false); }); diff --git a/lib/buildtools/src/commands/compile.ts b/lib/buildtools/src/commands/compile.ts new file mode 100644 index 0000000000..754fe72ed2 --- /dev/null +++ b/lib/buildtools/src/commands/compile.ts @@ -0,0 +1,20 @@ +import { Command } from '@commander-js/extra-typings'; +import { runTscCompileFromTsconfig } from '@sourceacademy/modules-repotools/tsc'; +import { formatTscResult } from '../formatter.js'; +import { logCommandErrorAndExit } from './commandUtils.js'; + +// TODO: Possibly look into supporting watch mode + +export const getCompileCommand = () => new Command('compile') + .description('Compiles the bundle at the given directory') + .argument('[bundle]', 'Directory in which the bundle\'s source files are located', process.cwd()) + .action(async directory => { + const result = await runTscCompileFromTsconfig(directory); + + if (result === undefined) logCommandErrorAndExit(`No bundle found at ${directory}!`); + else if (result.severity === 'error') { + logCommandErrorAndExit(result); + } + + console.log(formatTscResult(result as any)); + }); diff --git a/lib/buildtools/src/commands/main.ts b/lib/buildtools/src/commands/main.ts index 1aad7edcd2..c708555011 100644 --- a/lib/buildtools/src/commands/main.ts +++ b/lib/buildtools/src/commands/main.ts @@ -1,5 +1,6 @@ import { Command } from '@commander-js/extra-typings'; import { getBuildCommand, getBuildHtmlCommand, getManifestCommand } from './build.js'; +import { getCompileCommand } from './compile.js'; import { getListCommand, getValidateCommand } from './list.js'; import { getLintCommand, getLintGlobalCommand, getPrebuildAllCommand, getTscCommand } from './prebuild.js'; import getHttpServerCommand from './server.js'; @@ -9,6 +10,7 @@ import { getTestAllCommand, getTestCommand } from './testing.js'; const commands: (() => Command)[] = [ getBuildCommand, getBuildHtmlCommand, + getCompileCommand, getHttpServerCommand, getLintCommand, getLintGlobalCommand, diff --git a/lib/buildtools/src/commands/prebuild.ts b/lib/buildtools/src/commands/prebuild.ts index 294cbe50e4..6f56a3f600 100644 --- a/lib/buildtools/src/commands/prebuild.ts +++ b/lib/buildtools/src/commands/prebuild.ts @@ -2,12 +2,12 @@ import pathlib from 'path'; import { Command, InvalidArgumentError, Option } from '@commander-js/extra-typings'; import { outDir } from '@sourceacademy/modules-repotools/getGitRoot'; import { resolveEitherBundleOrTab } from '@sourceacademy/modules-repotools/manifest'; +import { formatTscResult, runTypecheckingFromTsconfig } from '@sourceacademy/modules-repotools/tsc/index'; import { divideAndRound } from '@sourceacademy/modules-repotools/utils'; import chalk from 'chalk'; import { ESLint } from 'eslint'; import { runPrebuild } from '../prebuild/index.js'; import { formatLintResult, lintGlobal, runEslint } from '../prebuild/lint.js'; -import { formatTscResult, runTsc } from '../prebuild/tsc.js'; import { logCommandErrorAndExit } from './commandUtils.js'; export const concurrencyOption = new Option('--concurrency ') @@ -84,7 +84,7 @@ export const getLintGlobalCommand = () => new Command('lintglobal') console.log(`${prefix} ${chalk.cyanBright(`Running ESLint v${ESLint.version}`)}`); console.log(`${prefix} ${chalk.cyanBright('Beginning linting with the following options:')}`); Object.entries(opts).forEach(([key, value], i) => { - console.log(` ${i+1}. ${chalk.greenBright(key)}: ${chalk.cyanBright(value)}`); + console.log(` ${i + 1}. ${chalk.greenBright(key)}: ${chalk.cyanBright(value)}`); }); const result = await lintGlobal(opts); @@ -117,11 +117,10 @@ export const getLintGlobalCommand = () => new Command('lintglobal') // This command is provided as an augmented way to run tsc, automatically // filtering out test files export const getTscCommand = () => new Command('tsc') - .description('Run tsc for the given directory, or the current directory if no directory is specified') + .description('Run type checking for the given directory, or the current directory if no directory is specified') .argument('[directory]', 'Directory to run tsc in', process.cwd()) - .option('--no-emit', 'Prevent the typescript compiler from outputting files regardless of the tsconfig setting') .option('--ci', process.env.CI) - .action(async (directory, { emit, ci }) => { + .action(async (directory, { ci }) => { const fullyResolved = pathlib.resolve(directory); const resolveResult = await resolveEitherBundleOrTab(fullyResolved); @@ -133,7 +132,7 @@ export const getTscCommand = () => new Command('tsc') logCommandErrorAndExit(resolveResult); } - const result = await runTsc(resolveResult.asset, !emit); + const result = await runTypecheckingFromTsconfig(directory); console.log(formatTscResult(result)); switch (result.severity) { diff --git a/lib/buildtools/src/formatter.ts b/lib/buildtools/src/formatter.ts new file mode 100644 index 0000000000..795468b189 --- /dev/null +++ b/lib/buildtools/src/formatter.ts @@ -0,0 +1,82 @@ +import pathlib from 'path'; +import chalk from 'chalk'; +import partition from 'lodash/partition.js'; +import ts from 'typescript'; +import type { OverallResultType, FormattableTscResult } from '@sourceacademy/modules-repotools/build'; +import type { LintResult } from '@sourceacademy/modules-repotools/prebuild/lint'; + +interface ResultsObject { + tsc?: FormattableTscResult; + lint: LintResult; +} + +export function formatLintResult({ severity, formatted, input }: LintResult): string { + const prefix = `${chalk.blueBright(`[${input.type} ${input.name}]:`)} ${chalk.cyanBright('Linting completed')}`; + + switch (severity) { + case 'error': + return `${prefix} ${chalk.cyanBright('with')} ${chalk.redBright('errors')}:\n${formatted}`; + case 'warn': + return `${prefix} ${chalk.cyanBright('with')} ${chalk.yellowBright('warnings')}:\n${formatted}`; + case 'success': + return `${prefix} ${chalk.greenBright('successfully')}`; + } +} + +export function formatTscResult(tscResult: FormattableTscResult): string { + const prefix = chalk.cyanBright('tsc completed'); + + if (tscResult.severity === 'success') { + return `${prefix} ${chalk.greenBright('successfully')}`; + } + + const host: ts.FormatDiagnosticsHost = { + getNewLine: () => '\n', + getCurrentDirectory: () => process.cwd(), + getCanonicalFileName: name => pathlib.basename(name) + }; + + if (tscResult.severity === 'warn') { + const diagStr = ts.formatDiagnosticsWithColorAndContext(tscResult.diagnostics, host); + return `${prefix} ${chalk.cyanBright('with')} ${chalk.yellowBright('warnings')}\n${diagStr}`; + } + + const [errDiags, tsDiags] = partition(tscResult.diagnostics, each => 'errors' in each) + + let errStr: string = ''; + + if (tsDiags.length > 0) { + errStr = ts.formatDiagnosticsWithColorAndContext(tsDiags, host); + } + + if (errDiags.length > 0) { + errStr += errDiags.flatMap(diag => diag.errors).join('\n') + } + + return `${prefix} ${chalk.cyanBright('with')} ${chalk.redBright('errors')}\n${errStr}`; +} + +/** + * Formats a larger result object, particularly one with prebuild results too. + */ +export function formatResultObject({ tsc, lint }: ResultsObject, result: OverallResultType): string { + const args: string[] = []; + + if (tsc) { + args.push(formatTscResult(tsc)); + } + + if (lint) { + args.push(formatLintResult(lint)); + } + + if (result.severity === 'error') { + result.diagnostics !== undefined + } + + if (result.diagnostics !== undefined) { + args.push(formatResult(results.results)); + } + + return args.join('\n'); +} diff --git a/lib/buildtools/src/prebuild/__tests__/tsc.test.ts b/lib/buildtools/src/prebuild/__tests__/tsc.test.ts deleted file mode 100644 index 10ce87b1f4..0000000000 --- a/lib/buildtools/src/prebuild/__tests__/tsc.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import pathlib from 'path'; -import ts from 'typescript'; -import { describe, expect, test, vi } from 'vitest'; -import { testMocksDir } from '../../__tests__/fixtures.js'; -import { runTsc } from '../tsc.js'; - -const mockedWriteFile = vi.hoisted(() => vi.fn<(arg0: string, arg1: string) => void>(() => undefined)); - -// Set a longer timeout just for this file -vi.setConfig({ - testTimeout: 20000 -}); - -vi.mock(import('typescript'), async importOriginal => { - const { default: original } = await importOriginal(); - - // @ts-expect-error createProgram has two overloads but we can only really define 1 - const createProgram: typeof original.createProgram = vi.fn((opts: ts.CreateProgramOptions) => { - const program = original.createProgram(opts); - const emit: typeof program.emit = (sourceFile, _, cancelToken, emitDts, transformers) => { - // We mock create program so that we can check what the writeFile callback is called with - return program.emit(sourceFile, mockedWriteFile, cancelToken, emitDts, transformers); - }; - - return { - ...program, - emit - }; - }); - - return { - default: { - ...original, - createProgram, - } - }; -}); - -describe('Test the augmented tsc functionality', () => { - test('tsc on a bundle', async () => { - await runTsc({ - type: 'bundle', - directory: pathlib.join(testMocksDir, 'bundles', 'test0'), - name: 'test0', - manifest: {} - }, false); - - expect(ts.createProgram).toHaveBeenCalledTimes(2); - console.log(mockedWriteFile.mock.calls); - expect(mockedWriteFile).toHaveBeenCalledTimes(1); - const [[writePath]] = mockedWriteFile.mock.calls; - - expect(writePath).not.toEqual(pathlib.join(testMocksDir, 'bundles', 'test0', '__tests__', 'test0.test.js')); - }); - - test('tsc on a bundle with --noEmit', async () => { - await runTsc({ - type: 'bundle', - directory: pathlib.join(testMocksDir, 'bundles', 'test0'), - name: 'test0', - manifest: {} - }, true); - - expect(ts.createProgram).toHaveBeenCalledTimes(1); - expect(mockedWriteFile).not.toBeCalled(); - }); - - test('tsc on a tab', async () => { - await runTsc({ - type: 'tab', - directory: pathlib.join(testMocksDir, 'tabs', 'tab0'), - entryPoint: pathlib.join(testMocksDir, 'tabs', 'tab0', 'src', 'index.tsx'), - name: 'tab0' - }, false); - - expect(ts.createProgram).toHaveBeenCalledTimes(1); - expect(mockedWriteFile).not.toBeCalled(); - }); -}); diff --git a/lib/buildtools/src/prebuild/tsc.ts b/lib/buildtools/src/prebuild/tsc.ts deleted file mode 100644 index 74de67c62e..0000000000 --- a/lib/buildtools/src/prebuild/tsc.ts +++ /dev/null @@ -1,158 +0,0 @@ -import fs from 'fs/promises'; -import pathlib from 'path'; -import type { InputAsset, Severity } from '@sourceacademy/modules-repotools/types'; -import { findSeverity } from '@sourceacademy/modules-repotools/utils'; -import chalk from 'chalk'; -import ts from 'typescript'; - -type TsconfigResult = { - severity: 'error'; - error: any; -} | { - severity: 'error'; - results: ts.Diagnostic[]; -} | { - severity: 'success'; - results: ts.CompilerOptions; - fileNames: string[]; -}; - -export type TscResult = { - input: InputAsset; -} & ({ - severity: 'error'; - error: any; -} | { - severity: Severity; - results: ts.Diagnostic[]; -}); - -async function getTsconfig(srcDir: string): Promise { - // Step 1: Read the text from tsconfig.json - const tsconfigLocation = pathlib.join(srcDir, 'tsconfig.json'); - try { - const configText = await fs.readFile(tsconfigLocation, 'utf-8'); - - // Step 2: Parse the raw text into a json object - const { error: configJsonError, config: configJson } = ts.parseConfigFileTextToJson(tsconfigLocation, configText); - if (configJsonError) { - return { - severity: 'error', - results: [configJsonError] - }; - } - - // Step 3: Parse the json object into a config object for use by tsc - const { errors: parseErrors, options: tsconfig, fileNames } = ts.parseJsonConfigFileContent(configJson, ts.sys, srcDir); - if (parseErrors.length > 0) { - return { - severity: 'error', - results: parseErrors - }; - } - - return { - severity: 'success', - results: tsconfig, - fileNames - }; - } catch (error) { - return { - severity: 'error', - error - }; - } -} - -export async function runTsc(input: InputAsset, noEmit: boolean): Promise { - const tsconfigRes = await getTsconfig(input.directory); - if (tsconfigRes.severity === 'error') { - return { - ...tsconfigRes, - input - }; - } - - const { results: tsconfig, fileNames } = tsconfigRes; - - try { - // tsc instance that only does typechecking - // Type checking for both tests and source code is performed - const typecheckProgram = ts.createProgram({ - rootNames: fileNames, - options: { - ...tsconfig, - noEmit: true - } - }); - const results = typecheckProgram.emit(); - const diagnostics = ts.getPreEmitDiagnostics(typecheckProgram) - .concat(results.diagnostics); - - const severity = findSeverity(diagnostics, ({ category }) => { - switch (category) { - case ts.DiagnosticCategory.Error: - return 'error'; - case ts.DiagnosticCategory.Warning: - return 'warn'; - default: - return 'success'; - } - }); - - if (input.type === 'bundle' && severity !== 'error' && !noEmit) { - // If noEmit isn't specified, then run tsc again without including test - // files and actually output the files - const filesWithoutTests = fileNames.filter(p => { - const segments = p.split(pathlib.posix.sep); - return !segments.includes('__tests__'); - }); - // tsc instance that does compilation - // only compiles non test files - const compileProgram = ts.createProgram({ - rootNames: filesWithoutTests, - options: { - ...tsconfig, - noEmit: false - }, - oldProgram: typecheckProgram - }); - compileProgram.emit(); - } - - return { - severity, - results: diagnostics, - input - }; - } catch (error) { - return { - severity: 'error', - input, - error - }; - } -} - -export function formatTscResult(tscResult: TscResult): string { - const prefix = chalk.cyanBright('tsc completed'); - - if (tscResult.severity === 'error' && 'error' in tscResult) { - return `${prefix} ${chalk.cyanBright('with')} ${chalk.redBright('errors')}: ${tscResult.error}`; - } - - const diagStr = ts.formatDiagnosticsWithColorAndContext(tscResult.results, { - getNewLine: () => '\n', - getCurrentDirectory: () => process.cwd(), - getCanonicalFileName: name => pathlib.basename(name) - }); - - switch (tscResult.severity) { - case 'error': - return `${prefix} ${chalk.cyanBright('with')} ${chalk.redBright('errors')}\n${diagStr}`; - case 'warn': - return `${prefix} ${chalk.cyanBright('with')} ${chalk.yellowBright('warnings')}\n${diagStr}`; - case 'success': - return `${prefix} ${chalk.greenBright('successfully')}`; - } -} diff --git a/lib/buildtools/src/templates/bundle.ts b/lib/buildtools/src/templates/bundle.ts index 7f967f9442..eea7ebe238 100644 --- a/lib/buildtools/src/templates/bundle.ts +++ b/lib/buildtools/src/templates/bundle.ts @@ -4,7 +4,7 @@ import type { Interface } from 'readline/promises'; import { getBundleManifests } from '@sourceacademy/modules-repotools/manifest'; import type { BundleManifest, ModulesManifest } from '@sourceacademy/modules-repotools/types'; import _package from '../../../../package.json' with { type: 'json' }; -import { formatResult } from '../build/formatter.js'; +import { formatResult } from '../formatter.js'; import sampleTsconfig from './bundle_tsconfig.json' with { type: 'json' }; import { askQuestion, error, success, warn } from './print.js'; import { check, isSnakeCase } from './utilities.js'; diff --git a/lib/buildtools/src/templates/tab.ts b/lib/buildtools/src/templates/tab.ts index a9e8b54d3c..491cc4224f 100644 --- a/lib/buildtools/src/templates/tab.ts +++ b/lib/buildtools/src/templates/tab.ts @@ -5,7 +5,7 @@ import { getBundleManifests } from '@sourceacademy/modules-repotools/manifest'; import type { BundleManifest, ModulesManifest } from '@sourceacademy/modules-repotools/types'; import omit from 'lodash/omit.js'; import _package from '../../../../package.json' with { type: 'json' }; -import { formatResult } from '../build/formatter.js'; +import { formatResult } from '../formatter.js'; import { askQuestion, error, success, warn } from './print.js'; import { check, isPascalCase } from './utilities.js'; diff --git a/lib/buildtools/tsconfig.json b/lib/buildtools/tsconfig.json index bd5b993d62..d671143eaf 100644 --- a/lib/buildtools/tsconfig.json +++ b/lib/buildtools/tsconfig.json @@ -6,7 +6,11 @@ "moduleResolution": "nodenext" }, "extends": "../tsconfig.json", - "include": ["./src", "vitest.setup.ts", "vitest.d.ts"], + "include": [ + "./src", + "vitest.setup.ts", + "vitest.d.ts", + ], "exclude": [ "dist", "../__test_mocks__" diff --git a/lib/markdown-tree/package.json b/lib/markdown-tree/package.json index 115475ccde..12dac11b47 100644 --- a/lib/markdown-tree/package.json +++ b/lib/markdown-tree/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "devDependencies": { + "@shikijs/types": "^3.15.0", "@sourceacademy/modules-buildtools": "workspace:^", "@sourceacademy/modules-repotools": "workspace:^", "@types/lodash": "^4.14.198", @@ -18,10 +19,12 @@ } }, "peerDependencies": { - "markdown-it": "*" + "markdown-it": "*", + "shiki": ">=2" }, "dependencies": { "lodash": "^4.17.21", + "tm-themes": "^1.10.12", "yaml": "^2.8.0" }, "scripts": { diff --git a/lib/markdown-tree/src/index.ts b/lib/markdown-tree/src/index.ts index 85cb4d6b53..c458f09a10 100644 --- a/lib/markdown-tree/src/index.ts +++ b/lib/markdown-tree/src/index.ts @@ -14,8 +14,9 @@ export function directoryTreePlugin(md: MarkdownIt, options: DirectoryTreePlugin md.renderer.rules.fence = (...args) => { const [tokens, idx,, env] = args; const token = tokens[idx]; + const infoTokens = token.info.split(' '); - if (token.info.trim() === 'dirtree') { + if (infoTokens.length > 0 && infoTokens[0] === 'dirtree') { const { realPath, path: _path } = env; const docdir = pathlib.resolve(pathlib.dirname(realPath ?? _path)); const [content, warnings] = parseContent(token.content, docdir, options); @@ -32,3 +33,5 @@ export function directoryTreePlugin(md: MarkdownIt, options: DirectoryTreePlugin return fence(...args); }; } + +export { dirtreeTransformer, grammar } from './transformer'; diff --git a/lib/markdown-tree/src/transformer.ts b/lib/markdown-tree/src/transformer.ts new file mode 100644 index 0000000000..796a144ab7 --- /dev/null +++ b/lib/markdown-tree/src/transformer.ts @@ -0,0 +1,142 @@ +import type { LanguageInput, ShikiTransformer, ThemedToken } from 'shiki/core'; +import githubDark from 'tm-themes/themes/github-dark.json' with { type: 'json' }; +import githubLight from 'tm-themes/themes/github-light.json' with { type: 'json' }; +import { LINE_STRINGS } from './tree'; + +// Vitepress uses the github themes by default, so we use the colours from +// those themes here +const githubLightColours = [ + githubLight.colors['terminal.ansiBlue'], + githubLight.colors['terminal.ansiMagenta'], + githubLight.colors['terminal.ansiCyan'], + githubLight.colors['terminal.ansiRed'], + githubLight.colors['terminal.ansiGreen'] +]; + +const githubDarkColours = [ + githubDark.colors['terminal.ansiBlue'], + githubDark.colors['terminal.ansiMagenta'], + githubDark.colors['terminal.ansiCyan'], + githubDark.colors['terminal.ansiRed'], + githubDark.colors['terminal.ansiGreen'] +]; + +// Assemble the Regex expression using the line strings +const reString = Object.entries(LINE_STRINGS) + .filter(([key]) => key !== 'EMPTY') + .map(([, value]) => `(?:${value})`) + .join('|'); +const branchRE = new RegExp(reString, 'g'); + +/** + * Finds the locations of every single branch token and returns them + * as a tuple of location and token + */ +function findBranches(value: string) { + const output: [loc: number, branch: string][] = []; + while (true) { + const match = branchRE.exec(value); + if (match === null) return output; + + output.push([match.index, match[0]]); + } +} + +/** + * A textmate grammar for the rendered directory tree + */ +export const grammar: LanguageInput = { + scopeName: 'source.dirtree', + name: 'dirtree', + repository: { + comment: { + match: /\/\/.+$/, + name: 'comment.line.dirtree' + }, + branch: { + match: branchRE, + name: 'dirtree.branch' + }, + identifier: { + match: new RegExp(`(?<=(?:${LINE_STRINGS.LAST_CHILD})|(?:${LINE_STRINGS.CHILD})|^)[.\\w-]+`), + name: 'entity.name' + }, + }, + patterns: [ + { include: '#branch' }, + { include: '#comment' }, + { include: '#identifier' } + ] +}; + +export interface TransformerOptions { + lightColours?: string[]; + darkColours?: string[]; +} + +/** + * Returns a {@link ShikiTransformer} for colouring dirtree diagrams + */ +export function dirtreeTransformer(options: TransformerOptions = {}): ShikiTransformer { + const lightColours = options?.lightColours || githubLightColours; + const darkColours = options?.darkColours || githubDarkColours; + + return { + tokens(tokens) { + // don't transform non-dirtree + if (this.options.lang !== 'dirtree') return tokens; + + const newTokens = tokens.map((line): ThemedToken[] => { + if (line.length <= 1) { + // Root identifier or empty line, do nothing + return line; + } + + const [firstToken, identifier, ...otherTokens] = line; + + const branches = findBranches(firstToken.content); + if (branches.length > 0) { + const [firstBranch] = branches; + const branchTokens = branches.map(([loc, branch]): ThemedToken => { + const tokenOffset = loc + firstToken.offset; + const indentLevel = loc / 4; + + return { + content: branch, + offset: tokenOffset, + htmlStyle: { + '--shiki-light': lightColours[indentLevel % lightColours.length], + '--shiki-dark': darkColours[indentLevel % darkColours.length] + } + }; + }); + + const newIdentifier: ThemedToken = { + content: identifier.content, + offset: identifier.offset, + htmlStyle: branchTokens[branchTokens.length - 1].htmlStyle + }; + + if (firstBranch[0] !== firstToken.offset) { + // First branch starts with some spaces, so we add them back + branchTokens.unshift({ + content: ' '.repeat(firstBranch[0]), + offset: line[0].offset + }); + } + + return [ + ...branchTokens, + newIdentifier, + ...otherTokens + ]; + } else { + // Root identifier with comment, do nothing + return line; + } + }); + + return newTokens; + } + }; +} diff --git a/lib/markdown-tree/src/tree.ts b/lib/markdown-tree/src/tree.ts index decb773627..7c6a728609 100644 --- a/lib/markdown-tree/src/tree.ts +++ b/lib/markdown-tree/src/tree.ts @@ -25,19 +25,11 @@ interface LineStringSet { } /** Contains all strings for tree rendering */ -const LINE_STRINGS: { [charset: string]: LineStringSet } = { - ascii: { - CHILD: '|-- ', - LAST_CHILD: '`-- ', - DIRECTORY: '| ', - EMPTY: ' ', - }, - 'utf-8': { - CHILD: '├── ', - LAST_CHILD: '└── ', - DIRECTORY: '│ ', - EMPTY: ' ', - }, +export const LINE_STRINGS: LineStringSet = { + CHILD: '├── ', + LAST_CHILD: '└── ', + DIRECTORY: '│ ', + EMPTY: ' ', }; /** @@ -45,12 +37,6 @@ const LINE_STRINGS: { [charset: string]: LineStringSet } = { * when calling `generateTree` */ interface GenerateTreeOptions { - /** - * Which set of characters to use when - * rendering directory lines - */ - charset?: 'ascii' | 'utf-8'; - /** * Whether or not to append trailing slashes * to directories. Items that already include a @@ -72,7 +58,6 @@ interface GenerateTreeOptions { /** The default options if no options are provided */ const defaultOptions: GenerateTreeOptions = { - charset: 'utf-8', trailingDirSlash: false, fullPath: false, rootDot: true, @@ -111,7 +96,7 @@ function getAsciiLine( commentLoc: number, options: GenerateTreeOptions ): string | null { - const lines = LINE_STRINGS[options.charset as string]; + const lines = LINE_STRINGS; // Special case for the root element if (!structure.parent) { diff --git a/lib/repotools/package.json b/lib/repotools/package.json index f70db02c45..bee8d3610f 100644 --- a/lib/repotools/package.json +++ b/lib/repotools/package.json @@ -10,7 +10,6 @@ "@types/node": "^22.15.30", "@vitejs/plugin-react": "^5.1.0", "@vitest/coverage-v8": "^4.0.4", - "typescript": "^5.8.2", "vitest": "^4.0.4", "vitest-browser-react": "^2.0.2" }, @@ -19,15 +18,19 @@ "chalk": "^5.0.1", "commander": "^14.0.0", "esbuild": "^0.27.0", + "eslint": "^9.35.0", "jsonschema": "^1.5.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "typedoc": "^0.28.9", + "typescript": "^5.8.2" }, "peerDependencies": { - "@vitejs/plugin-react": "*", - "vitest-browser-react": "*" + "@vitejs/plugin-react": ">=5", + "vitest-browser-react": ">=2" }, "exports": { "./testing": "./dist/testing/index.js", + "./build": "./dist/build/index.js", "./*": "./dist/*.js" }, "scripts": { diff --git a/lib/repotools/src/__tests__/manifest.test.ts b/lib/repotools/src/__tests__/manifest.test.ts index accf2f4797..bde09f6cdd 100644 --- a/lib/repotools/src/__tests__/manifest.test.ts +++ b/lib/repotools/src/__tests__/manifest.test.ts @@ -48,12 +48,13 @@ describe(manifest.resolveSingleTab, () => { }); }); - it('doesn\'t consider a non-directory a tab', () => { + it('doesn\'t consider a non-directory a tab', async () => { mockedFsStat.mockResolvedValueOnce({ isDirectory: () => false } as any); - return expect(manifest.resolveSingleTab(pathlib.join(tabsDir, 'tab1'))).resolves.toBeUndefined(); + await expect(manifest.resolveSingleTab(pathlib.join(tabsDir, 'tab1'))).resolves.toBeUndefined(); + expect(fs.stat).toHaveBeenCalledOnce(); }); it('returns undefined when the specified path doesn\'t exist', async ({ ENOENT }) => { @@ -62,9 +63,10 @@ describe(manifest.resolveSingleTab, () => { expect(fs.stat).toHaveBeenCalledOnce(); }); - it('throws the error when the error is of an unknown type', () => { + it('throws the error when the error is of an unknown type', async () => { mockedFsStat.mockRejectedValueOnce({}); - return expect(manifest.resolveSingleTab('/')).rejects.toEqual({}); + await expect(manifest.resolveSingleTab('/')).rejects.toEqual({}); + expect(fs.stat).toHaveBeenCalledOnce(); }); }); @@ -110,11 +112,14 @@ describe(manifest.getBundleManifest, () => { .resolves .toEqual({ severity: 'error', - errors: [ - 'test0: instance is not allowed to have the additional property "unknown"' - ] + diagnostics: [{ + severity: 'error', + bundleName: 'test0', + error: 'instance is not allowed to have the additional property "unknown"' + }] }); expect(fs.stat).not.toHaveBeenCalled(); + expect(fs.readFile).toHaveBeenCalledOnce(); }); test('invalid package name should not pass', async () => { @@ -124,8 +129,14 @@ describe(manifest.getBundleManifest, () => { const result = await manifest.getBundleManifest(bundle0Path, false); expect(result).toEqual({ severity: 'error', - errors: ['test0: The package name "invalid_name" does not follow the correct format!'] + diagnostics: [{ + severity: 'error', + bundleName: 'test0', + error: 'The package name "invalid_name" does not follow the correct format!' + }] }); + + expect(fs.readFile).toHaveBeenCalledTimes(2); }); test('unknown tabs should pass without tab check', async () => { @@ -141,6 +152,7 @@ describe(manifest.getBundleManifest, () => { }); expect(fs.stat).not.toHaveBeenCalled(); + expect(fs.readFile).toHaveBeenCalledTimes(2); }); test('unknown tab cannot pass tab check', async () => { @@ -149,12 +161,17 @@ describe(manifest.getBundleManifest, () => { const result = await manifest.getBundleManifest(bundle0Path, true); expect(result).toEqual({ severity: 'error', - errors: ['test0: Unknown tab "unknown"'] + diagnostics: [{ + severity: 'error', + bundleName: 'test0', + error: 'Unknown tab "unknown"' + }] }); expect(fs.stat).toHaveBeenCalledTimes(1); const [[path0]] = vi.mocked(fs.stat).mock.calls; expect(path0).toEqual(pathlib.join(tabsDir, 'unknown')); + expect(fs.readFile).toHaveBeenCalledOnce(); }); test('unknown tabs cannot pass tab check', async () => { @@ -163,13 +180,19 @@ describe(manifest.getBundleManifest, () => { const result = await manifest.getBundleManifest(bundle0Path, true); expect(result).toEqual({ severity: 'error', - errors: [ - 'test0: Unknown tab "unknown1"', - 'test0: Unknown tab "unknown2"', - ] + diagnostics: [{ + severity: 'error', + bundleName: 'test0', + error: 'Unknown tab "unknown1"' + }, { + severity: 'error', + bundleName: 'test0', + error: 'Unknown tab "unknown2"' + }] }); expect(fs.stat).toHaveBeenCalledTimes(2); + expect(fs.readFile).toHaveBeenCalledOnce(); const [[path0], [path1]] = vi.mocked(fs.stat).mock.calls; expect(path0).toEqual(pathlib.join(tabsDir, 'unknown1')); expect(path1).toEqual(pathlib.join(tabsDir, 'unknown2')); @@ -232,9 +255,17 @@ describe(manifest.getBundleManifests, () => { await expect(manifest.getBundleManifests(bundlesDir)).resolves.toEqual({ severity: 'error', - errors: [ - 'test0: instance is not allowed to have the additional property "unknown"', - 'test1: instance is not allowed to have the additional property "unknown"', + diagnostics: [ + { + severity: 'error', + error: 'instance is not allowed to have the additional property "unknown"', + bundleName: 'test0', + }, + { + severity: 'error', + error: 'instance is not allowed to have the additional property "unknown"', + bundleName: 'test1' + } ] }); } finally { @@ -268,7 +299,10 @@ describe(manifest.resolveSingleBundle, () => { await expect(manifest.resolveSingleBundle(pathlib.join(bundlePath))).resolves.toEqual({ severity: 'error', - errors: [`${bundlePath} is not a directory!`] + diagnostics: [{ + severity: 'error', + error: `${bundlePath} is not a directory!` + }] }); expect(fs.stat).toHaveBeenCalledOnce(); @@ -279,7 +313,10 @@ describe(manifest.resolveSingleBundle, () => { await expect(manifest.resolveSingleBundle(bundlePath)).resolves.toEqual({ severity: 'error', - errors: [`An error occurred while trying to read from ${bundlePath}: Error: Unknown message`] + diagnostics: [{ + severity: 'error', + error: `An error occurred while trying to read from ${bundlePath}: Error: Unknown message` + }] }); expect(fs.stat).toHaveBeenCalledOnce(); @@ -290,7 +327,10 @@ describe(manifest.resolveSingleBundle, () => { await expect(manifest.resolveSingleBundle(bundlePath)).resolves.toEqual({ severity: 'error', - errors: [`${bundlePath} does not exist!`] + diagnostics: [{ + severity: 'error', + error: `${bundlePath} does not exist!` + }] }); expect(fs.stat).toHaveBeenCalledOnce(); @@ -303,7 +343,10 @@ describe(manifest.resolveSingleBundle, () => { await expect(manifest.resolveSingleBundle(bundlePath)).resolves.toEqual({ severity: 'error', - errors: [`${pathlib.join(bundlePath, 'src', 'index.ts')} is not a file!`] + diagnostics: [{ + severity: 'error', + error: `${pathlib.join(bundlePath, 'src', 'index.ts')} is not a file!` + }] }); expect(fs.stat).toHaveBeenCalledTimes(2); @@ -323,7 +366,10 @@ describe(manifest.resolveSingleBundle, () => { await expect(manifest.resolveSingleBundle(bundlePath)).resolves.toEqual({ severity: 'error', - errors: ['Could not find entrypoint!'] + diagnostics: [{ + severity: 'error', + error: 'Could not find entrypoint!' + }] }); expect(fs.stat).toHaveBeenCalledTimes(2); @@ -348,13 +394,19 @@ describe(manifest.resolveEitherBundleOrTab, () => { }); }); - test('resolving bundle with manifest error returns the error', () => { + test('resolving bundle with manifest error returns the error', async () => { mockedReadFile.mockResolvedValueOnce('{ "unknown": true }'); - return expect(manifest.resolveEitherBundleOrTab(bundle0Path)).resolves.toEqual({ + await expect(manifest.resolveEitherBundleOrTab(bundle0Path)).resolves.toEqual({ severity: 'error', - errors: ['test0: instance is not allowed to have the additional property "unknown"'] + diagnostics: [{ + severity: 'error', + bundleName: 'test0', + error: 'instance is not allowed to have the additional property "unknown"' + }] }); + + expect(mockedReadFile).toHaveBeenCalledOnce(); }); const tab0OPath = pathlib.join(tabsDir, 'tab0'); @@ -373,7 +425,7 @@ describe(manifest.resolveEitherBundleOrTab, () => { test('resolving to nothing returns error severity and empty errors array', () => { return expect(manifest.resolveEitherBundleOrTab(testMocksDir)).resolves.toEqual({ severity: 'error', - errors : [] + diagnostics: [] }); }); }); diff --git a/lib/repotools/src/build/all.ts b/lib/repotools/src/build/all.ts new file mode 100644 index 0000000000..cca37ffd75 --- /dev/null +++ b/lib/repotools/src/build/all.ts @@ -0,0 +1,75 @@ +import type { LogLevel } from "typedoc"; +import type { InputAsset, ResolvedBundle, ResolvedTab, Severity } from "../types.js"; +import { buildBundle, buildTab } from "./modules/index.js"; +import { buildSingleBundleDocs } from "./docs/index.js"; +import { compareSeverity } from "../utils.js"; +import { runTypecheckingFromTsconfig } from "./tsc/index.js"; +import { runEslint, type LintResult } from "../prebuild/lint.js"; +import type { PrebuildOptions } from "../prebuild/index.js"; + +type TypecheckResult = Awaited>; + +interface BuildAllTabResult { + diagnostics: Awaited>; +} + +interface BuildAllBundleResult { + diagnostics: Awaited>; + docs: Awaited>; +} + +export type BuildAllResult< + TResult extends BuildAllBundleResult | BuildAllTabResult = BuildAllBundleResult | BuildAllTabResult +> = { + severity: Severity; + lint: LintResult | undefined; + tsc: TypecheckResult | undefined; +} & ({ + diagnostics: undefined +} | TResult); + +/** + * For a bundle, builds both the bundle itself and its JSON documentation\ + * For a tab, build just the tab + */ +export async function buildAll(input: ResolvedBundle, prebuild: PrebuildOptions, outDir: string, logLevel: LogLevel): Promise> +export async function buildAll(input: ResolvedTab, prebuild: PrebuildOptions, outDir: string, logLevel: LogLevel): Promise> +export async function buildAll(input: InputAsset, prebuild: PrebuildOptions, outDir: string, logLevel: LogLevel): Promise +export async function buildAll(input: InputAsset, prebuild: PrebuildOptions, outDir: string, logLevel: LogLevel): Promise { + const [tscResult, lintResult] = await Promise.all([ + prebuild.tsc ? runTypecheckingFromTsconfig(input.directory) : Promise.resolve(undefined), + prebuild.lint ? runEslint(input) : Promise.resolve(undefined) + ]); + + if (tscResult?.severity === 'error' || lintResult?.severity === 'error') { + return { + severity: 'error', + lint: lintResult, + tsc: tscResult, + diagnostics: undefined + }; + } + + if (input.type === 'bundle') { + const [bundleResult, docsResult] = await Promise.all([ + buildBundle(outDir, input, false), + buildSingleBundleDocs(input, outDir, logLevel) + ]); + + return { + severity: compareSeverity(bundleResult.severity, docsResult.severity), + diagnostics: bundleResult, + docs: docsResult, + lint: lintResult, + tsc: tscResult, + }; + } else { + const tabResult = await buildTab(outDir, input, false); + return { + severity: tabResult.severity, + diagnostics: tabResult, + lint: lintResult, + tsc: tscResult, + }; + } +} \ No newline at end of file diff --git a/lib/buildtools/src/build/docs/__tests__/index.test.ts b/lib/repotools/src/build/docs/__tests__/index.test.ts similarity index 83% rename from lib/buildtools/src/build/docs/__tests__/index.test.ts rename to lib/repotools/src/build/docs/__tests__/index.test.ts index ddcfa20ef2..32364e30ec 100644 --- a/lib/buildtools/src/build/docs/__tests__/index.test.ts +++ b/lib/repotools/src/build/docs/__tests__/index.test.ts @@ -1,10 +1,10 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import { bundlesDir, outDir } from '@sourceacademy/modules-repotools/getGitRoot'; -import type { ResolvedBundle } from '@sourceacademy/modules-repotools/types'; import * as td from 'typedoc'; import { describe, expect, test, vi } from 'vitest'; -import { expectError, expectSuccess } from '../../../__tests__/fixtures.js'; +import { bundlesDir, outDir } from '../../../getGitRoot.js'; +import { expectError, expectSuccess } from '../../../testing/fixtures.js'; +import type { ResolvedBundle } from '../../../types.js'; import { buildHtml, buildSingleBundleDocs } from '../index.js'; import * as json from '../json.js'; import * as init from '../typedoc.js'; @@ -30,8 +30,11 @@ describe(buildSingleBundleDocs, () => { const result = await buildSingleBundleDocs(mockBundle, outDir, td.LogLevel.None); expectError(result.severity); - expect(result.errors.length).toEqual(1); - expect(result.errors[0]).toEqual('Failed to generate reflection for test0, check that the bundle has no type errors!'); + expect(result.diagnostics.length).toEqual(1); + expect(result.diagnostics[0]).toEqual({ + severity: 'error', + error: 'Failed to generate reflection for test0, check that the bundle has no type errors!' + }); expect(fs.mkdir).not.toHaveBeenCalled(); expect(json.buildJson).not.toHaveBeenCalled(); @@ -45,7 +48,7 @@ describe(buildSingleBundleDocs, () => { mockedJsonInit.mockResolvedValueOnce({ convert: () => Promise.resolve(project), - generateJson: mockGenerateJson , + generateJson: mockGenerateJson, logger: { hasErrors: () => false } @@ -56,9 +59,9 @@ describe(buildSingleBundleDocs, () => { expect(mockGenerateJson).toHaveBeenCalledOnce(); const [[projectArg, calledPath]] = mockGenerateJson.mock.calls; - expect(calledPath).toMatchPath(pathlib.join(bundlesDir, 'test0', 'dist', 'docs.json')); + expect(calledPath).toEqual(pathlib.posix.join(bundlesDir, 'test0', 'dist', 'docs.json')); expect(projectArg).toMatchObject(project); - expect(fs.mkdir).toHaveBeenCalledExactlyOnceWith(pathlib.join(outDir,'jsons'), { recursive: true }); + expect(fs.mkdir).toHaveBeenCalledExactlyOnceWith(pathlib.join(outDir, 'jsons'), { recursive: true }); expect(json.buildJson).toHaveBeenCalledTimes(1); }); }); @@ -124,8 +127,11 @@ describe(buildHtml, () => { const result = await buildHtml(bundles, outDir, td.LogLevel.None); expectError(result.severity); - expect(result.errors.length).toEqual(1); - expect(result.errors[0]).toEqual('Could not find documentation for test1'); + expect(result.diagnostics.length).toEqual(1); + expect(result.diagnostics[0]).toEqual({ + severity: 'error', + error: 'Could not find documentation for test1' + }); expect(mockedApp.convert).not.toHaveBeenCalled(); expect(mockedApp.generateDocs).not.toHaveBeenCalled(); diff --git a/lib/buildtools/src/build/docs/__tests__/json.test.ts b/lib/repotools/src/build/docs/__tests__/json.test.ts similarity index 94% rename from lib/buildtools/src/build/docs/__tests__/json.test.ts rename to lib/repotools/src/build/docs/__tests__/json.test.ts index 0b978d997f..4d84b3ea02 100644 --- a/lib/buildtools/src/build/docs/__tests__/json.test.ts +++ b/lib/repotools/src/build/docs/__tests__/json.test.ts @@ -1,10 +1,10 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import { bundlesDir, outDir } from '@sourceacademy/modules-repotools/getGitRoot'; -import type { ResolvedBundle } from '@sourceacademy/modules-repotools/types'; import * as td from 'typedoc'; import { describe, expect, it, test as baseTest, vi } from 'vitest'; -import { expectSuccess, expectWarn } from '../../../__tests__/fixtures.js'; +import { bundlesDir, outDir } from '../../../getGitRoot.js'; +import { expectSuccess, expectWarn } from '../../../testing/fixtures.js'; +import type { ResolvedBundle } from '../../../types.js'; import { buildJson, parsers, type ParserError, type ParserResult, type ParserSuccess } from '../json.js'; import { initTypedocForJson } from '../typedoc.js'; @@ -21,7 +21,7 @@ describe(buildJson, () => { const mockedWriteFile = vi.spyOn(fs, 'writeFile'); const test = baseTest.extend({ - testBundle: ({}, use) => use({ + testBundle: ({ }, use) => use({ type: 'bundle', name: 'test0', manifest: {}, @@ -68,8 +68,11 @@ describe(buildJson, () => { const result = await buildJson(testBundle, outDir, project); expectWarn(result.severity); - expect(result.warnings.length).toEqual(1); - expect(result.warnings[0]).toEqual('No parser found for TestType which is of type TypeParameter.'); + expect(result.diagnostics.length).toEqual(1); + expect(result.diagnostics[0]).toEqual({ + severity: 'warn', + warning: 'No parser found for TestType which is of type TypeParameter.' + }); expect(fs.writeFile).toHaveBeenCalledOnce(); const { calls: [[path, data]] } = mockedWriteFile.mock; @@ -144,7 +147,7 @@ describe('Test parsers', () => { const signature = new td.SignatureReflection('testFunction', td.ReflectionKind.CallSignature, decl); signature.type = new td.IntrinsicType('void'); signature.comment = { - summary: [{ kind:'text', text: 'This is a summary' }] + summary: [{ kind: 'text', text: 'This is a summary' }] } as td.Comment; decl.signatures = [signature]; diff --git a/lib/buildtools/src/build/docs/drawdown.ts b/lib/repotools/src/build/docs/drawdown.ts similarity index 100% rename from lib/buildtools/src/build/docs/drawdown.ts rename to lib/repotools/src/build/docs/drawdown.ts diff --git a/lib/buildtools/src/build/docs/index.ts b/lib/repotools/src/build/docs/index.ts similarity index 66% rename from lib/buildtools/src/build/docs/index.ts rename to lib/repotools/src/build/docs/index.ts index ffe00f4a00..bac90d46e5 100644 --- a/lib/buildtools/src/build/docs/index.ts +++ b/lib/repotools/src/build/docs/index.ts @@ -1,8 +1,9 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import type { BuildResult, ResolvedBundle, ResultType } from '@sourceacademy/modules-repotools/types'; -import { mapAsync } from '@sourceacademy/modules-repotools/utils'; import * as td from 'typedoc'; +import type { Diagnostic, JsonResult, ResultType } from '../../types.js'; +import type { ResolvedBundle } from '../../types.js'; +import { mapAsync } from '../../utils.js'; import { buildJson } from './json.js'; import { initTypedocForHtml, initTypedocForJson } from './typedoc.js'; @@ -10,16 +11,17 @@ import { initTypedocForHtml, initTypedocForJson } from './typedoc.js'; * First builds an intermediate JSON file in the dist directory of the bundle\ * Then it builds the JSON documentation for that bundle */ -export async function buildSingleBundleDocs(bundle: ResolvedBundle, outDir: string, logLevel: td.LogLevel): Promise { +export async function buildSingleBundleDocs(bundle: ResolvedBundle, outDir: string, logLevel: td.LogLevel): Promise { const app = await initTypedocForJson(bundle, logLevel); const project = await app.convert(); if (!project) { return { - type: 'docs', severity: 'error', - errors: [`Failed to generate reflection for ${bundle.name}, check that the bundle has no type errors!`], - input: bundle + diagnostics: [{ + severity: 'error', + error: `Failed to generate reflection for ${bundle.name}, check that the bundle has no type errors!` + }] }; } @@ -29,10 +31,11 @@ export async function buildSingleBundleDocs(bundle: ResolvedBundle, outDir: stri if (app.logger.hasErrors()) { return { - type: 'docs', severity: 'error', - errors: ['Refer to the command line for Typedoc\'s error messages'], - input: bundle + diagnostics: [{ + severity: 'error', + error: 'Refer to the command line for Typedoc\'s error messages', + }] }; } @@ -40,7 +43,7 @@ export async function buildSingleBundleDocs(bundle: ResolvedBundle, outDir: stri return buildJson(bundle, outDir, project); } -type BuildHtmlResult = ResultType; +export type BuildHtmlResult = ResultType; /** * Builds HTML documentation for all bundles. Needs to be run after {@link buildSingleBundleDocs} @@ -60,7 +63,10 @@ export async function buildHtml(bundles: Record, outDir: if (missings.length > 0) { return { severity: 'error', - errors: missings.map(each => `Could not find documentation for ${each}`), + diagnostics: missings.map(each => ({ + severity: 'error', + error: `Could not find documentation for ${each}` + })), }; } @@ -70,7 +76,10 @@ export async function buildHtml(bundles: Record, outDir: if (!project) { return { severity: 'error', - errors: ['Failed to generate reflections, check that there are no type errors across all bundles!'] + diagnostics: [{ + severity: 'error', + error: 'Failed to generate reflections, check that there are no type errors across all bundles!' + }] }; } @@ -79,7 +88,10 @@ export async function buildHtml(bundles: Record, outDir: if (app.logger.hasErrors()) { return { severity: 'error', - errors: ['Refer to the command line for Typedoc\'s error messages'] + diagnostics: [{ + severity: 'error', + error: 'Refer to the command line for Typedoc\'s error messages' + }] }; } diff --git a/lib/buildtools/src/build/docs/json.ts b/lib/repotools/src/build/docs/json.ts similarity index 91% rename from lib/buildtools/src/build/docs/json.ts rename to lib/repotools/src/build/docs/json.ts index 03c6e5276b..84d4f1ecf7 100644 --- a/lib/buildtools/src/build/docs/json.ts +++ b/lib/repotools/src/build/docs/json.ts @@ -1,8 +1,8 @@ // Code for building JSON documentation specifically import fs from 'fs/promises'; import pathlib from 'path'; -import type { BuildResult, ResolvedBundle } from '@sourceacademy/modules-repotools/types'; import * as td from 'typedoc'; +import type { JsonResult, ResolvedBundle } from '../../types.js'; import drawdown from './drawdown.js'; interface VariableDocEntry { @@ -109,7 +109,7 @@ export const parsers: { /** * Converts a Typedoc reflection into the format as expected by the frontend and write it to disk as a JSON file */ -export async function buildJson(bundle: ResolvedBundle, outDir: string, reflection: td.ProjectReflection): Promise { +export async function buildJson(bundle: ResolvedBundle, outDir: string, reflection: td.ProjectReflection): Promise { const [jsonData, warnings, errors] = reflection.children!.reduce< [Record, string[], string[]] >(([res, warnings, errors], element) => { @@ -156,10 +156,11 @@ export async function buildJson(bundle: ResolvedBundle, outDir: string, reflecti if (errors.length > 0) { return { - type: 'docs', severity: 'error', - errors, - input: bundle + diagnostics: errors.map(each => ({ + severity: 'error', + error: each + })), }; } @@ -168,18 +169,17 @@ export async function buildJson(bundle: ResolvedBundle, outDir: string, reflecti if (warnings.length > 0) { return { - type: 'docs', severity: 'warn', - warnings, - path: outpath, - input: bundle + diagnostics: warnings.map(each => ({ + severity: 'warn', + warning: each + })), + outpath, }; } return { - type: 'docs', severity: 'success', - path: outpath, - input: bundle + outpath, }; } diff --git a/lib/buildtools/src/build/docs/typedoc.ts b/lib/repotools/src/build/docs/typedoc.ts similarity index 95% rename from lib/buildtools/src/build/docs/typedoc.ts rename to lib/repotools/src/build/docs/typedoc.ts index 14f3880da4..95c40ea939 100644 --- a/lib/buildtools/src/build/docs/typedoc.ts +++ b/lib/repotools/src/build/docs/typedoc.ts @@ -1,6 +1,6 @@ import pathlib from 'path'; -import type { ResolvedBundle } from '@sourceacademy/modules-repotools/types'; import * as td from 'typedoc'; +import type { ResolvedBundle } from '../../types.js'; // #region commonOpts const typedocPackageOptions: td.Configuration.TypeDocOptions = { diff --git a/lib/repotools/src/build/index.ts b/lib/repotools/src/build/index.ts new file mode 100644 index 0000000000..933fea1f6e --- /dev/null +++ b/lib/repotools/src/build/index.ts @@ -0,0 +1,37 @@ +import type { BuildAllResult } from './all.js'; +import type { JsonResult } from '../types.js'; +import type { BuildHtmlResult } from './docs/index.js'; +import type { BuildManifestResult } from './modules/manifest.js'; + +// Bundles, tabs exports +export { buildBundle, buildTab } from './modules/index.js'; +export { buildManifest } from './modules/manifest.js'; +export type { BuildManifestResult }; + +// Documentation exports +export { buildHtml, buildSingleBundleDocs } from './docs/index.js'; +export type { BuildHtmlResult }; + +export { buildJson } from './docs/json.js'; +export type { JsonResult }; + +// Typescript Exports +export { + convertTsDiagnostic, + getDiagnosticSeverity, + getTsconfig, + runTscCompile, + runTscCompileFromTsconfig, + runTypechecking, + runTypecheckingFromTsconfig, + type FormattableTscResult +} from './tsc/index.js'; + +export { buildAll } from './all.js'; +export type { BuildAllResult }; + +export type OverallResultType = + | BuildAllResult + | BuildHtmlResult + | BuildManifestResult + | JsonResult; diff --git a/lib/buildtools/src/build/modules/__tests__/building.test.ts b/lib/repotools/src/build/modules/__tests__/building.test.ts similarity index 90% rename from lib/buildtools/src/build/modules/__tests__/building.test.ts rename to lib/repotools/src/build/modules/__tests__/building.test.ts index 81e08b07c7..1196be0884 100644 --- a/lib/buildtools/src/build/modules/__tests__/building.test.ts +++ b/lib/repotools/src/build/modules/__tests__/building.test.ts @@ -1,9 +1,9 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import { outDir } from '@sourceacademy/modules-repotools/getGitRoot'; -import type { ResolvedBundle } from '@sourceacademy/modules-repotools/types'; import { beforeEach, expect, test, vi } from 'vitest'; -import { testMocksDir } from '../../../__tests__/fixtures.js'; +import { outDir } from '../../../getGitRoot.js'; +import { testMocksDir } from '../../../testing/fixtures.js'; +import type { ResolvedBundle } from '../../../types.js'; import { buildBundle, buildTab } from '../index.js'; import { buildManifest } from '../manifest.js'; @@ -85,13 +85,20 @@ test('build tab', async () => { expect(fs.open).toHaveBeenCalledExactlyOnceWith(pathlib.join(outDir, 'tabs', 'tab0.js'), 'w'); function mockRequire(path: string) { - console.log(path); + path = path.trim(); + // console.log(path); if (path === '@sourceacademy/modules-lib/tabs/utils') { return { defineTab: (x: any) => x }; } + if (path === 'react/jsx-runtime') { + return { + jsx: (x: any) => x + }; + } + return {}; } @@ -99,7 +106,7 @@ test('build tab', async () => { const trimmed = data.slice('export default'.length); const { default: tab } = eval(trimmed)(mockRequire); - expect(tab.body(0)).toEqual(0); + expect(tab.body(0)).toEqual('p'); expect(tab.toSpawn()).toEqual(true); }); diff --git a/lib/buildtools/src/build/modules/commons.ts b/lib/repotools/src/build/modules/commons.ts similarity index 84% rename from lib/buildtools/src/build/modules/commons.ts rename to lib/repotools/src/build/modules/commons.ts index 90d731098e..86a1ed2d96 100644 --- a/lib/buildtools/src/build/modules/commons.ts +++ b/lib/repotools/src/build/modules/commons.ts @@ -1,14 +1,12 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import type { BuildResult, InputAsset } from '@sourceacademy/modules-repotools/types'; import { parse } from 'acorn'; import { generate } from 'astring'; import chalk from 'chalk'; import type { BuildOptions as ESBuildOptions, OutputFile, Plugin as ESBuildPlugin } from 'esbuild'; import type es from 'estree'; +import type { BuildDiagnostic, DiagnosticWithoutWarn, InputAsset } from '../../types.js'; -// The region tag is used in the developer documentation. DON'T REMOVE -// #region esbuildOptions export const commonEsbuildOptions = { bundle: true, format: 'iife', @@ -26,17 +24,10 @@ export const commonEsbuildOptions = { target: 'es6', write: false } satisfies ESBuildOptions; -// #endregion esbuildOptions -type ConvertAstResult = { - severity: 'error'; - error: string; -} | { - severity: 'success'; - output: es.Node; -}; +type ConvertAstDiagnostic = DiagnosticWithoutWarn<{ output: es.Node }, { error: string }>; -function convertAst(parsed: es.Program): ConvertAstResult { +function convertAst(parsed: es.Program): ConvertAstDiagnostic { // Account for 'use strict'; directives let declStatement: es.VariableDeclaration; if (parsed.body[0].type === 'VariableDeclaration') { @@ -82,17 +73,16 @@ function convertAst(parsed: es.Program): ConvertAstResult { /** * Write the compiled output from ESBuild to the file system after performing AST transformation */ -export async function outputBundleOrTab({ text }: OutputFile, input: InputAsset, outDir: string): Promise { +export async function outputBundleOrTab({ text }: OutputFile, input: InputAsset, outDir: string): Promise { const parsed = parse(text, { ecmaVersion: 6 }) as es.Program; const astResult = convertAst(parsed); if (astResult.severity === 'error') { return { - type: input.type, severity: 'error', - input, - errors: [`${input.type} ${input.name} ${astResult.error}`] - } as BuildResult; + error: `${input.type} ${input.name} ${astResult.error}`, + input + }; } const { output } = astResult; @@ -108,11 +98,10 @@ export async function outputBundleOrTab({ text }: OutputFile, input: InputAsset, generate(output, { output: writeStream }); return { - type: input.type, severity: 'success', input, - path: outpath - } as BuildResult; + outpath + }; } finally { await file?.close(); } diff --git a/lib/buildtools/src/build/modules/index.ts b/lib/repotools/src/build/modules/index.ts similarity index 90% rename from lib/buildtools/src/build/modules/index.ts rename to lib/repotools/src/build/modules/index.ts index 2057475128..9be4e1df0f 100644 --- a/lib/buildtools/src/build/modules/index.ts +++ b/lib/repotools/src/build/modules/index.ts @@ -1,7 +1,7 @@ import pathlib from 'path'; -import { endBuildPlugin } from '@sourceacademy/modules-repotools/builder'; -import type { BuildResult, ResolvedBundle, ResolvedTab } from '@sourceacademy/modules-repotools/types'; import * as esbuild from 'esbuild'; +import { endBuildPlugin } from '../../builder.js'; +import type { BuildDiagnostic, ResolvedBundle, ResolvedTab } from '../../types.js'; import { builderPlugin, commonEsbuildOptions, outputBundleOrTab } from './commons.js'; /** @@ -9,7 +9,7 @@ import { builderPlugin, commonEsbuildOptions, outputBundleOrTab } from './common * build directory. */ export async function buildBundle(outDir: string, bundle: ResolvedBundle, watch: true): Promise; -export async function buildBundle(outDir: string, bundle: ResolvedBundle, watch: false): Promise; +export async function buildBundle(outDir: string, bundle: ResolvedBundle, watch: false): Promise; export async function buildBundle(outDir: string, bundle: ResolvedBundle, watch: boolean) { const bundleOptions = { ...commonEsbuildOptions, @@ -49,7 +49,7 @@ const tabContextPlugin: esbuild.Plugin = { * build directory. */ export async function buildTab(outDir: string, tab: ResolvedTab, watch: true): Promise; -export async function buildTab(outDir: string, tab: ResolvedTab, watch: false): Promise; +export async function buildTab(outDir: string, tab: ResolvedTab, watch: false): Promise; export async function buildTab(outDir: string, tab: ResolvedTab, watch: boolean) { const tabOptions = { ...commonEsbuildOptions, diff --git a/lib/buildtools/src/build/modules/manifest.ts b/lib/repotools/src/build/modules/manifest.ts similarity index 70% rename from lib/buildtools/src/build/modules/manifest.ts rename to lib/repotools/src/build/modules/manifest.ts index 25eccfa839..12a7dc43d1 100644 --- a/lib/buildtools/src/build/modules/manifest.ts +++ b/lib/repotools/src/build/modules/manifest.ts @@ -1,12 +1,14 @@ import fs from 'fs/promises'; import pathlib from 'path'; -import type { BundleManifest, ResolvedBundle, SuccessResult } from '@sourceacademy/modules-repotools/types'; -import { isNodeError, objectEntries } from '@sourceacademy/modules-repotools/utils'; +import type { BundleManifest, ErrorDiagnostic, ResolvedBundle, ResultTypeWithoutWarn } from '../../types.js'; +import { isNodeError, objectEntries } from '../../utils.js'; + +export type BuildManifestResult = ResultTypeWithoutWarn; /** * Writes the combined modules' manifest to the output directory */ -export async function buildManifest(bundles: Record, outDir: string): Promise { +export async function buildManifest(bundles: Record, outDir: string): Promise { const finalManifest = objectEntries(bundles).reduce>((res, [name, { manifest }]) => ({ ...res, [name]: manifest diff --git a/lib/repotools/src/build/tsc/__tests__/tsc.test.ts b/lib/repotools/src/build/tsc/__tests__/tsc.test.ts new file mode 100644 index 0000000000..60f8b8a42d --- /dev/null +++ b/lib/repotools/src/build/tsc/__tests__/tsc.test.ts @@ -0,0 +1,98 @@ +import pathlib from 'path'; +import ts from 'typescript'; +import { describe, expect, test, vi } from 'vitest'; +import { testMocksDir } from '../../../testing/fixtures.js'; +import { runTscCompileFromTsconfig, runTypecheckingFromTsconfig } from '../index.js'; + +const mockedWriteFile = vi.hoisted(() => vi.fn<(arg0: string, arg1: string) => void>(() => undefined)); + +// Set a longer timeout just for this file +vi.setConfig({ + testTimeout: 20000 +}); + +vi.mock(import('typescript'), async importOriginal => { + const { default: original } = await importOriginal(); + + // @ts-expect-error createProgram has two overloads but we can only really define 1 + const createProgram: typeof original.createProgram = vi.fn((fileNames: string[], opts: ts.CompilerOptions) => { + const program = original.createProgram(fileNames, opts); + const emit: typeof program.emit = (sourceFile, _, cancelToken, emitDts, transformers) => { + // We mock create program so that we can check what the writeFile callback is called with + return program.emit(sourceFile, mockedWriteFile, cancelToken, emitDts, transformers); + }; + + return { + ...program, + emit + }; + }); + + return { + default: { + ...original, + createProgram, + } + }; +}); + +describe(runTypecheckingFromTsconfig, () => { + test('on a bundle', async () => { + const bundlePath = pathlib.join(testMocksDir, 'bundles', 'test0'); + await expect(runTypecheckingFromTsconfig(bundlePath)).resolves.toMatchObject({ + severity: 'success', + diagnostics: expect.any(Array), + program: expect.any(Object) + }); + + expect(ts.createProgram).toHaveBeenCalledTimes(1); + const [[, programOpts]] = vi.mocked(ts.createProgram).mock.calls; + expect(programOpts).toHaveProperty('noEmit', true); + expect(mockedWriteFile).toHaveBeenCalledTimes(0); + }); + + test('on a tab', async () => { + const tabPath = pathlib.join(testMocksDir, 'tabs', 'tab0'); + const result = await runTypecheckingFromTsconfig(tabPath); + + expect(result).toMatchObject({ + severity: 'success', + diagnostics: expect.any(Array), + program: expect.any(Object) + }); + + expect(ts.createProgram).toHaveBeenCalledTimes(1); + const [[, programOpts]] = vi.mocked(ts.createProgram).mock.calls; + expect(programOpts).toHaveProperty('noEmit', true); + expect(mockedWriteFile).toHaveBeenCalledTimes(0); + }); +}); + +describe(runTscCompileFromTsconfig, () => { + test('on a bundle', async () => { + const bundlePath = pathlib.join(testMocksDir, 'bundles', 'test0'); + await expect(runTscCompileFromTsconfig(bundlePath)).resolves.toMatchObject({ + severity: 'success', + diagnostics: expect.any(Array), + }); + + expect(ts.createProgram).toHaveBeenCalledTimes(1); + const [[fileNames, programOpts]] = vi.mocked(ts.createProgram).mock.calls; + expect(programOpts).toHaveProperty('noEmit', false); + expect(fileNames).not.toContain(expect.stringContaining('__tests__')); + + // 2 times, one for the js and 1 for the declaration + expect(mockedWriteFile).toHaveBeenCalledTimes(2); + }); + + test('on a tab', async () => { + const tabPath = pathlib.join(testMocksDir, 'tabs', 'tab0'); + return expect(runTscCompileFromTsconfig(tabPath)).resolves.toMatchObject({ + severity: 'error', + diagnostics: [{ + severity: 'error', + error: 'runTscCompile can only be used with bundles!' + }] + }); + }); +}); diff --git a/lib/repotools/src/build/tsc/index.ts b/lib/repotools/src/build/tsc/index.ts new file mode 100644 index 0000000000..31a1dd01df --- /dev/null +++ b/lib/repotools/src/build/tsc/index.ts @@ -0,0 +1,89 @@ +import pathlib from 'path'; +import partition from 'lodash/partition.js'; +import ts from 'typescript'; +import { findSeverity } from '../../utils.js'; +import type { FormattableTscResult } from './types.js'; +import { convertTsDiagnostic, getDiagnosticSeverity, runWithTsconfig } from './utils.js'; + +/** + * Represents the result of running the typecheck operation + */ +export type TypecheckResult = FormattableTscResult<{ program: ts.Program }>; + +/** + * Run the Typescript compiler only for type checking + */ +export function runTypechecking(tsconfig: ts.CompilerOptions, fileNames: string[]): TypecheckResult { + const typecheckProgram = ts.createProgram( + fileNames, + { + ...tsconfig, + // Insist that we do not emit anything + noEmit: true + } + ); + const results = typecheckProgram.emit(); + const diagnostics = ts.getPreEmitDiagnostics(typecheckProgram) + .concat(results.diagnostics) + .map(convertTsDiagnostic); + + const [errDiags, nonErrDiags] = partition(diagnostics, each => each.severity === 'error'); + if (errDiags.length > 0) { + return { severity: 'error', diagnostics }; + } + + const severity = findSeverity(nonErrDiags); + + return { + severity, + diagnostics: nonErrDiags, + program: typecheckProgram + }; +} + +export type TscCompileResult = FormattableTscResult; + +/** + * Run the Typescript compiler but for producing Javascript files from Typescript + */ +export function runTscCompile(tsconfig: ts.CompilerOptions, fileNames: string[], program?: ts.Program): TscCompileResult { + const filesWithoutTests = fileNames.filter(p => { + const segments = p.split(pathlib.posix.sep); + return !segments.includes('__tests__'); + }); + + // tsc instance that does compilation + // only compiles non test files + const compileProgram = ts.createProgram(filesWithoutTests, { + ...tsconfig, + // Insist that we emit files + noEmit: false + }, undefined, program); + + const { diagnostics } = compileProgram.emit(); + const compileSeverity = getDiagnosticSeverity(diagnostics as ts.Diagnostic[]); + + return { + severity: compileSeverity, + // @ts-expect-error Typescript can't narrow the types properly + diagnostics: diagnostics.map(convertTsDiagnostic) + }; +} + +/** + * Run the typescript compiler for typechecking at the given directory + */ +export function runTypecheckingFromTsconfig(directory: string) { + return runWithTsconfig(directory, runTypechecking); +} + +/** + * Run the typescript compiler to convert Typescript into Javascript and + * Typescript declaration files for the bundle at the given directory + */ +export function runTscCompileFromTsconfig(directory: string) { + return runWithTsconfig(directory, runTscCompile, true); +} + +export * from './types.js'; +export * from './utils.js'; diff --git a/lib/repotools/src/build/tsc/types.ts b/lib/repotools/src/build/tsc/types.ts new file mode 100644 index 0000000000..e167f35e5c --- /dev/null +++ b/lib/repotools/src/build/tsc/types.ts @@ -0,0 +1,13 @@ +import type ts from 'typescript'; +import type { Diagnostic, ResultType } from '../../types.js'; + +/** + * Wrapper type around a {@link ts.Diagnostic | Diagnostic} to make it compatible with our + * own diagnostic type + */ +export type TSDiagnostic = Diagnostic; + +/** + * Represnts the result of running an operation with the Typescript compiler + */ +export type FormattableTscResult = ResultType; diff --git a/lib/repotools/src/build/tsc/utils.ts b/lib/repotools/src/build/tsc/utils.ts new file mode 100644 index 0000000000..29b5f2a467 --- /dev/null +++ b/lib/repotools/src/build/tsc/utils.ts @@ -0,0 +1,125 @@ +import fs from 'fs/promises'; +import pathlib from 'path'; +import ts from 'typescript'; +import { resolveSingleBundle, type ResolveSingleBundleResult } from '../../manifest.js'; +import type { ResultType } from '../../types.js'; +import { findSeverity } from '../../utils.js'; +import type { FormattableTscResult, TSDiagnostic } from './types.js'; + +export function getDiagnosticSeverity(diagnostics: ts.Diagnostic[]) { + return findSeverity(diagnostics, ({ category }) => { + switch (category) { + case ts.DiagnosticCategory.Error: + return 'error'; + case ts.DiagnosticCategory.Warning: + return 'warn'; + default: + return 'success'; + } + }); +} + +export function convertTsDiagnostic(diagnostic: ts.Diagnostic): TSDiagnostic { + switch (diagnostic.category) { + case ts.DiagnosticCategory.Error: + return { + severity: 'error', + ...diagnostic, + }; + case ts.DiagnosticCategory.Warning: + return { + severity: 'warn', + ...diagnostic + }; + default: + return { + severity: 'success', + ...diagnostic + }; + } +} + +type RunWithTsconfigResult> = + T | + TsconfigResult | + Extract; + +/** + * Wrapper for running the tsc functions that also resolves the tsconfig + */ +export async function runWithTsconfig>( + srcDir: string, + func: (tsconfig: ts.CompilerOptions, fileNames: string[]) => T, + requireBundle?: boolean +): Promise> { + if (requireBundle) { + const resolveResult = await resolveSingleBundle(srcDir); + if (resolveResult === undefined) { + return { + severity: 'error', + diagnostics: [{ + severity: 'error', + error: `${func.name} can only be used with bundles!` + }] + }; + } + + if (resolveResult.severity === 'error') return resolveResult; + } + + const tsconfigResult = await getTsconfig(srcDir); + if (tsconfigResult.severity === 'error') { + return tsconfigResult; + } + + const { tsconfig, fileNames } = tsconfigResult; + return func(tsconfig, fileNames); +} + +/** + * Result of trying to resolve a tsconfig file and its options + */ +export type TsconfigResult = FormattableTscResult<{ fileNames: string[], tsconfig: ts.CompilerOptions }>; + +/** + * Load and resolves the tsconfig at the given directory + */ +export async function getTsconfig(srcDir: string): Promise { + // Step 1: Read the text from tsconfig.json + const tsconfigLocation = pathlib.join(srcDir, 'tsconfig.json'); + try { + const configText = await fs.readFile(tsconfigLocation, 'utf-8'); + + // Step 2: Parse the raw text into a json object + const { error: configJsonError, config: configJson } = ts.parseConfigFileTextToJson(tsconfigLocation, configText); + if (configJsonError) { + return { + severity: 'error', + diagnostics: [convertTsDiagnostic(configJsonError)] + }; + } + + // Step 3: Parse the json object into a config object for use by tsc + const { errors: parseErrors, options: tsconfig, fileNames } = ts.parseJsonConfigFileContent(configJson, ts.sys, srcDir); + if (parseErrors.length > 0) { + return { + severity: 'error', + diagnostics: parseErrors.map(convertTsDiagnostic) + }; + } + + return { + severity: 'success', + tsconfig, + fileNames + }; + } catch (error) { + return { + severity: 'error', + diagnostics: [{ + severity: 'error', + errors: [`Error while reading ${srcDir}/tsconfig.json: ${error}`] + }] + }; + } +} diff --git a/lib/repotools/src/manifest.ts b/lib/repotools/src/manifest.ts index 529cb937ea..054075f4bb 100644 --- a/lib/repotools/src/manifest.ts +++ b/lib/repotools/src/manifest.ts @@ -5,10 +5,17 @@ import { validate } from 'jsonschema'; import uniq from 'lodash/uniq.js'; import { tabsDir } from './getGitRoot.js'; import manifestSchema from './manifest.schema.json' with { type: 'json' }; -import type { BundleManifest, InputAsset, ResolvedBundle, ResolvedTab, ResultType } from './types.js'; +import type * as resultTypes from './types.js'; import { filterAsync, isNodeError, mapAsync } from './utils.js'; -export type GetBundleManifestResult = ResultType<{ manifest: BundleManifest }>; +type BundleErrorDiagnostic = resultTypes.ErrorDiagnostic< + { error: string, bundleName?: string } +>; + +export type GetBundleManifestResult = resultTypes.ResultTypeWithoutWarn< + BundleErrorDiagnostic, + { manifest: resultTypes.BundleManifest } +>; const packageNameRegex = /^@sourceacademy\/(bundle|tab)-(.+)$/u; @@ -27,48 +34,50 @@ export async function getBundleManifest(directory: string, tabCheck?: boolean): if (isNodeError(error) && error.code === 'ENOENT') { return undefined; } - return { severity: 'error', errors: [`${error}`] }; - } - - let versionStr: string | undefined; - try { - let packageName: string; - const rawPackageJson = await fs.readFile(pathlib.join(directory, 'package.json'), 'utf-8') - ; ({ version: versionStr, name: packageName } = JSON.parse(rawPackageJson)); - if (!packageNameRegex.test(packageName)) { - return { + return { + severity: 'error', + diagnostics: [{ severity: 'error', - errors: [`${bundleName}: The package name "${packageName}" does not follow the correct format!`] - }; - } + error: `${error}` + }] + }; + } + let rawManifest: resultTypes.BundleManifest = JSON.parse(manifestStr); + try { + rawManifest = JSON.parse(manifestStr); } catch (error) { - if (isNodeError(error) && error.code === 'ENOENT') { - return undefined; - } - return { severity: 'error', errors: [`${error}`] }; + return { + severity: 'error', + diagnostics: [{ + severity: 'error', + bundleName, + error: `${error}` + }] + }; } - const rawManifest = JSON.parse(manifestStr) as BundleManifest; const validateResult = validate(rawManifest, manifestSchema, { throwError: false }); if (validateResult.errors.length > 0) { return { severity: 'error', - errors: validateResult.errors.map(each => `${bundleName}: ${each.toString()}`) + diagnostics: validateResult.errors.map( + each => ({ + severity: 'error', + bundleName, + error: each.toString() + }) + ) }; } - const manifest: BundleManifest = { - ...rawManifest, - tabs: !rawManifest.tabs ? rawManifest.tabs : rawManifest.tabs.map(each => each.trim()), - version: versionStr, - }; + const tabNames = rawManifest.tabs ? rawManifest.tabs.map(each => each.trim()) : undefined; // Make sure that all the tabs specified exist - if (tabCheck && manifest.tabs) { - const unknownTabs = await filterAsync(manifest.tabs, async tabName => { + if (tabCheck && tabNames) { + const unknownTabs = await filterAsync(tabNames, async tabName => { const resolvedTab = await resolveSingleTab(pathlib.join(tabsDir, tabName)); return resolvedTab === undefined; }); @@ -76,18 +85,61 @@ export async function getBundleManifest(directory: string, tabCheck?: boolean): if (unknownTabs.length > 0) { return { severity: 'error', - errors: unknownTabs.map(each => `${bundleName}: Unknown tab "${each}"`) + diagnostics: unknownTabs.map(each => ({ + severity: 'error', + bundleName, + error: `Unknown tab "${each}"` + })) }; } } + let versionStr: string | undefined; + try { + let packageName: string; + const rawPackageJson = await fs.readFile(pathlib.join(directory, 'package.json'), 'utf-8') + ; ({ version: versionStr, name: packageName } = JSON.parse(rawPackageJson)); + + if (!packageNameRegex.test(packageName)) { + return { + severity: 'error', + diagnostics: [{ + severity: 'error', + bundleName, + error: `The package name "${packageName}" does not follow the correct format!` + }] + }; + } + } catch (error) { + if (isNodeError(error) && error.code === 'ENOENT') { + return undefined; + } + + return { + severity: 'error', + diagnostics: [{ + severity: 'error', + error: `${error}` + }] + }; + } + + const manifest: resultTypes.BundleManifest = { + ...rawManifest, + tabs: tabNames, + version: versionStr, + }; + return { severity: 'success', manifest }; } -export type GetAllBundleManifestsResult = ResultType<{ manifests: Record }>; +export type GetAllBundleManifestsResult = resultTypes.ResultTypeWithoutWarn< + BundleErrorDiagnostic, + { manifests: Record } +>; /** * Get all bundle manifests @@ -99,7 +151,10 @@ export async function getBundleManifests(bundlesDir: string, tabCheck?: boolean) } catch (error) { return { severity: 'error', - errors: [`${error}`] + diagnostics: [{ + severity: 'error', + error: `${error}` + }] }; } @@ -115,8 +170,8 @@ export async function getBundleManifests(bundlesDir: string, tabCheck?: boolean) })); const [combinedManifests, errors] = manifests.reduce<[ - Record, - string[] + Record, + resultTypes.ErrorDiagnostic[] ]>(([res, errors], entry) => { if (entry === undefined) return [res, errors]; const [name, manifest] = entry; @@ -126,7 +181,7 @@ export async function getBundleManifests(bundlesDir: string, tabCheck?: boolean) res, [ ...errors, - ...manifest.errors + ...manifest.diagnostics, ] ]; } @@ -144,7 +199,7 @@ export async function getBundleManifests(bundlesDir: string, tabCheck?: boolean) if (errors.length > 0) { return { severity: 'error', - errors, + diagnostics: errors, }; } @@ -154,7 +209,10 @@ export async function getBundleManifests(bundlesDir: string, tabCheck?: boolean) }; } -type ResolveSingleBundleResult = ResultType<{ bundle: ResolvedBundle }>; +export type ResolveSingleBundleResult = resultTypes.ResultTypeWithoutWarn< + BundleErrorDiagnostic, + { bundle: resultTypes.ResolvedBundle } +>; /** * Attempts to resolve the information at the given directory as a bundle. Returns `undefined` if no bundle was detected @@ -169,20 +227,29 @@ export async function resolveSingleBundle(bundleDir: string): Promise }>; +type ResolveAllBundlesResult = resultTypes.ResultTypeWithoutWarn< + BundleErrorDiagnostic, + { bundles: Record } +>; /** * Find all the bundles within the given directory and returns their @@ -233,7 +309,7 @@ export async function resolveAllBundles(bundleDir: string): Promise, any[]]>(([res, errors], entry) => { + const [combinedManifests, errors] = manifests.reduce<[Record, resultTypes.ErrorDiagnostic[]]>(([res, errors], entry) => { if (entry === undefined) return [res, errors]; if (entry.severity === 'error') { @@ -241,7 +317,7 @@ export async function resolveAllBundles(bundleDir: string): Promise 0) { return { severity: 'error', - errors + diagnostics: errors }; } @@ -294,7 +370,7 @@ async function resolvePaths(isDir: boolean, ...paths: string[]) { * Attempts to resolve the information at the given directory as a tab. Returns `undefined` if no tab was detected * at the given directory. */ -export async function resolveSingleTab(tabDir: string): Promise { +export async function resolveSingleTab(tabDir: string): Promise { const fullyResolved = pathlib.resolve(tabDir); try { @@ -324,7 +400,10 @@ export async function resolveSingleTab(tabDir: string): Promise }>; +type ResolveAllTabsResult = resultTypes.ResultTypeWithoutWarn< + resultTypes.ErrorDiagnostic, + { tabs: Record } +>; /** * Find all the tabs within the given directory and returns their @@ -345,7 +424,7 @@ export async function resolveAllTabs(bundlesDir: string, tabsDir: string): Promi : { ...res, [tab.name]: tab - }, {} as Record); + }, {} as Record); return { severity: 'success', @@ -353,7 +432,11 @@ export async function resolveAllTabs(bundlesDir: string, tabsDir: string): Promi }; } -type ResolveEitherResult = ResultType<{ asset: InputAsset }>; +type ResolveEitherResult = resultTypes.ResultTypeWithoutWarn< + resultTypes.ErrorDiagnostic, + { asset: resultTypes.InputAsset }, + { asset?: undefined } +>; /** * Attempts to resolve the given directory as a bundle or a tab. Returns an object with `asset: undefined` @@ -366,7 +449,8 @@ export async function resolveEitherBundleOrTab(directory: string): Promise> | undefined; export interface RunPrebuildResult { - tsc: TscResult | undefined; + tsc: TypecheckResult; lint: LintResult | undefined; results?: T; } @@ -29,8 +31,8 @@ export async function runBuilderWithPrebuild> { - const promises: [Promise, Promise] = [ - !tsc ? Promise.resolve(undefined) : runTsc(asset, false), + const promises: [Promise, Promise] = [ + !tsc ? Promise.resolve(undefined) : runTypecheckingFromTsconfig(asset.directory), !lint ? Promise.resolve(undefined) : runEslint(asset), ]; @@ -63,7 +65,7 @@ export async function runBuilderWithPrebuild; diff --git a/lib/repotools/src/testing/__tests__/testing.test.ts b/lib/repotools/src/testing/__tests__/testing.test.ts index 27af4cf163..a2476cd524 100644 --- a/lib/repotools/src/testing/__tests__/testing.test.ts +++ b/lib/repotools/src/testing/__tests__/testing.test.ts @@ -368,7 +368,10 @@ describe(configs.getTestConfiguration, () => { .resolves .toMatchObject({ severity: 'error', - errors: [`Tests were found for ${libPath}, but no vitest config could be located`] + diagnostics: [{ + severity: 'error', + error: `Tests were found for ${libPath}, but no vitest config could be located` + }] }); expect(mockedIsTestDirectory).toHaveBeenCalledOnce(); @@ -479,7 +482,7 @@ describe(configs.getAllTestConfigurations, () => { // Force everything to be resolved as neither a tab nor a bundle mockedResolver.mockResolvedValue({ severity: 'error', - errors: [] + diagnostics: [] }); // Once for the root config diff --git a/lib/repotools/src/testing/fixtures.ts b/lib/repotools/src/testing/fixtures.ts new file mode 100644 index 0000000000..2c329795ca --- /dev/null +++ b/lib/repotools/src/testing/fixtures.ts @@ -0,0 +1,16 @@ +import { resolve } from 'path'; +import { expect } from 'vitest'; + +export const testMocksDir = resolve(import.meta.dirname, '../../../__test_mocks__'); + +export function expectError(obj: unknown): asserts obj is 'error' { + expect(obj).toEqual('error'); +} + +export function expectWarn(obj: unknown): asserts obj is 'warn' { + expect(obj).toEqual('warn'); +} + +export function expectSuccess(obj: unknown): asserts obj is 'success' { + expect(obj).toEqual('success'); +} diff --git a/lib/repotools/src/testing/index.ts b/lib/repotools/src/testing/index.ts index 11d21ed853..af4e951a91 100644 --- a/lib/repotools/src/testing/index.ts +++ b/lib/repotools/src/testing/index.ts @@ -10,7 +10,7 @@ import { defineProject, mergeConfig, type TestProjectInlineConfiguration, type V import type { BrowserConfigOptions, ProjectConfig } from 'vitest/node'; import { gitRoot, rootVitestConfigPath } from '../getGitRoot.js'; import { resolveSingleBundle, resolveSingleTab } from '../manifest.js'; -import type { ErrorResult } from '../types.js'; +import type { ErrorDiagnostic, ResultTypeWithoutWarn } from '../types.js'; import { isNodeError, isSamePath, mapAsync } from '../utils.js'; import loadVitestConfigFromDir from './loader.js'; import { isTestDirectory, testIncludePattern } from './testUtils.js'; @@ -132,10 +132,9 @@ export function setBrowserOptions(indivConfig: TestProjectInlineConfiguration, w return combinedConfig; } -export type GetTestConfigurationResult = ErrorResult | { - severity: 'success'; +export type GetTestConfigurationResult = ResultTypeWithoutWarn; /** * Based on a starting directory, locate the package.json that directory belongs to, then check @@ -194,7 +193,10 @@ export async function getTestConfiguration(directory: string, watch: boolean): P if (await isTestDirectory(jsonDir)) { return { severity: 'error', - errors: [`Tests were found for ${directory}, but no vitest config could be located`] + diagnostics: [{ + severity: 'error', + error: `Tests were found for ${directory}, but no vitest config could be located` + }] }; } @@ -221,7 +223,10 @@ export async function getTestConfiguration(directory: string, watch: boolean): P if (!tab) { return { severity: 'error', - errors: [`Invalid tab located at ${jsonDir}`] + diagnostics: [{ + severity: 'error', + error: `Tests were found for ${directory}, but no vitest config could be located` + }] }; } @@ -246,7 +251,10 @@ export async function getTestConfiguration(directory: string, watch: boolean): P if (!bundleResult) { return { severity: 'error', - errors: [`No bundle present at ${jsonDir}`] + diagnostics: [{ + severity: 'error', + error: `No bundle present at ${jsonDir}`, + }] }; } else if (bundleResult.severity === 'error') { return bundleResult; @@ -278,7 +286,6 @@ export async function getTestConfiguration(directory: string, watch: boolean): P severity: 'success', config: finalConfig }; - } /** diff --git a/lib/repotools/src/types.ts b/lib/repotools/src/types.ts index ca7a04e206..c93ef36477 100644 --- a/lib/repotools/src/types.ts +++ b/lib/repotools/src/types.ts @@ -4,8 +4,88 @@ export const severity = { SUCCESS: 'success' } as const; +interface DefaultErrorInfo { + error: TError; +}; + +interface DefaultWarnInfo { + warning: TWarn +}; + export type Severity = (typeof severity)[keyof typeof severity]; +export type BaseDiagnostic = { + severity: T; +} & Info; + +export type ErrorDiagnostic = BaseDiagnostic<'error', Info>; +export type WarnDiagnostic = BaseDiagnostic<'warn', Info>; +export type SuccessDiagnostic = BaseDiagnostic<'success', Info>; + +export type DiagnosticWithoutWarn< + SuccessInfo extends object = object, + ErrorInfo extends object = DefaultErrorInfo +> = ErrorDiagnostic | SuccessDiagnostic; + +export type Diagnostic< + SuccessInfo extends object = object, + ErrorInfo extends object = DefaultErrorInfo, + WarnInfo extends object = DefaultWarnInfo +> = DiagnosticWithoutWarn | WarnDiagnostic; + +export type ExtractDiagnosticSeverity> = + T extends 'success' + ? Extract + : T extends 'warn' + ? Extract + : T extends 'error' + ? Extract + : never; + +export type ResultTypeWithoutWarn< + T extends (SuccessDiagnostic | ErrorDiagnostic), + SuccInfo extends object = object, + ErrInfo extends object = object +> = ({ + severity: 'error'; + diagnostics: ExtractDiagnosticSeverity<'error', T>[]; +} & ErrInfo) | ({ + severity: 'success'; + // diagnostics: ExtractDiagnosticSeverity<'success', T>; +} & SuccInfo); + +export type ResultType< + T extends Diagnostic = Diagnostic, + SuccInfo extends object = object, + ErrInfo extends object = object, + WarnInfo extends object = object +> = (SuccInfo & { + severity: 'success'; +}) | (WarnInfo & { + severity: 'warn'; + diagnostics: ExtractDiagnosticSeverity<'warn', T>[]; +}) | (ErrInfo & { + severity: 'error'; + diagnostics: ExtractDiagnosticSeverity<'error', T>[]; +}); + +/** + * Represents the result of building either a tab or a bundle + */ +export type BuildDiagnostic = DiagnosticWithoutWarn<{ outpath: string }> & { input: InputAsset }; + +/** + * Represnts the overall results of building either a tab or a bundle + */ +export type BuildResult = ResultTypeWithoutWarn; + +/** + * Represnts the result of building JSON documentation + */ +export type JsonDiagnostic = Diagnostic; + +export type JsonResult = ResultType; + /** * A bundle manifest, including the `version` field from `package.json` */ @@ -71,48 +151,4 @@ export interface ResolvedTab { } // #endregion ResolvedTab -export interface ErrorResult { - severity: 'error'; - errors: string[]; -} - -export type WarningResult = { - severity: 'warn'; - warnings: string[]; -} & T; - -export type SuccessResult = { - severity: 'success'; -} & T; - export type InputAsset = ResolvedBundle | ResolvedTab; - -export type ResultType = ErrorResult | SuccessResult; - -export type ResultTypeWithWarn = ResultType | WarningResult; - -/** - * Represents the result of building a tab - */ -export type TabResult = ResultType & { - type: 'tab'; - input: ResolvedTab; -}; - -/** - * Represents the result of building a bundle - */ -export type BundleResult = ResultType & { - type: 'bundle'; - input: ResolvedBundle; -}; - -/** - * Represents the result of building JSON documentation - */ -export type DocsResult = ResultTypeWithWarn & { - type: 'docs'; - input: ResolvedBundle; -}; - -export type BuildResult = BundleResult | DocsResult | TabResult; diff --git a/lib/repotools/src/utils.ts b/lib/repotools/src/utils.ts index d3e973ceec..91f10d91d5 100644 --- a/lib/repotools/src/utils.ts +++ b/lib/repotools/src/utils.ts @@ -33,11 +33,28 @@ export function compareSeverity(lhs: Severity, rhs: Severity): Severity { * Given an array of items, map them to {@link Severity | Severity} objects and then * determine the overall severity of all the given items */ -export function findSeverity(items: T[], mapper: (each: T) => Severity): Severity { +export function findSeverity(items: { severity: Severity }[]): Severity; +export function findSeverity(items: T[], mapper: (each: T) => Severity): Severity; +export function findSeverity(items: T[], mapper?: (each: T) => Severity): Severity { let output: Severity = 'success'; for (const item of items) { - const severity = mapper(item); + let severity: Severity; + + if (mapper) { + severity = mapper(item); + } else if (isSeverity(item)) { + severity = item; + } else if (typeof item !== 'object' || item === null) { + throw new Error(`${item} is not convertible to severity!`); + } else { + if (!('severity' in item) || !isSeverity(item.severity)) { + throw new Error(`${item} is not convertible to severity!`); + } else { + severity = item.severity; + } + } + output = compareSeverity(output, severity); if (output === 'error') return 'error'; } diff --git a/lib/repotools/vitest.config.ts b/lib/repotools/vitest.config.ts index 5c1690761b..dad618d4c0 100644 --- a/lib/repotools/vitest.config.ts +++ b/lib/repotools/vitest.config.ts @@ -10,7 +10,8 @@ const config: ViteUserConfig = { root: import.meta.dirname, include: ['**/__tests__/**/*.test.ts'], watch: false, - projects: undefined + projects: undefined, + setupFiles: ['vitest.setup.ts'], } }; diff --git a/lib/repotools/vitest.setup.ts b/lib/repotools/vitest.setup.ts new file mode 100644 index 0000000000..5aca139b4e --- /dev/null +++ b/lib/repotools/vitest.setup.ts @@ -0,0 +1,45 @@ +// Buildtools vitest setup +import pathlib from 'path'; +import { vi } from 'vitest'; + + +vi.mock(import('fs/promises'), async importOriginal => { + const { default: original } = await importOriginal(); + return { + default: { + ...original, + cp: vi.fn().mockResolvedValue(undefined), + glob: vi.fn(), + mkdir: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + } + }; +}); + +vi.mock(import('typescript'), async importOriginal => { + const { default: original } = await importOriginal(); + + return { + default: { + ...original, + sys: { + ...original.sys, + writeFile: () => { }, + }, + } + }; +}); + +vi.mock(import('./src/getGitRoot.js'), async importOriginal => { + const { rootVitestConfigPath } = await importOriginal(); + const testMocksDir = pathlib.resolve(import.meta.dirname, '../__test_mocks__'); + return { + gitRoot: testMocksDir, + bundlesDir: pathlib.join(testMocksDir, 'bundles'), + tabsDir: pathlib.join(testMocksDir, 'tabs'), + outDir: '/build', + // retain the actual path to the root vitest config because we don't have a + // mocked version + rootVitestConfigPath + }; +}); diff --git a/tsconfig.json b/tsconfig.json index 06f9bc7913..05d827242f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ // so that ESLint doesn't complain { "include": [ - "**/vitest.config.ts", + "**/vitest.{config,setup}.ts", "./.github/actions/vitest.config.ts", "./devserver/vite.config.ts" ], diff --git a/yarn.lock b/yarn.lock index 72474ec683..42f1d0ae7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -258,16 +258,6 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": - version: 2.3.0 - resolution: "@ampproject/remapping@npm:2.3.0" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed - languageName: node - linkType: hard - "@antfu/install-pkg@npm:^1.1.0": version: 1.1.0 resolution: "@antfu/install-pkg@npm:1.1.0" @@ -316,6 +306,17 @@ __metadata: languageName: node linkType: hard +"@azure/core-auth@npm:^1.10.0": + version: 1.10.1 + resolution: "@azure/core-auth@npm:1.10.1" + dependencies: + "@azure/abort-controller": "npm:^2.1.2" + "@azure/core-util": "npm:^1.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/83fd96e43cf8ca3e1cf6c7677915ca1433d6e331cb7352b64a3f93d9fd71dcddf77e8b46f2bb2a5db49ce87016ed30ebaca88034a0acf321e86ba17c0eb3329e + languageName: node + linkType: hard + "@azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.8.0, @azure/core-auth@npm:^1.9.0": version: 1.10.0 resolution: "@azure/core-auth@npm:1.10.0" @@ -374,7 +375,22 @@ __metadata: languageName: node linkType: hard -"@azure/core-rest-pipeline@npm:^1.19.1, @azure/core-rest-pipeline@npm:^1.20.0": +"@azure/core-rest-pipeline@npm:^1.19.1": + version: 1.22.2 + resolution: "@azure/core-rest-pipeline@npm:1.22.2" + dependencies: + "@azure/abort-controller": "npm:^2.1.2" + "@azure/core-auth": "npm:^1.10.0" + "@azure/core-tracing": "npm:^1.3.0" + "@azure/core-util": "npm:^1.13.0" + "@azure/logger": "npm:^1.3.0" + "@typespec/ts-http-runtime": "npm:^0.3.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b5767a09ab8a944237e52523173fd2d6746f156962d368255bd66c5f328c2aee49e9b85a0898734c27e54ac8ee8b0a0f29d6044557fe077bf47946fada388fa2 + languageName: node + linkType: hard + +"@azure/core-rest-pipeline@npm:^1.20.0": version: 1.22.0 resolution: "@azure/core-rest-pipeline@npm:1.22.0" dependencies: @@ -389,7 +405,7 @@ __metadata: languageName: node linkType: hard -"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1, @azure/core-tracing@npm:^1.2.0": +"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1": version: 1.3.0 resolution: "@azure/core-tracing@npm:1.3.0" dependencies: @@ -398,6 +414,15 @@ __metadata: languageName: node linkType: hard +"@azure/core-tracing@npm:^1.2.0, @azure/core-tracing@npm:^1.3.0": + version: 1.3.1 + resolution: "@azure/core-tracing@npm:1.3.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/0cb26db9ab5336a1867cc9cd0bd42b1702406d0f76420385789d1a96c8702a38cb081838ea73cd707bb7b340c4386499cf6e77538cacfda4467c251fe2ffa32b + languageName: node + linkType: hard + "@azure/core-util@npm:^1.11.0, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.6.1": version: 1.13.0 resolution: "@azure/core-util@npm:1.13.0" @@ -409,6 +434,17 @@ __metadata: languageName: node linkType: hard +"@azure/core-util@npm:^1.13.0": + version: 1.13.1 + resolution: "@azure/core-util@npm:1.13.1" + dependencies: + "@azure/abort-controller": "npm:^2.1.2" + "@typespec/ts-http-runtime": "npm:^0.3.0" + tslib: "npm:^2.6.2" + checksum: 10c0/37067621cdac933c51775c26648fdcea315f07b08bd875cff4610e403eabf9c12532525f0bf094e258dadc03a55d35f12c9242f662526847b32c85cdcc2d6603 + languageName: node + linkType: hard + "@azure/core-xml@npm:^1.4.5": version: 1.5.0 resolution: "@azure/core-xml@npm:1.5.0" @@ -419,7 +455,7 @@ __metadata: languageName: node linkType: hard -"@azure/logger@npm:^1.0.0, @azure/logger@npm:^1.1.4": +"@azure/logger@npm:^1.0.0, @azure/logger@npm:^1.1.4, @azure/logger@npm:^1.3.0": version: 1.3.0 resolution: "@azure/logger@npm:1.3.0" dependencies: @@ -486,30 +522,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.10": - version: 7.27.1 - resolution: "@babel/core@npm:7.27.1" - dependencies: - "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.1" - "@babel/helper-compilation-targets": "npm:^7.27.1" - "@babel/helper-module-transforms": "npm:^7.27.1" - "@babel/helpers": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.1" - "@babel/template": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - convert-source-map: "npm:^2.0.0" - debug: "npm:^4.1.0" - gensync: "npm:^1.0.0-beta.2" - json5: "npm:^2.2.3" - semver: "npm:^6.3.1" - checksum: 10c0/0fc31f87f5401ac5d375528cb009f4ea5527fc8c5bb5b64b5b22c033b60fd0ad723388933a5f3f5db14e1edd13c958e9dd7e5c68f9b68c767aeb496199c8a4bb - languageName: node - linkType: hard - -"@babel/core@npm:^7.28.5": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.10, @babel/core@npm:^7.28.5": version: 7.28.5 resolution: "@babel/core@npm:7.28.5" dependencies: @@ -532,19 +545,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/generator@npm:7.27.1" - dependencies: - "@babel/parser": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" - jsesc: "npm:^3.0.2" - checksum: 10c0/c4156434b21818f558ebd93ce45f027c53ee570ce55a84fd2d9ba45a79ad204c17e0bff753c886fb6c07df3385445a9e34dc7ccb070d0ac7e80bb91c8b57f423 - languageName: node - linkType: hard - "@babel/generator@npm:^7.28.3": version: 7.28.3 resolution: "@babel/generator@npm:7.28.3" @@ -580,7 +580,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2": +"@babel/helper-compilation-targets@npm:^7.27.2": version: 7.27.2 resolution: "@babel/helper-compilation-targets@npm:7.27.2" dependencies: @@ -618,12 +618,12 @@ __metadata: linkType: hard "@babel/helper-member-expression-to-functions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" + version: 7.28.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" dependencies: - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/5762ad009b6a3d8b0e6e79ff6011b3b8fdda0fefad56cfa8bfbe6aa02d5a8a8a9680a45748fe3ac47e735a03d2d88c0a676e3f9f59f20ae9fadcc8d51ccd5a53 + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814 languageName: node linkType: hard @@ -637,20 +637,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-module-transforms@npm:7.27.1" - dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/196ab29635fe6eb5ba6ead2972d41b1c0d40f400f99bd8fc109cef21440de24c26c972fabf932585e618694d590379ab8d22def8da65a54459d38ec46112ead7 - languageName: node - linkType: hard - -"@babel/helper-module-transforms@npm:^7.28.3": +"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3": version: 7.28.3 resolution: "@babel/helper-module-transforms@npm:7.28.3" dependencies: @@ -709,14 +696,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-validator-identifier@npm:7.27.1" - checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.28.5": +"@babel/helper-validator-identifier@npm:^7.27.1, @babel/helper-validator-identifier@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-validator-identifier@npm:7.28.5" checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 @@ -730,16 +710,6 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helpers@npm:7.27.1" - dependencies: - "@babel/template": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/e078257b9342dae2c041ac050276c5a28701434ad09478e6dc6976abd99f721a5a92e4bebddcbca6b1c3a7e8acace56a946340c701aad5e7507d2c87446459ba - languageName: node - linkType: hard - "@babel/helpers@npm:^7.28.4": version: 7.28.4 resolution: "@babel/helpers@npm:7.28.4" @@ -750,14 +720,14 @@ __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.27.1, @babel/parser@npm:^7.27.2": - version: 7.27.2 - resolution: "@babel/parser@npm:7.27.2" +"@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.2, @babel/parser@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/parser@npm:7.28.5" dependencies: - "@babel/types": "npm:^7.27.1" + "@babel/types": "npm:^7.28.5" bin: parser: ./bin/babel-parser.js - checksum: 10c0/3c06692768885c2f58207fc8c2cbdb4a44df46b7d93135a083f6eaa49310f7ced490ce76043a2a7606cdcc13f27e3d835e141b692f2f6337a2e7f43c1dbb04b4 + checksum: 10c0/5bbe48bf2c79594ac02b490a41ffde7ef5aa22a9a88ad6bcc78432a6ba8a9d638d531d868bd1f104633f1f6bba9905746e15185b8276a3756c42b765d131b1ef languageName: node linkType: hard @@ -783,17 +753,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/parser@npm:7.28.5" - dependencies: - "@babel/types": "npm:^7.28.5" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/5bbe48bf2c79594ac02b490a41ffde7ef5aa22a9a88ad6bcc78432a6ba8a9d638d531d868bd1f104633f1f6bba9905746e15185b8276a3756c42b765d131b1ef - languageName: node - linkType: hard - "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -1052,7 +1011,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.27.1, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" dependencies: @@ -1063,18 +1022,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/traverse@npm:7.27.1" +"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/traverse@npm:7.28.5" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.1" - "@babel/template": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.5" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.5" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.5" debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10c0/d912110037b03b1d70a2436cfd51316d930366a5f54252da2bced1ba38642f644f848240a951e5caf12f1ef6c40d3d96baa92ea6e84800f2e891c15e97b25d50 + checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f languageName: node linkType: hard @@ -1093,21 +1052,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/traverse@npm:7.28.5" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.5" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.5" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.5" - debug: "npm:^4.3.1" - checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f - languageName: node - linkType: hard - "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.3.3": version: 7.27.1 resolution: "@babel/types@npm:7.27.1" @@ -1899,12 +1843,12 @@ __metadata: linkType: hard "@csstools/css-calc@npm:^2.1.3": - version: 2.1.3 - resolution: "@csstools/css-calc@npm:2.1.3" + version: 2.1.4 + resolution: "@csstools/css-calc@npm:2.1.4" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.4 - "@csstools/css-tokenizer": ^3.0.3 - checksum: 10c0/85f5b4f96d60f395d5f0108056b0ddee037b22d6deba448d74324b50f1c554de284f84715ebfac7b2888b78e09d20d02a7cd213ee7bdaa71011ea9b4eee3a251 + "@csstools/css-parser-algorithms": ^3.0.5 + "@csstools/css-tokenizer": ^3.0.4 + checksum: 10c0/42ce5793e55ec4d772083808a11e9fb2dfe36db3ec168713069a276b4c3882205b3507c4680224c28a5d35fe0bc2d308c77f8f2c39c7c09aad8747708eb8ddd8 languageName: node linkType: hard @@ -1944,7 +1888,7 @@ __metadata: languageName: node linkType: hard -"@dimforge/rapier3d-compat@npm:^0.12.0, @dimforge/rapier3d-compat@npm:~0.12.0": +"@dimforge/rapier3d-compat@npm:~0.12.0": version: 0.12.0 resolution: "@dimforge/rapier3d-compat@npm:0.12.0" checksum: 10c0/c66c24f90649c0fc870679c12e7fec1a111080d44450169b57561f957d7b6b284ad8a3ceeba95533e213176ea171351acebd3dd43885fafb33f18bfbd9d507db @@ -2539,18 +2483,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.7.0": - version: 4.7.0 - resolution: "@eslint-community/eslint-utils@npm:4.7.0" - dependencies: - eslint-visitor-keys: "npm:^3.4.3" - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf - languageName: node - linkType: hard - -"@eslint-community/eslint-utils@npm:^4.8.0": +"@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" dependencies: @@ -2868,12 +2801,12 @@ __metadata: linkType: hard "@jridgewell/gen-mapping@npm:^0.3.12": - version: 0.3.12 - resolution: "@jridgewell/gen-mapping@npm:0.3.12" + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/32f771ae2467e4d440be609581f7338d786d3d621bac3469e943b9d6d116c23c4becb36f84898a92bbf2f3c0511365c54a945a3b86a83141547a2a360a5ec0c7 + checksum: 10c0/9a7d65fb13bd9aec1fbab74cda08496839b7e2ceb31f5ab922b323e94d7c481ce0fc4fd7e12e2610915ed8af51178bdc61e168e92a8c8b8303b030b03489b13b languageName: node linkType: hard @@ -2912,27 +2845,27 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.31": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 languageName: node linkType: hard @@ -2946,16 +2879,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.31": - version: 0.3.31 - resolution: "@jridgewell/trace-mapping@npm:0.3.31" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 - languageName: node - linkType: hard - "@jscad/array-utils@npm:2.1.4": version: 2.1.4 resolution: "@jscad/array-utils@npm:2.1.4" @@ -4529,15 +4452,18 @@ __metadata: version: 0.0.0-use.local resolution: "@sourceacademy/markdown-plugin-directory-tree@workspace:lib/markdown-tree" dependencies: + "@shikijs/types": "npm:^3.15.0" "@sourceacademy/modules-buildtools": "workspace:^" "@sourceacademy/modules-repotools": "workspace:^" "@types/lodash": "npm:^4.14.198" "@types/markdown-it": "npm:^14.1.2" lodash: "npm:^4.17.21" + tm-themes: "npm:^1.10.12" typescript: "npm:^5.8.2" yaml: "npm:^2.8.0" peerDependencies: markdown-it: "*" + shiki: ">=2" languageName: unknown linkType: soft @@ -4563,8 +4489,7 @@ __metadata: http-server: "npm:^14.1.1" jsdom: "npm:^26.1.0" lodash: "npm:^4.17.21" - typedoc: "npm:^0.28.9" - typescript: "npm:^5.8.2" + typescript: "npm:^5.9.3" vite: "npm:^7.1.11" vitest: "npm:^4.0.4" bin: @@ -4679,12 +4604,13 @@ __metadata: esbuild: "npm:^0.27.0" jsonschema: "npm:^1.5.0" lodash: "npm:^4.17.21" + typedoc: "npm:^0.28.9" typescript: "npm:^5.8.2" vitest: "npm:^4.0.4" vitest-browser-react: "npm:^2.0.2" peerDependencies: - "@vitejs/plugin-react": "*" - vitest-browser-react: "*" + "@vitejs/plugin-react": ">=5" + vitest-browser-react: ">=2" languageName: unknown linkType: soft @@ -5769,18 +5695,18 @@ __metadata: languageName: node linkType: hard -"@types/three@npm:*": - version: 0.176.0 - resolution: "@types/three@npm:0.176.0" +"@types/three@npm:*, @types/three@npm:^0.181.0": + version: 0.181.0 + resolution: "@types/three@npm:0.181.0" dependencies: - "@dimforge/rapier3d-compat": "npm:^0.12.0" + "@dimforge/rapier3d-compat": "npm:~0.12.0" "@tweenjs/tween.js": "npm:~23.1.3" "@types/stats.js": "npm:*" "@types/webxr": "npm:*" "@webgpu/types": "npm:*" fflate: "npm:~0.8.2" - meshoptimizer: "npm:~0.18.1" - checksum: 10c0/a06960a71987e717382de3a4d8d22059b40312d85febda8a9dd1aaa1a152d88ff8df12bd9b93c64a45ff10dc68b94b2f5f81ab251f9f542578149a768745e064 + meshoptimizer: "npm:~0.22.0" + checksum: 10c0/c110de1a1934ef4cceb9071a52ccdab6df0c6502600322004a29c4b273c196a0e700fed4257a421190d8618a27f0e418e3ba14da3e3a7168cc014007be1b8f04 languageName: node linkType: hard @@ -5796,21 +5722,6 @@ __metadata: languageName: node linkType: hard -"@types/three@npm:^0.181.0": - version: 0.181.0 - resolution: "@types/three@npm:0.181.0" - dependencies: - "@dimforge/rapier3d-compat": "npm:~0.12.0" - "@tweenjs/tween.js": "npm:~23.1.3" - "@types/stats.js": "npm:*" - "@types/webxr": "npm:*" - "@webgpu/types": "npm:*" - fflate: "npm:~0.8.2" - meshoptimizer: "npm:~0.22.0" - checksum: 10c0/c110de1a1934ef4cceb9071a52ccdab6df0c6502600322004a29c4b273c196a0e700fed4257a421190d8618a27f0e418e3ba14da3e3a7168cc014007be1b8f04 - languageName: node - linkType: hard - "@types/treeify@npm:^1.0.0": version: 1.0.3 resolution: "@types/treeify@npm:1.0.3" @@ -6394,13 +6305,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.13": + version: 3.5.24 + resolution: "@vue/shared@npm:3.5.24" + checksum: 10c0/4fd5665539fa5be3d12280c1921a8db3a707115fef54d22d83ce347ea06e3b1089dfe07292e0c46bbebf23553c7c1ec98010972ebccf10532db82422801288ff + 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" @@ -6520,11 +6438,11 @@ __metadata: linkType: hard "@yarnpkg/fslib@npm:^3.1.2, @yarnpkg/fslib@npm:^3.1.3": - version: 3.1.3 - resolution: "@yarnpkg/fslib@npm:3.1.3" + version: 3.1.4 + resolution: "@yarnpkg/fslib@npm:3.1.4" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/ce4cebede6af53e445515b46ad1467e80cc7dce248de93c97ec42de1e26bae44fd766f07230152566fe4fcfba46a7496d21fa855e750c3d1036087608a56a329 + checksum: 10c0/d38e108a6349b34b6bd885ad9fc8dcc3b7d50be5e54246e260cba899a80b5c91c8e9b27ad1e9c59b2d46e02cc07f8b7d839a851e388aa568d6efc85f5d6d10dc languageName: node linkType: hard @@ -6855,21 +6773,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": - version: 3.1.8 - resolution: "array-includes@npm:3.1.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - is-string: "npm:^1.0.7" - checksum: 10c0/5b1004d203e85873b96ddc493f090c9672fd6c80d7a60b798da8a14bff8a670ff95db5aafc9abc14a211943f05220dacf8ea17638ae0af1a6a47b8c0b48ce370 - languageName: node - linkType: hard - -"array-includes@npm:^3.1.9": +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": version: 3.1.9 resolution: "array-includes@npm:3.1.9" dependencies: @@ -8832,15 +8736,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.1, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 languageName: node linkType: hard @@ -8862,18 +8766,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.4.3": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - "decimal.js@npm:^10.5.0": version: 10.5.0 resolution: "decimal.js@npm:10.5.0" @@ -9294,66 +9186,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" - dependencies: - array-buffer-byte-length: "npm:^1.0.2" - arraybuffer.prototype.slice: "npm:^1.0.4" - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - data-view-buffer: "npm:^1.0.2" - data-view-byte-length: "npm:^1.0.2" - data-view-byte-offset: "npm:^1.0.1" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-set-tostringtag: "npm:^2.1.0" - es-to-primitive: "npm:^1.3.0" - function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" - get-symbol-description: "npm:^1.1.0" - globalthis: "npm:^1.0.4" - gopd: "npm:^1.2.0" - has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.1.0" - is-array-buffer: "npm:^3.0.5" - is-callable: "npm:^1.2.7" - is-data-view: "npm:^1.0.2" - is-regex: "npm:^1.2.1" - is-shared-array-buffer: "npm:^1.0.4" - is-string: "npm:^1.1.1" - is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" - math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.7" - own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" - safe-array-concat: "npm:^1.1.3" - safe-push-apply: "npm:^1.0.0" - safe-regex-test: "npm:^1.1.0" - set-proto: "npm:^1.0.0" - string.prototype.trim: "npm:^1.2.10" - string.prototype.trimend: "npm:^1.0.9" - string.prototype.trimstart: "npm:^1.0.8" - typed-array-buffer: "npm:^1.0.3" - typed-array-byte-length: "npm:^1.0.3" - typed-array-byte-offset: "npm:^1.0.4" - typed-array-length: "npm:^1.0.7" - unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10c0/1de229c9e08fe13c17fe5abaec8221545dfcd57e51f64909599a6ae896df84b8fd2f7d16c60cb00d7bf495b9298ca3581aded19939d4b7276854a4b066f8422b - languageName: node - linkType: hard - -"es-abstract@npm:^1.24.0": +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": version: 1.24.0 resolution: "es-abstract@npm:1.24.0" dependencies: @@ -9989,14 +9822,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.0": - version: 4.2.0 - resolution: "eslint-visitor-keys@npm:4.2.0" - checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^4.2.1": +"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1": version: 4.2.1 resolution: "eslint-visitor-keys@npm:4.2.1" checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 @@ -10052,18 +9878,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.3.0": - version: 10.3.0 - resolution: "espree@npm:10.3.0" - dependencies: - acorn: "npm:^8.14.0" - acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462 - languageName: node - linkType: hard - -"espree@npm:^10.4.0, espree@npm:^9.6.1 || ^10.4.0": +"espree@npm:^10.0.1, espree@npm:^10.3.0, espree@npm:^10.4.0, espree@npm:^9.6.1 || ^10.4.0": version: 10.4.0 resolution: "espree@npm:10.4.0" dependencies: @@ -10394,18 +10209,6 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4": - version: 6.4.4 - resolution: "fdir@npm:6.4.4" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/6ccc33be16945ee7bc841e1b4178c0b4cf18d3804894cb482aa514651c962a162f96da7ffc6ebfaf0df311689fb70091b04dd6caffe28d56b9ebdc0e7ccadfdd - languageName: node - linkType: hard - "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -10837,13 +10640,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10c0/758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 - languageName: node - linkType: hard - "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -11028,18 +10824,7 @@ __metadata: languageName: node linkType: hard -"hash-base@npm:^3.0.0": - version: 3.1.0 - resolution: "hash-base@npm:3.1.0" - dependencies: - inherits: "npm:^2.0.4" - readable-stream: "npm:^3.6.0" - safe-buffer: "npm:^5.2.0" - checksum: 10c0/663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 - languageName: node - linkType: hard - -"hash-base@npm:^3.1.2": +"hash-base@npm:^3.0.0, hash-base@npm:^3.1.2": version: 3.1.2 resolution: "hash-base@npm:3.1.2" dependencies: @@ -11358,14 +11143,7 @@ __metadata: languageName: node linkType: hard -"import-meta-resolve@npm:^4.0.0": - version: 4.1.0 - resolution: "import-meta-resolve@npm:4.1.0" - checksum: 10c0/42f3284b0460635ddf105c4ad99c6716099c3ce76702602290ad5cbbcd295700cbc04e4bdf47bacf9e3f1a4cec2e1ff887dabc20458bef398f9de22ddff45ef5 - languageName: node - linkType: hard - -"import-meta-resolve@npm:^4.2.0": +"import-meta-resolve@npm:^4.0.0, import-meta-resolve@npm:^4.2.0": version: 4.2.0 resolution: "import-meta-resolve@npm:4.2.0" checksum: 10c0/3ee8aeecb61d19b49d2703987f977e9d1c7d4ba47db615a570eaa02fe414f40dfa63f7b953e842cbe8470d26df6371332bfcf21b2fd92b0112f9fea80dde2c4c @@ -11860,7 +11638,7 @@ __metadata: languageName: node linkType: hard -"is-string@npm:^1.0.7, is-string@npm:^1.1.1": +"is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" dependencies: @@ -11904,7 +11682,7 @@ __metadata: languageName: node linkType: hard -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0, is-weakref@npm:^1.1.1": +"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.1": version: 1.1.1 resolution: "is-weakref@npm:1.1.1" dependencies: @@ -12789,7 +12567,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.17, magic-string@npm:^0.30.3": +"magic-string@npm:^0.30.17": version: 0.30.17 resolution: "magic-string@npm:0.30.17" dependencies: @@ -12798,7 +12576,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.21": +"magic-string@npm:^0.30.21, magic-string@npm:^0.30.3": version: 0.30.21 resolution: "magic-string@npm:0.30.21" dependencies: @@ -13918,11 +13696,11 @@ __metadata: linkType: hard "minizlib@npm:^3.0.1": - version: 3.0.2 - resolution: "minizlib@npm:3.0.2" + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" dependencies: minipass: "npm:^7.1.2" - checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78 + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec languageName: node linkType: hard @@ -14686,7 +14464,20 @@ __metadata: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": +"parse-asn1@npm:^5.0.0": + version: 5.1.9 + resolution: "parse-asn1@npm:5.1.9" + dependencies: + asn1.js: "npm:^4.10.1" + browserify-aes: "npm:^1.2.0" + evp_bytestokey: "npm:^1.0.3" + pbkdf2: "npm:^3.1.5" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/6dfe27c121be3d63ebbf95f03d2ae0a07dd716d44b70b0bd3458790a822a80de05361c62147271fd7b845dcc2d37755d9c9c393064a3438fe633779df0bc07e7 + languageName: node + linkType: hard + +"parse-asn1@npm:^5.1.7": version: 5.1.7 resolution: "parse-asn1@npm:5.1.7" dependencies: @@ -14846,7 +14637,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.1.2": +"pbkdf2@npm:^3.1.2, pbkdf2@npm:^3.1.5": version: 3.1.5 resolution: "pbkdf2@npm:3.1.5" dependencies: @@ -14890,14 +14681,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.2": - version: 4.0.2 - resolution: "picomatch@npm:4.0.2" - checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc - languageName: node - linkType: hard - -"picomatch@npm:^4.0.3": +"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3": version: 4.0.3 resolution: "picomatch@npm:4.0.3" checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 @@ -15743,17 +15527,7 @@ __metadata: languageName: node linkType: hard -"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": - version: 2.0.2 - resolution: "ripemd160@npm:2.0.2" - dependencies: - hash-base: "npm:^3.0.0" - inherits: "npm:^2.0.1" - checksum: 10c0/f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a - languageName: node - linkType: hard - -"ripemd160@npm:^2.0.3": +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.3": version: 2.0.3 resolution: "ripemd160@npm:2.0.3" dependencies: @@ -15939,7 +15713,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -17126,17 +16900,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": - version: 0.2.13 - resolution: "tinyglobby@npm:0.2.13" - dependencies: - fdir: "npm:^6.4.4" - picomatch: "npm:^4.0.2" - checksum: 10c0/ef07dfaa7b26936601d3f6d999f7928a4d1c6234c5eb36896bb88681947c0d459b7ebe797022400e555fe4b894db06e922b95d0ce60cb05fd827a0a66326b18c - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -17178,6 +16942,13 @@ __metadata: languageName: node linkType: hard +"tm-themes@npm:^1.10.12": + version: 1.10.12 + resolution: "tm-themes@npm:1.10.12" + checksum: 10c0/dcb3ef2fede7ed373909caed11d8c6a89837eb7682515182cc9f52f0c580b5bb4d9f0019cb1c1199fea416bb8d327f31d5b696903aeaf66b2a88cbcad80cf632 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -17594,13 +17365,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.8.2": - version: 5.8.3 - resolution: "typescript@npm:5.8.3" +"typescript@npm:^5.8.2, typescript@npm:^5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 languageName: node linkType: hard @@ -17634,13 +17405,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": - version: 5.8.3 - resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" +"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 languageName: node linkType: hard @@ -18588,7 +18359,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies: @@ -18731,22 +18502,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0": - version: 8.18.2 - resolution: "ws@npm:8.18.2" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 - languageName: node - linkType: hard - -"ws@npm:^8.18.3": +"ws@npm:^8.18.0, ws@npm:^8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: