build: fix ts-api-guardian golden approval not working on windows (#36096)

Currently on Windows, it's not possible to approve goldens in
`ts-api-guardian`. This is because paths are resolved relatively
to the working directory. In Windows, golden files are resolved
to the actual workspace directory. The current logic tries to
compute a relative path to the runfile from the working directory.

This causes the file paths to have a lot of parent directory
path segments. Eventually, when joined with the build workspace
directory, the paths end up being incorrect. e.g.

```
fileName = ../../../../../../projects/angular/golden/<..>/common.d.ts`
outFile = BUILD_WORKSPACE_DIR + fileName;
```

To fix this, we no longer deal with confusing relative paths, but
instead always use absolute file system paths.

Additionally, this fixes that new goldens are generated at the wrong
location on all platforms.

PR Close #36096
This commit is contained in:
Paul Gschwendtner 2020-03-17 11:30:36 +01:00 committed by Andrew Kushnir
parent afc9839f43
commit f862536ec4
2 changed files with 53 additions and 31 deletions

View File

@ -16,8 +16,23 @@ import * as path from 'path';
import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile, discoverAllEntrypoints} from './main'; import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile, discoverAllEntrypoints} from './main';
/** Name of the CLI */
const CMD = 'ts-api-guardian'; const CMD = 'ts-api-guardian';
/** Name of the Bazel workspace that runs the CLI. */
const bazelWorkspaceName = process.env.BAZEL_WORKSPACE;
/**
* Path to the Bazel workspace directory. Only set if the CLI is run with `bazel run`.
* https://docs.bazel.build/versions/master/user-manual.html#run.
*/
const bazelWorkspaceDirectory = process.env.BUILD_WORKSPACE_DIRECTORY;
/**
* Regular expression that matches Bazel manifest paths that start with the
* current Bazel workspace, followed by a path delimiter.
*/
const bazelWorkspaceManifestPathRegex =
bazelWorkspaceName ? new RegExp(`^${bazelWorkspaceName}[/\\\\]`) : null;
export function startCli() { export function startCli() {
const {argv, mode, errors} = parseArguments(process.argv.slice(2)); const {argv, mode, errors} = parseArguments(process.argv.slice(2));
@ -210,35 +225,48 @@ Options:
} }
/** /**
* Resolves a given path to the associated relative path if the current process runs within * Resolves a given path in the file system. If `ts-api-guardian` runs with Bazel, file paths
* Bazel. We need to use the wrapped NodeJS resolve logic in order to properly handle the given * are resolved through runfiles. Additionally in Bazel, this method handles the case where
* runfiles files which are only part of the runfile manifest on Windows. * manifest file paths are not existing, but need to resolve to the Bazel workspace directory.
* This happens commonly when goldens are approved, but the golden file does not exist yet.
*/ */
function resolveBazelFilePath(fileName: string): string { function resolveFilePath(fileName: string): string {
// If the CLI has been launched through the NodeJS Bazel rules, we need to resolve the // If an absolute path is specified, the path is already resolved.
// actual file paths because otherwise this script won't work on Windows where runfiles if (path.isAbsolute(fileName)) {
// are not available in the working directory. In order to resolve the real path for the
// runfile, we need to use `require.resolve` which handles runfiles properly on Windows.
if (process.env['BAZEL_TARGET']) {
// This try/catch block is necessary because if the path is to the source file directly
// rather than via symlinks in the bazel output directories, require is not able to
// resolve it.
try {
return path.relative(process.cwd(), require.resolve(fileName));
} catch (err) {
return path.relative(process.cwd(), fileName);
}
}
return fileName; return fileName;
} }
// Outside of Bazel, file paths are resolved based on the current working directory.
if (!bazelWorkspaceName) {
return path.resolve(fileName);
}
// In Bazel, we first try to resolve the file through the runfiles. We do this by calling
// the `require.resolve` function that is patched by the Bazel NodeJS rules. Note that we
// need to catch errors because files inside tree artifacts cannot be resolved through
// runfile manifests. Hence, we need to have alternative resolution logic when resolving
// file paths. Additionally, it could happen that manifest paths which aren't part of the
// runfiles are specified (i.e. golden is approved but does not exist in the workspace yet).
try {
return require.resolve(fileName);
} catch {
}
// This handles cases where file paths cannot be resolved through runfiles. This happens
// commonly when goldens are approved while the golden does not exist in the workspace yet.
// In those cases, we want to build up a relative path based on the manifest path, and join
// it with the absolute bazel workspace directory (which is only set in `bazel run`).
// e.g. `angular/goldens/<..>/common` should become `{workspace_dir}/goldens/<...>/common`.
if (bazelWorkspaceManifestPathRegex !== null && bazelWorkspaceDirectory &&
bazelWorkspaceManifestPathRegex.test(fileName)) {
return path.join(bazelWorkspaceDirectory, fileName.substr(bazelWorkspaceName.length + 1));
}
throw Error(`Could not resolve file path in runfiles: ${fileName}`);
}
function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoints: string[]): function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoints: string[]):
{entrypoint: string, goldenFile: string}[] { {entrypoint: string, goldenFile: string}[] {
if (argv[mode]) { if (argv[mode]) {
return [{ return [{
entrypoint: resolveBazelFilePath(entrypoints[0]), entrypoint: resolveFilePath(entrypoints[0]),
goldenFile: resolveBazelFilePath(argv[mode]), goldenFile: resolveFilePath(argv[mode]),
}]; }];
} else { // argv[mode + 'Dir'] } else { // argv[mode + 'Dir']
let rootDir = argv['rootDir'] || '.'; let rootDir = argv['rootDir'] || '.';
@ -246,8 +274,8 @@ function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoin
return entrypoints.map((fileName: string) => { return entrypoints.map((fileName: string) => {
return { return {
entrypoint: resolveBazelFilePath(fileName), entrypoint: resolveFilePath(fileName),
goldenFile: resolveBazelFilePath(path.join(goldenDir, path.relative(rootDir, fileName))), goldenFile: resolveFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
}; };
}); });
} }

View File

@ -17,13 +17,6 @@ export function generateGoldenFile(
entrypoint: string, outFile: string, options: SerializationOptions = {}): void { entrypoint: string, outFile: string, options: SerializationOptions = {}): void {
const output = publicApi(entrypoint, options); const output = publicApi(entrypoint, options);
// BUILD_WORKSPACE_DIRECTORY environment variable is only available during bazel
// run executions. This workspace directory allows us to generate golden files directly
// in the source file tree rather than via a symlink.
if (process.env['BUILD_WORKSPACE_DIRECTORY']) {
outFile = path.join(process.env['BUILD_WORKSPACE_DIRECTORY'], outFile);
}
ensureDirectory(path.dirname(outFile)); ensureDirectory(path.dirname(outFile));
fs.writeFileSync(outFile, output); fs.writeFileSync(outFile, output);
} }
@ -36,7 +29,8 @@ export function verifyAgainstGoldenFile(
if (actual === expected) { if (actual === expected) {
return ''; return '';
} else { } else {
const patch = createPatch(goldenFile, expected, actual, 'Golden file', 'Generated API'); const displayFileName = path.relative(process.cwd(), goldenFile);
const patch = createPatch(displayFileName, expected, actual, 'Golden file', 'Generated API');
// Remove the header of the patch // Remove the header of the patch
const start = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1; const start = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1;