diff --git a/docs/dt-header.md b/docs/dt-header.md index b8aae0c8..29ef89c8 100644 --- a/docs/dt-header.md +++ b/docs/dt-header.md @@ -61,6 +61,16 @@ export { f } from "./subModule"; export function f(): number; ``` +`foo/ts3.1/index.d.ts`: +```ts +// Type definitions for abs 1.2 +// Project: https://github.com/foo/foo +// Definitions by: My Name +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +export function f(): number; +``` + + **Good**: `foo/index.d.ts`: Same @@ -70,4 +80,9 @@ export function f(): number; export function f(): number; ``` -Don't use a header twice -- only do it in the index. +`foo/ts3.1/index.d.ts`: +```ts +export function f(): number; +``` + +Don't repeat the header -- only do it in the index of the root. diff --git a/package-lock.json b/package-lock.json index bb5b097d..9c5cd7a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,9 @@ } }, "@types/node": { - "version": "7.0.70", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.70.tgz", - "integrity": "sha512-bAcW/1aM8/s5iFKhRpu/YJiQf/b1ZwnMRqqsWRCmAqEDQF2zY8Ez3Iu9AcZKFKc3vCJc8KJVpJ6Pn54sJ1BvXQ==", + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.71.tgz", + "integrity": "sha512-wpTYiRPPsjw/wiwlmP11mnln9be499B58XwoGsCy2hT8jSrRj7DE84FiIu3TBAQZ7L1ky1ibz5J9AG2YN1qZlQ==", "dev": true }, "@types/parsimmon": { @@ -142,9 +142,9 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "concat-map": { "version": "0.0.1", @@ -152,7 +152,7 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "definitelytyped-header-parser": { - "version": "github:Microsoft/definitelytyped-header-parser#7d90e8437ca8a405c8f6467ded5a52ef86ff168c", + "version": "github:Microsoft/definitelytyped-header-parser#c79916047989994b21f3332771c6b97191c03d38", "from": "github:Microsoft/definitelytyped-header-parser#production", "requires": { "@types/parsimmon": "^1.3.0", @@ -312,7 +312,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -361,9 +361,9 @@ } }, "typescript": { - "version": "3.1.0-dev.20180901", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.0-dev.20180901.tgz", - "integrity": "sha512-pKlqTQEoTPqnR2TGnDdS6taNQkwnbnrXxWJlAij1xa21n5bjB8fZ31s+I2Qc/2YztyDgyrkmMDm+kjo6wyRY4w==" + "version": "3.2.0-dev.20181006", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.0-dev.20181006.tgz", + "integrity": "sha512-Hxbkj5GZAUyaRZKc4q2wBeCBerO5yymUehnPHFLM2nYWdTmnZGKKfnCbgrwRgIBSYmrhGQZChoc1ksOi3+9LcQ==" }, "universalify": { "version": "0.1.2", diff --git a/src/checks.ts b/src/checks.ts index cd7b92f1..0d03d64e 100644 --- a/src/checks.ts +++ b/src/checks.ts @@ -1,11 +1,20 @@ +import assert = require("assert"); +import { makeTypesVersionsForPackageJson, TypeScriptVersion } from "definitelytyped-header-parser"; import { pathExists } from "fs-extra"; -import * as path from "path"; +import { join as joinPaths } from "path"; import { getCompilerOptions, readJson } from "./util"; -export async function checkPackageJson(dirPath: string): Promise { - const pkgJsonPath = path.join(dirPath, "package.json"); +export async function checkPackageJson( + dirPath: string, + typesVersions: ReadonlyArray, +): Promise { + const pkgJsonPath = joinPaths(dirPath, "package.json"); + const needsTypesVersions = typesVersions.length !== 0; if (!await pathExists(pkgJsonPath)) { + if (needsTypesVersions) { + throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`); + } return; } const pkgJson = await readJson(pkgJsonPath) as {}; @@ -13,12 +22,26 @@ export async function checkPackageJson(dirPath: string): Promise { if ((pkgJson as any).private !== true) { throw new Error(`${pkgJsonPath} should set \`"private": true\``); } + + if (needsTypesVersions) { + assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`); + const expected = makeTypesVersionsForPackageJson(typesVersions); + assert.deepEqual((pkgJson as any).typesVersions, expected, + `"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`); + } + for (const key in pkgJson) { // tslint:disable-line forin switch (key) { case "private": case "dependencies": case "license": - // "private" checked above, "dependencies" / "license" checked by types-publisher + // "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher, + break; + case "typesVersions": + case "types": + if (!needsTypesVersions) { + throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`); + } break; default: throw new Error(`${pkgJsonPath} should not include field ${key}`); @@ -26,19 +49,22 @@ export async function checkPackageJson(dirPath: string): Promise { } } -export async function checkTsconfig(dirPath: string, dt: boolean): Promise { +export interface DefinitelyTypedInfo { + /** "../" or "../../" or "../../../" */ + readonly relativeBaseUrl: string; +} +export async function checkTsconfig(dirPath: string, dt: DefinitelyTypedInfo | undefined): Promise { const options = await getCompilerOptions(dirPath); if (dt) { - const isOlderVersion = /^v\d+$/.test(path.basename(dirPath)); - const baseUrl = isOlderVersion ? "../../" : "../"; + const { relativeBaseUrl } = dt; const mustHave = { module: "commonjs", noEmit: true, forceConsistentCasingInFileNames: true, - baseUrl, - typeRoots: [baseUrl], + baseUrl: relativeBaseUrl, + typeRoots: [relativeBaseUrl], types: [], }; diff --git a/src/index.ts b/src/index.ts index eeaff01e..88dec1e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,12 @@ #!/usr/bin/env node -import { parseTypeScriptVersionLine, TypeScriptVersion } from "definitelytyped-header-parser"; -import { readFile } from "fs-extra"; +import { isTypeScriptVersion, parseTypeScriptVersionLine, TypeScriptVersion } from "definitelytyped-header-parser"; +import { readdir, readFile } from "fs-extra"; import { basename, dirname, join as joinPaths } from "path"; import { checkPackageJson, checkTsconfig } from "./checks"; import { cleanInstalls, installAll } from "./installer"; -import { checkTslintJson, lint } from "./lint"; +import { checkTslintJson, lint, TsVersion } from "./lint"; +import { assertDefined, last, mapDefined, withoutPrefix } from "./util"; async function main(): Promise { const args = process.argv.slice(2); @@ -82,22 +83,80 @@ function listen(dirPath: string): void { } async function runTests(dirPath: string, onlyTestTsNext: boolean): Promise { - const text = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8"); + const isOlderVersion = /^v\d+$/.test(basename(dirPath)); + + const indexText = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8"); // If this *is* on DefinitelyTyped, types-publisher will fail if it can't parse the header. - const dt = text.includes("// Type definitions for"); + const dt = indexText.includes("// Type definitions for"); if (dt) { // Someone may have copied text from DefinitelyTyped to their type definition and included a header, // so assert that we're really on DefinitelyTyped. assertPathIsInDefinitelyTyped(dirPath); } - const minVersion = getTypeScriptVersion(text); - await checkTslintJson(dirPath, dt); + const typesVersions = mapDefined(await readdir(dirPath), name => { + if (name === "tsconfig.json" || name === "tslint.json") { return undefined; } + const version = withoutPrefix(name, "ts"); + if (version === undefined) { return undefined; } + if (!isTypeScriptVersion(version)) { + throw new Error(`There is an entry named ${name}, but ${version} is not a valid TypeScript version.`); + } + if (!TypeScriptVersion.isRedirectable(version)) { + throw new Error(`At ${dirPath}/${name}: TypeScript version directories only available starting with ts3.1.`); + } + return version; + }); + if (dt) { - await checkPackageJson(dirPath); + await checkPackageJson(dirPath, typesVersions); } - await checkTsconfig(dirPath, dt); - const err = await lint(dirPath, minVersion, onlyTestTsNext); + + if (onlyTestTsNext) { + if (typesVersions.length === 0) { + await testTypesVersion(dirPath, "next", "next", isOlderVersion, dt, indexText); + } else { + const latestTypesVersion = last(typesVersions); + const versionPath = joinPaths(dirPath, `ts${latestTypesVersion}`); + const versionIndexText = await readFile(joinPaths(versionPath, "index.d.ts"), "utf-8"); + await testTypesVersion(versionPath, "next", "next", isOlderVersion, dt, versionIndexText); + } + } else { + await testTypesVersion(dirPath, undefined, getTsVersion(0), isOlderVersion, dt, indexText); + for (let i = 0; i < typesVersions.length; i++) { + const version = typesVersions[i]; + const versionPath = joinPaths(dirPath, `ts${version}`); + const versionIndexText = await readFile(joinPaths(versionPath, "index.d.ts"), "utf-8"); + await testTypesVersion(versionPath, version, getTsVersion(i + 1), isOlderVersion, dt, versionIndexText); + } + + function getTsVersion(i: number): TsVersion { + return i === typesVersions.length ? "next" : assertDefined(TypeScriptVersion.previous(typesVersions[i])); + } + } +} + +async function testTypesVersion( + dirPath: string, + lowVersion: TsVersion | undefined, + maxVersion: TsVersion, + isOlderVersion: boolean, + dt: boolean, + indexText: string, +): Promise { + const minVersionFromComment = getTypeScriptVersionFromComment(indexText); + if (minVersionFromComment !== undefined && lowVersion !== undefined) { + throw new Error(`Already in the \`ts${lowVersion}\` directory, don't need \`// TypeScript Version\`.`); + } + if (minVersionFromComment !== undefined && TypeScriptVersion.isRedirectable(minVersionFromComment)) { + throw new Error(`Don't use \`// TypeScript Version\` for newer TS versions, use typesVerisons instead.`); + } + const minVersion = lowVersion || minVersionFromComment || TypeScriptVersion.lowest; + + await checkTslintJson(dirPath, dt); + await checkTsconfig(dirPath, dt + ? { relativeBaseUrl: joinPaths("..", isOlderVersion ? ".." : "", lowVersion !== undefined ? ".." : "") + "/" } + : undefined); + const err = await lint(dirPath, minVersion, maxVersion); if (err) { throw new Error(err); } @@ -114,11 +173,11 @@ function assertPathIsInDefinitelyTyped(dirPath: string): void { } } -function getTypeScriptVersion(text: string): TypeScriptVersion { +function getTypeScriptVersionFromComment(text: string): TypeScriptVersion | undefined { const searchString = "// TypeScript Version: "; const x = text.indexOf(searchString); if (x === -1) { - return "2.0"; + return undefined; } let line = text.slice(x, text.indexOf("\n", x)); diff --git a/src/installer.ts b/src/installer.ts index 100e8394..3a3f4378 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -2,6 +2,7 @@ import { exec } from "child_process"; import { TypeScriptVersion } from "definitelytyped-header-parser"; import * as fs from "fs-extra"; import * as path from "path"; +import { TsVersion } from "./lint"; const installsDir = path.join(__dirname, "..", "typescript-installs"); @@ -12,7 +13,7 @@ export async function installAll() { await install("next"); } -async function install(version: TypeScriptVersion | "next"): Promise { +async function install(version: TsVersion): Promise { const dir = installDir(version); if (!await fs.pathExists(dir)) { console.log(`Installing to ${dir}...`); @@ -27,11 +28,11 @@ export function cleanInstalls(): Promise { return fs.remove(installsDir); } -export function typeScriptPath(version: TypeScriptVersion | "next"): string { +export function typeScriptPath(version: TsVersion): string { return path.join(installDir(version), "node_modules", "typescript"); } -function installDir(version: TypeScriptVersion | "next"): string { +function installDir(version: TsVersion): string { return path.join(installsDir, version); } @@ -49,7 +50,7 @@ async function execAndThrowErrors(cmd: string, cwd?: string): Promise { }); } -function packageJson(version: TypeScriptVersion | "next"): {} { +function packageJson(version: TsVersion): {} { return { description: `Installs typescript@${version}`, repository: "N/A", diff --git a/src/lint.ts b/src/lint.ts index 9166178e..1f522c31 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -1,3 +1,4 @@ +import assert = require("assert"); import { TypeScriptVersion } from "definitelytyped-header-parser"; import { pathExists, readFile } from "fs-extra"; import { join as joinPaths } from "path"; @@ -10,11 +11,7 @@ import { Options as ExpectOptions } from "./rules/expectRule"; import { typeScriptPath } from "./installer"; import { readJson } from "./util"; -export async function lint( - dirPath: string, - minVersion: TypeScriptVersion, - onlyTestTsNext: boolean, -): Promise { +export async function lint(dirPath: string, minVersion: TsVersion, maxVersion: TsVersion): Promise { const lintConfigPath = getConfigPath(dirPath); const tsconfigPath = joinPaths(dirPath, "tsconfig.json"); const program = Linter.createProgram(tsconfigPath); @@ -24,7 +21,7 @@ export async function lint( formatter: "stylish", }; const linter = new Linter(lintOptions, program); - const config = await getLintConfig(lintConfigPath, tsconfigPath, minVersion, onlyTestTsNext); + const config = await getLintConfig(lintConfigPath, tsconfigPath, minVersion, maxVersion); for (const filename of program.getRootFileNames()) { const contents = await readFile(filename, "utf-8"); @@ -91,8 +88,8 @@ function getConfigPath(dirPath: string): string { async function getLintConfig( expectedConfigPath: string, tsconfigPath: string, - minVersion: TypeScriptVersion, - onlyTestTsNext: boolean, + minVersion: TsVersion, + maxVersion: TsVersion, ): Promise { const configExists = await pathExists(expectedConfigPath); const configPath = configExists ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json"); @@ -104,14 +101,29 @@ async function getLintConfig( const expectRule = config.rules.get("expect"); if (expectRule) { - const expectOptions: ExpectOptions = { - tsconfigPath, - tsNextPath: typeScriptPath("next"), - olderInstalls: TypeScriptVersion.range(minVersion).map(versionName => - ({ versionName, path: typeScriptPath(versionName) })), - onlyTestTsNext, - }; + const versionsToTest = range(minVersion, maxVersion).map(versionName => + ({ versionName, path: typeScriptPath(versionName) })); + const expectOptions: ExpectOptions = { tsconfigPath, versionsToTest }; expectRule.ruleArguments = [expectOptions]; } return config; } + +function range(minVersion: TsVersion, maxVersion: TsVersion): ReadonlyArray { + if (minVersion === "next") { + assert(maxVersion === "next"); + return ["next"]; + } + + const minIdx = TypeScriptVersion.all.indexOf(minVersion); + assert(minIdx >= 0); + if (maxVersion === "next") { + return [...TypeScriptVersion.all.slice(minIdx), "next"]; + } + + const maxIdx = TypeScriptVersion.all.indexOf(maxVersion); + assert(maxIdx >= minIdx); + return TypeScriptVersion.all.slice(minIdx, maxIdx + 1); +} + +export type TsVersion = TypeScriptVersion | "next"; diff --git a/src/rules/dtHeaderRule.ts b/src/rules/dtHeaderRule.ts index 6a9ea8c3..91aa6e3b 100644 --- a/src/rules/dtHeaderRule.ts +++ b/src/rules/dtHeaderRule.ts @@ -32,7 +32,7 @@ function walk(ctx: Lint.WalkContext): void { } }; - lookFor("// Type definitions for", "Header should only be in `index.d.ts`."); + lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root."); lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`."); return; } @@ -58,6 +58,7 @@ function isMainFile(fileName: string) { let parent = dirname(fileName); // May be a directory for an older version, e.g. `v0`. + // Note a types redirect `foo/ts3.1` should not have its own header. if (/^v\d+$/.test(basename(parent))) { parent = dirname(parent); } diff --git a/src/rules/expectRule.ts b/src/rules/expectRule.ts index 330bbbeb..c1587f86 100644 --- a/src/rules/expectRule.ts +++ b/src/rules/expectRule.ts @@ -1,8 +1,8 @@ -import assert = require("assert"); import { existsSync, readFileSync } from "fs"; import { dirname, resolve as resolvePath } from "path"; import * as Lint from "tslint"; import * as TsType from "typescript"; +import { last } from "../util"; type Program = TsType.Program; type SourceFile = TsType.SourceFile; @@ -37,34 +37,30 @@ export class Rule extends Lint.Rules.TypedRule { walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined)); } - const getFailures = (versionName: string, path: string, nextHigherVersion: string | undefined) => { + const { tsconfigPath, versionsToTest } = options; + + const getFailures = ({ versionName, path }: VersionToTest, nextHigherVersion: string | undefined) => { const ts = require(path); - const program = getProgram(options.tsconfigPath, ts, versionName, lintProgram); + const program = getProgram(tsconfigPath, ts, versionName, lintProgram); return this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts, versionName, nextHigherVersion)); }; - const nextFailures = getFailures("next", options.tsNextPath, /*nextHigherVersion*/ undefined); - if (options.onlyTestTsNext || nextFailures.length) { - return nextFailures; + const maxFailures = getFailures(last(versionsToTest), undefined); + if (maxFailures.length) { + return maxFailures; } - assert(options.olderInstalls.length); - // As an optimization, check the earliest version for errors; - // assume that if it works on min and next, it works for everything in between. - const minInstall = options.olderInstalls[0]; - const minFailures = getFailures(minInstall.versionName, minInstall.path, undefined); + // assume that if it works on min and max, it works for everything in between. + const minFailures = getFailures(versionsToTest[0], undefined); if (!minFailures.length) { return []; } - // There are no failures in `next`, but there are failures in `min`. + // There are no failures in the max version, but there are failures in the min version. // Work backward to find the newest version with failures. - for (let i = options.olderInstalls.length - 1; i >= 0; i--) { - const { versionName, path } = options.olderInstalls[i]; - console.log(`Test with ${versionName}`); - const nextHigherVersion = i === options.olderInstalls.length - 1 ? "next" : options.olderInstalls[i + 1].versionName; - const failures = getFailures(versionName, path, nextHigherVersion); + for (let i = versionsToTest.length - 2; i >= 0; i--) { + const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName); if (failures.length) { return failures; } @@ -76,10 +72,12 @@ export class Rule extends Lint.Rules.TypedRule { export interface Options { readonly tsconfigPath: string; - readonly tsNextPath: string; // These should be sorted with oldest first. - readonly olderInstalls: ReadonlyArray<{ versionName: string, path: string }>; - readonly onlyTestTsNext: boolean; + readonly versionsToTest: ReadonlyArray; +} +export interface VersionToTest { + readonly versionName: string; + readonly path: string; } const programCache = new WeakMap>(); diff --git a/src/util.ts b/src/util.ts index 85ea581f..ac2dd13d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import assert = require("assert"); import { pathExists, readFile } from "fs-extra"; import { basename, dirname, join } from "path"; import stripJsonComments = require("strip-json-comments"); @@ -59,3 +60,28 @@ export async function getCompilerOptions(dirPath: string): Promise(a: ReadonlyArray): T { + assert(a.length !== 0); + return a[a.length - 1]; +} + +export function assertDefined(a: T | undefined): T { + if (a === undefined) { throw new Error(); } + return a; +} + +export function mapDefined(arr: Iterable, mapper: (t: T) => U | undefined): U[] { + const out = []; + for (const a of arr) { + const res = mapper(a); + if (res !== undefined) { + out.push(res); + } + } + return out; +} diff --git a/test/dt-header/wrong/types/foo/notIndex.d.ts.lint b/test/dt-header/wrong/types/foo/notIndex.d.ts.lint index ce64d6a2..5b2c9803 100644 --- a/test/dt-header/wrong/types/foo/notIndex.d.ts.lint +++ b/test/dt-header/wrong/types/foo/notIndex.d.ts.lint @@ -1,2 +1,2 @@ // Type definitions for -~~~~~~~~~~~~~~~~~~~~~~~ [Header should only be in `index.d.ts`. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md] +~~~~~~~~~~~~~~~~~~~~~~~ [Header should only be in `index.d.ts` of the root. See: https://github.com/Microsoft/dtslint/blob/master/docs/dt-header.md]