[Docs Site] Add WranglerCLI component (#25137)

* [Docs Site] Add WranglerCLI component

* unused import

* Add optional arguments below command

---------

Co-authored-by: kodster28 <kody@cloudflare.com>
This commit is contained in:
Kian 2025-09-15 20:35:43 +01:00 committed by GitHub
parent 5e215fc478
commit ae2c927193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 269 additions and 23 deletions

View file

@ -0,0 +1,105 @@
---
import { z } from "astro:schema";
import { PackageManagers } from "starlight-package-managers";
import { commands, getCommand } from "~/util/wrangler";
import WranglerArg from "./WranglerArg.astro";
import Details from "./Details.astro";
function validateArg(value: any, expected: string): boolean {
if (Array.isArray(expected)) {
for (const choice of expected) {
if (value === choice) {
return true;
}
}
return false;
}
return typeof value === expected;
}
type Props = z.input<typeof props>;
const props = z.object({
command: z.string(),
positionals: z.array(z.string()).optional(),
flags: z.record(z.string(), z.any()).optional(),
showArgs: z.boolean().default(false),
});
const { command, positionals, flags, showArgs } = props.parse(Astro.props);
const definition = getCommand(command);
const { globalFlags } = commands;
let args = [];
if (flags) {
for (const [key, value] of Object.entries(flags)) {
const flagDef = definition.args?.[key];
if (!flagDef) {
throw new Error(
`[WranglerCLI] Received "${key}" for "${command}" but no such arg exists`,
);
}
const type = flagDef.type ?? flagDef.choices;
const valid = validateArg(value, type);
if (!valid) {
throw new Error(
`[WranglerCLI] Expected "${type}" for "${key}" but got "${typeof value}"`,
);
}
args.push(...[`--${key}`, value]);
}
}
if (positionals) {
const positionalsDef = definition.positionalArgs ?? [];
if (positionalsDef.length === 0) {
throw new Error(
`[WranglerCLI] Expected 0 positional arguments for "${command}" but received ${positionals.length}`,
);
}
args.push(...positionals);
}
---
<PackageManagers
pkg="wrangler"
type="exec"
args={`${command} ${args.join(" ")}`}
/>
{
showArgs && definition.args && (
<Details header="Arguments">
<p>
<strong>Command flags</strong>
</p>
<ul>
{Object.entries(definition.args)
.filter(([_, value]) => !value.hidden)
.map(([key, value]) => {
return <WranglerArg key={key} definition={value} />;
})}
</ul>
<p>
<strong>Global flags</strong>
</p>
<ul>
{Object.entries(globalFlags).map(([key, value]) => {
return <WranglerArg key={key} definition={value} />;
})}
</ul>
</Details>
)
}

View file

@ -0,0 +1,82 @@
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { expect, test, describe } from "vitest";
import WranglerCLI from "./WranglerCLI.astro";
type Options = Parameters<(typeof container)["renderToString"]>[1];
const container = await AstroContainer.create();
const renderWithOptions = (options?: Options) => {
return container.renderToString(WranglerCLI, options);
};
describe("WranglerCLI", () => {
test("succeeds with valid input", async () => {
await expect(
renderWithOptions({
props: {
command: "deploy",
},
}),
).resolves.toContain("pnpm wrangler deploy");
});
test("errors with no props", async () => {
await expect(renderWithOptions()).rejects
.toThrowErrorMatchingInlineSnapshot(`
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"command"
],
"message": "Required"
}
]]
`);
});
test("errors with non-existent command", async () => {
await expect(
renderWithOptions({
props: {
command: "not-a-valid-command",
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: [wrangler.ts] Command "not-a-valid-command" not found]`,
);
});
test("errors with bad flags for 'deploy'", async () => {
await expect(
renderWithOptions({
props: {
command: "deploy",
flags: {
foo: "bar",
},
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: [WranglerCLI] Received "foo" for "deploy" but no such arg exists]`,
);
});
test("errors with bad value for 'container-rollout' flag", async () => {
await expect(
renderWithOptions({
props: {
command: "deploy",
flags: {
"containers-rollout": "not-a-valid-option",
},
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: [WranglerCLI] Expected "immediate,gradual" for "containers-rollout" but got "string"]`,
);
});
});

View file

@ -1,32 +1,11 @@
---
import { z } from "astro:schema";
import { experimental_getWranglerCommands } from "wrangler";
import AnchorHeading from "./AnchorHeading.astro";
import { PackageManagers } from "starlight-package-managers";
import WranglerArg from "./WranglerArg.astro";
import Details from "./Details.astro";
import { marked } from "marked";
function getCommand(path: string) {
const segments = path.trim().split(/\s+/);
const { registry } = experimental_getWranglerCommands();
let node = registry.subtree;
for (const segment of segments) {
const next = node.get(segment);
if (!next) break;
if (next.subtree.size === 0 && next.definition?.type === "command") {
return next.definition;
}
node = next.subtree;
}
throw new Error(`[WranglerCommand] Command "${path}" not found`);
}
import { commands, getCommand } from "~/util/wrangler";
const props = z.object({
command: z.string(),
@ -44,7 +23,7 @@ if (!definition.args) {
throw new Error(`[WranglerCommand] "${command}" has no arguments`);
}
const { globalFlags } = experimental_getWranglerCommands();
const { globalFlags } = commands;
const positionals = definition.positionalArgs
?.map((p) => `[${p.toUpperCase()}]`)

View file

@ -58,6 +58,7 @@ export { default as TagsUsage } from "./TagsUsage.astro";
export { default as TunnelCalculator } from "./TunnelCalculator.astro";
export { default as Type } from "./Type.astro";
export { default as TypeScriptExample } from "./TypeScriptExample.astro";
export { default as WranglerCLI } from "./WranglerCLI.astro";
export { default as WranglerCommand } from "./WranglerCommand.astro";
export { default as WranglerNamespace } from "./WranglerNamespace.astro";
export { default as WranglerConfig } from "./WranglerConfig.astro";

View file

@ -0,0 +1,47 @@
---
title: WranglerCLI
styleGuide:
component: WranglerCLI
---
import { Type, MetaInfo } from "~/components";
The `WranglerCLI` component validates your Wrangler command & wraps it in the [`PackageManagers`](/style-guide/components/package-managers/) component.
This is generated using the Wrangler version in the [`cloudflare-docs` repository](https://github.com/cloudflare/cloudflare-docs/blob/production/package.json).
## Import
{/* prettier-ignore */}
```mdx
import { WranglerCLI } from "~/components";
```
## Usage
```mdx live
import { WranglerCLI } from "~/components";
<WranglerCLI
command="deploy"
positionals={["src/index.mjs"]}
flags={{
name: "my-worker",
"containers-rollout": "immediate",
}}
/>
```
## Arguments
- `command` <Type text="string" /> <MetaInfo text="required" />
- The name of the command, i.e `d1 execute`.
- `positionals` <Type text="string[]" />
- Any positional argument values, i.e `{["src/index.mjs]}"` for the optional `[SCRIPT]` positional argument on `deploy`.
- `flags` <Type text="Record<string, any>" />
- Any named argument values, i.e `name: "my-worker"` for the optional `name` argument on `deploy`.
- `showArgs` <Type text="boolean" /> <MetaInfo text="default (false)" />
- Show the available arguments in a [`Details` component](/style-guide/components/details/) below the command.

24
src/util/wrangler.ts Normal file
View file

@ -0,0 +1,24 @@
import { experimental_getWranglerCommands } from "wrangler";
export const commands = experimental_getWranglerCommands();
export function getCommand(path: string) {
const segments = path.trim().split(/\s+/);
const { registry } = commands;
let node = registry.subtree;
for (const segment of segments) {
const next = node.get(segment);
if (!next) break;
if (next.subtree.size === 0 && next.definition?.type === "command") {
return next.definition;
}
node = next.subtree;
}
throw new Error(`[wrangler.ts] Command "${path}" not found`);
}

View file

@ -1,5 +1,6 @@
import { defineWorkspace, defineProject } from "vitest/config";
import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config";
import { getViteConfig } from "astro/config";
import tsconfigPaths from "vite-tsconfig-paths";
@ -30,6 +31,13 @@ const workspace = defineWorkspace([
},
plugins: [tsconfigPaths()],
}),
getViteConfig({
test: {
name: "Astro",
include: ["**/*.astro.test.ts"],
},
plugins: [tsconfigPaths()],
}),
]);
export default workspace;