@gusnips/sdkgen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { writeGenerated } from "./write.ts";
6
+
7
+ function repo(): string {
8
+ const root = mkdtempSync(join(tmpdir(), "sdkgen-write-"));
9
+ // The repo's own style, which the output must follow rather than prettier's defaults.
10
+ writeFileSync(join(root, ".prettierrc"), JSON.stringify({ tabWidth: 4 }));
11
+ return root;
12
+ }
13
+
14
+ const UGLY = "export interface A {\na: string\n}\n";
15
+ const PRETTY = "export interface A {\n a: string;\n}\n";
16
+
17
+ describe("writeGenerated", () => {
18
+ it("writes each file through the repo's prettier config, creating its folder", async () => {
19
+ const root = repo();
20
+ const path = join(root, "src/generated/a.ts");
21
+ expect(await writeGenerated({ [path]: UGLY })).toEqual({ written: [path], stale: [] });
22
+ expect(readFileSync(path, "utf8")).toBe(PRETTY);
23
+ });
24
+
25
+ it("reads a relative path from root, and reports it as given", async () => {
26
+ const root = repo();
27
+ const name = "src/generated/a.ts";
28
+ expect(await writeGenerated({ [name]: UGLY }, { root, check: true })).toEqual({
29
+ written: [],
30
+ stale: [name],
31
+ });
32
+ expect(await writeGenerated({ [name]: UGLY }, { root })).toEqual({
33
+ written: [name],
34
+ stale: [],
35
+ });
36
+ expect(readFileSync(join(root, name), "utf8")).toBe(PRETTY);
37
+ });
38
+
39
+ it("leaves a current file alone", async () => {
40
+ const root = repo();
41
+ const path = join(root, "a.ts");
42
+ writeFileSync(path, PRETTY);
43
+ expect(await writeGenerated({ [path]: UGLY })).toEqual({ written: [], stale: [] });
44
+ expect(await writeGenerated({ [path]: UGLY }, { check: true })).toEqual({
45
+ written: [],
46
+ stale: [],
47
+ });
48
+ });
49
+
50
+ it("when checking, lists a missing or different file and writes nothing", async () => {
51
+ const root = repo();
52
+ const changed = join(root, "a.ts");
53
+ const missing = join(root, "b.ts");
54
+ writeFileSync(changed, "export {};\n");
55
+ expect(await writeGenerated({ [changed]: UGLY, [missing]: UGLY }, { check: true })).toEqual({
56
+ written: [],
57
+ stale: [changed, missing],
58
+ });
59
+ expect(readFileSync(changed, "utf8")).toBe("export {};\n");
60
+ });
61
+
62
+ it("fails on a file it cannot read, rather than calling it stale", async () => {
63
+ const folder = join(repo(), "a.ts");
64
+ mkdirSync(folder);
65
+ await expect(writeGenerated({ [folder]: UGLY }, { check: true })).rejects.toThrow(/EISDIR/);
66
+ });
67
+ });
package/src/write.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Writing generated files, or proving the ones on disk are current.
3
+ *
4
+ * Every file goes through the repo's own prettier config first. The output is checked in and read
5
+ * by whoever opens the SDK, and formatting it here is also what keeps `format:check` and the
6
+ * generator's own check from ever disagreeing about one file.
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { format, resolveConfig } from "prettier";
11
+
12
+ /** The generated text for each file, by path: absolute, or relative to `root`. */
13
+ export type GeneratedFiles = Readonly<Record<string, string>>;
14
+
15
+ export interface WriteOptions {
16
+ /** What a relative path in `files` starts from, usually the repo root. Default: the working directory. */
17
+ root?: string;
18
+ /** Write nothing, and list the files that would change. */
19
+ check?: boolean;
20
+ }
21
+
22
+ /** Paths as `files` spells them, so a message can print them as they are. */
23
+ export interface WriteResult {
24
+ /** Files that were missing or different and were written. Always empty when checking. */
25
+ written: string[];
26
+ /** Files that are missing or differ from what the API says today. Always empty when writing. */
27
+ stale: string[];
28
+ }
29
+
30
+ /**
31
+ * Formats each file with the prettier config that applies at its path, then writes it — or, with
32
+ * `check`, writes nothing and lists the files that would change.
33
+ *
34
+ * ```ts
35
+ * const { stale } = await writeGenerated(files, { root, check: process.argv.includes("--check") });
36
+ * if (stale.length > 0) {
37
+ * console.error(`Out of date:\n ${stale.join("\n ")}\nRun \`bun run sdk:gen\` and commit.`);
38
+ * process.exitCode = 1;
39
+ * }
40
+ * ```
41
+ */
42
+ export async function writeGenerated(
43
+ files: GeneratedFiles,
44
+ { root = ".", check = false }: WriteOptions = {},
45
+ ): Promise<WriteResult> {
46
+ const result: WriteResult = { written: [], stale: [] };
47
+ for (const [name, text] of Object.entries(files)) {
48
+ const path = resolve(root, name);
49
+ const formatted = await format(text, { ...(await resolveConfig(path)), filepath: path });
50
+ if (readOrNull(path) === formatted) continue;
51
+ if (check) {
52
+ result.stale.push(name);
53
+ continue;
54
+ }
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, formatted);
57
+ result.written.push(name);
58
+ }
59
+ return result;
60
+ }
61
+
62
+ function readOrNull(path: string): string | null {
63
+ try {
64
+ return readFileSync(path, "utf8");
65
+ } catch (err) {
66
+ // Missing is stale. Anything else, such as a permission error, is not an answer to "is it
67
+ // current", so it throws.
68
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return null;
69
+ throw err;
70
+ }
71
+ }