Skip to content

Commit 4c08bf8

Browse files
authored
feat(cli): Added cli-config-file option. (#20)
This PR add a new option that can point to JSON file. This file can contain any option supported by CLI (either in `kebab-case` or `camelCase` format) and will applied accordingly. Other options explicitly provided via command line override the one in the JSON file (so that the file can act as a "template"). Fixes swc-project/cli#284.
1 parent 7f3ecaa commit 4c08bf8

File tree

7 files changed

+252
-36
lines changed

7 files changed

+252
-36
lines changed

.changeset/slimy-cameras-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@swc/cli": patch
3+
---
4+
5+
feat(cli): Added cli-config-file option.

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"dependencies": {
4848
"@mole-inc/bin-wrapper": "^8.0.1",
4949
"@swc/counter": "workspace:^",
50-
"commander": "^7.1.0",
50+
"commander": "^8.3.0",
5151
"fast-glob": "^3.2.5",
5252
"minimatch": "^9.0.3",
5353
"piscina": "^4.3.0",

packages/cli/src/spack/options.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,26 @@ export interface SpackCliOptions {
99
debug: boolean;
1010
}
1111

12-
commander.option("--config [path]", "Path to a spack.config.js file to use.");
12+
const program = new commander.Command();
13+
program.option("--config [path]", "Path to a spack.config.js file to use.");
1314
// TODO: allow using ts. See: https://github.com/swc-project/swc/issues/841
1415

15-
commander.option("--mode <development | production | none>", "Mode to use");
16-
commander.option("--target [browser | node]", "Target runtime environment");
16+
program.option("--mode <development | production | none>", "Mode to use");
17+
program.option("--target [browser | node]", "Target runtime environment");
1718

18-
commander.option(
19+
program.option(
1920
"--context [path]",
2021
`The base directory (absolute path!) for resolving the 'entry'` +
2122
` option. If 'output.pathinfo' is set, the included pathinfo is shortened to this directory`,
2223
"The current directory"
2324
);
2425

25-
commander.option("--entry [list]", "List of entries", collect);
26+
program.option("--entry [list]", "List of entries", collect);
2627

27-
// commander.option('-W --watch', `Enter watch mode, which rebuilds on file change.`)
28+
// program.option('-W --watch', `Enter watch mode, which rebuilds on file change.`)
2829

29-
commander.option("--debug", `Switch loaders to debug mode`);
30-
// commander.option('--devtool', `Select a developer tool to enhance debugging.`)
30+
program.option("--debug", `Switch loaders to debug mode`);
31+
// program.option('--devtool', `Select a developer tool to enhance debugging.`)
3132

3233
// -d shortcut for --debug --devtool eval-cheap-module-source-map
3334
// --output-pathinfo [여부]
@@ -40,11 +41,11 @@ commander.option("--debug", `Switch loaders to debug mode`);
4041
// --module-bind-pre Bind an extension to a pre loader [문자열]
4142

4243
// Output options:
43-
commander.option(
44+
program.option(
4445
"-o --output",
4546
`The output path and file for compilation assets`
4647
);
47-
commander.option("--output-path", `The output directory as **absolute path**`);
48+
program.option("--output-path", `The output directory as **absolute path**`);
4849
// --output-filename Specifies the name of each output file on disk.
4950
// You must **not** specify an absolute path here!
5051
// The `output.path` option determines the location
@@ -158,7 +159,7 @@ commander.option("--output-path", `The output directory as **absolute path**`);
158159
// --silent Prevent output from being displayed in stdout [boolean]
159160
// --json, -j Prints the result as JSON. [boolean]
160161

161-
commander.version(
162+
program.version(
162163
`@swc/cli: ${pkg.version}
163164
@swc/core: ${swcCoreVersion}`
164165
);
@@ -168,7 +169,7 @@ export default async function parseSpackArgs(args: string[]): Promise<{
168169
spackOptions: BundleOptions;
169170
}> {
170171
//
171-
const cmd = commander.parse(args);
172+
const cmd = program.parse(args);
172173
const opts = cmd.opts();
173174

174175
const cliOptions: SpackCliOptions = {

packages/cli/src/swc/__mocks__/fs.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,34 @@ import type { Stats } from "fs";
33

44
export interface MockHelpers {
55
resetMockStats: () => void;
6+
resetMockFiles: () => void;
67
setMockStats: (stats: Record<string, Stats | Error>) => void;
8+
setMockFile: (path: string, contents: string) => void;
79
}
810

911
const fsMock = jest.createMockFromModule<typeof fs & MockHelpers>("fs");
1012

1113
let mockStats: Record<string, Stats | Error> = {};
14+
let mockFiles: Record<string, string> = {};
1215

1316
function setMockStats(stats: Record<string, Stats | Error>) {
1417
Object.entries(stats).forEach(([path, stats]) => {
1518
mockStats[path] = stats;
1619
});
1720
}
21+
22+
function setMockFile(path: string, contents: string) {
23+
mockFiles[path] = contents;
24+
}
25+
1826
function resetMockStats() {
1927
mockStats = {};
2028
}
2129

30+
function resetMockFiles() {
31+
mockFiles = {};
32+
}
33+
2234
export function stat(path: string, cb: (err?: Error, stats?: Stats) => void) {
2335
const result = mockStats[path];
2436
if (result instanceof Error) {
@@ -28,9 +40,21 @@ export function stat(path: string, cb: (err?: Error, stats?: Stats) => void) {
2840
}
2941
}
3042

43+
export function readFileSync(path: string): string {
44+
if (!mockFiles[path]) {
45+
throw new Error("Non existent.");
46+
}
47+
48+
return mockFiles[path];
49+
}
50+
3151
fsMock.setMockStats = setMockStats;
3252
fsMock.resetMockStats = resetMockStats;
3353

54+
fsMock.setMockFile = setMockFile;
55+
fsMock.resetMockFiles = resetMockFiles;
56+
3457
fsMock.stat = stat as typeof fs.stat;
58+
fsMock.readFileSync = readFileSync as typeof fs.readFileSync;
3559

3660
export default fsMock;

packages/cli/src/swc/__tests__/options.test.ts

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Options } from "@swc/core";
22
import deepmerge from "deepmerge";
3+
import fs from "fs";
4+
import { resolve } from "path";
5+
6+
jest.mock("fs");
37

48
import parserArgs, { CliOptions, initProgram } from "../options";
59

@@ -56,6 +60,7 @@ describe("parserArgs", () => {
5660
beforeEach(() => {
5761
defaultResult = createDefaultResult();
5862
initProgram();
63+
(fs as any).resetMockFiles();
5964
});
6065

6166
it("minimal args returns default result", async () => {
@@ -91,7 +96,7 @@ describe("parserArgs", () => {
9196
"src",
9297
];
9398
const result = parserArgs(args);
94-
expect(result.cliOptions.outFileExtension).toEqual("js");
99+
expect(result!.cliOptions.outFileExtension).toEqual("js");
95100
});
96101
});
97102

@@ -270,7 +275,7 @@ describe("parserArgs", () => {
270275
const expectedOptions = deepmerge(defaultResult.swcOptions, {
271276
jsc: { transform: { react: { development: true } } },
272277
});
273-
expect(result.swcOptions).toEqual(expectedOptions);
278+
expect(result!.swcOptions).toEqual(expectedOptions);
274279
});
275280

276281
it("react development and commonjs (two config options)", async () => {
@@ -288,7 +293,7 @@ describe("parserArgs", () => {
288293
jsc: { transform: { react: { development: true } } },
289294
module: { type: "commonjs" },
290295
});
291-
expect(result.swcOptions).toEqual(expectedOptions);
296+
expect(result!.swcOptions).toEqual(expectedOptions);
292297
});
293298

294299
it("react development and commonjs (comma-separated)", async () => {
@@ -304,7 +309,7 @@ describe("parserArgs", () => {
304309
jsc: { transform: { react: { development: true } } },
305310
module: { type: "commonjs" },
306311
});
307-
expect(result.swcOptions).toEqual(expectedOptions);
312+
expect(result!.swcOptions).toEqual(expectedOptions);
308313
});
309314

310315
it("no equals sign", async () => {
@@ -319,7 +324,110 @@ describe("parserArgs", () => {
319324
const expectedOptions = deepmerge(defaultResult.swcOptions, {
320325
no_equals: true,
321326
});
322-
expect(result.swcOptions).toEqual(expectedOptions);
327+
expect(result!.swcOptions).toEqual(expectedOptions);
328+
});
329+
});
330+
331+
describe("--cli-config-file", () => {
332+
it("reads a JSON config file with both camel and kebab case options", async () => {
333+
(fs as any).setMockFile(
334+
resolve(process.cwd(), "/swc/cli.json"),
335+
JSON.stringify({
336+
outFileExtension: "mjs",
337+
deleteDirOnStart: "invalid",
338+
"delete-dir-on-start": true,
339+
})
340+
);
341+
342+
const args = [
343+
"node",
344+
"/path/to/node_modules/swc-cli/bin/swc.js",
345+
"src",
346+
"--cli-config-file",
347+
"/swc/cli.json",
348+
];
349+
const result = parserArgs(args);
350+
const expectedOptions = deepmerge(defaultResult, {
351+
cliOptions: { outFileExtension: "mjs", deleteDirOnStart: true },
352+
});
353+
354+
expect(result).toEqual(expectedOptions);
355+
});
356+
357+
it("reads a JSON but options are overriden from CLI", async () => {
358+
(fs as any).setMockFile(
359+
resolve(process.cwd(), "/swc/cli.json"),
360+
JSON.stringify({
361+
outFileExtension: "mjs",
362+
"delete-dir-on-start": true,
363+
})
364+
);
365+
366+
const args = [
367+
"node",
368+
"/path/to/node_modules/swc-cli/bin/swc.js",
369+
"src",
370+
"--cli-config-file",
371+
"/swc/cli.json",
372+
"--out-file-extension",
373+
"cjs",
374+
];
375+
const result = parserArgs(args);
376+
const expectedOptions = deepmerge(defaultResult, {
377+
cliOptions: { outFileExtension: "cjs", deleteDirOnStart: true },
378+
});
379+
380+
expect(result).toEqual(expectedOptions);
381+
});
382+
383+
describe("exits", () => {
384+
let mockExit: jest.SpyInstance;
385+
let mockConsoleError: jest.SpyInstance;
386+
387+
beforeEach(() => {
388+
mockExit = jest
389+
.spyOn(process, "exit")
390+
// @ts-expect-error
391+
.mockImplementation(() => {});
392+
mockConsoleError = jest
393+
.spyOn(console, "error")
394+
.mockImplementation(() => {});
395+
});
396+
397+
afterEach(() => {
398+
mockExit.mockRestore();
399+
mockConsoleError.mockRestore();
400+
});
401+
402+
it("if the config file is missing", async () => {
403+
const args = [
404+
"node",
405+
"/path/to/node_modules/swc-cli/bin/swc.js",
406+
"src",
407+
"--cli-config-file",
408+
"/swc/cli.json",
409+
];
410+
411+
parserArgs(args);
412+
expect(mockExit).toHaveBeenCalledWith(2);
413+
expect(mockConsoleError).toHaveBeenCalledTimes(2);
414+
});
415+
416+
it("if the config file is not valid JSON", async () => {
417+
(fs as any).setMockFile("/swc/cli.json", "INVALID");
418+
419+
const args = [
420+
"node",
421+
"/path/to/node_modules/swc-cli/bin/swc.js",
422+
"src",
423+
"--cli-config-file",
424+
"/swc/cli.json",
425+
];
426+
427+
parserArgs(args);
428+
expect(mockExit).toHaveBeenCalledWith(2);
429+
expect(mockConsoleError).toHaveBeenCalledTimes(2);
430+
});
323431
});
324432
});
325433
});

0 commit comments

Comments
 (0)