Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions workspaces/repository-scripts/src/main.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;

Expand Down
68 changes: 33 additions & 35 deletions workspaces/repository-scripts/src/runCommands.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 0 additions & 15 deletions workspaces/repository-scripts/src/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ReadonlySet<Script>>
> = {
"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"]),
};
7 changes: 7 additions & 0 deletions workspaces/util/src/chdirToCurrentGitRepositoryRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import process from "node:process";

import { getCurrentGitRepositoryRoot } from "@code-chronicles/util/getCurrentGitRepositoryRoot";

export async function chdirToCurrentGitRepositoryRoot(): Promise<void> {
process.chdir(await getCurrentGitRepositoryRoot());
}
26 changes: 26 additions & 0 deletions workspaces/util/src/readPackageJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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(),
workspaces: z.array(z.string()).optional(),
});

export type PackageJson = z.infer<typeof packageJsonZodType>;

export async function readPackageJson(
directory: string = ".",
): Promise<PackageJson> {
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;
}
25 changes: 18 additions & 7 deletions workspaces/util/src/readWorkspaces.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
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<string[]> {
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<typeof workspaceZodType>;

export async function readWorkspaces(): Promise<Workspace[]> {
const yarnCommandResult = await execWithArgsOrThrowOnNzec("yarn", [
"workspaces",
"list",
"--json",
]);

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 ?? ""));
}
22 changes: 19 additions & 3 deletions yarn.config.cjs
Original file line number Diff line number Diff line change
@@ -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: "." });
Expand All @@ -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");
Expand Down Expand Up @@ -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");
}
}

Expand Down
Loading