Skip to content
Draft
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
120 changes: 120 additions & 0 deletions packages/dtslint/src/index-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { deepEquals } from "@definitelytyped/utils";
import fs from "fs";
import { basename, dirname, join as joinPaths } from "path";

/** @internal */
export function findDTRoot(dirPath: string) {
let path = dirPath;
while (basename(path) !== "types" && dirname(path) !== "." && dirname(path) !== "/" && dirname(path) !== path) {
path = dirname(path);
}
return dirname(path);
}

/** @internal */
export function assertPathIsInDefinitelyTyped(dirPath: string, dtRoot: string): void {
// TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines (perhaps because DT isn't cloned into DefinitelyTyped)
// Re-enable it later if it makes sense.
// if (basename(dtRoot) !== "DefinitelyTyped")) {
if (!fs.existsSync(joinPaths(dtRoot, "types"))) {
throw new Error(
"Since this type definition includes a header (a comment starting with `// Type definitions for`), " +
"assumed this was a DefinitelyTyped package.\n" +
"But it is not in a `DefinitelyTyped/types/xxx` directory: " +
dirPath,
);
}
}

/**
* Starting at some point in time, npm has banned all new packages whose names
* contain the word `download`. However, some older packages exist that still
* contain this name.
* @NOTE for contributors: The list of literal exceptions below should ONLY be
* extended with packages for which there already exists a corresponding type
* definition package in the `@types` scope. More information:
* https://github.com/microsoft/DefinitelyTyped-tools/pull/381.
* @internal
*/
export function assertPathIsNotBanned(packageName: string) {
if (
/(^|\W)download($|\W)/.test(packageName) &&
packageName !== "download" &&
packageName !== "downloadjs" &&
packageName !== "s3-download-stream"
) {
// Since npm won't release their banned-words list, we'll have to manually add to this list.
throw new Error(`${packageName}: Contains the word 'download', which is banned by npm.`);
}
}

/** @internal */
export function combineErrorsAndWarnings(errors: string[], warnings: string[]): Error | string {
const message = errors.concat(warnings).join("\n\n");
return errors.length ? new Error(message) : message;
}

/** @internal */
export function checkExpectedFiles(dirPath: string, isLatest: boolean): { errors: string[] } {
const errors = [];

if (isLatest) {
const expectedNpmIgnore = ["*", "!**/*.d.ts", "!**/*.d.cts", "!**/*.d.mts", "!**/*.d.*.ts"];

if (basename(dirname(dirPath)) === "types") {
for (const subdir of fs.readdirSync(dirPath, { withFileTypes: true })) {
if (subdir.isDirectory() && /^v(\d+)(\.(\d+))?$/.test(subdir.name)) {
expectedNpmIgnore.push(`/${subdir.name}/`);
}
}
} else {
const thisDir = `/${basename(dirPath)}/`;
const parentNpmIgnorePath = joinPaths(dirname(dirPath), ".npmignore");
if (!fs.existsSync(parentNpmIgnorePath)) {
errors.push(`${dirPath}: Missing parent '.npmignore'`);
} else {
const parentNpmIgnore = tryReadFileSync(parentNpmIgnorePath)?.trim().split(/\r?\n/);
if (!parentNpmIgnore || !parentNpmIgnore.includes(thisDir)) {
errors.push(`${dirPath}: Parent package '.npmignore' should contain ${thisDir}`);
}
}
}

const expectedNpmIgnoreAsString = expectedNpmIgnore.join("\n");
const npmIgnorePath = joinPaths(dirPath, ".npmignore");
if (!fs.existsSync(npmIgnorePath)) {
errors.push(`${dirPath}: Missing '.npmignore'; should contain:\n${expectedNpmIgnoreAsString}`);
}

const actualNpmIgnore = tryReadFileSync(npmIgnorePath)?.trim().split(/\r?\n/);
if (!actualNpmIgnore || !deepEquals(actualNpmIgnore, expectedNpmIgnore)) {
errors.push(`${dirPath}: Incorrect '.npmignore'; should be:\n${expectedNpmIgnoreAsString}`);
}

if (fs.existsSync(joinPaths(dirPath, "OTHER_FILES.txt"))) {
errors.push(
`${dirPath}: Should not contain 'OTHER_FILES.txt'. All files matching "**/*.d.{ts,cts,mts,*.ts}" are automatically included.`,
);
}
}

if (!fs.existsSync(joinPaths(dirPath, "index.d.ts"))) {
errors.push(`${dirPath}: Must contain 'index.d.ts'.`);
}

if (fs.existsSync(joinPaths(dirPath, "tslint.json"))) {
errors.push(
`${dirPath}: Should not contain 'tslint.json'. This file is no longer required; place all lint-related options into .eslintrc.json.`,
);
}

return { errors };
}

function tryReadFileSync(path: string): string | undefined {
try {
return fs.readFileSync(path, "utf-8");
} catch {
return undefined;
}
}
118 changes: 9 additions & 109 deletions packages/dtslint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

import { getTypesVersions } from "@definitelytyped/header-parser";
import { AllTypeScriptVersion, TypeScriptVersion } from "@definitelytyped/typescript-versions";
import { deepEquals, readJson } from "@definitelytyped/utils";
import { readJson } from "@definitelytyped/utils";
import fs from "fs";
import { basename, dirname, join as joinPaths, resolve } from "path";
import { join as joinPaths, resolve } from "path";
import {
checkNpmVersionAndGetMatchingImplementationPackage,
checkPackageJson,
checkTsconfig,
runAreTheTypesWrong,
} from "./checks";
import {
assertPathIsInDefinitelyTyped,
assertPathIsNotBanned,
checkExpectedFiles,
combineErrorsAndWarnings,
findDTRoot,
} from "./index-helpers";
import { TsVersion, lint } from "./lint";
import { getCompilerOptions, packageDirectoryNameWithVersionFromPath, packageNameFromPath } from "./util";
import assert = require("assert");
Expand Down Expand Up @@ -261,10 +268,6 @@ async function runTests(
return result;
}

function combineErrorsAndWarnings(errors: string[], warnings: string[]): Error | string {
const message = errors.concat(warnings).join("\n\n");
return errors.length ? new Error(message) : message;
}

function maxVersion(v1: AllTypeScriptVersion, v2: TypeScriptVersion): TypeScriptVersion {
// Note: For v1 to be later than v2, it must be a current Typescript version. So the type assertion is safe.
Expand Down Expand Up @@ -306,48 +309,6 @@ async function testTypesVersion(
return { errors };
}

function findDTRoot(dirPath: string) {
let path = dirPath;
while (basename(path) !== "types" && dirname(path) !== "." && dirname(path) !== "/") {
path = dirname(path);
}
return dirname(path);
}

function assertPathIsInDefinitelyTyped(dirPath: string, dtRoot: string): void {
// TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines (perhaps because DT isn't cloned into DefinitelyTyped)
// Re-enable it later if it makes sense.
// if (basename(dtRoot) !== "DefinitelyTyped")) {
if (!fs.existsSync(joinPaths(dtRoot, "types"))) {
throw new Error(
"Since this type definition includes a header (a comment starting with `// Type definitions for`), " +
"assumed this was a DefinitelyTyped package.\n" +
"But it is not in a `DefinitelyTyped/types/xxx` directory: " +
dirPath,
);
}
}

/**
* Starting at some point in time, npm has banned all new packages whose names
* contain the word `download`. However, some older packages exist that still
* contain this name.
* @NOTE for contributors: The list of literal exceptions below should ONLY be
* extended with packages for which there already exists a corresponding type
* definition package in the `@types` scope. More information:
* https://github.com/microsoft/DefinitelyTyped-tools/pull/381.
*/
function assertPathIsNotBanned(packageName: string) {
if (
/(^|\W)download($|\W)/.test(packageName) &&
packageName !== "download" &&
packageName !== "downloadjs" &&
packageName !== "s3-download-stream"
) {
// Since npm won't release their banned-words list, we'll have to manually add to this list.
throw new Error(`${packageName}: Contains the word 'download', which is banned by npm.`);
}
}

export function assertPackageIsNotDeprecated(packageName: string, notNeededPackages: string) {
const unneeded = JSON.parse(notNeededPackages).packages;
Expand All @@ -365,66 +326,5 @@ if (require.main === module) {
});
}

