Skip to content

Commit ab4e821

Browse files
committed
Read workspace scripts from package.json files
1 parent 56f9b58 commit ab4e821

File tree

7 files changed

+101
-60
lines changed

7 files changed

+101
-60
lines changed

workspaces/repository-scripts/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import process from "node:process";
22

3+
import { chdirToCurrentGitRepositoryRoot } from "@code-chronicles/util/chdirToCurrentGitRepositoryRoot";
4+
35
import { runCommands } from "./runCommands.ts";
46
import { SCRIPTS, isScript } from "./scripts.ts";
57

@@ -16,6 +18,8 @@ async function main() {
1618
throw new Error(`Invalid script: ${script}`);
1719
}
1820

21+
await chdirToCurrentGitRepositoryRoot();
22+
1923
const action = async () => await runCommands(script, scriptArgs);
2024
const actionWrapper = SCRIPTS[script]?.run;
2125

workspaces/repository-scripts/src/runCommands.ts

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import type { SpawnOptions } from "node:child_process";
22
import process from "node:process";
33

4-
import { getCurrentGitRepositoryRoot } from "@code-chronicles/util/getCurrentGitRepositoryRoot";
4+
import nullthrows from "nullthrows";
5+
import { z } from "zod";
6+
57
import { maybeThrow } from "@code-chronicles/util/maybeThrow";
68
import { promiseAllLimitingConcurrency } from "@code-chronicles/util/promiseAllLimitingConcurrency";
9+
import { readPackageJson } from "@code-chronicles/util/readPackageJson";
710
import { readWorkspaces } from "@code-chronicles/util/readWorkspaces";
811
import { runWithLogGroupAsync } from "@code-chronicles/util/runWithLogGroupAsync";
912
import { spawnWithSafeStdio } from "@code-chronicles/util/spawnWithSafeStdio";
1013
import { stripPrefixOrThrow } from "@code-chronicles/util/stripPrefixOrThrow";
1114

12-
import {
13-
SCRIPTS,
14-
SCRIPTS_TO_SKIP_BY_WORKSPACE,
15-
type Script,
16-
} from "./scripts.ts";
15+
import { SCRIPTS, type Script } from "./scripts.ts";
1716

1817
type FailedCommand = {
1918
command: string;
@@ -44,41 +43,41 @@ export async function runCommands(
4443
}
4544
};
4645

