Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Added cli-config-file option. #20

Merged
merged 3 commits into from
Feb 22, 2024
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
5 changes: 5 additions & 0 deletions .changeset/slimy-cameras-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@swc/cli": patch
---

feat(cli): Added cli-config-file option.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"dependencies": {
"@mole-inc/bin-wrapper": "^8.0.1",
"@swc/counter": "workspace:^",
"commander": "^7.1.0",
"commander": "^8.3.0",
"fast-glob": "^3.2.5",
"minimatch": "^9.0.3",
"piscina": "^4.3.0",
Expand Down
25 changes: 13 additions & 12 deletions packages/cli/src/spack/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,26 @@ export interface SpackCliOptions {
debug: boolean;
}

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

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

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

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

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

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

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

// Output options:
commander.option(
program.option(
"-o --output",
`The output path and file for compilation assets`
);
commander.option("--output-path", `The output directory as **absolute path**`);
program.option("--output-path", `The output directory as **absolute path**`);
// --output-filename Specifies the name of each output file on disk.
// You must **not** specify an absolute path here!
// The `output.path` option determines the location
Expand Down Expand Up @@ -158,7 +159,7 @@ commander.option("--output-path", `The output directory as **absolute path**`);
// --silent Prevent output from being displayed in stdout [boolean]
// --json, -j Prints the result as JSON. [boolean]

commander.version(
program.version(
`@swc/cli: ${pkg.version}
@swc/core: ${swcCoreVersion}`
);
Expand All @@ -168,7 +169,7 @@ export default async function parseSpackArgs(args: string[]): Promise<{
spackOptions: BundleOptions;
}> {
//
const cmd = commander.parse(args);
const cmd = program.parse(args);
const opts = cmd.opts();

const cliOptions: SpackCliOptions = {
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/swc/__mocks__/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,34 @@ import type { Stats } from "fs";

export interface MockHelpers {
resetMockStats: () => void;
resetMockFiles: () => void;
setMockStats: (stats: Record<string, Stats | Error>) => void;
setMockFile: (path: string, contents: string) => void;
}

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

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

function setMockStats(stats: Record<string, Stats | Error>) {
Object.entries(stats).forEach(([path, stats]) => {
mockStats[path] = stats;
});
}

function setMockFile(path: string, contents: string) {
mockFiles[path] = contents;
}

function resetMockStats() {
mockStats = {};
}

function resetMockFiles() {
mockFiles = {};
}

export function stat(path: string, cb: (err?: Error, stats?: Stats) => void) {
const result = mockStats[path];
if (result instanceof Error) {
Expand All @@ -28,9 +40,21 @@ export function stat(path: string, cb: (err?: Error, stats?: Stats) => void) {
}
}

export function readFileSync(path: string): string {
if (!mockFiles[path]) {
throw new Error("Non existent.");
}

return mockFiles[path];
}

fsMock.setMockStats = setMockStats;
fsMock.resetMockStats = resetMockStats;

fsMock.setMockFile = setMockFile;
fsMock.resetMockFiles = resetMockFiles;

fsMock.stat = stat as typeof fs.stat;
fsMock.readFileSync = readFileSync as typeof fs.readFileSync;

export default fsMock;
118 changes: 113 additions & 5 deletions packages/cli/src/swc/__tests__/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Options } from "@swc/core";
import deepmerge from "deepmerge";
import fs from "fs";
import { resolve } from "path";

jest.mock("fs");
ShogunPanda marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -56,6 +60,7 @@ describe("parserArgs", () => {
beforeEach(() => {
defaultResult = createDefaultResult();
initProgram();
(fs as any).resetMockFiles();
});

it("minimal args returns default result", async () => {
Expand Down Expand Up @@ -91,7 +96,7 @@ describe("parserArgs", () => {
"src",
];
const result = parserArgs(args);
expect(result.cliOptions.outFileExtension).toEqual("js");
expect(result!.cliOptions.outFileExtension).toEqual("js");
});
});

Expand Down Expand Up @@ -270,7 +275,7 @@ describe("parserArgs", () => {
const expectedOptions = deepmerge(defaultResult.swcOptions, {
jsc: { transform: { react: { development: true } } },
});
expect(result.swcOptions).toEqual(expectedOptions);
expect(result!.swcOptions).toEqual(expectedOptions);
});

it("react development and commonjs (two config options)", async () => {
Expand All @@ -288,7 +293,7 @@ describe("parserArgs", () => {
jsc: { transform: { react: { development: true } } },
module: { type: "commonjs" },
});
expect(result.swcOptions).toEqual(expectedOptions);
expect(result!.swcOptions).toEqual(expectedOptions);
});

it("react development and commonjs (comma-separated)", async () => {
Expand All @@ -304,7 +309,7 @@ describe("parserArgs", () => {
jsc: { transform: { react: { development: true } } },
module: { type: "commonjs" },
});
expect(result.swcOptions).toEqual(expectedOptions);
expect(result!.swcOptions).toEqual(expectedOptions);
});

it("no equals sign", async () => {
Expand All @@ -319,7 +324,110 @@ describe("parserArgs", () => {
const expectedOptions = deepmerge(defaultResult.swcOptions, {
no_equals: true,
});
expect(result.swcOptions).toEqual(expectedOptions);
expect(result!.swcOptions).toEqual(expectedOptions);
});
});

describe("--cli-config-file", () => {
it("reads a JSON config file with both camel and kebab case options", async () => {
(fs as any).setMockFile(
resolve(process.cwd(), "/swc/cli.json"),
JSON.stringify({
outFileExtension: "mjs",
deleteDirOnStart: "invalid",
"delete-dir-on-start": true,
})
);

const args = [
"node",
"/path/to/node_modules/swc-cli/bin/swc.js",
"src",
"--cli-config-file",
"/swc/cli.json",
];
const result = parserArgs(args);
const expectedOptions = deepmerge(defaultResult, {
cliOptions: { outFileExtension: "mjs", deleteDirOnStart: true },
});

expect(result).toEqual(expectedOptions);
});

it("reads a JSON but options are overriden from CLI", async () => {
(fs as any).setMockFile(
resolve(process.cwd(), "/swc/cli.json"),
JSON.stringify({
outFileExtension: "mjs",
"delete-dir-on-start": true,
})
);

const args = [
"node",
"/path/to/node_modules/swc-cli/bin/swc.js",
"src",
"--cli-config-file",
"/swc/cli.json",
"--out-file-extension",
"cjs",
];
const result = parserArgs(args);
const expectedOptions = deepmerge(defaultResult, {
cliOptions: { outFileExtension: "cjs", deleteDirOnStart: true },
});

expect(result).toEqual(expectedOptions);
});

describe("exits", () => {
let mockExit: jest.SpyInstance;
let mockConsoleError: jest.SpyInstance;

beforeEach(() => {
mockExit = jest
.spyOn(process, "exit")
// @ts-expect-error
.mockImplementation(() => {});
mockConsoleError = jest
.spyOn(console, "error")
.mockImplementation(() => {});
});

afterEach(() => {
mockExit.mockRestore();
mockConsoleError.mockRestore();
});

it("if the config file is missing", async () => {
const args = [
"node",
"/path/to/node_modules/swc-cli/bin/swc.js",
"src",
"--cli-config-file",
"/swc/cli.json",
];

parserArgs(args);
expect(mockExit).toHaveBeenCalledWith(2);
expect(mockConsoleError).toHaveBeenCalledTimes(2);
});

it("if the config file is not valid JSON", async () => {
(fs as any).setMockFile("/swc/cli.json", "INVALID");

const args = [
"node",
"/path/to/node_modules/swc-cli/bin/swc.js",
"src",
"--cli-config-file",
"/swc/cli.json",
];

parserArgs(args);
expect(mockExit).toHaveBeenCalledWith(2);
expect(mockConsoleError).toHaveBeenCalledTimes(2);
});
});
});
});
Loading
Loading