From e36f95af825d92a5d6b005953c0a6d9e51fbdce6 Mon Sep 17 00:00:00 2001 From: Miorel-Lucian Palii Date: Sat, 28 Sep 2024 23:53:09 -0700 Subject: [PATCH 1/3] Read workspace scripts from `package.json` files --- workspaces/repository-scripts/src/main.ts | 4 ++ .../repository-scripts/src/runCommands.ts | 68 +++++++++---------- workspaces/repository-scripts/src/scripts.ts | 15 ---- .../src/chdirToCurrentGitRepositoryRoot.ts | 7 ++ workspaces/util/src/readPackageJson.ts | 19 ++++++ workspaces/util/src/readWorkspaces.ts | 25 +++++-- yarn.config.cjs | 22 +++++- 7 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 workspaces/util/src/chdirToCurrentGitRepositoryRoot.ts create mode 100644 workspaces/util/src/readPackageJson.ts diff --git a/workspaces/repository-scripts/src/main.ts b/workspaces/repository-scripts/src/main.ts index acbf1f08..f0817eb3 100644 --- a/workspaces/repository-scripts/src/main.ts +++ b/workspaces/repository-scripts/src/main.ts @@ -1,5 +1,7 @@ import process from "node:process"; +import { chdirToCurrentGitRepositoryRoot } from "@code-chronicles/util/chdirToCurrentGitRepositoryRoot"; + import { runCommands } from "./runCommands.ts"; import { SCRIPTS, isScript } from "./scripts.ts"; @@ -16,6 +18,8 @@ async function main() { throw new Error(`Invalid script: ${script}`); } + await chdirToCurrentGitRepositoryRoot(); + const action = async () => await runCommands(script, scriptArgs); const actionWrapper = SCRIPTS[script]?.run; diff --git a/workspaces/repository-scripts/src/runCommands.ts b/workspaces/repository-scripts/src/runCommands.ts index 91eb06df..88470cc8 100644 --- a/workspaces/repository-scripts/src/runCommands.ts +++ b/workspaces/repository-scripts/src/runCommands.ts @@ -1,19 +1,17 @@ import type { SpawnOptions } from "node:child_process"; import process from "node:process"; -import { getCurrentGitRepositoryRoot } from "@code-chronicles/util/getCurrentGitRepositoryRoot"; +import nullthrows from "nullthrows"; + import { maybeThrow } from "@code-chronicles/util/maybeThrow"; import { promiseAllLimitingConcurrency } from "@code-chronicles/util/promiseAllLimitingConcurrency"; +import { readPackageJson } from "@code-chronicles/util/readPackageJson"; import { readWorkspaces } from "@code-chronicles/util/readWorkspaces"; import { runWithLogGroupAsync } from "@code-chronicles/util/runWithLogGroupAsync"; import { spawnWithSafeStdio } from "@code-chronicles/util/spawnWithSafeStdio"; import { stripPrefixOrThrow } from "@code-chronicles/util/stripPrefixOrThrow"; -import { - SCRIPTS, - SCRIPTS_TO_SKIP_BY_WORKSPACE, - type Script, -} from "./scripts.ts"; +import { SCRIPTS, type Script } from "./scripts.ts"; type FailedCommand = { command: string; @@ -44,41 +42,41 @@ export async function runCommands( } }; - const commands = [ - async () => { + const commands = (await readWorkspaces()).map((workspace) => async () => { + if (workspace.location === ".") { const rootCommand = SCRIPTS[script]?.repositoryRootCommand; - if (rootCommand != null) { - const currentGitRepositoryRoot = await getCurrentGitRepositoryRoot(); - - await runWithLogGroupAsync( - `Running script ${script} for repository root!`, - async () => - await run.apply(null, [ - ...rootCommand, - { cwd: currentGitRepositoryRoot }, - ]), - ); - } - }, - - ...(await readWorkspaces()).map((workspace) => async () => { - const workspaceShortName = stripPrefixOrThrow( - workspace, - "@code-chronicles/", - ); - if (SCRIPTS_TO_SKIP_BY_WORKSPACE[workspaceShortName]?.has(script)) { - console.error( - `Skipping script ${script} for workspace: ${workspaceShortName}`, - ); + if (rootCommand == null) { + console.error(`Skipping script ${script} for repository root!`); return; } await runWithLogGroupAsync( - `Running script ${script} for workspace: ${workspaceShortName}`, - async () => await run("yarn", ["workspace", workspace, script]), + `Running script ${script} for repository root!`, + async () => await run(...rootCommand), + ); + return; + } + + const workspaceName = nullthrows(workspace.name); + const workspaceShortName = stripPrefixOrThrow( + workspaceName, + "@code-chronicles/", + ); + + const { scripts } = await readPackageJson(workspace.location); + + if (scripts?.[script] == null) { + console.error( + `Skipping script ${script} for workspace: ${workspaceShortName}`, ); - }), - ]; + return; + } + + await runWithLogGroupAsync( + `Running script ${script} for workspace: ${workspaceShortName}`, + async () => await run("yarn", ["workspace", workspaceName, script]), + ); + }); await promiseAllLimitingConcurrency( commands, diff --git a/workspaces/repository-scripts/src/scripts.ts b/workspaces/repository-scripts/src/scripts.ts index 12080c6d..b84a17e1 100644 --- a/workspaces/repository-scripts/src/scripts.ts +++ b/workspaces/repository-scripts/src/scripts.ts @@ -63,18 +63,3 @@ export type Script = keyof typeof SCRIPTS; export function isScript(value: string): value is Script { return Object.hasOwn(SCRIPTS, value); } - -// TODO: maybe read this from the package.json of each workspace -export const SCRIPTS_TO_SKIP_BY_WORKSPACE: Readonly< - Record> -> = { - "download-leetcode-submissions": new Set(["test"]), - "eslint-config": new Set(["test", "typecheck"]), - "fetch-leetcode-problem-list": new Set(["test"]), - "fetch-recent-accepted-leetcode-submissions": new Set(["test"]), - "generate-health-report": new Set(["test"]), - "javascript-leetcode-month": new Set(["test"]), - "leetcode-zen-mode": new Set(["test"]), - "post-leetcode-potd-to-discord": new Set(["test"]), - "repository-scripts": new Set(["test"]), -}; diff --git a/workspaces/util/src/chdirToCurrentGitRepositoryRoot.ts b/workspaces/util/src/chdirToCurrentGitRepositoryRoot.ts new file mode 100644 index 00000000..a81960af --- /dev/null +++ b/workspaces/util/src/chdirToCurrentGitRepositoryRoot.ts @@ -0,0 +1,7 @@ +import process from "node:process"; + +import { getCurrentGitRepositoryRoot } from "@code-chronicles/util/getCurrentGitRepositoryRoot"; + +export async function chdirToCurrentGitRepositoryRoot(): Promise { + process.chdir(await getCurrentGitRepositoryRoot()); +} diff --git a/workspaces/util/src/readPackageJson.ts b/workspaces/util/src/readPackageJson.ts new file mode 100644 index 00000000..55c6f317 --- /dev/null +++ b/workspaces/util/src/readPackageJson.ts @@ -0,0 +1,19 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { z } from "zod"; + +const packageJsonZodType = z.object({ + scripts: z.record(z.string(), z.string()).optional(), +}); + +export type PackageJson = z.infer; + +export async function readPackageJson( + directory: string = ".", +): Promise { + const text = await readFile(path.join(directory, "package.json"), { + encoding: "utf8", + }); + return packageJsonZodType.passthrough().parse(JSON.parse(text)); +} diff --git a/workspaces/util/src/readWorkspaces.ts b/workspaces/util/src/readWorkspaces.ts index 9813580d..d19644fe 100644 --- a/workspaces/util/src/readWorkspaces.ts +++ b/workspaces/util/src/readWorkspaces.ts @@ -1,10 +1,23 @@ -import { assertIsObject } from "@code-chronicles/util/assertIsObject"; -import { assertIsString } from "@code-chronicles/util/assertIsString"; +import { z } from "zod"; + import { compareStringsCaseInsensitive } from "@code-chronicles/util/compareStringsCaseInsensitive"; import { getLines } from "@code-chronicles/util/getLines"; import { execWithArgsOrThrowOnNzec } from "@code-chronicles/util/execWithArgsOrThrowOnNzec"; -export async function readWorkspaces(): Promise { +const workspaceZodType = z.union([ + z.object({ + location: z.string(), + name: z.string(), + }), + z.object({ + location: z.literal("."), + name: z.null(), + }), +]); + +export type Workspace = z.infer; + +export async function readWorkspaces(): Promise { const yarnCommandResult = await execWithArgsOrThrowOnNzec("yarn", [ "workspaces", "list", @@ -12,8 +25,6 @@ export async function readWorkspaces(): Promise { ]); return [...getLines(yarnCommandResult.stdout)] - .map((line) => assertIsObject(JSON.parse(line))) - .filter((workspace) => workspace.location !== ".") - .map(({ name }) => assertIsString(name)) - .sort(compareStringsCaseInsensitive); + .map((line) => workspaceZodType.parse(JSON.parse(line))) + .sort((a, b) => compareStringsCaseInsensitive(a.name ?? "", b.name ?? "")); } diff --git a/yarn.config.cjs b/yarn.config.cjs index b68732b7..306d9982 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -1,5 +1,7 @@ const { defineConfig } = require("@yarnpkg/types"); +const NAME_PREFIX = "@code-chronicles/"; + module.exports = defineConfig({ async constraints({ Yarn }) { const rootWorkspace = Yarn.workspace({ cwd: "." }); @@ -8,6 +10,20 @@ module.exports = defineConfig({ return; } + // Expect each workspace to specify a name, except for the root. + for (const workspace of Yarn.workspaces()) { + if (workspace.cwd === ".") { + workspace.unset("name"); + } else { + const { name } = workspace.manifest; + if (typeof name !== "string" || !name.startsWith(NAME_PREFIX)) { + workspace.error( + `Repository name didn't start with the prefix ${JSON.stringify(NAME_PREFIX)}`, + ); + } + } + } + // Use ESM everywhere. for (const workspace of Yarn.workspaces()) { workspace.set("type", "module"); @@ -40,10 +56,10 @@ module.exports = defineConfig({ // Expect each workspace to specify a version, except for the root. for (const workspace of Yarn.workspaces()) { - if (workspace.cwd !== ".") { - workspace.set("version", workspace.manifest.version ?? "0.0.1"); - } else { + if (workspace.cwd === ".") { workspace.unset("version"); + } else { + workspace.set("version", workspace.manifest.version ?? "0.0.1"); } } From 341a1eb1b2ac744d2882d1929713bcfc96e5e524 Mon Sep 17 00:00:00 2001 From: Miorel-Lucian Palii Date: Sun, 29 Sep 2024 00:12:39 -0700 Subject: [PATCH 2/3] preserve ordering --- workspaces/util/src/readPackageJson.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/workspaces/util/src/readPackageJson.ts b/workspaces/util/src/readPackageJson.ts index 55c6f317..b7ff941f 100644 --- a/workspaces/util/src/readPackageJson.ts +++ b/workspaces/util/src/readPackageJson.ts @@ -5,6 +5,7 @@ import { z } from "zod"; const packageJsonZodType = z.object({ scripts: z.record(z.string(), z.string()).optional(), + workspaces: z.array(z.string()).optional(), }); export type PackageJson = z.infer; @@ -12,8 +13,14 @@ export type PackageJson = z.infer; export async function readPackageJson( directory: string = ".", ): Promise { - const text = await readFile(path.join(directory, "package.json"), { - encoding: "utf8", - }); - return packageJsonZodType.passthrough().parse(JSON.parse(text)); + const data = JSON.parse( + await readFile(path.join(directory, "package.json"), { + encoding: "utf8", + }), + ); + + // Using Zod for validation only, not for returning, to preserve the ordering + // of keys in the original data. + packageJsonZodType.parse(data); + return data; } From 33e7c4559b2410a71cbe7c24a46717ac2a0a3b5e Mon Sep 17 00:00:00 2001 From: Miorel-Lucian Palii Date: Sun, 29 Sep 2024 00:15:28 -0700 Subject: [PATCH 3/3] Update a comment --- .github/workflows/run-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 08d32909..7a1fd69b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -27,6 +27,7 @@ jobs: uses: ./.github/workflows/set-up-everything # Yarn constraints are kinda like tests. + # TODO: own job anyway - name: Check Yarn constraints run: yarn constraints # TODO: Maybe write up a job summary per https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary