Skip to content

Commit 56315df

Browse files
committed
Replace CLI argument parsing with reusable func
1 parent 5eaa5ad commit 56315df

3 files changed

Lines changed: 134 additions & 81 deletions

File tree

src/args.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
type StringLiteral = { position: number; value: string }
2+
3+
type KeyValuePair = { key: string; value: string }
4+
5+
type ParsedArgs = (StringLiteral | KeyValuePair)[]
6+
7+
export function isStringLiteral(value: unknown): value is StringLiteral {
8+
return value !== null && typeof value === "object" && "position" in value && "value" in value
9+
}
10+
11+
export function isKeyValuePair(value: unknown): value is KeyValuePair {
12+
return value !== null && typeof value === "object" && "key" in value && "value" in value
13+
}
14+
15+
export function parseArgv(argv: string[]): ParsedArgs {
16+
if (argv.length <= 2) return []
17+
18+
const rawArgs = argv.slice(2, argv.length)
19+
const parsedArgs: (StringLiteral | KeyValuePair)[] = []
20+
21+
let isParsingKeyValuePair: boolean = false
22+
23+
for (let index = 0; index < rawArgs.length; index++) {
24+
const currentRawArg = rawArgs[index]
25+
const lastProcessedArg = parsedArgs[parsedArgs.length - 1]
26+
27+
if (currentRawArg.startsWith("-")) {
28+
parsedArgs.push({ key: currentRawArg.replace(/^\-\-?/, ""), value: "" })
29+
isParsingKeyValuePair = true
30+
continue
31+
}
32+
33+
if (isParsingKeyValuePair && !currentRawArg.startsWith("-")) {
34+
lastProcessedArg.value = currentRawArg
35+
isParsingKeyValuePair = false
36+
continue
37+
}
38+
39+
if (!isParsingKeyValuePair) {
40+
parsedArgs.push({ position: index, value: currentRawArg })
41+
continue
42+
}
43+
}
44+
45+
return parsedArgs
46+
}

src/cli.ts

Lines changed: 64 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,128 +3,114 @@ import * as fs from "fs"
33
import * as path from "path"
44
import { stdout } from "process"
55

6-
import { staticbuild } from "."
6+
import type { StaticBuildOptions } from "./staticbuild"
77

8-
interface Args {
9-
watch?: boolean
10-
dryRun?: boolean
11-
port?: string
12-
check?: boolean
13-
}
8+
import { staticbuild } from "."
9+
import { isKeyValuePair, isStringLiteral, parseArgv } from "./args"
1410

1511
const ERROR_CODE = {
1612
SUCCESS: 0,
1713
CALLED_WITH_ILLEGAL_PARAMETERS: 1,
1814
FAILED_T0_READ_LOCAL_FILE: 11,
1915
}
2016

21-
const DEFAULT_ARGS: Required<Args> = {
22-
watch: false,
23-
dryRun: false,
24-
port: "8082",
25-
check: false,
26-
}
27-
2817
function logUsage() {
29-
stdout.write(`Usage: staticbuild <inputDirectory> <outputDirectory> [--watch, --dry-run, --port, --check]\n`)
18+
stdout.write(`🙅🏻‍♀️ staticbuild - a static site generator that isn't for you!\n\n`)
19+
stdout.write(`Usage: staticbuild <inputDirectory> <outputDirectory> [--watch, --dry-run, --check]\n`)
3020

3121
stdout.write(`\nArguments:\n`)
3222
stdout.write(`<inputDirectory> Path to directory containing content.\n`)
3323
stdout.write(`<outputDirectory> Path to directory for built site assets.\n`)
3424
stdout.write(`--watch, -w Watch the input directory and rebuild if there are changes.\n`)
3525
stdout.write(`--dry-run Prevent writing files to disk, instead logging the file list out.\n`)
36-
stdout.write(`--port, -p The expected port the site will be served from.\n`)
3726
stdout.write(`--check, -c Perform a dead link check on the output files.\n`)
3827
}
3928

