Skip to content

Commit

Permalink
feat(cli): wing gen-docs (#7056)
Browse files Browse the repository at this point in the history
Co-authored-by: wingbot <[email protected]>
Co-authored-by: monada-bot[bot] <[email protected]>
  • Loading branch information
3 people authored Aug 30, 2024
1 parent f773d4a commit b976493
Show file tree
Hide file tree
Showing 48 changed files with 1,191 additions and 222 deletions.
25 changes: 25 additions & 0 deletions apps/wing/fixtures/valid1/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<h2>API Reference</h2>

<h3>Table of Contents</h3>

- **Classes**
- <a href="#valid1.Foo">Foo</a>

<h3 id="valid1.Foo">Foo (preflight class)</h3>

A test class

<h4>Constructor</h4>

<pre>
new(): Foo
</pre>

<h4>Properties</h4>

*No properties*

<h4>Methods</h4>

*No methods*

2 changes: 2 additions & 0 deletions apps/wing/fixtures/valid1/lib.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// A test class
pub class Foo {}
6 changes: 6 additions & 0 deletions apps/wing/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ async function main() {
.hook("preAction", collectAnalyticsHook)
.action(runSubCommand("lsp"));

program
.command("gen-docs")
.description("Generate documentation for the current project")
.hook("preAction", collectAnalyticsHook)
.action(runSubCommand("generateDocs"));

program
.command("compile")
.description("Compiles a Wing program")
Expand Down
73 changes: 7 additions & 66 deletions apps/wing/src/commands/compile.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { promises as fsPromise } from "fs";
import { dirname, relative, resolve } from "path";
import { dirname, resolve } from "path";

import * as wingCompiler from "@winglang/compiler";
import { loadEnvVariables } from "@winglang/sdk/lib/helpers";
import { prettyPrintError } from "@winglang/sdk/lib/util/enhanced-error";
import chalk from "chalk";
import { CHARS_ASCII, emitDiagnostic, File, Label } from "codespan-wasm";
import debug from "debug";
import { glob } from "glob";
import { formatDiagnostics } from "./diagnostics";
import { COLORING } from "../util";

// increase the stack trace limit to 50, useful for debugging Rust panics
// (not setting the limit too high in case of infinite recursion)
Expand Down Expand Up @@ -88,80 +88,21 @@ export async function compile(entrypoint?: string, options?: CompileOptions): Pr
modes: options?.testing ? ["test"] : ["compile"],
cwd: resolve(dirname(entrypoint)),
});
const coloring = chalk.supportsColor ? chalk.supportsColor.hasBasic : false;
const compileOutput = await wingCompiler.compile(entrypoint, {
...options,
log,
color: coloring,
color: COLORING,
platform: options?.platform ?? ["sim"],
});
if (compileOutput.wingcErrors.length > 0) {
// Print any errors or warnings from the compiler.
const diagnostics = compileOutput.wingcErrors;
const cwd = process.cwd();
const result = [];

for (const diagnostic of diagnostics) {
const { message, span, annotations, hints, severity } = diagnostic;
const files: File[] = [];
const labels: Label[] = [];

// file_id might be "" if the span is synthetic (see #2521)
if (span?.file_id) {
// `span` should only be null if source file couldn't be read etc.
const source = await fsPromise.readFile(span.file_id, "utf8");
const start = span.start_offset;
const end = span.end_offset;
const filePath = relative(cwd, span.file_id);
files.push({ name: filePath, source });
labels.push({
fileId: filePath,
rangeStart: start,
rangeEnd: end,
message: "",
style: "primary",
});
}

for (const annotation of annotations) {
// file_id might be "" if the span is synthetic (see #2521)
if (!annotation.span?.file_id) {
continue;
}
const source = await fsPromise.readFile(annotation.span.file_id, "utf8");
const start = annotation.span.start_offset;
const end = annotation.span.end_offset;
const filePath = relative(cwd, annotation.span.file_id);
files.push({ name: filePath, source });
labels.push({
fileId: filePath,
rangeStart: start,
rangeEnd: end,
message: annotation.message,
style: "secondary",
});
}

const diagnosticText = emitDiagnostic(
files,
{
message,
severity,
labels,
notes: hints.map((hint) => `hint: ${hint}`),
},
{
chars: CHARS_ASCII,
},
coloring
);
result.push(diagnosticText);
}
const formatted = await formatDiagnostics(diagnostics);

if (compileOutput.wingcErrors.map((e) => e.severity).includes("error")) {
throw new Error(result.join("\n").trimEnd());
throw new Error(formatted);
} else {
console.error(result.join("\n").trimEnd());
console.error(formatted);
}
}

Expand Down
69 changes: 69 additions & 0 deletions apps/wing/src/commands/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { readFile } from "fs/promises";
import { relative } from "path";
import { WingDiagnostic } from "@winglang/compiler";
import { Label, File, emitDiagnostic, CHARS_ASCII } from "codespan-wasm";
import { COLORING } from "../util";

