Skip to content
Source
Sign inSign up

sync-exports.ts

121 lines4.3 KB

1// Regenerate every package's export maps (see `exports.generate` in
2// package-build.ts) without compiling anything: each package's
3// `scripts/build.ts` runs in exports-only mode.
4//
5// node scripts/sync-exports.ts rewrite the package.json files
6// node scripts/sync-exports.ts --check fail if any of them is out of date, or
7// if package source or an example
8// imports another package's internal
9import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
10import * as NodeServices from "@effect/platform-node/NodeServices";
11import { Command, Flag } from "effect/cli";
12import * as Console from "effect/Console";
13import * as Data from "effect/Data";
14import * as Effect from "effect/Effect";
15import * as FileSystem from "effect/FileSystem";
16import * as Path from "effect/Path";
17import { ChildProcess } from "effect/process";
18import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
19​
20class InvalidManifest extends Data.TaggedError("InvalidManifest")<{
21 readonly path: string;
22 readonly cause: unknown;
23}> {}
24​
25const run = Effect.fn(function* (command: string, args: Array<string>, cwd: string) {
26 const spawner = yield* ChildProcessSpawner;
27 return yield* spawner.exitCode(
28 ChildProcess.make(command, args, {
29 cwd,
30 env: { PACKAGE_BUILD_EXPORTS_ONLY: "1" },
31 extendEnv: true,
32 stdout: "inherit",
33 stderr: "inherit",
34 }),
35 );
36});
37​
38const command = Command.make(
39 "sync-exports",
40 {
41 check: Flag.Boolean("check").pipe(
42 Flag.withDescription(
43 "Fail if any export map is out of date, or if package source or an example imports another package's internal",
44 ),
45 Flag.withDefault(false),
46 ),
47 },
48 Effect.fn(function* ({ check }) {
49 const fs = yield* FileSystem.FileSystem;
50 const path = yield* Path.Path;
51 const root = path.resolve(import.meta.dirname, "..");
52 const packages = path.join(root, "packages");
53​
54 const directories = (yield* fs.readDirectory(packages)).sort();
55 // Packages whose published export maps block everything named `internal`.
56 const blocking: Array<string> = [];
57 for (const name of directories) {
58 const directory = path.join(packages, name);
59 if (!(yield* fs.exists(path.join(directory, "scripts", "build.ts")))) continue;
60 const exitCode = yield* run("node", ["scripts/build.ts"], directory);
61 if (exitCode !== 0) {
62 return yield* Effect.fail(
63 new Error(`packages/${name}: export sync exited with ${exitCode}`),
64 );
65 }
66 const text = yield* fs.readFileString(path.join(directory, "package.json"));
67 const manifest = yield* Effect.try({
68 try: () =>
69 JSON.parse(text) as {
70 name: string;
71 publishConfig?: { exports?: Record<string, unknown> };
72 },
73 catch: (cause) => new InvalidManifest({ path: `packages/${name}/package.json`, cause }),
74 });
75 if (Object.values(manifest.publishConfig?.exports ?? {}).includes(null)) {
76 blocking.push(manifest.name.replaceAll(".", String.raw`\.`));
77 }
78 }
79​
80 if (check) {
81 const exitCode = yield* run(
82 "git",
83 ["diff", "--exit-code", "--stat", "--", "packages/*/package.json"],
84 root,
85 );
86 if (exitCode !== 0) {
87 return yield* Effect.fail(
88 new Error(
89 "Package exports are out of date. Run `pnpm exports:sync` and commit the result.",
90 ),
91 );
92 }
93 // Published packages block `internal/` and `internal.ts`, so only tests may reach into them.
94 const found = yield* run(
95 "git",
96 [
97 "grep",
98 "-nE",
99 `"(${blocking.join("|")})/([^"]*/)?internal[/"]`,
100 "--",
101 "packages/*/src/**",
102 "examples",
103 ],
104 root,
105 );
106 if (found === 0) {
107 return yield* Effect.fail(
108 new Error(
109 "Package source and examples must not import another package's internal/ or internal.ts.",
110 ),
111 );
112 }
113 yield* Console.log("Package exports are up to date.");
114 }
115 }),
116).pipe(Command.withDescription("Regenerate every package's export maps"));
117​
118Command.run(command, { version: "0.0.0" }).pipe(
119 Effect.provide(NodeServices.layer),
120 NodeRuntime.runMain,
121);