Skip to content
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
105 changes: 105 additions & 0 deletions src/components/WranglerCLI.astro
Original file line number Diff line number Diff line change
@@ -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>
)
}
82 changes: 82 additions & 0 deletions src/components/WranglerCLI.astro.test.ts
Original file line number Diff line number Diff line change
@@ -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"]`,
);
});
});
25 changes: 2 additions & 23 deletions src/components/WranglerCommand.astro
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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()}]`)
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
47 changes: 47 additions & 0 deletions src/content/docs/style-guide/components/wrangler-cli.mdx
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions src/util/wrangler.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
8 changes: 8 additions & 0 deletions vitest.workspace.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -30,6 +31,13 @@ const workspace = defineWorkspace([
},
plugins: [tsconfigPaths()],
}),
getViteConfig({
test: {
name: "Astro",
include: ["**/*.astro.test.ts"],
},
plugins: [tsconfigPaths()],
}),
]);

export default workspace;
Loading