export async function formatDiagnostics(diagnostics: WingDiagnostic[]): Promise<string> {
const cwd = process.cwd();
const result = [];

for (const diagnostic of diagnostics) {
const { message, span, annotations, hints, severity } = diagnostic;
const files: File[] = [];
const labels: Label[] = [];

// file_id might be "" if the span is synthetic (see #2521)
if (span?.file_id) {
// `span` should only be null if source file couldn't be read etc.
const source = await readFile(span.file_id, "utf8");
const start = span.start_offset;
const end = span.end_offset;
const filePath = relative(cwd, span.file_id);
files.push({ name: filePath, source });
labels.push({
fileId: filePath,
rangeStart: start,
rangeEnd: end,
message: "",
style: "primary",
});
}

for (const annotation of annotations) {
// file_id might be "" if the span is synthetic (see #2521)
if (!annotation.span?.file_id) {
continue;
}
const source = await readFile(annotation.span.file_id, "utf8");
const start = annotation.span.start_offset;
const end = annotation.span.end_offset;
const filePath = relative(cwd, annotation.span.file_id);
files.push({ name: filePath, source });
labels.push({
fileId: filePath,
rangeStart: start,
rangeEnd: end,
message: annotation.message,
style: "secondary",
});
}

const diagnosticText = emitDiagnostic(
files,
{
message,
severity,
labels,
notes: hints.map((hint) => `hint: ${hint}`),
},
{
chars: CHARS_ASCII,
},
COLORING
);
result.push(diagnosticText);
}

return result.join("\n").trimEnd();
}
49 changes: 49 additions & 0 deletions apps/wing/src/commands/generateDocs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as fs from "fs/promises";
import { basename, join } from "path";
import { describe, it, expect, afterEach, vi } from "vitest";
import { generateDocs } from "./generateDocs";

const fixturesDir = join(__dirname, "..", "..", "fixtures");

console.log = vi.fn();

describe("wing gen-docs", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("throws an error if a project is missing package.json", async () => {
// GIVEN
const projectDir = join(fixturesDir, "invalid1");
process.chdir(projectDir);

// THEN
await expect(generateDocs()).rejects.toThrow("No package.json found in project directory");
await expectNoDocs(projectDir);
});

it("generates docs for a Wing library", async () => {
// GIVEN
const projectDir = join(fixturesDir, "valid1");
process.chdir(projectDir);

await fs.rm(join(projectDir, "API.md"));

// WHEN
await generateDocs();

// THEN
const files = await fs.readdir(projectDir);
expect(files.findIndex((path) => basename(path) === "API.md")).not.toEqual(-1);
const apiMd = await fs.readFile(join(projectDir, "API.md"), "utf8");
expect(apiMd).toContain("Foo");
});
});

/**
* Asserts that no docs were created in the specified directory.
*/
async function expectNoDocs(projectDir: string) {
const files = await fs.readdir(projectDir);
expect(files.findIndex((path) => basename(path) === "API.md")).toEqual(-1);
}
34 changes: 34 additions & 0 deletions apps/wing/src/commands/generateDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { writeFile } from "fs/promises";
import { relative, resolve } from "path";
import * as wingCompiler from "@winglang/compiler";
import chalk from "chalk";
import debug from "debug";
import { formatDiagnostics } from "./diagnostics";

const log = debug("wing:generateDocs");
const color = chalk.supportsColor ? chalk.supportsColor.hasBasic : false;

export async function generateDocs() {
// TODO: calculate the workDir by looking up for a wing.toml file
// For now, assume the workDir is the current directory
const workDir = ".";

const docs = await wingCompiler.generateWingDocs({
projectDir: workDir,
color,
log,
});

if (docs.diagnostics.length > 0) {
if (docs.diagnostics.some((d) => d.severity === "error")) {
throw new Error(await formatDiagnostics(docs.diagnostics));
} else {
console.error(await formatDiagnostics(docs.diagnostics));
}
}

const docsFile = resolve(workDir, "API.md");
await writeFile(docsFile, docs.docsContents);

console.log(`Docs generated at ${relative(".", docsFile)}.`);
}
3 changes: 3 additions & 0 deletions apps/wing/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import * as fs from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { promisify } from "util";
import chalk from "chalk";

export const DEFAULT_PARALLEL_SIZE = 10;

export const COLORING = chalk.supportsColor ? chalk.supportsColor.hasBasic : false;

/**
* Normalize windows paths to be posix-like.
*/
Expand Down
12 changes: 11 additions & 1 deletion docs/api/02-cli-user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,17 @@ This will compile your current Wing directory, and bundle it as a tarball that c
See [Libraries](/docs/category/wing-libraries-winglibs) for more details on packaging and consuming Wing libraries.
:::
## Generate Docs: `wing gen-docs`
The `wing gen-docs` command can be used to generate API documentation for your Wing project.
Usage:
```sh
$ wing gen-docs
```
This will generate a file named `API.md` in the root of your project.
## Store Secrets: `wing secrets`
Expand Down
Loading

0 comments on commit b976493

Please sign in to comment.