Skip to content

Commit d42dd1a

Browse files
committed
refactor: use yargs for the cli
1 parent 040731c commit d42dd1a

File tree

15 files changed

+337
-380
lines changed

15 files changed

+337
-380
lines changed

.changeset/hungry-ideas-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": minor
3+
---
4+
5+
refactor: use yargs for the cli

packages/cloudflare/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
"cloudflare": "^4.4.1",
5858
"enquirer": "^2.4.1",
5959
"glob": "catalog:",
60-
"ts-tqdm": "^0.8.6"
60+
"ts-tqdm": "^0.8.6",
61+
"yargs": "catalog:"
6162
},
6263
"devDependencies": {
6364
"@cloudflare/workers-types": "catalog:",
@@ -66,6 +67,7 @@
6667
"@types/mock-fs": "catalog:",
6768
"@types/node": "catalog:",
6869
"@types/picomatch": "^4.0.0",
70+
"@types/yargs": "catalog:",
6971
"diff": "^8.0.2",
7072
"esbuild": "catalog:",
7173
"eslint": "catalog:",

packages/cloudflare/src/cli/args.spec.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 109 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,118 @@
1-
import { mkdirSync, type Stats, statSync } from "node:fs";
2-
import { resolve } from "node:path";
3-
import type { ParseArgsConfig } from "node:util";
4-
import { parseArgs } from "node:util";
1+
import yargs from "yargs";
52

6-
import type { WranglerTarget } from "./utils/run-wrangler.js";
7-
import { getWranglerEnvironmentFlag, isWranglerTarget } from "./utils/run-wrangler.js";
3+
import { buildCommand } from "./commands/build.js";
4+
import { deployCommand } from "./commands/deploy.js";
5+
import { populateCacheCommand } from "./commands/populate-cache.js";
6+
import { previewCommand } from "./commands/preview.js";
7+
import { uploadCommand } from "./commands/upload.js";
88

9-
export type Arguments = (
10-
| {
11-
command: "build";
12-
skipNextBuild: boolean;
13-
skipWranglerConfigCheck: boolean;
14-
minify: boolean;
15-
}
16-
| {
17-
command: "preview" | "deploy" | "upload";
18-
passthroughArgs: string[];
19-
cacheChunkSize?: number;
20-
}
21-
| {
22-
command: "populateCache";
23-
target: WranglerTarget;
24-
environment?: string;
25-
cacheChunkSize?: number;
26-
}
27-
) & { outputDir?: string };
28-
29-
// Config for parsing CLI arguments
30-
const config = {
31-
allowPositionals: true,
32-
strict: false,
33-
options: {
34-
skipBuild: { type: "boolean", short: "s", default: false },
35-
output: { type: "string", short: "o" },
36-
noMinify: { type: "boolean", default: false },
37-
skipWranglerConfigCheck: { type: "boolean", default: false },
38-
cacheChunkSize: { type: "string" },
39-
},
40-
} as const satisfies ParseArgsConfig;
41-
42-
export function getArgs(): Arguments {
43-
const { positionals, values } = parseArgs(config);
44-
45-
const outputDir = typeof values.output === "string" ? resolve(values.output) : undefined;
46-
if (outputDir) assertDirArg(outputDir, "output", true);
47-
48-
switch (positionals[0]) {
49-
case "build":
50-
return {
51-
command: "build",
52-
outputDir,
53-
skipNextBuild:
54-
!!values.skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)),
55-
skipWranglerConfigCheck:
56-
!!values.skipWranglerConfigCheck ||
57-
["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)),
58-
minify: !values.noMinify,
59-
};
60-
case "preview":
61-
case "deploy":
62-
case "upload":
63-
return {
64-
command: positionals[0],
65-
outputDir,
66-
passthroughArgs: getPassthroughArgs(process.argv, config),
67-
...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
68-
};
69-
case "populateCache":
70-
if (!isWranglerTarget(positionals[1])) {
71-
throw new Error(`Error: invalid target for populating the cache, expected 'local' | 'remote'`);
72-
}
73-
return {
74-
command: "populateCache",
75-
outputDir,
76-
target: positionals[1],
77-
environment: getWranglerEnvironmentFlag(process.argv),
78-
...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
79-
};
80-
default:
81-
throw new Error(
82-
"Error: invalid command, expected 'build' | 'preview' | 'deploy' | 'upload' | 'populateCache'"
83-
);
84-
}
9+
export function runCommand() {
10+
return yargs(process.argv.slice(2))
11+
.scriptName("opennextjs-cloudflare")
12+
.parserConfiguration({ "unknown-options-as-args": true })
13+
.command(
14+
"build",
15+
"Build an OpenNext Cloudflare worker",
16+
(c) =>
17+
withWranglerOptions(c)
18+
.option("skipNextBuild", {
19+
type: "boolean",
20+
alias: ["skipBuild", "s"],
21+
default: ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)),
22+
desc: "Skip building the Next.js app",
23+
})
24+
.option("noMinify", {
25+
type: "boolean",
26+
alias: "s",
27+
default: false,
28+
desc: "Disable worker minification",
29+
})
30+
.option("skipWranglerConfigCheck", {
31+
type: "boolean",
32+
alias: "s",
33+
default: ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)),
34+
desc: "Skip checking for a Wrangler config",
35+
}),
36+
(args) => buildCommand(withWranglerPassthroughArgs(args))
37+
)
38+
.command(
39+
"preview",
40+
"Preview a built OpenNext app with a Wrangler dev server",
41+
(c) => withPopulateCacheOptions(c),
42+
(args) => previewCommand(withWranglerPassthroughArgs(args))
43+
)
44+
.command(
45+
"deploy",
46+
"Deploy a built OpenNext app to Cloudflare Workers",
47+
(c) => withPopulateCacheOptions(c),
48+
(args) => deployCommand(withWranglerPassthroughArgs(args))
49+
)
50+
.command(
51+
"upload",
52+
"Upload a built OpenNext app to Cloudflare Workers",
53+
(c) => withPopulateCacheOptions(c),
54+
(args) => uploadCommand(withWranglerPassthroughArgs(args))
55+
)
56+
.command("populateCache", "Populate the cache for a built Next.js app", (c) =>
57+
c
58+
.command(
59+
"local",
60+
"Local dev server cache",
61+
(c) => withPopulateCacheOptions(c),
62+
(args) => populateCacheCommand("local", withWranglerPassthroughArgs(args))
63+
)
64+
.command(
65+
"remote",
66+
"Remote Cloudflare Worker cache",
67+
(c) => withPopulateCacheOptions(c),
68+
(args) => populateCacheCommand("remote", withWranglerPassthroughArgs(args))
69+
)
70+
.demandCommand(1, 1)
71+
)
72+
.demandCommand(1, 1)
73+
.parse();
8574
}
8675

