Skip to content

Commit

Permalink
Refactor cli: Organize in different actions (#2174)
Browse files Browse the repository at this point in the history
This split up unrelated content in the cli into different files. This PR
keeps it to a minimum of moving content around. I have a follow up where
all actions should try to reuse the logger/diagnostics instead of
managing errors themself.
  • Loading branch information
timotheeguerin authored Jul 12, 2023
1 parent 140d6ce commit 6fae63c
Show file tree
Hide file tree
Showing 13 changed files with 553 additions and 467 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "Internal: Refactoring of cli code",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { expandConfigVariables } from "../../config/config-interpolation.js";
import { expandConfigVariables } from "../../../../config/config-interpolation.js";
import {
loadTypeSpecConfigForPath,
validateConfigPathsAbsolute,
} from "../../config/config-loader.js";
import { EmitterOptions, TypeSpecConfig } from "../../config/types.js";
import { createDiagnosticCollector } from "../index.js";
import { CompilerOptions } from "../options.js";
import { getDirectoryPath, normalizePath, resolvePath } from "../path-utils.js";
import { CompilerHost, Diagnostic } from "../types.js";
import { deepClone, omitUndefined } from "../util.js";
} from "../../../../config/config-loader.js";
import { EmitterOptions, TypeSpecConfig } from "../../../../config/types.js";
import { createDiagnosticCollector } from "../../../index.js";
import { CompilerOptions } from "../../../options.js";
import { getDirectoryPath, normalizePath, resolvePath } from "../../../path-utils.js";
import { CompilerHost, Diagnostic } from "../../../types.js";
import { deepClone, omitUndefined } from "../../../util.js";

