diff --git a/.nvmrc b/.nvmrc index ec09f38d..1d9b7831 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.17.0 \ No newline at end of file +22.12.0 diff --git a/package-lock.json b/package-lock.json index 322b2ef0..8286da02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9027,7 +9027,7 @@ }, "packages/examples/react/basic": { "name": "@connectrpc/connect-query-example-basic", - "version": "0.0.0", + "version": "2.0.0", "dependencies": { "@bufbuild/buf": "1.46.0", "@bufbuild/protobuf": "^2.2.1", diff --git a/packages/examples/react/basic/package.json b/packages/examples/react/basic/package.json index 7c17e7f8..f862414d 100644 --- a/packages/examples/react/basic/package.json +++ b/packages/examples/react/basic/package.json @@ -1,6 +1,6 @@ { "name": "@connectrpc/connect-query-example-basic", - "version": "0.0.0", + "version": "2.0.0", "private": true, "type": "module", "scripts": { diff --git a/scripts/set-workspace-version.js b/scripts/set-workspace-version.js index 5d757b52..692d0c9d 100755 --- a/scripts/set-workspace-version.js +++ b/scripts/set-workspace-version.js @@ -14,9 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { readdirSync, readFileSync, writeFileSync } from "fs"; -import { join } from "path"; -import { existsSync } from "node:fs"; +// eslint-disable-next-line n/no-unsupported-features/node-builtins +import { readFileSync, writeFileSync, existsSync, globSync } from "node:fs"; +import { dirname, join } from "node:path"; import assert from "node:assert"; if (process.argv.length < 3) { @@ -24,8 +24,8 @@ if (process.argv.length < 3) { [ `USAGE: ${process.argv[1]} `, "", - "Walks through all packages in the given workspace directory and", - "sets the version of each package to the given version.", + "Walks through all workspace packages and sets the version of each ", + "package to the given version.", "If a package depends on another package from the workspace, the", "dependency version is updated as well.", "", @@ -36,15 +36,16 @@ if (process.argv.length < 3) { try { const newVersion = process.argv[2]; - const packagesDir = "packages"; const lockFile = "package-lock.json"; - const packages = readPackages(packagesDir); + const workspaces = findWorkspacePackages("package.json"); const lock = tryReadLock(lockFile); - const updates = setVersion(packages, lock, newVersion); + const updates = setVersion(workspaces, lock, newVersion); if (updates.length > 0) { - writePackages(packagesDir, packages); + for (const { path, pkg } of workspaces) { + writeJson(path, pkg); + } if (lock) { - writeLock(lockFile, lock); + writeJson(lockFile, lock); } process.stdout.write(formatUpdates(updates) + "\n"); } @@ -53,9 +54,22 @@ try { process.exit(1); } -function setVersion(packages, lock, newVersion) { +/** + * @typedef {{path: string; pkg: Package}} Workspace + * @typedef {{name: string; version?: string}} Package + * @typedef {{packages: Record}} Lockfile + * @typedef {{message: string, pkg: Package}} Update + */ + +/** + * @param {Workspace[]} workspaces + * @param {Lockfile | null} lock + * @param {string} newVersion + * @return {Update[]} + */ +function setVersion(workspaces, lock, newVersion) { const updates = []; - for (const pkg of packages) { + for (const { pkg } of workspaces) { if (typeof pkg.version !== "string") { continue; } @@ -65,31 +79,39 @@ function setVersion(packages, lock, newVersion) { } pkg.version = newVersion; if (lock) { - const l = Array.from(Object.values(lock.packages)).find( - (l) => l.name === pkg.name, - ); - if (!pkg.private) { - assert( - l, - `Cannot find lock entry for ${pkg.name} and it is not private`, + const l = Object.entries(lock.packages).find(([path, l]) => { + if ("name" in l) { + return l.name === pkg.name; + } + // In some situations, the entry for a local package doesn't have a "name" property. + // We check the path of the entry instead: If the last path element is the same as + // the package name without scope, it's the entry we are looking for. + return ( + !path.startsWith("node_modules/") && + path.split("/").pop() === pkg.name.split("/").pop() ); - } - if (l) { - l.version = newVersion; - } + })?.[1]; + assert(l, `Cannot find lock entry for ${pkg.name} and it is not private`); + l.version = newVersion; } updates.push({ - package: pkg, + pkg, message: `updated version from ${pkg.version} to ${newVersion}`, }); } - updates.push(...syncDeps(packages, packages)); + const pkgs = workspaces.map(({ pkg }) => pkg); + updates.push(...syncDeps(pkgs, pkgs)); if (lock) { - syncDeps(Object.values(lock.packages), packages); + syncDeps(Object.values(lock.packages), pkgs); } return updates; } +/** + * @param {Record} packages + * @param {Package[]} deps + * @return {Update[]} + */ function syncDeps(packages, deps) { const updates = []; for (const pkg of packages) { @@ -122,7 +144,7 @@ function syncDeps(packages, deps) { } pkg[key][name] = wantVersion; updates.push({ - package: pkg, + pkg, message: `updated ${key}["${name}"] from ${version} to ${wantVersion}`, }); } @@ -131,45 +153,54 @@ function syncDeps(packages, deps) { return updates; } -function readPackages(packagesDir) { - const packagesByPath = readPackagesByPath(packagesDir); - return Object.values(packagesByPath); -} - -function writePackages(packagesDir, packages) { - const packagesByPath = readPackagesByPath(packagesDir); - for (const [path, oldPkg] of Object.entries(packagesByPath)) { - const newPkg = packages.find((p) => p.name === oldPkg.name); - writeFileSync(path, JSON.stringify(newPkg, null, 2) + "\n"); +/** + * Read the given root package.json file, and return an array of workspace + * packages. + * + * @param {string} rootPackageJsonPath + * @return {Workspace[]} + */ +function findWorkspacePackages(rootPackageJsonPath) { + const root = JSON.parse(readFileSync(rootPackageJsonPath, "utf-8")); + if ( + !Array.isArray(root.workspaces) || + root.workspaces.some((w) => typeof w !== "string") + ) { + throw new Error( + `Missing or malformed "workspaces" array in ${rootPackageJsonPath}`, + ); } + const rootDir = dirname(rootPackageJsonPath); + return root.workspaces + .flatMap((ws) => globSync(join(rootDir, ws, "package.json"))) + .filter((path) => existsSync(path)) + .map((path) => { + const pkg = JSON.parse(readFileSync(path, "utf-8")); + return { path, pkg }; + }); } -function readPackagesByPath(packagesDir) { - const packages = {}; - for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - const path = join(packagesDir, entry.name, "package.json"); - if (existsSync(path)) { - const pkg = JSON.parse(readFileSync(path, "utf-8")); - if (!pkg.name) { - throw new Error(`${path} is missing "name"`); - } - packages[path] = pkg; - } - } - return packages; +/** + * @param {string} path + * @param {Record} json + */ +function writeJson(path, json) { + writeFileSync(path, JSON.stringify(json, null, 2) + "\n"); } +/** + * + * @param {Update[]} updates + * @return {string} + */ function formatUpdates(updates) { const lines = []; const updatesByName = {}; for (const update of updates) { - if (updatesByName[update.package.name] === undefined) { - updatesByName[update.package.name] = []; + if (updatesByName[update.pkg.name] === undefined) { + updatesByName[update.pkg.name] = []; } - updatesByName[update.package.name].push(update); + updatesByName[update.pkg.name].push(update); } for (const name of Object.keys(updatesByName).sort()) { lines.push(`${name}:`); @@ -180,6 +211,10 @@ function formatUpdates(updates) { return lines.join("\n"); } +/** + * @param {string} lockFile + * @return {Lockfile | null} + */ function tryReadLock(lockFile) { if (!existsSync(lockFile)) { return null; @@ -190,7 +225,3 @@ function tryReadLock(lockFile) { assert(lock.packages !== null); return lock; } - -function writeLock(lockFile, lock) { - writeFileSync(lockFile, JSON.stringify(lock, null, 2) + "\n"); -}