Use real resolution for used file detection (#615)

* Use real TypeScript resolution for determining used files

* getTypingInfo needs the whole DT FS

* Fix a few things not showing up in tests

* Disable superfluous OTHER_FILES error

* Replace internal API

* Bring back error for relatively referencing other packages

* Fix `getLocallyInstalledDefinitelyTyped`

* PR suggestions
This commit is contained in:
Andrew Branch
2023-02-27 13:57:22 -08:00
committed by GitHub
parent 8bff9d7e83
commit ed2a4f3350
27 changed files with 324 additions and 229 deletions

View File

@@ -1,4 +1,3 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "/Developer/microsoft/TypeScript/built/local"
}

View File

@@ -3,6 +3,7 @@ import { DiskFS, downloadAndExtractFile, LoggerWithErrors, FS, exec } from "@def
import { dataDirPath, definitelyTypedZipUrl } from "./lib/settings";
import { ExecException } from "child_process";
import path from "path";
/** Settings that may be determined dynamically. */
export interface ParseDefinitionsOptions {
@@ -36,9 +37,9 @@ export async function getDefinitelyTyped(options: ParseDefinitionsOptions, log:
throw new Error(`'git diff' should be empty. Following files changed:\n${stdout}`);
}
log.info(`Using local DefinitelyTyped at ${options.definitelyTypedPath}`);
return new DiskFS(`${options.definitelyTypedPath}/`);
return new DiskFS(`${path.resolve(options.definitelyTypedPath)}/`);
}
export function getLocallyInstalledDefinitelyTyped(path: string): FS {
return new DiskFS(`${path}/`);
export function getLocallyInstalledDefinitelyTyped(definitelyTypedPath: string): FS {
return new DiskFS(`${path.resolve(definitelyTypedPath)}/`);
}

View File

@@ -3,6 +3,7 @@ import process = require("process");
import { logUncaughtErrors } from "@definitelytyped/utils";
import { getLocallyInstalledDefinitelyTyped } from "../get-definitely-typed";
import { getTypingInfo } from "./definition-parser";
import { dirname } from "path";
// This file is "called" by runWithChildProcesses from parse-definition.ts
export const definitionParserWorkerFilename = __filename;
@@ -16,7 +17,7 @@ if (!module.parent) {
for (const packageName of message as readonly string[]) {
const data = await getTypingInfo(
packageName,
getLocallyInstalledDefinitelyTyped(typesPath).subDir(packageName)
getLocallyInstalledDefinitelyTyped(dirname(typesPath))
);
process.send!({ data, packageName });
}

View File

@@ -9,6 +9,7 @@ import {
TypingsDataRaw,
TypingsVersionsRaw,
DirectoryParsedTypingVersion,
getMangledNameForScopedPackage,
} from "../packages";
import { getAllowedPackageJsonDependencies } from "./settings";
import {
@@ -27,8 +28,10 @@ import {
removeVersionFromPackageName,
hasVersionNumberInMapping,
mangleScopedPackage,
createModuleResolutionHost,
} from "@definitelytyped/utils";
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import path from "path";
function matchesVersion(
typingsDataRaw: TypingsDataRaw,
@@ -47,8 +50,7 @@ function formattedLibraryVersion(typingsDataRaw: TypingsDataRaw): `${number}.${n
return `${typingsDataRaw.libraryMajorVersion}.${typingsDataRaw.libraryMinorVersion}`;
}
/** @param fs Rooted at the package's directory, e.g. `DefinitelyTyped/types/abs` */
export async function getTypingInfo(packageName: string, fs: FS): Promise<TypingsVersionsRaw> {
export async function getTypingInfo(packageName: string, dt: FS): Promise<TypingsVersionsRaw> {
if (packageName !== packageName.toLowerCase()) {
throw new Error(`Package name \`${packageName}\` should be strictly lowercase`);
}
@@ -56,6 +58,8 @@ export async function getTypingInfo(packageName: string, fs: FS): Promise<Typing
readonly directoryName: string;
readonly version: DirectoryParsedTypingVersion;
}
const fs = dt.subDir("types").subDir(getMangledNameForScopedPackage(packageName));
const [rootDirectoryLs, olderVersionDirectories] = split<string, OlderVersionDir>(
fs.readdir(),
(fileOrDirectoryName) => {
@@ -64,11 +68,12 @@ export async function getTypingInfo(packageName: string, fs: FS): Promise<Typing
}
);
const moduleResolutionHost = createModuleResolutionHost(dt);
const considerLibraryMinorVersion = olderVersionDirectories.some(({ version }) => version.minor !== undefined);
const latestData: TypingsDataRaw = {
libraryVersionDirectoryName: undefined,
...(await combineDataForAllTypesVersions(packageName, rootDirectoryLs, fs, undefined)),
...(await combineDataForAllTypesVersions(packageName, rootDirectoryLs, fs, undefined, moduleResolutionHost)),
};
const older = await Promise.all(
@@ -77,9 +82,9 @@ export async function getTypingInfo(packageName: string, fs: FS): Promise<Typing
const latest = `${latestData.libraryMajorVersion}.${latestData.libraryMinorVersion}`;
throw new Error(
`The latest version of the '${packageName}' package is ${latest}, so the subdirectory '${directoryName}' is not allowed` +
(`v${latest}` === directoryName
? "."
: `; since it applies to any ${latestData.libraryMajorVersion}.* version, up to and including ${latest}.`)
(`v${latest}` === directoryName
? "."
: `; since it applies to any ${latestData.libraryMajorVersion}.* version, up to and including ${latest}.`)
);
}
@@ -87,19 +92,19 @@ export async function getTypingInfo(packageName: string, fs: FS): Promise<Typing
const ls = fs.readdir(directoryName);
const data: TypingsDataRaw = {
libraryVersionDirectoryName: formatTypingVersion(directoryVersion),
...(await combineDataForAllTypesVersions(packageName, ls, fs.subDir(directoryName), directoryVersion)),
...(await combineDataForAllTypesVersions(packageName, ls, fs.subDir(directoryName), directoryVersion, moduleResolutionHost)),
};
if (!matchesVersion(data, directoryVersion, considerLibraryMinorVersion)) {
if (considerLibraryMinorVersion) {
throw new Error(
`Directory ${directoryName} indicates major.minor version ${directoryVersion.major}.${directoryVersion.minor}, ` +
`but header indicates major.minor version ${data.libraryMajorVersion}.${data.libraryMinorVersion}`
`but header indicates major.minor version ${data.libraryMajorVersion}.${data.libraryMinorVersion}`
);
}
throw new Error(
`Directory ${directoryName} indicates major version ${directoryVersion.major}, but header indicates major version ` +
data.libraryMajorVersion.toString()
data.libraryMajorVersion.toString()
);
}
return data;
@@ -193,9 +198,20 @@ async function combineDataForAllTypesVersions(
typingsPackageName: string,
ls: readonly string[],
fs: FS,
directoryVersion: DirectoryParsedTypingVersion | undefined
directoryVersion: DirectoryParsedTypingVersion | undefined,
moduleResolutionHost: ts.ModuleResolutionHost,
): Promise<Omit<TypingsDataRaw, "libraryVersionDirectoryName">> {
const { remainingLs, typesVersions, hasPackageJson } = getTypesVersionsAndPackageJson(ls);
const packageJson = hasPackageJson
? (fs.readJson(packageJsonName) as {
readonly license?: unknown;
readonly dependencies?: unknown;
readonly imports?: unknown;
readonly exports?: unknown;
readonly type?: unknown;
})
: {};
const packageJsonType = checkPackageJsonType(packageJson.type, packageJsonName);
// Every typesVersion has an index.d.ts, but only the root index.d.ts should have a header.
const {
@@ -210,33 +226,23 @@ async function combineDataForAllTypesVersions(
const dataForRoot = getTypingDataForSingleTypesVersion(
undefined,
typingsPackageName,
fs.debugPath(),
remainingLs,
fs,
directoryVersion
directoryVersion,
moduleResolutionHost,
);
const dataForOtherTypesVersions = typesVersions.map((tsVersion) => {
const subFs = fs.subDir(`ts${tsVersion}`);
return getTypingDataForSingleTypesVersion(
tsVersion,
typingsPackageName,
fs.debugPath(),
subFs.readdir(),
subFs,
directoryVersion
directoryVersion,
moduleResolutionHost,
);
});
const allTypesVersions = [dataForRoot, ...dataForOtherTypesVersions];
const packageJson = hasPackageJson
? (fs.readJson(packageJsonName) as {
readonly license?: unknown;
readonly dependencies?: unknown;
readonly imports?: unknown;
readonly exports?: unknown;
readonly type?: unknown;
})
: {};
const license = getLicenseFromPackageJson(packageJson.license);
const packageJsonDependencies = await checkPackageJsonDependencies(packageJson.dependencies, packageJsonName);
@@ -271,7 +277,7 @@ async function combineDataForAllTypesVersions(
declaredModules: getAllUniqueValues<"declaredModules", string>(allTypesVersions, "declaredModules"),
imports: checkPackageJsonImports(packageJson.imports, packageJsonName),
exports: checkPackageJsonExportsAndAddPJsonEntry(packageJson.exports, packageJsonName),
type: checkPackageJsonType(packageJson.type, packageJsonName),
type: packageJsonType,
};
}
@@ -300,26 +306,36 @@ interface TypingDataFromIndividualTypeScriptVersion {
function getTypingDataForSingleTypesVersion(
typescriptVersion: TypeScriptVersion | undefined,
packageName: string,
packageDirectory: string,
ls: readonly string[],
fs: FS,
directoryVersion: DirectoryParsedTypingVersion | undefined
directoryVersion: DirectoryParsedTypingVersion | undefined,
moduleResolutionHost: ts.ModuleResolutionHost,
): TypingDataFromIndividualTypeScriptVersion {
const tsconfig = fs.readJson("tsconfig.json") as TsConfig;
const configHost: ts.ParseConfigHost = {
...moduleResolutionHost,
readDirectory: dir => fs.readdir(dir),
useCaseSensitiveFileNames: true,
};
const compilerOptions = ts.parseJsonConfigFileContent(tsconfig, configHost, path.resolve("/", fs.debugPath())).options;
checkFilesFromTsConfig(packageName, tsconfig, fs.debugPath());
const { types, tests, hasNonRelativeImports } = allReferencedFiles(
tsconfig.files!,
fs,
packageName,
packageDirectory
moduleResolutionHost,
compilerOptions,
);
const usedFiles = new Set([...types.keys(), ...tests.keys(), "tsconfig.json", "tslint.json"]);
const otherFiles = ls.includes(unusedFilesName)
? fs
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
.readFile(unusedFilesName)
.split(/\r?\n/g)
.filter(Boolean)
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
.readFile(unusedFilesName)
.split(/\r?\n/g)
.filter(Boolean)
: [];
if (ls.includes(unusedFilesName) && !otherFiles.length) {
throw new Error(`In ${packageName}: OTHER_FILES.txt is empty.`);
@@ -336,7 +352,11 @@ function getTypingDataForSingleTypesVersion(
)) {
// add d.ts files from OTHER_FILES.txt in order get their dependencies
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
types.set(untestedTypeFile, createSourceFile(untestedTypeFile, fs.readFile(untestedTypeFile)));
types.set(untestedTypeFile, createSourceFile(
untestedTypeFile,
fs.readFile(untestedTypeFile),
moduleResolutionHost,
compilerOptions));
}
const { dependencies: dependenciesWithDeclaredModules, globals, declaredModules } = getModuleInfo(packageName, types);
@@ -348,7 +368,7 @@ function getTypingDataForSingleTypesVersion(
.map((m) => rootName(m, types, packageName))
.filter((dependency) => dependency !== packageName)
);
const testDependencies = [...getTestDependencies(packageName, tests.keys(), dependenciesSet, fs)]
const testDependencies = [...getTestDependencies(packageName, tests.keys(), dependenciesSet, fs, moduleResolutionHost, compilerOptions)]
.filter((m) => !declaredModulesSet.has(m))
.map((m) => rootName(m, types, packageName))
.filter((dependency) => dependency !== packageName);
@@ -514,7 +534,7 @@ Other d.ts files must either be referenced through index.d.ts, tests, or added t
file.endsWith(".ts") || file.endsWith(".tsx")
? `Expected file '${file}' to be named '${expectedName}' or to be inside a '${directoryPath}/test/' directory`
: `Unexpected file extension for '${file}' -- expected '.ts' or '.tsx' (maybe this should not be in "files", but ` +
"OTHER_FILES.txt)";
"OTHER_FILES.txt)";
throw new Error(message);
}
}
@@ -773,9 +793,12 @@ function checkAllUsedRecur(ls: Iterable<string>, usedFiles: Set<string>, unusedF
for (const unusedFile of unusedFiles) {
if (usedFiles.has(unusedFile)) {
throw new Error(
`File ${fs.debugPath()}/${unusedFile} listed in ${unusedFilesName} is already reachable from tsconfig.json.`
);
// TODO: reenable after fixing up DT
console.log(`File ${fs.debugPath()}/${unusedFile} listed in ${unusedFilesName} is already reachable from tsconfig.json.`);
return;
// throw new Error(
// `File ${fs.debugPath()}/${unusedFile} listed in ${unusedFilesName} is already reachable from tsconfig.json.`
// );
}
throw new Error(`File ${fs.debugPath()}/${unusedFile} listed in ${unusedFilesName} does not exist.`);
}

View File

@@ -1,7 +1,7 @@
import assert = require("assert");
import * as path from "path";
import * as ts from "typescript";
import { sort, joinPaths, FS, normalizeSlashes, hasWindowsSlashes } from "@definitelytyped/utils";
import { sort, joinPaths, FS, hasWindowsSlashes, assertDefined } from "@definitelytyped/utils";
import { readFileAndThrowOnBOM } from "./definition-parser";
import { getMangledNameForScopedPackage } from "../packages";
@@ -19,7 +19,7 @@ export function getModuleInfo(packageName: string, all: Map<string, ts.SourceFil
for (const sourceFile of all.values()) {
for (const ref of imports(sourceFile)) {
addDependency(ref);
addDependency(ref.text);
}
for (const ref of sourceFile.typeReferenceDirectives) {
addDependency(ref.fileName);
@@ -130,72 +130,97 @@ export function allReferencedFiles(
entryFilenames: readonly string[],
fs: FS,
packageName: string,
baseDirectory: string
moduleResolutionHost: ts.ModuleResolutionHost,
compilerOptions: ts.CompilerOptions,
): { types: Map<string, ts.SourceFile>; tests: Map<string, ts.SourceFile>; hasNonRelativeImports: boolean } {
const seenReferences = new Set<string>();
const types = new Map<string, ts.SourceFile>();
const tests = new Map<string, ts.SourceFile>();
// The directory where the tsconfig/index.d.ts is - i.e., may be a version within the package
const baseDirectory = path.resolve("/", fs.debugPath());
// The root of the package and all versions, i.e., the direct subdirectory of types/
const packageDirectory = baseDirectory.slice(0, baseDirectory.lastIndexOf(`types/${getMangledNameForScopedPackage(packageName)}`) + `types/${getMangledNameForScopedPackage(packageName)}`.length);
let hasNonRelativeImports = false;
entryFilenames.forEach((text) => recur({ text, exact: true }));
entryFilenames.forEach((fileName) => recur(undefined, { text: fileName, kind: "path" }));
return { types, tests, hasNonRelativeImports };
function recur({ text, exact }: Reference): void {
const resolvedFilename = exact ? text : resolveModule(text, fs);
if (seenReferences.has(resolvedFilename)) {
function recur(containingFileName: string | undefined, ref: Reference): void {
// An absolute file name for use with TS resolution, e.g. '/DefinitelyTyped/types/foo/index.d.ts'
const resolvedFileName = resolveReference(containingFileName, ref);
if (!resolvedFileName) {
return;
}
seenReferences.add(resolvedFilename);
if (seenReferences.has(resolvedFileName)) {
return;
}
seenReferences.add(resolvedFileName);
// E.g. 'index.d.ts' - suitable for lookups in `fs` and for our result
const relativeFileName = path.relative(baseDirectory, resolvedFileName);
if (path.relative(packageDirectory, resolvedFileName).startsWith("..")) {
throw new Error(
`${containingFileName ?? "tsconfig.json"}: ` +
'Definitions must use global references to other packages, not parent ("../xxx") references.' +
`(Based on reference '${ref.text}')`
);
}
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
if (fs.exists(resolvedFilename)) {
const src = createSourceFile(resolvedFilename, readFileAndThrowOnBOM(resolvedFilename, fs));
if (fs.exists(relativeFileName)) {
const src = createSourceFile(resolvedFileName, readFileAndThrowOnBOM(relativeFileName, fs), moduleResolutionHost, compilerOptions);
if (
resolvedFilename.endsWith(".d.ts") ||
resolvedFilename.endsWith(".d.mts") ||
resolvedFilename.endsWith(".d.cts")
relativeFileName.endsWith(".d.ts") ||
relativeFileName.endsWith(".d.mts") ||
relativeFileName.endsWith(".d.cts")
) {
types.set(resolvedFilename, src);
types.set(relativeFileName, src);
} else {
tests.set(resolvedFilename, src);
tests.set(relativeFileName, src);
}
const { refs, hasNonRelativeImports: result } = findReferencedFiles(
src,
packageName,
path.dirname(resolvedFilename),
normalizeSlashes(path.relative(baseDirectory, fs.debugPath()))
);
refs.forEach(recur);
const { refs, hasNonRelativeImports: result } = findReferencedFiles(src, packageName);
refs.forEach(ref => recur(resolvedFileName, ref));
hasNonRelativeImports = hasNonRelativeImports || result;
}
}
}
function resolveModule(importSpecifier: string, fs: FS): string {
importSpecifier = importSpecifier.endsWith("/")
? importSpecifier.slice(0, importSpecifier.length - 1)
: importSpecifier;
if (importSpecifier !== "." && importSpecifier !== "..") {
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
if (fs.exists(importSpecifier + ".d.ts")) {
return importSpecifier + ".d.ts";
}
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
if (fs.exists(importSpecifier + ".ts")) {
return importSpecifier + ".ts";
}
// tslint:disable-next-line:non-literal-fs-path -- Not a reference to the fs package
if (fs.exists(importSpecifier + ".tsx")) {
return importSpecifier + ".tsx";
function resolveReference(containingFileName: string | undefined, { kind, text, resolutionMode }: Reference): string | undefined {
switch (kind) {
case "path":
if (containingFileName) {
return ts.resolveTripleslashReference(text, containingFileName);
} else {
return path.resolve(baseDirectory, text);
}
case "types":
return ts.resolveTypeReferenceDirective(
text,
assertDefined(containingFileName, "Must have a containing file to resolve a type reference directive"),
compilerOptions,
moduleResolutionHost,
/*redirectedReference*/ undefined,
/*cache*/ undefined,
resolutionMode).resolvedTypeReferenceDirective?.resolvedFileName;
case "import":
return ts.resolveModuleName(
text,
assertDefined(containingFileName, "Must have an containing file to resolve an import"),
compilerOptions,
moduleResolutionHost,
/*cache*/ undefined,
/*redirectedReference*/ undefined,
resolutionMode
).resolvedModule?.resolvedFileName;
}
}
return importSpecifier === "." ? "index.d.ts" : joinPaths(importSpecifier, "index.d.ts");
}
interface Reference {
/** <reference path> includes exact filename, so true. import "foo" may reference "foo.d.ts" or "foo/index.d.ts", so false. */
readonly exact: boolean;
kind: "path" | "import" | "types";
text: string;
resolutionMode?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS;
}
/**
@@ -203,76 +228,40 @@ interface Reference {
* For example, `baseDirectory` may be `react-router` and `subDirectory` may be `react-router/lib`.
* versionsBaseDirectory may be "" when not in typesVersions or ".." when inside `react-router/ts3.1`
*/
function findReferencedFiles(src: ts.SourceFile, packageName: string, subDirectory: string, baseDirectory: string) {
function findReferencedFiles(src: ts.SourceFile, packageName: string) {
const refs: Reference[] = [];
let hasNonRelativeImports = false;
for (const ref of src.referencedFiles) {
// Any <reference path="foo"> is assumed to be local
addReference({ text: ref.fileName, exact: true });
refs.push({
text: ref.fileName,
kind: "path",
resolutionMode: ref.resolutionMode,
});
}
for (const ref of src.typeReferenceDirectives) {
// only <reference types="../packagename/x" /> references are local (or "packagename/x", though in 3.7 that doesn't work in DT).
if (ref.fileName.startsWith("../" + packageName + "/")) {
addReference({ text: ref.fileName, exact: false });
} else if (ref.fileName.startsWith(packageName + "/")) {
addReference({ text: convertToRelativeReference(ref.fileName), exact: false });
if (ref.fileName.startsWith("../" + packageName + "/") || ref.fileName.startsWith(packageName + "/")) {
refs.push({ kind: "types", text: ref.fileName, resolutionMode: ref.resolutionMode });
}
}
for (const ref of imports(src)) {
if (ref.startsWith(".")) {
addReference({ text: ref, exact: false });
} else if (getMangledNameForScopedPackage(ref).startsWith(packageName + "/")) {
addReference({ text: convertToRelativeReference(ref), exact: false });
hasNonRelativeImports = true;
const resolutionMode = ts.getModeForUsageLocation(src, ref);
if (ref.text.startsWith(".") || getMangledNameForScopedPackage(ref.text).startsWith(packageName + "/")) {
refs.push({ kind: "import", text: ref.text, resolutionMode });
hasNonRelativeImports = !ref.text.startsWith(".");
}
}
return { refs, hasNonRelativeImports };
function addReference(ref: Reference): void {
// `path.normalize` may add windows slashes
let full = normalizeSlashes(
path.normalize(joinPaths(subDirectory, assertNoWindowsSlashes(src.fileName, ref.text)))
);
// allow files in typesVersions directories (i.e. 'ts3.1') to reference files in parent directory
if (full.startsWith("../" + packageName + "/")) {
full = full.slice(packageName.length + 4);
} else if (baseDirectory && full.startsWith("../" + baseDirectory + "/")) {
full = full.slice(baseDirectory.length + 4);
} else if (
full.startsWith("..") &&
(baseDirectory === "" || path.normalize(joinPaths(baseDirectory, full)).startsWith(".."))
) {
throw new Error(
`${src.fileName}: ` +
'Definitions must use global references to other packages, not parent ("../xxx") references.' +
`(Based on reference '${ref.text}')`
);
}
ref.text = full;
refs.push(ref);
}
/** boring/foo -> ./foo when subDirectory === '.'; ../foo when it's === 'x'; ../../foo when it's 'x/y' */
function convertToRelativeReference(name: string) {
let relative = ".";
if (subDirectory !== ".") {
relative += "/..".repeat(subDirectory.split("/").length);
if (baseDirectory && subDirectory.startsWith("..")) {
relative = relative.slice(0, -2) + baseDirectory;
}
}
return relative + name.slice(packageName.length);
}
}
/**
* All strings referenced in `import` statements.
* Does *not* include <reference> directives.
*/
function imports({ statements }: ts.SourceFile): Iterable<string> {
const result: string[] = [];
function imports({ statements }: ts.SourceFile): Iterable<ts.StringLiteralLike> {
const result: ts.StringLiteralLike[] = [];
for (const node of statements) {
recur(node);
}
@@ -284,7 +273,7 @@ function imports({ statements }: ts.SourceFile): Iterable<string> {
case ts.SyntaxKind.ExportDeclaration: {
const { moduleSpecifier } = node as ts.ImportDeclaration | ts.ExportDeclaration;
if (moduleSpecifier && moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) {
result.push((moduleSpecifier as ts.StringLiteral).text);
result.push((moduleSpecifier as ts.StringLiteral));
}
break;
}
@@ -300,7 +289,7 @@ function imports({ statements }: ts.SourceFile): Iterable<string> {
case ts.SyntaxKind.ImportType: {
const { argument } = node as ts.ImportTypeNode;
if (ts.isLiteralTypeNode(argument) && ts.isStringLiteral(argument.literal)) {
result.push(argument.literal.text);
result.push(argument.literal);
}
break;
}
@@ -311,12 +300,12 @@ function imports({ statements }: ts.SourceFile): Iterable<string> {
}
}
function parseRequire(reference: ts.ExternalModuleReference): string {
function parseRequire(reference: ts.ExternalModuleReference): ts.StringLiteralLike {
const { expression } = reference;
if (!expression || !ts.isStringLiteral(expression)) {
throw new Error(`Bad 'import =' reference: ${reference.getText()}`);
}
return expression.text;
return expression;
}
function isValueNamespace(ns: ts.ModuleDeclaration): boolean {
@@ -360,12 +349,14 @@ export function getTestDependencies(
packageName: string,
testFiles: Iterable<string>,
dependencies: ReadonlySet<string>,
fs: FS
fs: FS,
moduleResolutionHost: ts.ModuleResolutionHost,
compilerOptions: ts.CompilerOptions,
): Iterable<string> {
const testDependencies = new Set<string>();
for (const filename of testFiles) {
const content = readFileAndThrowOnBOM(filename, fs);
const sourceFile = createSourceFile(filename, content);
const sourceFile = createSourceFile(filename, content, moduleResolutionHost, compilerOptions);
const { fileName, referencedFiles, typeReferenceDirectives } = sourceFile;
const filePath = () => path.join(packageName, fileName);
let hasImports = false;
@@ -388,8 +379,8 @@ export function getTestDependencies(
}
for (const imported of imports(sourceFile)) {
hasImports = true;
if (!imported.startsWith(".") && !dependencies.has(imported)) {
testDependencies.add(imported);
if (!imported.text.startsWith(".") && !dependencies.has(imported.text)) {
testDependencies.add(imported.text);
}
}
@@ -413,6 +404,8 @@ export function getTestDependencies(
return testDependencies;
}
export function createSourceFile(filename: string, content: string): ts.SourceFile {
return ts.createSourceFile(filename, content, ts.ScriptTarget.Latest, /*setParentNodes*/ false);
export function createSourceFile(filename: string, content: string, moduleResolutionHost: ts.ModuleResolutionHost, compilerOptions: ts.CompilerOptions): ts.SourceFile {
const file = ts.createSourceFile(filename, content, ts.ScriptTarget.Latest, /*setParentNodes*/ false);
file.impliedNodeFormat = ts.getImpliedNodeFormatForFile(filename as ts.Path, /*packageJsonInfoCache*/ undefined, moduleResolutionHost, compilerOptions);
return file;
}

View File

@@ -2,7 +2,7 @@ import { parseHeaderOrFail } from "@definitelytyped/header-parser";
import { Dir, FS, InMemoryFS, mangleScopedPackage } from "@definitelytyped/utils";
import * as semver from "semver";
class DTMock {
export class DTMock {
public readonly fs: FS;
private readonly root: Dir;
@@ -19,7 +19,7 @@ class DTMock {
},
})
);
this.fs = new InMemoryFS(this.root, "DefinitelyTyped");
this.fs = new InMemoryFS(this.root, "/DefinitelyTyped/");
}
public pkgDir(packageName: string): Dir {
@@ -172,12 +172,7 @@ untested.d.ts
declare var x: number
`
);
globby.set(
"merges.d.ts",
`
declare var y: number
`
);
globby.set(
"sneaky.d.ts",
`
@@ -194,7 +189,6 @@ var z = x;
tests.set(
"other-tests.ts",
`
/// <reference types="globby/merges" />
var z = y;
`
);

View File

@@ -40,7 +40,7 @@ export async function parseDefinitions(
} else {
log.info("Parsing in main process...");
for (const packageName of packageNames) {
typings[packageName] = await getTypingInfo(packageName, typesFS.subDir(packageName));
typings[packageName] = await getTypingInfo(packageName, dt);
}
}
log.info("Parsing took " + (Date.now() - start) / 1000 + " s");

View File

@@ -1,3 +1,4 @@
import path from "path";
import { DiskFS } from "@definitelytyped/utils";
import { createMockDT } from "../src/mocks";
import { getTypingInfo } from "../src/lib/definition-parser";
@@ -7,14 +8,14 @@ describe(getTypingInfo, () => {
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "1.42");
dt.addOldVersionOfPackage("jquery", "2");
const info = await getTypingInfo("jquery", dt.pkgFS("jquery"));
const info = await getTypingInfo("jquery", dt.fs);
expect(Object.keys(info).sort()).toEqual(["1.42", "2.0", "3.3"]);
});
it("works for a package with dependencies", async () => {
const dt = createMockDT();
const info = await getTypingInfo("has-dependency", dt.pkgFS("has-dependency"));
const info = await getTypingInfo("has-dependency", dt.fs);
expect(info).toBeDefined();
});
@@ -65,7 +66,7 @@ export function myFunction(arg:string): string;
dt.addOldVersionOfPackage("@ckeditor/ckeditor5-utils", "10");
const info = await getTypingInfo("@ckeditor/ckeditor5-engine", dt.pkgFS("ckeditor__ckeditor5-engine"));
const info = await getTypingInfo("@ckeditor/ckeditor5-engine", dt.fs);
expect(info).toBeDefined();
});
@@ -124,7 +125,7 @@ export * from 'buffer';
} `
);
const info = await getTypingInfo("safer", dt.pkgFS("safer"));
const info = await getTypingInfo("safer", dt.fs);
expect(info).toBeDefined();
expect(info["1.0"].dependencies).toEqual({ node: "*" });
});
@@ -189,7 +190,7 @@ const a = new webpack.AutomaticPrefetchPlugin();
}`
);
const info = await getTypingInfo("webpack", dt.pkgFS("webpack"));
const info = await getTypingInfo("webpack", dt.fs);
expect(info).toBeDefined();
});
@@ -198,7 +199,7 @@ const a = new webpack.AutomaticPrefetchPlugin();
getTypingInfo(
"typeref-fails",
new DiskFS(
"packages/definitions-parser/test/fixtures/rejects-references-to-old-versions-of-other-types-packages/"
path.resolve(__dirname, "fixtures/rejects-references-to-old-versions-of-other-types-packages/")
)
)
).rejects.toThrow("do not directly import specific versions of another types package");
@@ -207,7 +208,7 @@ const a = new webpack.AutomaticPrefetchPlugin();
it("allows references to old versions of self", async () => {
const info = await getTypingInfo(
"fail",
new DiskFS("packages/definitions-parser/test/fixtures/allows-references-to-old-versions-of-self/")
new DiskFS(path.resolve(__dirname, "fixtures/allows-references-to-old-versions-of-self/"))
);
expect(info).toBeDefined();
});
@@ -261,7 +262,7 @@ import route = require('@ember/routing/route');
}`
);
const info = await getTypingInfo("ember", dt.pkgFS("ember"));
const info = await getTypingInfo("ember", dt.fs);
expect(info["2.8"].testDependencies).toEqual([]);
});
@@ -269,18 +270,27 @@ import route = require('@ember/routing/route');
const info = await getTypingInfo(
"styled-components-react-native",
new DiskFS(
"packages/definitions-parser/test/fixtures/doesnt-omit-dependencies-if-only-some-deep-modules-are-declared/"
path.resolve(__dirname, "fixtures/doesnt-omit-dependencies-if-only-some-deep-modules-are-declared/")
)
);
expect(info["5.1"].dependencies).toEqual({ "styled-components": "*" });
});
it("rejects relative references to other packages", async () => {
expect(() => getTypingInfo(
"referencing",
new DiskFS(
path.resolve(__dirname, "fixtures/rejects-relative-references-to-other-packages/")
)
)).rejects.toThrow("Definitions must use global references to other packages");
});
describe("concerning multiple versions", () => {
it("records what the version directory looks like on disk", async () => {
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "2");
dt.addOldVersionOfPackage("jquery", "1.5");
const info = await getTypingInfo("jquery", dt.pkgFS("jquery"));
const info = await getTypingInfo("jquery", dt.fs);
expect(info).toEqual({
"1.5": expect.objectContaining({
@@ -300,7 +310,7 @@ import route = require('@ember/routing/route');
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "2");
dt.addOldVersionOfPackage("jquery", "1.5");
const info = await getTypingInfo("jquery", dt.pkgFS("jquery"));
const info = await getTypingInfo("jquery", dt.fs);
expect(info).toEqual({
"1.5": expect.objectContaining({
@@ -343,7 +353,7 @@ import route = require('@ember/routing/route');
dt.addOldVersionOfPackage("@ckeditor/ckeditor5-utils", "10");
const info = await getTypingInfo("@ckeditor/ckeditor5-utils", dt.pkgFS("ckeditor__ckeditor5-utils"));
const info = await getTypingInfo("@ckeditor/ckeditor5-utils", dt.fs);
expect(info).toEqual({
"10.0": expect.objectContaining({
pathMappings: {
@@ -362,7 +372,7 @@ import route = require('@ember/routing/route');
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "3");
return expect(getTypingInfo("jquery", dt.pkgFS("jquery"))).rejects.toThrow(
return expect(getTypingInfo("jquery", dt.fs)).rejects.toThrow(
"The latest version of the 'jquery' package is 3.3, so the subdirectory 'v3' is not allowed; " +
"since it applies to any 3.* version, up to and including 3.3."
);
@@ -372,7 +382,7 @@ import route = require('@ember/routing/route');
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "3.3");
return expect(getTypingInfo("jquery", dt.pkgFS("jquery"))).rejects.toThrow(
return expect(getTypingInfo("jquery", dt.fs)).rejects.toThrow(
"The latest version of the 'jquery' package is 3.3, so the subdirectory 'v3.3' is not allowed."
);
});
@@ -381,7 +391,7 @@ import route = require('@ember/routing/route');
const dt = createMockDT();
dt.addOldVersionOfPackage("jquery", "3.0");
return expect(getTypingInfo("jquery", dt.pkgFS("jquery"))).resolves.toBeDefined();
return expect(getTypingInfo("jquery", dt.fs)).resolves.toBeDefined();
});
it("checks that older versions with non-relative imports have wildcard path mappings", () => {
@@ -393,7 +403,7 @@ import route = require('@ember/routing/route');
`
);
dt.addOldVersionOfPackage("jquery", "1");
return expect(getTypingInfo("jquery", dt.pkgFS("jquery"))).rejects.toThrow(
return expect(getTypingInfo("jquery", dt.fs)).rejects.toThrow(
'jquery: Older version 1 must have a "paths" entry of "jquery/*": ["jquery/v1/*"]'
);
});
@@ -423,7 +433,7 @@ import first from "@ckeditor/ckeditor5-utils/src/first";
dt.addOldVersionOfPackage("@ckeditor/ckeditor5-utils", "10");
return expect(
getTypingInfo("ckeditor__ckeditor5-utils", dt.pkgFS("ckeditor__ckeditor5-utils"))
getTypingInfo("ckeditor__ckeditor5-utils", dt.fs)
).rejects.toThrow(
'@ckeditor/ckeditor5-utils: Older version 10 must have a "paths" entry of "@ckeditor/ckeditor5-utils/*": ["ckeditor__ckeditor5-utils/v10/*"]'
);
@@ -433,6 +443,6 @@ import first from "@ckeditor/ckeditor5-utils/src/first";
it("allows wildcard scope path mappings", () => {
const dt = createMockDT();
return expect(getTypingInfo("wordpress__plugins", dt.pkgFS("wordpress__plugins"))).resolves.toBeDefined();
return expect(getTypingInfo("wordpress__plugins", dt.fs)).resolves.toBeDefined();
});
});

View File

@@ -0,0 +1,6 @@
// Type definitions for referenced 1.0
// Project: https://youtube.com/s-fails
// Definitions by: Type Ref Fails <https://github.com/typeref-fails>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export interface Referenced {}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": ["../"],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": ["index.d.ts"]
}

View File

@@ -0,0 +1,8 @@
// Type definitions for referencing 1.0
// Project: https://youtube.com/s-fails
// Definitions by: Type Ref Fails <https://github.com/typeref-fails>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
import { Referenced } from "../referenced";
export interface Referencing extends Referenced {}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": ["../"],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": ["index.d.ts"]
}

View File

@@ -26,7 +26,7 @@ testo({
root.set("file1.txt", "ok");
const dir = root.subdir("sub1");
dir.set("file2.txt", "x");
const fs: FS = new InMemoryFS(root, "test/");
const fs: FS = new InMemoryFS(root, "/test/");
expect(fs.exists("file1.txt")).toBe(true);
expect(fs.readFile("file1.txt")).toBe("ok");
expect(fs.readFile("sub1/file2.txt")).toBe("x");

View File

@@ -1,16 +1,20 @@
import * as ts from "typescript";
import { Dir, InMemoryFS } from "@definitelytyped/utils";
import { createMockDT } from "../src/mocks";
import { createModuleResolutionHost } from "@definitelytyped/utils";
import { DTMock, createMockDT } from "../src/mocks";
import { testo } from "./utils";
import { allReferencedFiles, getModuleInfo, getTestDependencies } from "../src/lib/module-info";
const fs = createMockDT().fs;
const moduleResolutionHost = createModuleResolutionHost(fs);
const compilerOptions = { module: ts.ModuleKind.CommonJS, baseUrl: "/DefinitelyTyped/types", typeRoots: ["/DefinitelyTyped/types"] };
function getBoringReferences() {
return allReferencedFiles(
["index.d.ts", "boring-tests.ts"],
fs.subDir("types").subDir("boring"),
"boring",
"types/boring"
moduleResolutionHost,
compilerOptions
);
}
testo({
@@ -31,7 +35,8 @@ testo({
["boring-tests.ts"],
fs.subDir("types").subDir("boring"),
"boring",
"types/boring"
moduleResolutionHost,
compilerOptions
);
expect(Array.from(types.keys())).toEqual([
"secondary.d.ts",
@@ -47,7 +52,8 @@ testo({
["jquery-tests.ts", "index.d.ts"],
fs.subDir("types").subDir("jquery"),
"jquery",
"types/jquery"
moduleResolutionHost,
compilerOptions
);
expect(Array.from(types.keys())).toEqual(["index.d.ts", "JQuery.d.ts"]);
expect(Array.from(tests.keys())).toEqual(["jquery-tests.ts"]);
@@ -57,21 +63,22 @@ testo({
["globby-tests.ts", "test/other-tests.ts"],
fs.subDir("types").subDir("globby"),
"globby",
"types/globby"
moduleResolutionHost,
compilerOptions
);
expect(Array.from(types.keys())).toEqual(["merges.d.ts"]);
expect(Array.from(types.keys())).toEqual([]);
expect(Array.from(tests.keys())).toEqual(["globby-tests.ts", "test/other-tests.ts"]);
},
allReferencedFilesIncludesTypesImports() {
const pkg = new Dir(undefined);
const dtMock = new DTMock();
const pkg = dtMock.pkgDir("mock");
pkg.set(
"index.d.ts",
`type T = import("./types");
`
);
pkg.set("types.d.ts", "");
const memFS = new InMemoryFS(pkg, "types/mock");
const { types, tests } = allReferencedFiles(["index.d.ts"], memFS, "mock", "types/mock");
const { types, tests } = allReferencedFiles(["index.d.ts"], dtMock.fs.subDir("types/mock"), "mock", createModuleResolutionHost(dtMock.fs), compilerOptions);
expect(Array.from(types.keys())).toEqual(["index.d.ts", "types.d.ts"]);
expect(Array.from(tests.keys())).toEqual([]);
},
@@ -97,32 +104,35 @@ testo({
["index.d.ts", "globby-tests.ts", "test/other-tests.ts"],
fs.subDir("types").subDir("globby"),
"globby",
"types/globby"
moduleResolutionHost,
compilerOptions
);
expect(Array.from(types.keys())).toEqual(["index.d.ts", "sneaky.d.ts", "merges.d.ts"]);
expect(Array.from(types.keys())).toEqual(["index.d.ts", "sneaky.d.ts"]);
const i = getModuleInfo("globby", types);
expect(i.dependencies).toEqual(new Set(["andere/snee"]));
},
selfInScopedPackage() {
const scoped = new Dir(undefined);
const dtMock = new DTMock();
const scoped = dtMock.pkgDir("rdfjs__to-ntriples");
scoped.set(
"index.d.ts",
`import "@rdfjs/to-ntriples/component";
`
);
scoped.set("component.d.ts", "");
const memFS = new InMemoryFS(scoped, "types/rdfjs__to-ntriples");
const { types, tests } = allReferencedFiles(
["index.d.ts"],
memFS,
dtMock.fs.subDir("types/rdfjs__to-ntriples"),
"rdfjs__to-ntriples",
"types/rdfjs__to-ntriples"
createModuleResolutionHost(dtMock.fs),
{ ...compilerOptions, paths: { "@rdfjs/to-ntriples/*": ["rdfjs__to-ntriples/*"] } }
);
expect(Array.from(types.keys())).toEqual(["index.d.ts", "component.d.ts"]);
expect(Array.from(tests.keys())).toEqual([]);
},
selfInTypesVersionsParent() {
const pkg = new Dir(undefined);
const dtMock = new DTMock();
const pkg = dtMock.pkgDir("mock");
const ts20 = pkg.subdir("ts2.0");
ts20.set(
"index.d.ts",
@@ -136,15 +146,20 @@ testo({
`import "mock/component";
`
);
const memFS = new InMemoryFS(ts20, "types/mock/ts2.0");
const { types, tests } = allReferencedFiles(["index.d.ts"], memFS, "mock", "types/mock");
const { types, tests } = allReferencedFiles(
["index.d.ts"],
dtMock.fs.subDir("types/mock/ts2.0"),
"mock",
createModuleResolutionHost(dtMock.fs),
{ ...compilerOptions, paths: { "mock/*": ["mock/ts2.0/*"] } });
expect(Array.from(types.keys())).toEqual(["index.d.ts", "../ts1.0/index.d.ts", "component.d.ts"]);
expect(Array.from(tests.keys())).toEqual([]);
},
getTestDependenciesWorks() {
const { types, tests } = getBoringReferences();
const i = getModuleInfo("boring", types);
const d = getTestDependencies("boring", tests.keys(), i.dependencies, fs.subDir("types").subDir("boring"));
const d = getTestDependencies("boring", tests.keys(), i.dependencies, fs.subDir("types").subDir("boring"), moduleResolutionHost, compilerOptions);
expect(d).toEqual(new Set(["boring", "boring/commonjs", "boring/secondary", "boring/v1", "super-big-fun-hus"]));
},
});

View File

@@ -66,7 +66,7 @@ describe(TypingsVersions, () => {
dt.addOldVersionOfPackage("jquery", "1");
dt.addOldVersionOfPackage("jquery", "2");
dt.addOldVersionOfPackage("jquery", "2.5");
versions = new TypingsVersions(await getTypingInfo("jquery", dt.pkgFS("jquery")));
versions = new TypingsVersions(await getTypingInfo("jquery", dt.fs));
});
it("sorts the data from latest to oldest version", () => {

View File

@@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"types": ["node"],
},
"include": ["src"],
"references": [

View File

@@ -14,7 +14,7 @@ export interface TesterOptions extends ParseDefinitionsOptions {
export const defaultLocalOptions: TesterOptions = {
definitelyTypedPath: "../../../DefinitelyTyped",
progress: true,
parseInParallel: true,
parseInParallel: !process.env.VSCODE_INSPECTOR_OPTIONS,
};
export const defaultRemoteOptions: ParseDefinitionsOptions = {

View File

@@ -39,7 +39,7 @@ if (!module.parent) {
}
async function single(singleName: string, dt: FS): Promise<void> {
const data = await getTypingInfo(singleName, dt.subDir("types").subDir(singleName));
const data = await getTypingInfo(singleName, dt);
const typings = { [singleName]: data };
await writeDataFile(typesDataFilename, typings);
console.log(JSON.stringify(data, undefined, 4));

View File

@@ -60,7 +60,7 @@ function defaultFS(): FS {
`
);
pkg.set("jquery.test.ts", "// tests");
const memFS = new InMemoryFS(pkg, "types/mock");
const memFS = new InMemoryFS(pkg, "/types/mock/");
return memFS;
}

View File

@@ -1,4 +1,5 @@
import assert from "assert";
import { relative, resolve } from "path";
import { assertDefined } from "./assertions";
import { pathExistsSync, readdirSync, statSync } from "fs-extra";
import { readFileSync, readJsonSync } from "./io";
@@ -37,6 +38,18 @@ export interface FS {
debugPath(): string;
}
export function createModuleResolutionHost(fs: FS): import("typescript").ModuleResolutionHost {
return {
fileExists: (filename) => fs.exists(filename),
readFile: (filename) => fs.readFile(filename),
directoryExists: (directoryName) => fs.exists(directoryName),
getCurrentDirectory: () => "",
realpath: (path) => path,
useCaseSensitiveFileNames: () => true,
};
}
interface ReadonlyDir extends ReadonlyMap<string, ReadonlyDir | string> {
readonly parent: Dir | undefined;
}
@@ -70,15 +83,28 @@ export class Dir extends Map<string, Dir | string> implements ReadonlyDir {
}
}
function ensureTrailingSlash(dir: string) {
return dir.endsWith("/") ? dir : dir + "/";
}
export class InMemoryFS implements FS {
/** pathToRoot is just for debugging */
constructor(readonly curDir: ReadonlyDir, readonly pathToRoot: string) {}
constructor(readonly curDir: ReadonlyDir, readonly rootPrefix: string) {
this.rootPrefix = ensureTrailingSlash(rootPrefix);
assert(rootPrefix[0] === "/", `rootPrefix must be absolute: ${rootPrefix}`);
}
private tryGetEntry(path: string): ReadonlyDir | string | undefined {
validatePath(path);
if (path[0] === "/") {
path = relative(this.rootPrefix, path);
}
if (path === "") {
return this.curDir;
}
const needsDir = path.endsWith("/");
if (needsDir) {
path = path.slice(0, -1);
}
const components = path.split("/");
const baseName = assertDefined(components.pop());
let dir = this.curDir;
@@ -89,18 +115,19 @@ export class InMemoryFS implements FS {
}
if (!(entry instanceof Dir)) {
throw new Error(
`No file system entry at ${this.pathToRoot}/${path}. Siblings are: ${Array.from(dir.keys()).toString()}`
`No file system entry at ${this.rootPrefix}/${path}. Siblings are: ${Array.from(dir.keys()).toString()}`
);
}
dir = entry;
}
return dir.get(baseName);
const res = dir.get(baseName);
return needsDir ? (res instanceof Dir ? res : undefined) : res;
}
private getEntry(path: string): ReadonlyDir | string {
const entry = this.tryGetEntry(path);
if (entry === undefined) {
throw new Error(`No file system entry at ${this.pathToRoot}/${path}`);
throw new Error(`No file system entry at ${this.rootPrefix}/${path}`);
}
return entry;
}
@@ -108,7 +135,7 @@ export class InMemoryFS implements FS {
private getDir(dirPath: string): Dir {
const res = this.getEntry(dirPath);
if (!(res instanceof Dir)) {
throw new Error(`${this.pathToRoot}/${dirPath} is a file, not a directory.`);
throw new Error(`${this.rootPrefix}/${dirPath} is a file, not a directory.`);
}
return res;
}
@@ -116,7 +143,7 @@ export class InMemoryFS implements FS {
readFile(filePath: string): string {
const res = this.getEntry(filePath);
if (typeof res !== "string") {
throw new Error(`${this.pathToRoot}/${filePath} is a directory, not a file.`);
throw new Error(`${this.rootPrefix}/${filePath} is a directory, not a file.`);
}
return res;
}
@@ -138,25 +165,23 @@ export class InMemoryFS implements FS {
}
subDir(path: string): FS {
return new InMemoryFS(this.getDir(path), joinPaths(this.pathToRoot, path));
assert(path[0] !== "/", "Cannot use absolute paths with InMemoryFS.subDir");
return new InMemoryFS(this.getDir(path), resolve(this.rootPrefix, path));
}
debugPath(): string {
return this.pathToRoot;
return this.rootPrefix;
}
}
export class DiskFS implements FS {
constructor(private readonly rootPrefix: string) {
assert(rootPrefix.endsWith("/"));
assert(rootPrefix.startsWith("/"), "DiskFS must use absolute paths");
this.rootPrefix = ensureTrailingSlash(rootPrefix);
}
private getPath(path: string | undefined): string {
if (path === undefined) {
return this.rootPrefix;
}
validatePath(path);
return this.rootPrefix + path;
return resolve(this.rootPrefix, path ?? "");
}
readdir(dirPath?: string): readonly string[] {
@@ -189,16 +214,3 @@ export class DiskFS implements FS {
return this.rootPrefix.slice(0, this.rootPrefix.length - 1); // remove trailing '/'
}
}
/** FS only handles simple paths like `foo/bar` or `../foo`. No `./foo` or `/foo`. */
function validatePath(path: string): void {
if (path.startsWith(".") && path !== ".editorconfig" && path !== ".eslintrc.json" && !path.startsWith("../")) {
throw new Error(`${path}: filesystem doesn't support paths of the form './x'.`);
}
if (path.startsWith("/")) {
throw new Error(`${path}: filesystem doesn't support paths of the form '/xxx'.`);
}
if (path.endsWith("/")) {
throw new Error(`${path}: filesystem doesn't support paths of the form 'xxx/'.`);
}
}

View File

@@ -237,7 +237,7 @@ export function downloadAndExtractFile(url: string, log: LoggerWithErrors): Prom
extract.on("finish", () => {
log.info("Done receiving " + url);
clearTimeout(timeout);
resolve(new InMemoryFS(root.finish(), ""));
resolve(new InMemoryFS(root.finish(), "/"));
});
response.pipe(zlib.createGunzip()).pipe(extract);