47-
const commands = [
48-
async () => {
46+
const commands = (await readWorkspaces()).map((workspace) => async () => {
47+
if (workspace.location === ".") {
4948
const rootCommand = SCRIPTS[script]?.repositoryRootCommand;
50-
if (rootCommand != null) {
51-
const currentGitRepositoryRoot = await getCurrentGitRepositoryRoot();
52-
53-
await runWithLogGroupAsync(
54-
`Running script ${script} for repository root!`,
55-
async () =>
56-
await run.apply(null, [
57-
...rootCommand,
58-
{ cwd: currentGitRepositoryRoot },
59-
]),
60-
);
61-
}
62-
},
63-
64-
...(await readWorkspaces()).map((workspace) => async () => {
65-
const workspaceShortName = stripPrefixOrThrow(
66-
workspace,
67-
"@code-chronicles/",
68-
);
69-
if (SCRIPTS_TO_SKIP_BY_WORKSPACE[workspaceShortName]?.has(script)) {
70-
console.error(
71-
`Skipping script ${script} for workspace: ${workspaceShortName}`,
72-
);
49+
if (rootCommand == null) {
50+
console.error(`Skipping script ${script} for repository root!`);
7351
return;
7452
}
7553

7654
await runWithLogGroupAsync(
77-
`Running script ${script} for workspace: ${workspaceShortName}`,
78-
async () => await run("yarn", ["workspace", workspace, script]),
55+
`Running script ${script} for repository root!`,
56+
async () => await run(...rootCommand),
57+
);
58+
return;
59+
}
60+
61+
const workspaceName = nullthrows(workspace.name);
62+
const workspaceShortName = stripPrefixOrThrow(
63+
workspaceName,
64+
"@code-chronicles/",
65+
);
66+
67+
const { scripts } = await readPackageJson(workspace.location);
68+
69+
if (scripts?.[script] == null) {
70+
console.error(
71+
`Skipping script ${script} for workspace: ${workspaceShortName}`,
7972
);
80-
}),
81-
];
73+
return;
74+
}
75+
76+
await runWithLogGroupAsync(
77+
`Running script ${script} for workspace: ${workspaceShortName}`,
78+
async () => await run("yarn", ["workspace", workspaceName, script]),
79+
);
80+
});
8281

8382
await promiseAllLimitingConcurrency(
8483
commands,

workspaces/repository-scripts/src/scripts.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,3 @@ export type Script = keyof typeof SCRIPTS;
6363
export function isScript(value: string): value is Script {
6464
return Object.hasOwn(SCRIPTS, value);
6565
}
66-
67-
// TODO: maybe read this from the package.json of each workspace
68-
export const SCRIPTS_TO_SKIP_BY_WORKSPACE: Readonly<
69-
Record<string, ReadonlySet<Script>>
70-
> = {
71-
"download-leetcode-submissions": new Set(["test"]),
72-
"eslint-config": new Set(["test", "typecheck"]),
73-
"fetch-leetcode-problem-list": new Set(["test"]),
74-
"fetch-recent-accepted-leetcode-submissions": new Set(["test"]),
75-
"generate-health-report": new Set(["test"]),
76-
"javascript-leetcode-month": new Set(["test"]),
77-
"leetcode-zen-mode": new Set(["test"]),
78-
"post-leetcode-potd-to-discord": new Set(["test"]),
79-
"repository-scripts": new Set(["test"]),
80-
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import process from "node:process";
2+
3+
import { getCurrentGitRepositoryRoot } from "@code-chronicles/util/getCurrentGitRepositoryRoot";
4+
5+
export async function chdirToCurrentGitRepositoryRoot(): Promise<void> {
6+
process.chdir(await getCurrentGitRepositoryRoot());
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { readFile } from "node:fs/promises";
2+
import path from "node:path";
3+
4+
import { z } from "zod";
5+
6+
const packageJsonZodType = z.object({
7+
scripts: z.record(z.string(), z.string()).optional(),
8+
});
9+
10+
export type PackageJson = z.infer<typeof packageJsonZodType>;
11+
12+
export async function readPackageJson(
13+
directory: string = ".",
14+
): Promise<PackageJson> {
15+
const text = await readFile(path.join(directory, "package.json"), {
16+
encoding: "utf8",
17+
});
18+
return packageJsonZodType.passthrough().parse(JSON.parse(text));
19+
}
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
1-
import { assertIsObject } from "@code-chronicles/util/assertIsObject";
2-
import { assertIsString } from "@code-chronicles/util/assertIsString";
1+
import { z } from "zod";
2+
33
import { compareStringsCaseInsensitive } from "@code-chronicles/util/compareStringsCaseInsensitive";
44
import { getLines } from "@code-chronicles/util/getLines";
55
import { execWithArgsOrThrowOnNzec } from "@code-chronicles/util/execWithArgsOrThrowOnNzec";
66

7-
export async function readWorkspaces(): Promise<string[]> {
7+
const workspaceZodType = z.union([
8+
z.object({
9+
location: z.string(),
10+
name: z.string(),
11+
}),
12+
z.object({
13+
location: z.literal("."),
14+
name: z.null(),
15+
}),
16+
]);
17+
18+
export type Workspace = z.infer<typeof workspaceZodType>;
19+
20+
export async function readWorkspaces(): Promise<Workspace[]> {
821
const yarnCommandResult = await execWithArgsOrThrowOnNzec("yarn", [
922
"workspaces",
1023
"list",
1124
"--json",
1225
]);
1326

1427
return [...getLines(yarnCommandResult.stdout)]
15-
.map((line) => assertIsObject(JSON.parse(line)))
16-
.filter((workspace) => workspace.location !== ".")
17-
.map(({ name }) => assertIsString(name))
18-
.sort(compareStringsCaseInsensitive);
28+
.map((line) => workspaceZodType.parse(JSON.parse(line)))
29+
.sort((a, b) => compareStringsCaseInsensitive(a.name ?? "", b.name ?? ""));
1930
}

yarn.config.cjs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { defineConfig } = require("@yarnpkg/types");
22

3+
const NAME_PREFIX = "@code-chronicles/";
4+
35
module.exports = defineConfig({
46
async constraints({ Yarn }) {
57
const rootWorkspace = Yarn.workspace({ cwd: "." });
@@ -8,6 +10,20 @@ module.exports = defineConfig({
810
return;
911
}
1012

13+
// Expect each workspace to specify a name, except for the root.
14+
for (const workspace of Yarn.workspaces()) {
15+
if (workspace.cwd === ".") {
16+
workspace.unset("name");
17+
} else {
18+
const { name } = workspace.manifest;
19+
if (typeof name !== "string" || !name.startsWith(NAME_PREFIX)) {
20+
workspace.error(
21+
`Repository name didn't start with the prefix ${JSON.stringify(NAME_PREFIX)}`,
22+
);
23+
}
24+
}
25+
}
26+
1127
// Use ESM everywhere.
1228
for (const workspace of Yarn.workspaces()) {
1329
workspace.set("type", "module");
@@ -40,10 +56,10 @@ module.exports = defineConfig({
4056

4157
// Expect each workspace to specify a version, except for the root.
4258
for (const workspace of Yarn.workspaces()) {
43-
if (workspace.cwd !== ".") {
44-
workspace.set("version", workspace.manifest.version ?? "0.0.1");
45-
} else {
59+
if (workspace.cwd === ".") {
4660
workspace.unset("version");
61+
} else {
62+
workspace.set("version", workspace.manifest.version ?? "0.0.1");
4763
}
4864
}
4965

0 commit comments

Comments
 (0)