Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): wing gen-docs #7056

Merged
merged 19 commits into from
Aug 30, 2024
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
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>
Chriscbr marked this conversation as resolved.
Show resolved Hide resolved

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