in eng/tools/typespec-validation/src/rules/compile.ts [15:214]
async execute(folder: string): Promise<RuleResult> {
let success = true;
let stdOutput = "";
let errorOutput = "";
if (await fileExists(path.join(folder, "main.tsp"))) {
let [err, stdout, stderr] = await runNpm([
"exec",
"--no",
"--",
"tsp",
"compile",
"--list-files",
"--warn-as-error",
folder,
]);
stdOutput += stdout;
// Rule output is easier to read if "tsp compile" stderr is redirected to stdOutput
stdOutput += stderr;
if (
stdout.toLowerCase().includes("no emitter was configured") ||
stdout.toLowerCase().includes("no output was generated")
) {
success = false;
errorOutput += "No emitter was configured and/or no output was generated.";
}
if (success) {
if (!err) {
// Check for *extra* typespec-generated swagger files under the output folder, which
// indicates a mismatch between TypeSpec and swaggers.
// Example 'stdout':
//
// TypeSpec compiler v0.67.2
//
// ../resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json
// ../resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Operations_List.json
//
// Compilation completed successfully.
// Remove ANSI color codes, handle windows and linux line endings
const lines = stripAnsi(stdout).split(/\r?\n/);
// TODO: Use helpers in /.github once they support platform-specific paths
// Header, footer, and empty lines should be excluded by JSON filter
const outputSwaggers = lines
// Remove leading and trailing whitespace
.map((l) => l.trim())
// Normalize to platform-specific path
.map((l) => normalize(l))
// Filter to JSON files
.filter((p) => basename(p).toLowerCase().endsWith(".json"))
// Exclude examples
.filter((p) => !p.split(path.sep).includes("examples"));
stdOutput += "\nGenerated Swaggers:\n";
stdOutput += outputSwaggers.join("\n") + "\n";
if (outputSwaggers.length === 0) {
throw new Error("No generated swaggers found in output of 'tsp compile'");
}
// ../resource-manager/Microsoft.Contoso
const outputFolder = dirname(dirname(dirname(outputSwaggers[0])));
const outputFilename = basename(outputSwaggers[0]);
stdOutput += "\nOutput folder:\n";
stdOutput += outputFolder + "\n";
// Filter to only specs matching the folder and filename extracted from the first output-file.
// Necessary to handle multi-project specs like keyvault.
//
// Globby only accepts patterns like posix paths.
const pattern = path.posix.join(
...outputFolder.split(path.win32.sep),
"**",
outputFilename,
);
const allSwaggers = (await globby(pattern, { ignore: ["**/examples/**"] })).map(
// Globby always returns posix paths
(p) => normalize(p),
);
// Filter to files generated by TypeSpec
const tspGeneratedSwaggers = await filterAsync(
allSwaggers,
async (swaggerPath: string) => {
const swaggerText = await readFile(swaggerPath, { encoding: "utf8" });
const swaggerObj = JSON.parse(swaggerText);
return (
swaggerObj["info"]?.["x-typespec-generated"] ||
swaggerObj["info"]?.["x-cadl-generated"]
);
},
);
stdOutput += `\nSwaggers matching output folder and filename:\n`;
stdOutput += tspGeneratedSwaggers.join("\n") + "\n";
const suppressedSwaggers = await filterAsync(
tspGeneratedSwaggers,
async (swaggerPath: string) => {
const suppressions = await getSuppressions(swaggerPath);
const extraSwaggerSuppressions = suppressions.filter(
(s) => s.rules?.includes(this.name) && s.subRules?.includes("ExtraSwagger"),
);
// Each path must specify a single version (without wildcards) under "preview|stable"
//
// Allowed: data-plane/Azure.Contoso.WidgetManager/preview/2022-11-01-preview/**/*.json
// Disallowed: data-plane/Azure.Contoso.WidgetManager/preview/**/*.json
// Disallowed: data-plane/**/*.json
//
// Include "." since a few specs use versions like "X.Y" instead of "YYYY-MM-DD"
const singleVersionPattern = "/(preview|stable)/[A-Za-z0-9._-]+/";
for (const suppression of extraSwaggerSuppressions) {
for (const p of suppression.paths) {
if (!p.match(singleVersionPattern)) {
throw new Error(
`Invalid path '${p}'. Path must only include one version per suppression.`,
);
}
}
}
return extraSwaggerSuppressions.length > 0;
},
);
stdOutput += `\nSwaggers excluded via suppressions.yaml:\n`;
stdOutput += suppressedSwaggers.join("\n") + "\n";
const remainingSwaggers = tspGeneratedSwaggers.filter(
(s) => !suppressedSwaggers.includes(s),
);
stdOutput += `\nRemaining swaggers:\n`;
stdOutput += remainingSwaggers.join("\n") + "\n";
const extraSwaggers = remainingSwaggers.filter((s) => !outputSwaggers.includes(s));
if (extraSwaggers.length > 0) {
success = false;
errorOutput += pc.red(
`\nOutput folder '${outputFolder}' appears to contain TypeSpec-generated ` +
`swagger files, not generated from the current TypeSpec sources. ` +
`Perhaps you deleted a version from your TypeSpec, but didn't delete ` +
`the associated swaggers?\n\n`,
);
errorOutput += pc.red(extraSwaggers.join("\n") + "\n");
}
} else {
success = false;
errorOutput += err.message;
}
}
}
const clientTsp = path.join(folder, "client.tsp");
if (await fileExists(clientTsp)) {
let [err, stdout, stderr] = await runNpm([
"exec",
"--no",
"--",
"tsp",
"compile",
"--no-emit",
"--warn-as-error",
clientTsp,
]);
if (err) {
success = false;
errorOutput += err.message;
}
stdOutput += stdout;
errorOutput += stderr;
}
if (success) {
const gitDiffResult = await gitDiffTopSpecFolder(folder);
stdOutput += gitDiffResult.stdOutput;
if (!gitDiffResult.success) {
success = false;
errorOutput += gitDiffResult.errorOutput;
errorOutput += `\nFiles have been changed after \`tsp compile\`. Run \`tsp compile\` and ensure all files are included in your change.`;
}
}
return {
success: success,
stdOutput: stdOutput,
errorOutput: errorOutput,
};
}