87-
export function getPassthroughArgs<T extends ParseArgsConfig>(args: string[], { options = {} }: T) {
88-
const passthroughArgs: string[] = [];
89-
90-
for (let i = 0; i < args.length; i++) {
91-
if (args[i] === "--") {
92-
passthroughArgs.push(...args.slice(i + 1));
93-
return passthroughArgs;
94-
}
95-
96-
// look for `--arg(=value)`, `-arg(=value)`
97-
const [, name] = /^--?(\w[\w-]*)(=.+)?$/.exec(args[i]!) ?? [];
98-
if (name && !(name in options)) {
99-
passthroughArgs.push(args[i]!);
100-
101-
// Array args can have multiple values
102-
// ref https://github.com/yargs/yargs-parser/blob/main/README.md#greedy-arrays
103-
while (i < args.length - 1 && !args[i + 1]?.startsWith("-")) {
104-
passthroughArgs.push(args[++i]!);
105-
}
106-
}
107-
}
76+
function withWranglerOptions<T extends yargs.Argv>(args: T) {
77+
return args
78+
.options("config", {
79+
type: "string",
80+
alias: "c",
81+
desc: "Wrangler config file path",
82+
})
83+
.options("env", {
84+
type: "string",
85+
alias: "e",
86+
desc: "Wrangler environment",
87+
});
88+
}
10889

109-
return passthroughArgs;
90+
function withPopulateCacheOptions<T extends yargs.Argv>(args: T) {
91+
return withWranglerOptions(args).options("cacheChunkSize", {
92+
type: "number",
93+
default: 25,
94+
desc: "Number of entries per chunk when populating the cache",
95+
});
11096
}
11197

112-
function assertDirArg(path: string, argName?: string, make?: boolean) {
113-
let dirStats: Stats;
114-
try {
115-
dirStats = statSync(path);
116-
} catch {
117-
if (!make) {
118-
throw new Error(`Error: the provided${argName ? ` "${argName}"` : ""} input is not a valid path`);
119-
}
120-
mkdirSync(path);
121-
return;
122-
}
98+
function getWranglerArgs(args: {
99+
_: (string | number)[];
100+
config: string | undefined;
101+
env: string | undefined;
102+
}): string[] {
103+
return [
104+
...(args.config ? ["--config", args.config] : []),
105+
...(args.env ? ["--env", args.env] : []),
106+
// Note: the first args in `_` will be the commands.
107+
...args._.slice(args._[0] === "populateCache" ? 2 : 1).map((a) => `${a}`),
108+
];
109+
}
123110

124-
if (!dirStats.isDirectory()) {
125-
throw new Error(`Error: the provided${argName ? ` "${argName}"` : ""} input is not a directory`);
126-
}
111+
function withWranglerPassthroughArgs<
112+
T extends yargs.ArgumentsCamelCase<{
113+
config: string | undefined;
114+
env: string | undefined;
115+
}>,
116+
>(args: T) {
117+
return { ...args, passthrough: getWranglerArgs(args) };
127118
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
2+
3+
import { build as buildImpl } from "../build/build.js";
4+
import { createOpenNextConfigIfNotExistent } from "../build/utils/create-config-files.js";
5+
import { setupCLI } from "./setup-cli.js";
6+
7+
export async function buildCommand(args: {
8+
passthrough: string[];
9+
config: string | undefined;
10+
env: string | undefined;
11+
skipNextBuild: boolean;
12+
noMinify: boolean;
13+
skipWranglerConfigCheck: boolean;
14+
}) {
15+
const { options, config, wranglerConfig, baseDir } = await setupCLI("build", args, async (baseDir) => {
16+
await createOpenNextConfigIfNotExistent(baseDir);
17+
return compileOpenNextConfig(baseDir, undefined, { compileEdge: true });
18+
});
19+
20+
return buildImpl(options, config, { ...args, minify: !args.noMinify, sourceDir: baseDir }, wranglerConfig);
21+
}

packages/cloudflare/src/cli/commands/deploy.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
1-
import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2-
3-
import type { OpenNextConfig } from "../../api/config.js";
41
import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js";
5-
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
2+
import { runWrangler } from "../utils/run-wrangler.js";
63
import { getEnvFromPlatformProxy, quoteShellMeta } from "./helpers.js";
74
import { populateCache } from "./populate-cache.js";
5+
import type { WithWranglerArgs } from "./setup-cli.js";
6+
import { setupCompiledAppCLI } from "./setup-cli.js";
87
import { getDeploymentMapping } from "./skew-protection.js";
98

10-
export async function deploy(
11-
options: BuildOptions,
12-
config: OpenNextConfig,
13-
deployOptions: { passthroughArgs: string[]; cacheChunkSize?: number }
14-
) {
9+
export async function deployCommand(args: WithWranglerArgs<{ cacheChunkSize: number }>) {
10+
const { options, config, wranglerConfig } = await setupCompiledAppCLI("deploy", args);
11+
1512
const envVars = await getEnvFromPlatformProxy({
16-
// TODO: Pass the configPath, update everywhere applicable
17-
environment: getWranglerEnvironmentFlag(deployOptions.passthroughArgs),
13+
configPath: args.config,
14+
environment: args.env,
1815
});
1916

2017
const deploymentMapping = await getDeploymentMapping(options, config, envVars);
2118

22-
await populateCache(options, config, {
19+
await populateCache(options, config, wranglerConfig, {
2320
target: "remote",
24-
environment: getWranglerEnvironmentFlag(deployOptions.passthroughArgs),
25-
cacheChunkSize: deployOptions.cacheChunkSize,
21+
environment: args.env,
22+
configPath: args.config,
23+
cacheChunkSize: args.cacheChunkSize,
2624
});
2725

2826
runWrangler(
2927
options,
3028
[
3129
"deploy",
32-
...deployOptions.passthroughArgs,
30+
...args.passthrough,
3331
...(deploymentMapping
3432
? [`--var ${DEPLOYMENT_MAPPING_ENV_NAME}:${quoteShellMeta(JSON.stringify(deploymentMapping))}`]
3533
: []),

0 commit comments

Comments
 (0)