function checkExpectedFiles(dirPath: string, isLatest: boolean): { errors: string[] } {
const errors = [];

if (isLatest) {
const expectedNpmIgnore = ["*", "!**/*.d.ts", "!**/*.d.cts", "!**/*.d.mts", "!**/*.d.*.ts"];

if (basename(dirname(dirPath)) === "types") {
for (const subdir of fs.readdirSync(dirPath, { withFileTypes: true })) {
if (subdir.isDirectory() && /^v(\d+)(\.(\d+))?$/.test(subdir.name)) {
expectedNpmIgnore.push(`/${subdir.name}/`);
}
}
} else {
const thisDir = `/${basename(dirPath)}/`;
const parentNpmIgnorePath = joinPaths(dirname(dirPath), ".npmignore");
if (!fs.existsSync(parentNpmIgnorePath)) {
errors.push(`${dirPath}: Missing parent '.npmignore'`);
} else {
const parentNpmIgnore = tryReadFileSync(parentNpmIgnorePath)?.trim().split(/\r?\n/);
if (!parentNpmIgnore || !parentNpmIgnore.includes(thisDir)) {
errors.push(`${dirPath}: Parent package '.npmignore' should contain ${thisDir}`);
}
}
}

const expectedNpmIgnoreAsString = expectedNpmIgnore.join("\n");
const npmIgnorePath = joinPaths(dirPath, ".npmignore");
if (!fs.existsSync(npmIgnorePath)) {
errors.push(`${dirPath}: Missing '.npmignore'; should contain:\n${expectedNpmIgnoreAsString}`);
}

const actualNpmIgnore = tryReadFileSync(npmIgnorePath)?.trim().split(/\r?\n/);
if (!actualNpmIgnore || !deepEquals(actualNpmIgnore, expectedNpmIgnore)) {
errors.push(`${dirPath}: Incorrect '.npmignore'; should be:\n${expectedNpmIgnoreAsString}`);
}

if (fs.existsSync(joinPaths(dirPath, "OTHER_FILES.txt"))) {
errors.push(
`${dirPath}: Should not contain 'OTHER_FILES.txt'. All files matching "**/*.d.{ts,cts,mts,*.ts}" are automatically included.`,
);
}
}

if (!fs.existsSync(joinPaths(dirPath, "index.d.ts"))) {
errors.push(`${dirPath}: Must contain 'index.d.ts'.`);
}

if (fs.existsSync(joinPaths(dirPath, "tslint.json"))) {
errors.push(
`${dirPath}: Should not contain 'tslint.json'. This file is no longer required; place all lint-related options into .eslintrc.json.`,
);
}

return { errors };
}

function tryReadFileSync(path: string): string | undefined {
try {
return fs.readFileSync(path, "utf-8");
} catch {
return undefined;
}
}
18 changes: 12 additions & 6 deletions packages/dtslint/src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,31 +136,36 @@ export function isExternalDependency(file: TsType.SourceFile, dirPath: string, p
return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
}

function normalizePath(file: string) {
/** @internal */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@internal doesn't actually mean anything in Typescript. It's a tag used by the typescript team to build the typescript compiler but nowhere else. It doesn't mean anything more than "please don't use this" here, which I guess is O.K., but still a little misleading.

export function normalizePath(file: string) {
// replaces '\' with '/' and forces all DOS drive letters to be upper-case
return normalize(file)
.replace(/\\/g, "/")
.replace(/^[a-z](?=:)/, (c) => c.toUpperCase());
}

function isTypesVersionPath(fileName: string, dirPath: string) {
/** @internal */
export function isTypesVersionPath(fileName: string, dirPath: string) {
const normalFileName = normalizePath(fileName);
const normalDirPath = normalizePath(dirPath);
const subdirPath = withoutStart(normalFileName, normalDirPath);
return subdirPath && /^\/ts\d+\.\d/.test(subdirPath);
}

function startsWithDirectory(filePath: string, dirPath: string): boolean {
/** @internal */
export function startsWithDirectory(filePath: string, dirPath: string): boolean {
const normalFilePath = normalizePath(filePath);
const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
}

interface Err {
/** @internal */
export interface Err {
pos: number;
message: string;
}
function testNoLintDisables(text: string): Err | undefined {
/** @internal */
export function testNoLintDisables(text: string): Err | undefined {
const disabler = "eslint-disable";
let lastIndex = 0;
while (true) {
Expand All @@ -182,7 +187,8 @@ function testNoLintDisables(text: string): Err | undefined {
}
}

function range(minVersion: TsVersion, maxVersion: TsVersion): readonly TsVersion[] {
/** @internal */
export function range(minVersion: TsVersion, maxVersion: TsVersion): readonly TsVersion[] {
if (minVersion === "local") {
assert(maxVersion === "local");
return ["local"];
Expand Down
Loading
Loading