diff --git a/bun.lock b/bun.lock index 8f2e4aae..23dfb1df 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,10 @@ "": { "name": "@opencor/opencor", "dependencies": { - "@napi-rs/keyring": "^1.2.0", + "@napi-rs/keyring": "^1.3.0", "@primeuix/themes": "^2.0.3", "@primevue/auto-import-resolver": "4.2.5", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "primeicons": "^7.0.0", "primevue": "4.2.5", "vue": "3.4.21", @@ -42,7 +42,7 @@ "tailwindcss-primeui": "^0.6.1", "tar": "^7.5.20", "typescript": "^7.0.2", - "typescript-6": "npm:typescript@^6.0.0", + "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.4", "vue-tsc": "^3.3.7", diff --git a/package.json b/package.json index f2ab168d..8db5c80e 100644 --- a/package.json +++ b/package.json @@ -23,28 +23,28 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260714.0", + "version": "0.20260714.1", "engines": { "bun": ">=1.2.0" }, "scripts": { - "archive:web": "bun src/renderer/scripts/archive.web.js", - "build": "bun src/renderer/scripts/libopencor.js && electron-vite build", + "archive:web": "bun src/renderer/scripts/archive.web.ts", + "build": "bun src/renderer/scripts/libopencor.ts && electron-vite build", "build:web": "bun --cwd src/renderer build", - "clean": "bun src/renderer/scripts/clean.js", - "dependencies:update": "bun src/renderer/scripts/dependencies.update.js", - "dev": "bun src/renderer/scripts/libopencor.js && electron-vite dev --watch", + "clean": "bun src/renderer/scripts/clean.ts", + "dependencies:update": "bun src/renderer/scripts/dependencies.update.ts", + "dev": "bun src/renderer/scripts/libopencor.ts && electron-vite dev --watch", "dev:web": "bun --cwd src/renderer dev", "format": "bunx --bun biome format --fix --max-diagnostics=none && clang-format -i src/renderer/src/libopencor/src/*", "format:check": "bunx --bun biome format --max-diagnostics=none && clang-format --dry-run -Werror src/renderer/src/libopencor/src/*", "lint": "bunx --bun biome lint --fix --error-on-warnings --max-diagnostics=none && bunx stylelint '**/*.css' --fix", - "postinstall": "bun src/renderer/scripts/ensure.electron.js", - "release": "bun src/renderer/scripts/libopencor.js && electron-builder", - "release:local": "bun src/renderer/scripts/libopencor.js && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.notarize=false --publish=never", - "start": "bun src/renderer/scripts/libopencor.js && electron-vite preview", + "postinstall": "bun src/renderer/scripts/ensure.electron.ts", + "release": "bun src/renderer/scripts/libopencor.ts && electron-builder", + "release:local": "bun src/renderer/scripts/libopencor.ts && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.notarize=false --publish=never", + "start": "bun src/renderer/scripts/libopencor.ts && electron-vite preview", "start:web": "bun --cwd src/renderer start", - "typecheck": "bun --bun src/renderer/scripts/type.check.js --noEmit -p tsconfig.app.json", - "version:new": "bun src/renderer/scripts/version.new.js" + "typecheck": "bun --bun src/renderer/scripts/type.check.ts --noEmit -p tsconfig.app.json", + "version:new": "bun src/renderer/scripts/version.new.ts" }, "bun": { "overrides": { @@ -55,10 +55,10 @@ "vue": "3.4.21" }, "dependencies": { - "@napi-rs/keyring": "^1.2.0", + "@napi-rs/keyring": "^1.3.0", "@primeuix/themes": "^2.0.3", "@primevue/auto-import-resolver": "4.2.5", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "primeicons": "^7.0.0", "primevue": "4.2.5", "vue": "3.4.21", @@ -92,7 +92,7 @@ "tailwindcss-primeui": "^0.6.1", "tar": "^7.5.20", "typescript": "^7.0.2", - "typescript-6": "npm:typescript@^6.0.0", + "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.4", "vue-tsc": "^3.3.7" diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index 994d76fc..0ada8758 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -5,10 +5,10 @@ "": { "name": "@opencor/opencor", "dependencies": { - "@napi-rs/keyring": "^1.2.0", + "@napi-rs/keyring": "^1.3.0", "@primeuix/themes": "^2.0.3", "@primevue/auto-import-resolver": "4.2.5", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "primeicons": "^7.0.0", "primevue": "4.2.5", "vue": "3.4.21", @@ -31,7 +31,7 @@ "tailwindcss": "^4.3.2", "tailwindcss-primeui": "^0.6.1", "typescript": "^7.0.2", - "typescript-6": "npm:typescript@^6.0.0", + "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.4", "vue-tsc": "^3.3.7", diff --git a/src/renderer/package.json b/src/renderer/package.json index 0ab100d3..33b7ec02 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,20 +42,20 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260714.0", + "version": "0.20260714.1", "libopencorVersion": "0.20260619.0", "scripts": { - "build": "vite build && bun scripts/generate.version.js", - "build:lib": "vite build --config vite.lib.config.ts && bun --bun scripts/type.check.js --project tsconfig.lib.types.json", - "clean": "bun scripts/clean.js", - "dependencies:update": "bun scripts/dependencies.update.js", + "build": "vite build && bun scripts/generate.version.ts", + "build:lib": "vite build --config vite.lib.config.ts && bun --bun scripts/type.check.ts --project tsconfig.lib.types.json", + "clean": "bun scripts/clean.ts", + "dependencies:update": "bun scripts/dependencies.update.ts", "dev": "vite dev", "format": "bunx --bun biome format --fix --max-diagnostics=none", "format:check": "bunx --bun biome format --max-diagnostics=none", "lint": "bunx --bun biome lint --fix --error-on-warnings --max-diagnostics=none && bunx stylelint '**/*.css' --fix", "start": "bun run build && vite preview", - "typecheck": "bun --bun scripts/type.check.js --noEmit", - "version:new": "bun scripts/version.new.js" + "typecheck": "bun --bun scripts/type.check.ts --noEmit", + "version:new": "bun scripts/version.new.ts" }, "bun": { "overrides": { @@ -66,10 +66,10 @@ "vue": "3.4.21" }, "dependencies": { - "@napi-rs/keyring": "^1.2.0", + "@napi-rs/keyring": "^1.3.0", "@primeuix/themes": "^2.0.3", "@primevue/auto-import-resolver": "4.2.5", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "primeicons": "^7.0.0", "primevue": "4.2.5", "vue": "3.4.21", @@ -92,7 +92,7 @@ "tailwindcss": "^4.3.2", "tailwindcss-primeui": "^0.6.1", "typescript": "^7.0.2", - "typescript-6": "npm:typescript@^6.0.0", + "typescript-6": "npm:typescript@^6.0.3", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.4", "vue-tsc": "^3.3.7" diff --git a/src/renderer/scripts/archive.web.js b/src/renderer/scripts/archive.web.ts similarity index 73% rename from src/renderer/scripts/archive.web.js rename to src/renderer/scripts/archive.web.ts index 03126325..9c28ad87 100644 --- a/src/renderer/scripts/archive.web.js +++ b/src/renderer/scripts/archive.web.ts @@ -7,7 +7,7 @@ if (!fs.existsSync('dist')) { fs.mkdirSync('dist'); } -const version = JSON.parse(fs.readFileSync('package.json')).version; +const version = (JSON.parse(fs.readFileSync('package.json')) as { version: string }).version; tar.c( { diff --git a/src/renderer/scripts/clean.js b/src/renderer/scripts/clean.ts similarity index 100% rename from src/renderer/scripts/clean.js rename to src/renderer/scripts/clean.ts diff --git a/src/renderer/scripts/dependencies.update.js b/src/renderer/scripts/dependencies.update.js deleted file mode 100644 index 5bd4f570..00000000 --- a/src/renderer/scripts/dependencies.update.js +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bun - -import { spawnSync } from 'bun'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const run = (command, args) => { - const res = spawnSync({ - cmd: [command, ...args], - stdio: ['inherit', 'inherit', 'inherit'] - }); - - if (res.exitCode !== 0) { - console.error(`Command failed: ${command} ${args.join(' ')}`); - - process.exit(res.exitCode); - } -}; - -const updateDependencies = (dir) => { - process.chdir(dir); - - run('bun', ['clean']); - run('bun', ['update', '-i']); - - fs.rmSync(path.join(dir, 'bun.lock'), { force: true }); - - run('bun', ['install']); - run('bun', ['clean']); -}; - -// Update dependencies in our root and renderer directories. - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -updateDependencies(path.join(__dirname, '../../..')); -updateDependencies(path.join(__dirname, '..')); diff --git a/src/renderer/scripts/dependencies.update.ts b/src/renderer/scripts/dependencies.update.ts new file mode 100644 index 00000000..0b204cf4 --- /dev/null +++ b/src/renderer/scripts/dependencies.update.ts @@ -0,0 +1,290 @@ +#!/usr/bin/env bun + +import { $, spawnSync } from 'bun'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as readline from 'node:readline/promises'; + +type Dependency = { + packageName: string; + range: string; + alias: string | null; +}; + +type Candidate = { + name: string; + packageName: string; + range: string; + alias: string | null; + current: string | null; + latest: string; +}; + +// Run a command synchronously, inheriting stdio. Exit on failure. + +const run = (command: string, args: string[]): void => { + const res = spawnSync({ + cmd: [command, ...args], + stdio: ['inherit', 'inherit', 'inherit'] + }); + + if (res.exitCode !== 0) { + console.error(`Command failed: ${command} ${args.join(' ')}`); + + process.exit(res.exitCode); + } +}; + +// Check if a version string is pinned (i.e. starts with a digit) or is a range (i.e. starts with ^, ~, etc.). + +const isVersionPinned = (version: string): boolean => { + return /^\d/.test(version); +}; + +// Parse a dependency into its components: package name, version range, and optional alias name (e.g., +// "npm:@scope/pkg@^1.2.3" -> { packageName: "@scope/pkg", range: "^1.2.3", alias: "npm" }). + +const parseDependency = (name: string, version: string): Dependency => { + if (version.startsWith('npm:')) { + const at = version.lastIndexOf('@'); + const packageName = at > 4 ? version.substring(4, at) : version.substring(4); + const range = at > 4 ? version.substring(at + 1) : 'latest'; + + return { packageName, range, alias: name }; + } + + return { packageName: name, range: version, alias: null }; +}; + +// Compare two semver strings. Return 0 if v1 === v2, a positive number if v1 > v2, and a negative number if v1 < v2. + +const compareVersions = (v1: string, v2: string): number => { + const v1List = v1.split('.').map(Number); + const v2List = v2.split('.').map(Number); + + for (let i = 0; i < 3; ++i) { + if (v1List[i] !== v2List[i]) { + return v1List[i] - v2List[i]; + } + } + + return 0; +}; + +// Extract the minimum concrete version from a range string (e.g., "^1.2.3" -> "1.2.3"). + +const extractVersion = (range: string): string | null => { + const match = range.match(/(\d+\.\d+\.\d+)/); + + return match ? match[1] : null; +}; + +// Build a new version range keeping the original prefix but pointing at a newer version (e.g., ("^6.0.0", "6.0.3") -> +// "^6.0.3"). + +const buildRange = (oldRange: string, newVersion: string): string => { + const prefix = oldRange.match(/^[~^><≄≤]+/)?.[0] || ''; + + return `${prefix}${newVersion}`; +}; + +// Check for updates to dependencies in a given directory. + +const checkUpdates = async ( + label: string, + allDependencies: Record, + padEnd: number +): Promise => { + const candidates: Candidate[] = []; + + console.log(`\n${label} dependencies:\n`); + + const entries = Object.entries(allDependencies).filter(([_, version]) => { + return !isVersionPinned(version); + }); + const results = await Promise.all( + entries.map(async ([name, version]) => { + const dependency = parseDependency(name, version); + + try { + let latest: string; + + if (name === 'typescript-6') { + // Special case of a dependency for which we want to stick to the version range, even if a newer major version + // is available. + + const dependencyVersion = + await $`npm view ${dependency.packageName}@${dependency.range} version --json`.text(); + const versions = JSON.parse(dependencyVersion.trim()); + + latest = Array.isArray(versions) ? versions[versions.length - 1] : versions; + } else { + // Retrieve the absolute latest version of the dependency, which may be a major bump. + + const dependencyVersion = await $`npm view ${dependency.packageName} version --json`.text(); + + latest = JSON.parse(dependencyVersion.trim()); + } + + return { + name, + version, + dependency, + current: extractVersion(dependency.range), + latest, + ok: true as const + }; + } catch { + return { + name, + ok: false as const + }; + } + }) + ); + + for (const result of results) { + if (!result.ok) { + console.log(` āš ļø ${result.name.padEnd(padEnd)} Could not be checked`); + + continue; + } + + if (result.latest && result.current && compareVersions(result.latest, result.current) > 0) { + candidates.push({ + name: result.name, + ...result.dependency, + current: result.current, + latest: result.latest + }); + + console.log(` šŸ“¦ ${result.name.padEnd(padEnd)} ${result.current} → ${result.latest}`); + } else { + console.log(` āœ… ${result.name.padEnd(padEnd)} ${result.current} (up to date)`); + } + } + + // Print pinned dependencies at the end so they are visible but clearly separated. + + const pinned = Object.entries(allDependencies).filter(([_, version]) => { + return isVersionPinned(version); + }); + + for (const [name, version] of pinned) { + console.log(` ā­ļø ${name.padEnd(padEnd)} Pinned (${version})`); + } + + return candidates; +}; + +const updateDependencies = async (dir: string): Promise => { + const label = path.basename(dir) === 'renderer' ? 'Renderer' : 'Root'; + const packageJsonPath = path.join(dir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { + dependencies?: Record; + devDependencies?: Record; + }; + const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; + const padEnd = Math.max(0, ...Object.keys(allDependencies).map((name) => name.length)) + 1; + + process.chdir(dir); + + const candidates = await checkUpdates(label, allDependencies, padEnd); + + if (candidates.length === 0) { + console.log(`\n✨ All ${label} dependencies are up to date!`); + + return; + } + + console.log(`\nSelect the dependencies to update (${label}):`); + + candidates.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + + for (const [index, candidate] of candidates.entries()) { + console.log( + ` ${(index + 1).toString().padStart(2)}. ${candidate.name.padEnd(padEnd - 1)} ${candidate.current} → ${candidate.latest}` + ); + } + + console.log(` ${'a'.padStart(2)}. All`); + console.log(` ${'0'.padStart(2)}. Skip`); + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = (await rl.question('\nEnter numbers (comma/space separated), or a/A for all: ')).trim(); + + rl.close(); + + if (answer === '0' || answer === '') { + console.log(' Skipped.'); + + return; + } + + let selected: Candidate[]; + + if (answer.toLowerCase() === 'a') { + selected = candidates; + } else { + const indices = [ + ...new Set( + answer + .split(/[,\s]+/) + .map(Number) + .filter((index) => { + return index > 0 && index <= candidates.length; + }) + ) + ]; + + selected = indices.map((index) => { + return candidates[index - 1]; + }); + } + + if (selected.length === 0) { + console.log(' No valid selection. Skipped.'); + + return; + } + + // Update package.json with the selected dependencies. + + for (const item of selected) { + const newRange = buildRange(item.range, item.latest); + const newVersion = item.alias ? `npm:${item.packageName}@${newRange}` : newRange; + + if (packageJson.dependencies?.[item.name] !== undefined) { + packageJson.dependencies[item.name] = newVersion; + } else if (packageJson.devDependencies?.[item.name] !== undefined) { + packageJson.devDependencies[item.name] = newVersion; + } + + console.log(` āœ“ ${item.name}: ${item.range} → ${newRange}`); + } + + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + + // Run Bun commands to update dependencies and clean up. + + run('bun', ['clean']); + fs.rmSync(path.join(dir, 'bun.lock'), { force: true }); + run('bun', ['install']); + run('bun', ['clean']); + + console.log(`\nāœ… ${label} dependencies updated!`); +}; + +// Update our dependencies in both the root and renderer directories. + +console.log('šŸ” Dependency Update Checker'); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +await updateDependencies(path.join(__dirname, '../../..')); +await updateDependencies(path.join(__dirname, '..')); + +console.log('\nāœ… All done!'); diff --git a/src/renderer/scripts/ensure.electron.js b/src/renderer/scripts/ensure.electron.ts similarity index 100% rename from src/renderer/scripts/ensure.electron.js rename to src/renderer/scripts/ensure.electron.ts diff --git a/src/renderer/scripts/generate.version.js b/src/renderer/scripts/generate.version.ts similarity index 88% rename from src/renderer/scripts/generate.version.js rename to src/renderer/scripts/generate.version.ts index 4a13ceec..567a7db2 100644 --- a/src/renderer/scripts/generate.version.js +++ b/src/renderer/scripts/generate.version.ts @@ -8,8 +8,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageJsonPath = path.join(__dirname, '../package.json'); -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); -const { version } = packageJson; +const version = (JSON.parse(fs.readFileSync(packageJsonPath)) as { version: string }).version; // Make sure that the dist/assets folder exists. diff --git a/src/renderer/scripts/libopencor.js b/src/renderer/scripts/libopencor.ts similarity index 75% rename from src/renderer/scripts/libopencor.js rename to src/renderer/scripts/libopencor.ts index 70406cf6..4154ab66 100644 --- a/src/renderer/scripts/libopencor.js +++ b/src/renderer/scripts/libopencor.ts @@ -4,7 +4,7 @@ import * as bun from 'bun'; (async () => { if (!(await bun.file('./dist/libOpenCOR/Release/libOpenCOR.node').exists())) { - const build = await bun.spawn(['cmake-js', 'build', '-B', 'Release', '-O', './dist/libOpenCOR'], { + const build = bun.spawn(['cmake-js', 'build', '-B', 'Release', '-O', './dist/libOpenCOR'], { stdio: ['inherit', 'inherit', 'inherit'] }); const exitCode = await build.exited; diff --git a/src/renderer/scripts/type.check.js b/src/renderer/scripts/type.check.ts similarity index 91% rename from src/renderer/scripts/type.check.js rename to src/renderer/scripts/type.check.ts index f484aa56..cc83808f 100644 --- a/src/renderer/scripts/type.check.js +++ b/src/renderer/scripts/type.check.ts @@ -16,7 +16,7 @@ const projectRoot = : currentWorkingDir; const origResolveFilename = Module._resolveFilename; -Module._resolveFilename = function (request, parent, ...args) { +Module._resolveFilename = function (request: string, parent: Module, ...args: unknown[]) { if (request === 'typescript/lib/tsc') { return path.join(projectRoot, 'node_modules', 'typescript-6', 'lib', 'tsc.js'); } diff --git a/src/renderer/scripts/version.new.js b/src/renderer/scripts/version.new.ts similarity index 83% rename from src/renderer/scripts/version.new.js rename to src/renderer/scripts/version.new.ts index c2ae61ff..6456f9a9 100644 --- a/src/renderer/scripts/version.new.js +++ b/src/renderer/scripts/version.new.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'; // Current version from our package.json file. const scriptDirName = path.dirname(fileURLToPath(import.meta.url)); -const oldVersion = JSON.parse(fs.readFileSync(`${scriptDirName}/../package.json`)).version; +const oldVersion = (JSON.parse(fs.readFileSync(`${scriptDirName}/../package.json`)) as { version: string }).version; const oldVersionParts = oldVersion.split('.'); const oldMajorVersion = oldVersionParts[0]; @@ -30,8 +30,8 @@ const newVersion = `${newMajorVersion}.${newMinorVersion}.${newPatchVersion}`; // Update our package.json files. -const updatePackageJsonFile = (filePath) => { - const contents = JSON.parse(fs.readFileSync(filePath)); +const updatePackageJsonFile = (filePath: string): void => { + const contents = JSON.parse(fs.readFileSync(filePath)) as { version: string }; contents.version = newVersion;