export interface CompileCliArgs {
"output-dir"?: string;
Expand Down
148 changes: 148 additions & 0 deletions packages/compiler/src/core/cli/actions/compile/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import watch from "node-watch";
import { resolve } from "path";
import { logDiagnostics } from "../../../diagnostics.js";
import { resolveTypeSpecEntrypoint } from "../../../entrypoint-resolution.js";
import { CompilerOptions } from "../../../options.js";
import { getAnyExtensionFromPath, resolvePath } from "../../../path-utils.js";
import { Program, compile as compileProgram } from "../../../program.js";
import { CompilerHost, Diagnostic } from "../../../types.js";
import {
createCLICompilerHost,
handleInternalCompilerError,
logDiagnosticCount,
} from "../../utils.js";
import { CompileCliArgs, getCompilerOptions } from "./args.js";

export async function compileAction(args: CompileCliArgs & { path: string; pretty?: boolean }) {
const host = createCLICompilerHost(args);
const diagnostics: Diagnostic[] = [];
const entrypoint = await resolveTypeSpecEntrypoint(
host,
resolvePath(process.cwd(), args.path),
(diag) => diagnostics.push(diag)
);
if (entrypoint === undefined || diagnostics.length > 0) {
logDiagnostics(diagnostics, host.logSink);
process.exit(1);
}
const cliOptions = await getCompilerOptionsOrExit(host, entrypoint, args);

const program = await compileInput(host, entrypoint, cliOptions);
if (program.hasError()) {
process.exit(1);
}
if (program.emitters.length === 0 && !program.compilerOptions.noEmit) {
// eslint-disable-next-line no-console
console.log(
"No emitter was configured, no output was generated. Use `--emit <emitterName>` to pick emitter or specify it in the typespec config."
);
}
}

async function getCompilerOptionsOrExit(
host: CompilerHost,
entrypoint: string,
args: CompileCliArgs
): Promise<CompilerOptions> {
const [options, diagnostics] = await getCompilerOptions(
host,
entrypoint,
process.cwd(),
args,
process.env
);
if (diagnostics.length > 0) {
logDiagnostics(diagnostics, host.logSink);
}
if (options === undefined) {
logDiagnosticCount(diagnostics);
process.exit(1);
}

return options;
}

function compileInput(
host: CompilerHost,
path: string,
compilerOptions: CompilerOptions,
printSuccess = true
): Promise<Program> {
let compileRequested: boolean = false;
let currentCompilePromise: Promise<Program> | undefined = undefined;
const log = (message?: any, ...optionalParams: any[]) => {
const prefix = compilerOptions.watchForChanges ? `[${new Date().toLocaleTimeString()}] ` : "";
// eslint-disable-next-line no-console
console.log(`${prefix}${message}`, ...optionalParams);
};

const runCompilePromise = () => {
// Don't run the compiler if it's already running
if (!currentCompilePromise) {
// Clear the console before compiling in watch mode
if (compilerOptions.watchForChanges) {
// eslint-disable-next-line no-console
console.clear();
}

currentCompilePromise = compileProgram(host, resolve(path), compilerOptions)
.then(onCompileFinished)
.catch(handleInternalCompilerError);
} else {
compileRequested = true;
}

return currentCompilePromise;
};

const runCompile = () => void runCompilePromise();

const onCompileFinished = (program: Program) => {
if (program.diagnostics.length > 0) {
log("Diagnostics were reported during compilation:\n");
logDiagnostics(program.diagnostics, host.logSink);
logDiagnosticCount(program.diagnostics);
} else {
if (printSuccess) {
log("Compilation completed successfully.");
}
}

// eslint-disable-next-line no-console
console.log(); // Insert a newline
currentCompilePromise = undefined;
if (compilerOptions.watchForChanges && compileRequested) {
compileRequested = false;
runCompile();
}

return program;
};

if (compilerOptions.watchForChanges) {
runCompile();
return new Promise((resolve, reject) => {
const watcher = (watch as any)(
path,
{
recursive: true,
filter: (f: string) =>
[".js", ".tsp", ".cadl"].indexOf(getAnyExtensionFromPath(f)) > -1 &&
!/node_modules/.test(f),
},
(e: any, name: string) => {
runCompile();
}
);

// Handle Ctrl+C for termination
process.on("SIGINT", () => {
watcher.close();
// eslint-disable-next-line no-console
console.info("Terminating watcher...\n");
});
});
} else {
return runCompilePromise();
}
}
30 changes: 30 additions & 0 deletions packages/compiler/src/core/cli/actions/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { findUnformattedTypeSpecFiles, formatTypeSpecFiles } from "../../formatter-fs.js";

export interface FormatArgs {
include: string[];
exclude?: string[];
debug?: boolean;
check?: boolean;
}
export async function formatAction(args: FormatArgs) {
if (args["check"]) {
const unformatted = await findUnformattedTypeSpecFiles(args["include"], {
exclude: args["exclude"],
debug: args.debug,
});
if (unformatted.length > 0) {
// eslint-disable-next-line no-console
console.log(`Found ${unformatted.length} unformatted files:`);
for (const file of unformatted) {
// eslint-disable-next-line no-console
console.log(` - ${file}`);
}
process.exit(1);
}
} else {
await formatTypeSpecFiles(args["include"], {
exclude: args["exclude"],
debug: args.debug,
});
}
}
30 changes: 30 additions & 0 deletions packages/compiler/src/core/cli/actions/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import { fileURLToPath } from "url";
import { loadTypeSpecConfigForPath } from "../../../config/config-loader.js";
import { logDiagnostics } from "../../diagnostics.js";
import { CompilerHost } from "../../types.js";
import { logDiagnosticCount } from "../utils.js";

/**
* Print the resolved TypeSpec configuration.
*/
export async function printInfoAction(host: CompilerHost) {
const cwd = process.cwd();
console.log(`Module: ${fileURLToPath(import.meta.url)}`);

const config = await loadTypeSpecConfigForPath(host, cwd);
const jsyaml = await import("js-yaml");
const excluded = ["diagnostics", "filename"];
const replacer = (emitter: string, value: any) =>
excluded.includes(emitter) ? undefined : value;

console.log(`User Config: ${config.filename ?? "No config file found"}`);
console.log("-----------");
console.log(jsyaml.dump(config, { replacer }));
console.log("-----------");
logDiagnostics(config.diagnostics, host.logSink);
logDiagnosticCount(config.diagnostics);
if (config.diagnostics.some((d) => d.severity === "error")) {
process.exit(1);
}
}
21 changes: 21 additions & 0 deletions packages/compiler/src/core/cli/actions/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { InitTemplateError, initTypeSpecProject } from "../../../init/init.js";
import { logDiagnostics } from "../../diagnostics.js";
import { createCLICompilerHost } from "../utils.js";

export interface InitArgs {
templatesUrl?: string;
pretty?: boolean;
}

export async function initAction(args: InitArgs) {
const host = createCLICompilerHost(args);
try {
await initTypeSpecProject(host, process.cwd(), args.templatesUrl);
} catch (e) {
if (e instanceof InitTemplateError) {
logDiagnostics(e.diagnostics, host.logSink);
process.exit(1);
}
throw e;
}
}
72 changes: 72 additions & 0 deletions packages/compiler/src/core/cli/actions/vs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { joinPaths } from "../../path-utils.js";
import { installVsix } from "../install-vsix.js";
import { run } from "../utils.js";

const VSIX_ALREADY_INSTALLED = 1001;
const VSIX_NOT_INSTALLED = 1002;
const VSIX_USER_CANCELED = 2005;
const VS_SUPPORTED_VERSION_RANGE = "[17.0,)";

export async function installVSExtension(debug: boolean) {
const vsixInstaller = getVsixInstallerPath();

if (!isVSInstalled(VS_SUPPORTED_VERSION_RANGE)) {
// eslint-disable-next-line no-console
console.error("error: No compatible version of Visual Studio found.");
process.exit(1);
}

await installVsix(
"typespec-vs",
(vsixPaths) => {
for (const vsix of vsixPaths) {
// eslint-disable-next-line no-console
console.log(`Installing extension for Visual Studio...`);
run(vsixInstaller, [vsix], {
allowedExitCodes: [VSIX_ALREADY_INSTALLED, VSIX_USER_CANCELED],
});
}
},
debug
);
}

export async function uninstallVSExtension() {
const vsixInstaller = getVsixInstallerPath();
run(vsixInstaller, ["/uninstall:88b9492f-c019-492c-8aeb-f325a7e4cf23"], {
allowedExitCodes: [VSIX_NOT_INSTALLED, VSIX_USER_CANCELED],
});
}

function getVsixInstallerPath(): string {
return getVSInstallerPath(
"resources/app/ServiceHub/Services/Microsoft.VisualStudio.Setup.Service/VSIXInstaller.exe"
);
}

function getVSWherePath(): string {
return getVSInstallerPath("vswhere.exe");
}

function getVSInstallerPath(relativePath: string) {
if (process.platform !== "win32") {
// eslint-disable-next-line no-console
console.error("error: Visual Studio extension is not supported on non-Windows.");
process.exit(1);
}

return joinPaths(
process.env["ProgramFiles(x86)"] ?? "",
"Microsoft Visual Studio/Installer",
relativePath
);
}

function isVSInstalled(versionRange: string) {
const vswhere = getVSWherePath();
const proc = run(vswhere, ["-property", "instanceid", "-prerelease", "-version", versionRange], {
stdio: [null, "pipe", "inherit"],
allowNotFound: true,
});
return proc.status === 0 && proc.stdout;
}
43 changes: 43 additions & 0 deletions packages/compiler/src/core/cli/actions/vscode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { installVsix } from "../install-vsix.js";
import { run } from "../utils.js";

export async function installVSCodeExtension(insiders: boolean, debug: boolean) {
await installVsix(
"typespec-vscode",
(vsixPaths) => {
runCode(["--install-extension", vsixPaths[0]], insiders, debug);
},
debug
);
}

export async function uninstallVSCodeExtension(insiders: boolean, debug: boolean) {
await runCode(["--uninstall-extension", "microsoft.typespec-vscode"], insiders, debug);
}

function runCode(codeArgs: string[], insiders: boolean, debug: boolean) {
try {
run(insiders ? "code-insiders" : "code", codeArgs, {
// VS Code's CLI emits node warnings that we can't do anything about. Suppress them.
extraEnv: { NODE_NO_WARNINGS: "1" },
debug,
allowNotFound: true,
});
} catch (error: any) {
if (error.code === "ENOENT") {
// eslint-disable-next-line no-console
console.error(
`error: Couldn't find VS Code 'code' command in PATH. Make sure you have the VS Code executable added to the system PATH.`
);
if (process.platform === "darwin") {
// eslint-disable-next-line no-console
console.log("See instruction for Mac OS here https://code.visualstudio.com/docs/setup/mac");
}
if (debug) {
// eslint-disable-next-line no-console
console.log(error.stack);
}
process.exit(1);
}
}
}
Loading

0 comments on commit 6fae63c

Please sign in to comment.