4029
async function main() {
41-
// Remove the first 2 arguments that nodejs provides.
42-
const args = process.argv.splice(2, process.argv.length)
43-
44-
if (args.length === 0) {
45-
logUsage()
46-
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
30+
const options: StaticBuildOptions = {
31+
inputDirectory: "",
32+
outputDirectory: "",
33+
baseURL:
34+
(process.env.context === "production" ? process.env.URL : process.env.DEPLOY_PRIME_URL) ||
35+
"http://localhost:8082",
36+
logger: {
37+
info: console.log,
38+
warn: console.warn,
39+
error: console.error,
40+
time: console.time,
41+
timeEnd: console.timeEnd,
42+
},
4743
}
4844

49-
const inputDirectory = args[0]
50-
const outputDirectory = args[1]
45+
const argv = parseArgv(process.argv)
5146

52-
// Parse options that user has provided as an args object.
53-
const options = args.reduce(
54-
(mem, arg) => {
55-
if (arg === "--watch" || arg === "-w") {
56-
mem["watch"] = true
57-
}
47+
if (argv.length === 0) {
48+
return logUsage()
49+
}
5850

59-
if (arg === "--dry-run") {
60-
mem["dryRun"] = true
61-
}
51+
for (const arg of argv) {
52+
if (isStringLiteral(arg) && arg.position === 0) {
53+
options.inputDirectory = path.join(process.cwd(), arg.value)
54+
}
55+
56+
if (isStringLiteral(arg) && arg.position === 1) {
57+
options.outputDirectory = path.join(process.cwd(), arg.value)
58+
}
59+
60+
if (isStringLiteral(arg) && arg.position > 1) {
61+
stdout.write(`Error: Unknown argument ${arg.value}.\n`)
62+
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
63+
}
64+
65+
if (isKeyValuePair(arg)) {
66+
switch (arg.key) {
67+
case "watch":
68+
case "w": {
69+
options.watch = true
70+
continue
71+
}
6272

63-
if (arg === "--check" || arg === "-c") {
64-
mem["check"] = true
65-
}
73+
case "dry-run": {
74+
options.dryRun = true
75+
continue
76+
}
6677

67-
if (arg.startsWith("--port") || arg.startsWith("-p")) {
68-
const splitValue = arg.split("=")
78+
case "check":
79+
case "c": {
80+
options.check = true
81+
continue
82+
}
6983

70-
if (splitValue.length === 2) {
71-
mem["port"] = splitValue[1].trim()
84+
default: {
85+
stdout.write(`Error: Unknown argument ${arg.key}.\n`)
86+
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
7287
}
7388
}
74-
75-
return mem
76-
},
77-
{ ...DEFAULT_ARGS },
78-
)
89+
}
90+
}
7991

8092
// Check that the first argument is a path and not a command.
81-
if (!inputDirectory) {
93+
if (!options.inputDirectory) {
8294
stdout.write(`Error: Missing input directory.\n`)
8395
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
8496
}
8597

86-
if (inputDirectory.slice(0, 1) === "-") {
87-
stdout.write(`Error: Invalid input directory.\n> ${inputDirectory}`)
88-
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
89-
}
90-
9198
// Check that the second argument is a path and not a command.
92-
if (!outputDirectory) {
99+
if (!options.outputDirectory) {
93100
stdout.write(`Error: Missing output directory.\n`)
94101
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
95102
}
96103

97-
if (outputDirectory.slice(0, 1) === "-") {
98-
stdout.write(`Error: Invalid output directory.\n> ${inputDirectory}`)
99-
return ERROR_CODE.CALLED_WITH_ILLEGAL_PARAMETERS
100-
}
101-
102-
if (!fs.existsSync(inputDirectory)) {
103-
stdout.write(`Error: Input directory "${inputDirectory}" does not exist.\n`)
104+
if (!fs.existsSync(options.inputDirectory)) {
105+
stdout.write(`Error: Input directory "${options.inputDirectory}" does not exist.\n`)
104106
return ERROR_CODE.FAILED_T0_READ_LOCAL_FILE
105107
}
106108

107-
if (!fs.existsSync(outputDirectory)) {
108-
fs.mkdirSync(outputDirectory)
109+
if (!fs.existsSync(options.outputDirectory)) {
110+
fs.mkdirSync(options.outputDirectory)
109111
}
110112

111-
const baseURL =
112-
(process.env.context === "production" ? process.env.URL : process.env.DEPLOY_PRIME_URL) ||
113-
`http://localhost:${options?.port || DEFAULT_ARGS.port}`
114-
115-
await staticbuild({
116-
inputDirectory: path.join(process.cwd(), inputDirectory),
117-
outputDirectory: path.join(process.cwd(), outputDirectory),
118-
baseURL,
119-
...options,
120-
logger: {
121-
info: console.log,
122-
warn: console.warn,
123-
error: console.error,
124-
time: console.time,
125-
timeEnd: console.timeEnd,
126-
},
127-
})
113+
await staticbuild(options)
128114
}
129115

130116
main()

test/index.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path"
33
import { performance } from "node:perf_hooks"
44

55
import staticbuild, { StaticBuildOptions } from "../src/staticbuild"
6+
import { isKeyValuePair, isStringLiteral, parseArgv } from "../src/args"
67
import { scan } from "../src/fs"
78
import { stdout } from "node:process"
89

@@ -83,8 +84,28 @@ function expectDirectoriesEqual(actualDirectoryPath: string, expectedDirectoryPa
8384
}
8485

8586
async function test() {
86-
// Remove the first 2 arguments that nodejs provides.
87-
const args = process.argv.splice(2, process.argv.length)
87+
const options = {
88+
spec: "",
89+
}
90+
91+
for (const arg of parseArgv(process.argv)) {
92+
if (isStringLiteral(arg)) {
93+
throw Error(`Error: Unknown argument ${arg.value}.\n`)
94+
}
95+
96+
if (isKeyValuePair(arg)) {
97+
switch (arg.key) {
98+
case "spec": {
99+
options.spec = arg.value
100+
continue
101+
}
102+
103+
default: {
104+
throw Error(`Error: Unknown argument ${arg.key}.\n`)
105+
}
106+
}
107+
}
108+
}
88109

89110
const timings: Record<string, { startTime: number; endTime: number }> = {}
90111

@@ -102,7 +123,7 @@ async function test() {
102123
}
103124

104125
for (const directory of scan("./test", [], { recursive: false })) {
105-
if (args[0] && args[0] !== directory.name) continue
126+
if (options.spec && options.spec !== directory.name) continue
106127

107128
if (!directory.isDirectory) continue
108129

0 commit comments

Comments
 (0)