diff --git a/.gitignore b/.gitignore index b5aecdbe35d..fdcea6a6800 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,5 @@ tests/cases/user/*/**/*.d.ts !tests/cases/user/zone.js/ !tests/cases/user/bignumber.js/ !tests/cases/user/discord.js/ -tests/baselines/reference/dt \ No newline at end of file +tests/baselines/reference/dt +.failed-tests \ No newline at end of file diff --git a/Gulpfile.js b/Gulpfile.js index 68b1391cf52..b1109a3e340 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -10,13 +10,8 @@ const insert = require("gulp-insert"); const { append } = require("gulp-insert"); const sourcemaps = require("gulp-sourcemaps"); const del = require("del"); -const browserify = require("browserify"); -const through2 = require("through2"); const fold = require("travis-fold"); const rename = require("gulp-rename"); -const convertMap = require("convert-source-map"); -const sorcery = require("sorcery"); -const Vinyl = require("vinyl"); const mkdirp = require("./scripts/build/mkdirp"); const gulp = require("./scripts/build/gulp"); const getDirSize = require("./scripts/build/getDirSize"); @@ -28,7 +23,7 @@ const getDiffTool = require("./scripts/build/getDiffTool"); const baselineAccept = require("./scripts/build/baselineAccept"); const cmdLineOptions = require("./scripts/build/options"); const exec = require("./scripts/build/exec"); -const _debugMode = require("./scripts/build/debugMode"); +const browserify = require("./scripts/build/browserify"); const { libraryTargets, generateLibs } = require("./scripts/build/lib"); const { runConsoleTests, cleanTestDirs, writeTestConfigFile, refBaseline, localBaseline, refRwcBaseline, localRwcBaseline } = require("./scripts/build/tests"); @@ -72,7 +67,7 @@ gulp.task( "publish-nightly", "Runs `npm publish --tag next` to create a new nightly build on npm", ["LKG"], - () => runSequence("clean", "useDebugMode", "runtests-parallel", + () => runSequence("clean", "runtests-parallel", () => exec("npm", ["publish", "--tag", "next"]))); const importDefinitelyTypedTestsProject = "scripts/importDefinitelyTypedTests/tsconfig.json"; @@ -187,6 +182,10 @@ const tscProject = "src/tsc/tsconfig.json"; const tscJs = "built/local/tsc.js"; gulp.task(tscJs, /*help*/ false, [typescriptServicesJs], () => project.compile(tscProject, { typescript: "built" })); +const tscReleaseProject = "src/tsc/tsconfig.release.json"; +const tscReleaseJs = "built/local/tsc.release.js"; +gulp.task(tscReleaseJs, /*help*/ false, () => project.compile(tscReleaseProject)); + const cancellationTokenProject = "src/cancellationToken/tsconfig.json"; const cancellationTokenJs = "built/local/cancellationToken.js"; gulp.task(cancellationTokenJs, /*help*/ false, [typescriptServicesJs], () => project.compile(cancellationTokenProject, { typescript: "built" })); @@ -258,9 +257,9 @@ gulp.task( "Generates a Markdown version of the Language Specification", [specMd]); -gulp.task("produce-LKG", /*help*/ false, ["scripts", "local", cancellationTokenJs, typingsInstallerJs, watchGuardJs], () => { +gulp.task("produce-LKG", /*help*/ false, ["scripts", "local", cancellationTokenJs, typingsInstallerJs, watchGuardJs, tscReleaseJs], () => { const expectedFiles = [ - tscJs, + tscReleaseJs, typescriptServicesJs, tsserverJs, typescriptJs, @@ -289,7 +288,7 @@ gulp.task("produce-LKG", /*help*/ false, ["scripts", "local", cancellationTokenJ gulp.task( "LKG", "Makes a new LKG out of the built js files", - () => runSequence("clean-built", "dontUseDebugMode", "produce-LKG")); + () => runSequence("clean-built", "produce-LKG")); // Task to build the tests infrastructure using the built compiler const testRunnerProject = "src/testRunner/tsconfig.json"; @@ -301,11 +300,6 @@ gulp.task( "Builds the test infrastructure using the built compiler", [runJs]); -gulp.task( - "tests-debug", - "Builds the test sources and automation in debug mode", - () => runSequence("useDebugMode", "tests")); - gulp.task( "runtests-parallel", "Runs all the tests in parallel using the built run.js file. Optional arguments are: --t[ests]=category1|category2|... --d[ebug]=true.", @@ -325,77 +319,17 @@ gulp.task("clean:" + webTestServerJs, /*help*/ false, () => project.clean(webTes const bundlePath = path.resolve("built/local/bundle.js"); -// TODO(rbuckton): Clean up browserify logic gulp.task( "browserify", "Runs browserify on run.js to produce a file suitable for running tests in the browser", [runJs], - (done) => { - /** @type {*} */ - let originalMap; - /** @type {string} */ - let prebundledContent; - browserify(gulp.src([runJs]) - .pipe(newer(bundlePath)) - .pipe(sourcemaps.init({ loadMaps: true })) - .pipe(through2.obj((file, enc, next) => { - if (originalMap) { - throw new Error("Should only recieve one file!"); - } - log(`Saving sourcemaps for ${file.path}`); - originalMap = file.sourceMap; - prebundledContent = file.contents.toString(); - // Make paths absolute to help sorcery deal with all the terrible paths being thrown around - originalMap.sources = originalMap.sources.map(s => path.resolve(path.join("src/harness", s))); - // browserify names input files this when they are streamed in, so this is what it puts in the sourcemap - originalMap.file = "built/local/_stream_0.js"; - - next(/*err*/ undefined, file.contents); - })) - .on("error", err => { - return done(err); - }), { debug: true, basedir: __dirname }) // Attach error handler to inner stream - .bundle((err, contents) => { - if (err) { - if (err.message.match(/Cannot find module '.*_stream_0.js'/)) { - return done(); // Browserify errors when we pass in no files when `newer` filters the input, we should count that as a success, though - } - return done(err); - } - const stringContent = contents.toString(); - const file = new Vinyl({ contents, path: bundlePath }); - log(`Fixing sourcemaps for ${file.path}`); - // assumes contents is a Buffer, since that's what browserify yields - const maps = convertMap.fromSource(stringContent).toObject(); - delete maps.sourceRoot; - maps.sources = maps.sources.map(s => path.resolve(s === "_stream_0.js" ? "built/local/_stream_0.js" : s)); - // Strip browserify's inline comments away (could probably just let sorcery do this, but then we couldn't fix the paths) - file.contents = new Buffer(convertMap.removeComments(stringContent)); - const chain = sorcery.loadSync(bundlePath, { - content: { - "built/local/_stream_0.js": prebundledContent, - [bundlePath]: stringContent - }, - sourcemaps: { - "built/local/_stream_0.js": originalMap, - [bundlePath]: maps, - "node_modules/source-map-support/source-map-support.js": undefined, - } - }); - const finalMap = chain.apply(); - file.sourceMap = finalMap; - - const stream = through2.obj((file, enc, callback) => { - return callback(/*err*/ undefined, file); - }); - stream.pipe(sourcemaps.write(".", { includeContent: false })) - .pipe(gulp.dest(".")) - .on("end", done) - .on("error", done); - stream.write(file); - stream.end(); - }); - }); + () => gulp.src([runJs], { base: "built/local" }) + .pipe(newer(bundlePath)) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(browserify()) + .pipe(rename("bundle.js")) + .pipe(sourcemaps.write(".", /**@type {*}*/({ includeContent: false, destPath: "built/local" }))) + .pipe(gulp.dest("built/local"))); gulp.task( "runtests-browser", @@ -530,12 +464,17 @@ gulp.task( "Runs 'local'", ["local"]); -// TODO(rbuckton): Investigate restoring gulp.watch() functionality. -// gulp.task( -// "watch", -// "Watches the src/ directory for changes and executes runtests-parallel.", -// [], -// () => gulp.watch("src/**/*.*", ["runtests-parallel"])); +gulp.task( + "watch-tsc", + "Watches for changes to the build inputs for built/local/tsc.js", + [typescriptServicesJs], + () => project.watch(tscProject, { typescript: "built" })); + +gulp.task( + "watch", + "Watches for changes to the build inputs for built/local/run.js executes runtests-parallel.", + [typescriptServicesJs], + () => project.watch(testRunnerProject, { typescript: "built" }, ["runtests-parallel"])); gulp.task("clean-built", /*help*/ false, ["clean:" + diagnosticInformationMapTs], () => del(["built"])); gulp.task( diff --git a/Jakefile.js b/Jakefile.js index ce5576036f4..3553600bf01 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -59,6 +59,7 @@ Paths.builtLocal = "built/local"; Paths.builtLocalCompiler = "built/local/tsc.js"; Paths.builtLocalTSServer = "built/local/tsserver.js"; Paths.builtLocalRun = "built/local/run.js"; +Paths.releaseCompiler = "built/local/tsc.release.js"; Paths.typesMapOutput = "built/local/typesMap.json"; Paths.typescriptFile = "built/local/typescript.js"; Paths.servicesFile = "built/local/typescriptServices.js"; @@ -95,6 +96,7 @@ Paths.versionFile = "src/compiler/core.ts"; const ConfigFileFor = { tsc: "src/tsc", + tscRelease: "src/tsc/tsconfig.release.json", tsserver: "src/tsserver", runjs: "src/testRunner", lint: "scripts/tslint", @@ -157,6 +159,12 @@ task(TaskNames.scripts, [TaskNames.coreBuild], function() { }); }, { async: true }); +task(Paths.releaseCompiler, function () { + tsbuild([ConfigFileFor.tscRelease], true, () => { + complete(); + }); +}, { async: true }); + // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory desc("Makes a new LKG out of the built js files"); task(TaskNames.lkg, [ @@ -165,6 +173,7 @@ task(TaskNames.lkg, [ TaskNames.local, Paths.servicesDefinitionFile, Paths.tsserverLibraryDefinitionFile, + Paths.releaseCompiler, ...libraryTargets ], () => { const sizeBefore = getDirSize(Paths.lkg); @@ -410,6 +419,8 @@ function runConsoleTests(defaultReporter, runInParallel) { const runners = process.env.runners || process.env.runner || process.env.ru; const tests = process.env.test || process.env.tests || process.env.t; const light = process.env.light === undefined || process.env.light !== "false"; + const failed = process.env.failed; + const keepFailed = process.env.keepFailed || failed; const stackTraceLimit = process.env.stackTraceLimit; const colorsFlag = process.env.color || process.env.colors; const colors = colorsFlag !== "false" && colorsFlag !== "0"; @@ -440,8 +451,8 @@ function runConsoleTests(defaultReporter, runInParallel) { testTimeout = 800000; } - if (tests || runners || light || testTimeout || taskConfigsFolder) { - writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout); + if (tests || runners || light || testTimeout || taskConfigsFolder || keepFailed) { + writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout, keepFailed); } // timeout normally isn't necessary but Travis-CI has been timing out on compiler baselines occasionally @@ -449,7 +460,8 @@ function runConsoleTests(defaultReporter, runInParallel) { if (!runInParallel) { var startTime = Travis.mark(); var args = []; - args.push("-R", reporter); + args.push("-R", "scripts/failed-tests"); + args.push("-O", '"reporter=' + reporter + (keepFailed ? ",keepFailed=true" : "") + '"'); if (tests) args.push("-g", `"${tests}"`); args.push(colors ? "--colors" : "--no-colors"); if (bail) args.push("--bail"); @@ -460,7 +472,14 @@ function runConsoleTests(defaultReporter, runInParallel) { } args.push(Paths.builtLocalRun); - var cmd = "mocha " + args.join(" "); + var cmd; + if (failed) { + args.unshift("scripts/run-failed-tests.js"); + cmd = host + " " + args.join(" "); + } + else { + cmd = "mocha " + args.join(" "); + } var savedNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; exec(cmd, function () { @@ -521,7 +540,7 @@ function runConsoleTests(defaultReporter, runInParallel) { } // used to pass data from jake command line directly to run.js -function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout) { +function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout, keepFailed) { var testConfigContents = JSON.stringify({ runners: runners ? runners.split(",") : undefined, test: tests ? [tests] : undefined, @@ -530,7 +549,8 @@ function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCou taskConfigsFolder: taskConfigsFolder, stackTraceLimit: stackTraceLimit, noColor: !colors, - timeout: testTimeout + timeout: testTimeout, + keepFailed: keepFailed }); fs.writeFileSync('test.config', testConfigContents, { encoding: "utf-8" }); } diff --git a/package-lock.json b/package-lock.json index ff52eb7be20..5b8e90db339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,9 +36,9 @@ } }, "@octokit/rest": { - "version": "15.8.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.8.2.tgz", - "integrity": "sha512-hMUDI6NveJE49rGYfNfXT2CiHODhQMfbqFAa2h8TjR3GrfI1wnfSlsYeGZe4D/Qu+Svqlg9eUisoeIvYWz1yZw==", + "version": "15.9.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.9.2.tgz", + "integrity": "sha512-UpV9ZTI9ok73E0iFK+LH//c2/WIm6w/FGQ9LFF5GZFANsXut7z75LE0TCcgMZYdCS4eFm525qa3s+0INkPXigA==", "dev": true, "requires": { "before-after-hook": "1.1.0", @@ -233,9 +233,9 @@ } }, "@types/mocha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.2.tgz", - "integrity": "sha512-tfg9rh2qQhBW6SBqpvfqTgU6lHWGhQURoTrn7NeDF+CEkO9JGYbkzU23EXu6p3bnvDxLeeSX8ohAA23urvWeNw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.3.tgz", + "integrity": "sha512-C1wVVr7xhKu6c3Mb27dFzNYR05qvHwgtpN+JOYTGc1pKA7dCEDDYpscn7kul+bCUwa3NoGDbzI1pdznSOa397w==", "dev": true }, "@types/node": { @@ -312,16 +312,6 @@ "@types/node": "8.5.5" } }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", - "dev": true, - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -786,9 +776,9 @@ "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", "dev": true, "requires": { - "JSONStream": "1.3.3", "combine-source-map": "0.8.0", "defined": "1.0.0", + "JSONStream": "1.3.3", "safe-buffer": "5.1.2", "through2": "2.0.3", "umd": "3.0.3" @@ -815,7 +805,6 @@ "integrity": "sha512-fMES05wq1Oukts6ksGUU2TMVHHp06LyQt0SIwbXIHm7waSrQmNBZePsU0iM/4f94zbvb/wHma+D1YrdzWYnF/A==", "dev": true, "requires": { - "JSONStream": "1.3.3", "assert": "1.4.1", "browser-pack": "6.1.0", "browser-resolve": "1.11.2", @@ -837,6 +826,7 @@ "https-browserify": "1.0.0", "inherits": "2.0.3", "insert-module-globals": "7.1.0", + "JSONStream": "1.3.3", "labeled-stream-splicer": "2.0.1", "mkdirp": "0.5.1", "module-deps": "6.1.0", @@ -951,12 +941,6 @@ "ieee754": "1.1.12" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, "buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", @@ -1703,12 +1687,6 @@ "es6-symbol": "3.1.1" } }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", - "dev": true - }, "es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", @@ -3189,10 +3167,10 @@ "integrity": "sha512-LbYZdybvKjbbcKLp03lB323Cgc8f0iL0Rjh8U6JZ7K1gZSf7MxQH191iCNUcLX4qIQ6/yWe4Q4ZsQ+opcReNFg==", "dev": true, "requires": { - "JSONStream": "1.3.3", "combine-source-map": "0.8.0", "concat-stream": "1.6.2", "is-buffer": "1.1.6", + "JSONStream": "1.3.3", "lexical-scope": "1.2.0", "path-is-absolute": "1.0.1", "process": "0.11.10", @@ -3615,6 +3593,16 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "JSONStream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", + "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "kew": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", @@ -4095,7 +4083,6 @@ "integrity": "sha512-NPs5N511VD1rrVJihSso/LiBShRbJALYBKzDW91uZYy7BpjnO4bGnZL3HjZ9yKcFdZUWwaYjDz9zxbuP7vKMuQ==", "dev": true, "requires": { - "JSONStream": "1.3.3", "browser-resolve": "1.11.2", "cached-path-relative": "1.0.1", "concat-stream": "1.6.2", @@ -4103,6 +4090,7 @@ "detective": "5.1.0", "duplexer2": "0.1.4", "inherits": "2.0.3", + "JSONStream": "1.3.3", "parents": "1.0.1", "readable-stream": "2.3.6", "resolve": "1.7.1", @@ -5106,26 +5094,6 @@ "ret": "0.1.15" } }, - "sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=", - "dev": true, - "requires": { - "es6-promise": "3.3.1", - "graceful-fs": "4.1.11", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - } - } - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -5339,18 +5307,6 @@ } } }, - "sorcery": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", - "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=", - "dev": true, - "requires": { - "buffer-crc32": "0.2.13", - "minimist": "1.2.0", - "sander": "0.5.1", - "sourcemap-codec": "1.4.1" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5394,12 +5350,6 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, - "sourcemap-codec": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.1.tgz", - "integrity": "sha512-hX1eNBNuilj8yfFnECh0DzLgwKpBLMIvmhgEhixXNui8lMLBInTI8Kyxt++RwJnMNu7cAUo635L2+N1TxMJCzA==", - "dev": true - }, "sparkles": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", @@ -5532,6 +5482,15 @@ } } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -5559,15 +5518,6 @@ } } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -6180,6 +6130,15 @@ } } }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, "vm-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.0.1.tgz", diff --git a/package.json b/package.json index 37e4cc663d6..af7f0369a67 100644 --- a/package.json +++ b/package.json @@ -84,13 +84,13 @@ "q": "latest", "remove-internal": "^2.9.2", "run-sequence": "latest", - "sorcery": "latest", "source-map-support": "latest", "through2": "latest", "travis-fold": "latest", "tslint": "latest", "typescript": "next", "vinyl": "latest", + "vinyl-sourcemaps-apply": "latest", "xml2js": "^0.4.19" }, "scripts": { diff --git a/scripts/build/browserify.js b/scripts/build/browserify.js new file mode 100644 index 00000000000..3d9fffa9fa6 --- /dev/null +++ b/scripts/build/browserify.js @@ -0,0 +1,120 @@ +// @ts-check +const Browserify = require("browserify"); +const Vinyl = require("vinyl"); +const fs = require("fs"); +const path = require("path"); +const convertMap = require("convert-source-map"); +const applySourceMap = require("vinyl-sourcemaps-apply"); +const { Transform, Readable } = require("stream"); + +module.exports = browserify; + +/** + * @param {import("browserify").Options} [opts] + */ +function browserify(opts) { + return new Transform({ + objectMode: true, + /** + * @param {string | Buffer | File} input + */ + transform(input, _, cb) { + if (typeof input === "string" || Buffer.isBuffer(input)) return cb(new Error("Only Vinyl files are supported.")); + try { + const sourceMap = input.sourceMap; + const cwd = input.cwd || process.cwd(); + const base = input.base || cwd; + const output = /**@type {File}*/(new Vinyl({ path: input.path, base: input.base })); + const stream = streamFromFile(input); + const b = new Browserify(Object.assign({}, opts, { debug: !!sourceMap, basedir: input.base })); + b.add(stream, { file: input.path, basedir: input.base }); + b.bundle((err, contents) => { + if (err) return cb(err); + output.contents = contents; + if (sourceMap) { + output.sourceMap = typeof sourceMap === "string" ? JSON.parse(sourceMap) : sourceMap; + const sourceRoot = output.sourceMap.sourceRoot; + makeAbsoluteSourceMap(cwd, base, output.sourceMap); + const stringContents = contents.toString("utf8"); + const newSourceMapConverter = convertMap.fromSource(stringContents); + if (newSourceMapConverter) { + const newSourceMap = newSourceMapConverter.toObject(); + makeAbsoluteSourceMap(cwd, base, newSourceMap); + applySourceMap(output, newSourceMap); + makeRelativeSourceMap(cwd, base, sourceRoot, output.sourceMap); + output.contents = new Buffer(convertMap.removeComments(stringContents), "utf8"); + } + } + cb(null, output); + }); + } + catch (e) { + cb(e); + } + } + }); +} + +/** + * @param {string | undefined} cwd + * @param {string | undefined} base + * @param {RawSourceMap} sourceMap + * + * @typedef RawSourceMap + * @property {string} version + * @property {string} file + * @property {string} [sourceRoot] + * @property {string[]} sources + * @property {string[]} [sourcesContents] + * @property {string} mappings + * @property {string[]} [names] + */ +function makeAbsoluteSourceMap(cwd = process.cwd(), base = "", sourceMap) { + const sourceRoot = sourceMap.sourceRoot || ""; + const resolvedBase = path.resolve(cwd, base); + const resolvedSourceRoot = path.resolve(resolvedBase, sourceRoot); + sourceMap.file = path.resolve(resolvedBase, sourceMap.file).replace(/\\/g, "/"); + sourceMap.sources = sourceMap.sources.map(source => path.resolve(resolvedSourceRoot, source).replace(/\\/g, "/")); + sourceMap.sourceRoot = ""; +} + +/** + * @param {string | undefined} cwd + * @param {string | undefined} base + * @param {string} sourceRoot + * @param {RawSourceMap} sourceMap + */ +function makeRelativeSourceMap(cwd = process.cwd(), base = "", sourceRoot, sourceMap) { + makeAbsoluteSourceMap(cwd, base, sourceMap); + const resolvedBase = path.resolve(cwd, base); + const resolvedSourceRoot = path.resolve(resolvedBase, sourceRoot); + sourceMap.file = path.relative(resolvedBase, sourceMap.file).replace(/\\/g, "/"); + sourceMap.sources = sourceMap.sources.map(source => path.relative(resolvedSourceRoot, source).replace(/\\/g, "/")); + sourceMap.sourceRoot = sourceRoot; +} + +/** + * @param {File} file + */ +function streamFromFile(file) { + return file.isBuffer() ? streamFromBuffer(file.contents) : + file.isStream() ? file.contents : + fs.createReadStream(file.path, { autoClose: true }); +} + +/** + * @param {Buffer} buffer + */ +function streamFromBuffer(buffer) { + return new Readable({ + read() { + this.push(buffer); + this.push(null); + } + }); +} + +/** + * @typedef {import("vinyl") & { sourceMap?: any }} File + */ +void 0; \ No newline at end of file diff --git a/scripts/build/debugMode.js b/scripts/build/debugMode.js deleted file mode 100644 index 3373191b63c..00000000000 --- a/scripts/build/debugMode.js +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-check -const gulp = require("./gulp"); - -exports.useDebugMode = true; - -gulp.task("useDebugMode", /*help*/ false, [], (done) => { exports["useDebugMode"] = true; done(); }); -gulp.task("dontUseDebugMode", /*help*/ false, [], (done) => { exports["useDebugMode"] = false; done(); }); diff --git a/scripts/build/options.js b/scripts/build/options.js index df0fa223026..b69358f0f98 100644 --- a/scripts/build/options.js +++ b/scripts/build/options.js @@ -4,7 +4,7 @@ const os = require("os"); /** @type {CommandLineOptions} */ module.exports = minimist(process.argv.slice(2), { - boolean: ["debug", "inspect", "light", "colors", "lint", "soft", "fix"], + boolean: ["debug", "inspect", "light", "colors", "lint", "soft", "fix", "failed", "keepFailed"], string: ["browser", "tests", "host", "reporter", "stackTraceLimit", "timeout"], alias: { "b": "browser", @@ -32,6 +32,8 @@ module.exports = minimist(process.argv.slice(2), { lint: process.env.lint || true, fix: process.env.fix || process.env.f, workers: process.env.workerCount || os.cpus().length, + failed: false, + keepFailed: false } }); @@ -52,6 +54,8 @@ module.exports = minimist(process.argv.slice(2), { * @property {string} reporter * @property {string} stackTraceLimit * @property {string|number} timeout + * @property {boolean} failed + * @property {boolean} keepFailed * * @typedef {import("minimist").ParsedArgs & TypedOptions} CommandLineOptions */ diff --git a/scripts/build/project.js b/scripts/build/project.js index 8189ccec4a2..badeb606d3b 100644 --- a/scripts/build/project.js +++ b/scripts/build/project.js @@ -16,16 +16,16 @@ const { reportDiagnostics } = require("./diagnostics"); class CompilationGulp extends gulp.Gulp { /** - * @param {boolean} [verbose] + * @param {boolean} [verbose] */ fork(verbose) { const child = new ForkedGulp(this.tasks); if (verbose) { - this.on("task_start", e => gulp.emit("task_start", e)); - this.on("task_stop", e => gulp.emit("task_stop", e)); - this.on("task_err", e => gulp.emit("task_err", e)); - this.on("task_not_found", e => gulp.emit("task_not_found", e)); - this.on("task_recursion", e => gulp.emit("task_recursion", e)); + child.on("task_start", e => gulp.emit("task_start", e)); + child.on("task_stop", e => gulp.emit("task_stop", e)); + child.on("task_err", e => gulp.emit("task_err", e)); + child.on("task_not_found", e => gulp.emit("task_not_found", e)); + child.on("task_recursion", e => gulp.emit("task_recursion", e)); } return child; } @@ -58,13 +58,15 @@ const typescriptAliasMap = new Map(); /** * Defines a gulp orchestration for a TypeScript project, returning a callback that can be used to trigger compilation. * @param {string} projectSpec The path to a tsconfig.json file or its containing directory. - * @param {ProjectOptions} [options] Project compilation options. + * @param {CompileOptions} [options] Project compilation options. * @returns {() => Promise} */ function createCompiler(projectSpec, options) { - const resolvedOptions = resolveProjectOptions(options); + const resolvedOptions = resolveCompileOptions(options); const resolvedProjectSpec = resolveProjectSpec(projectSpec, resolvedOptions.paths, /*referrer*/ undefined); - const taskName = compileTaskName(ensureCompileTask(getOrCreateProjectGraph(resolvedProjectSpec, resolvedOptions.paths), resolvedOptions), resolvedOptions.typescript); + const projectGraph = getOrCreateProjectGraph(resolvedProjectSpec, resolvedOptions.paths); + projectGraph.isRoot = true; + const taskName = compileTaskName(ensureCompileTask(projectGraph, resolvedOptions), resolvedOptions.typescript); return () => new Promise((resolve, reject) => compilationGulp .fork(resolvedOptions.verbose) .start(taskName, err => err ? reject(err) : resolve())); @@ -74,10 +76,10 @@ exports.createCompiler = createCompiler; /** * Defines and executes a gulp orchestration for a TypeScript project. * @param {string} projectSpec The path to a tsconfig.json file or its containing directory. - * @param {ProjectOptions} [options] Project compilation options. + * @param {CompileOptions} [options] Project compilation options. * @returns {Promise} - * - * @typedef ProjectOptions + * + * @typedef CompileOptions * @property {string} [cwd] The path to use for the current working directory. Defaults to `process.cwd()`. * @property {string} [base] The path to use as the base for relative paths. Defaults to `cwd`. * @property {string} [typescript] A module specifier or path (relative to gulpfile.js) to the version of TypeScript to use. @@ -86,7 +88,7 @@ exports.createCompiler = createCompiler; * @property {boolean} [verbose] Indicates whether verbose logging is enabled. * @property {boolean} [force] Force recompilation (no up-to-date check). * @property {boolean} [inProcess] Indicates whether to run gulp-typescript in-process or out-of-process (default). - * + * * @typedef {(stream: NodeJS.ReadableStream) => NodeJS.ReadWriteStream} Hook */ function compile(projectSpec, options) { @@ -103,7 +105,9 @@ exports.compile = compile; function createCleaner(projectSpec, options) { const paths = resolvePathOptions(options); const resolvedProjectSpec = resolveProjectSpec(projectSpec, paths, /*referrer*/ undefined); - const taskName = cleanTaskName(ensureCleanTask(getOrCreateProjectGraph(resolvedProjectSpec, paths))); + const projectGraph = getOrCreateProjectGraph(resolvedProjectSpec, paths); + projectGraph.isRoot = true; + const taskName = cleanTaskName(ensureCleanTask(projectGraph)); return () => new Promise((resolve, reject) => compilationGulp .fork() .start(taskName, err => err ? reject(err) : resolve())); @@ -121,6 +125,25 @@ function clean(projectSpec, options) { } exports.clean = clean; +/** + * Defines a watcher to execute a gulp orchestration to recompile a TypeScript project. + * @param {string} projectSpec + * @param {WatchCallback | string[] | CompileOptions} [options] + * @param {WatchCallback | string[]} [tasks] + * @param {WatchCallback} [callback] + */ +function watch(projectSpec, options, tasks, callback) { + if (typeof tasks === "function") callback = tasks, tasks = /**@type {string[] | undefined}*/(undefined); + if (typeof options === "function") callback = options, tasks = /**@type {string[] | undefined}*/(undefined), options = /**@type {CompileOptions | undefined}*/(undefined); + if (Array.isArray(options)) tasks = options, options = /**@type {CompileOptions | undefined}*/(undefined); + const resolvedOptions = resolveCompileOptions(options); + const resolvedProjectSpec = resolveProjectSpec(projectSpec, resolvedOptions.paths, /*referrer*/ undefined); + const projectGraph = getOrCreateProjectGraph(resolvedProjectSpec, resolvedOptions.paths); + projectGraph.isRoot = true; + ensureWatcher(projectGraph, resolvedOptions, tasks, callback); +} +exports.watch = watch; + /** * Adds a named alias for a TypeScript language service path * @param {string} alias An alias for a TypeScript version. @@ -138,7 +161,7 @@ exports.addTypeScript = addTypeScript; * @param {string} projectSpec The path to a tsconfig.json file or its containing directory. * @param {string} flattenedProjectSpec The output path for the flattened tsconfig.json file. * @param {FlattenOptions} [options] Options used to flatten a project hierarchy. - * + * * @typedef FlattenOptions * @property {string} [cwd] The path to use for the current working directory. Defaults to `process.cwd()`. * @property {CompilerOptions} [compilerOptions] Compiler option overrides. @@ -167,7 +190,7 @@ function flatten(projectSpec, flattenedProjectSpec, options = {}) { } /** - * @param {ProjectGraph} projectGraph + * @param {ProjectGraph} projectGraph */ function recur(projectGraph) { if (skipProjects.has(projectGraph)) return; @@ -190,14 +213,14 @@ exports.flatten = flatten; * @param {string} typescript An unresolved module specifier to a TypeScript version. * @param {ResolvedPathOptions} paths Paths used to resolve `typescript`. * @returns {ResolvedTypeScript} - * + * * @typedef {string & {_isResolvedTypeScript: never}} ResolvedTypeScriptSpec - * + * * @typedef ResolvedTypeScript * @property {ResolvedTypeScriptSpec} typescript * @property {string} [alias] */ -function resolveTypeScript(typescript, paths) { +function resolveTypeScript(typescript = "default", paths) { let alias; while (typescriptAliasMap.has(typescript)) { ({ typescript, alias, paths } = typescriptAliasMap.get(typescript)); @@ -226,31 +249,34 @@ function getTaskNameSuffix(typescript, paths) { } /** @type {ResolvedPathOptions} */ -const defaultPaths = { cwd: process.cwd(), base: process.cwd() }; +const defaultPaths = (() => { + const cwd = /**@type {AbsolutePath}*/(normalizeSlashes(process.cwd())); + return { cwd, base: cwd }; +})(); /** * @param {PathOptions | undefined} options Path options to resolve and normalize. * @returns {ResolvedPathOptions} - * + * * @typedef PathOptions * @property {string} [cwd] The path to use for the current working directory. Defaults to `process.cwd()`. * @property {string} [base] The path to use as the base for relative paths. Defaults to `cwd`. - * + * * @typedef ResolvedPathOptions - * @property {string} cwd The path to use for the current working directory. Defaults to `process.cwd()`. - * @property {string} base The path to use as the base for relative paths. Defaults to `cwd`. + * @property {AbsolutePath} cwd The path to use for the current working directory. Defaults to `process.cwd()`. + * @property {AbsolutePath} base The path to use as the base for relative paths. Defaults to `cwd`. */ function resolvePathOptions(options) { - const cwd = options && options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd(); - const base = options && options.base ? path.resolve(cwd, options.base) : cwd; + const cwd = options && options.cwd ? resolvePath(defaultPaths.cwd, options.cwd) : defaultPaths.cwd; + const base = options && options.base ? resolvePath(cwd, options.base) : cwd; return cwd === defaultPaths.cwd && base === defaultPaths.base ? defaultPaths : { cwd, base }; } /** - * @param {ProjectOptions} [options] - * @returns {ResolvedProjectOptions} - * - * @typedef ResolvedProjectOptions + * @param {CompileOptions} [options] + * @returns {ResolvedCompileOptions} + * + * @typedef ResolvedCompileOptions * @property {ResolvedPathOptions} paths * @property {ResolvedTypeScript} typescript A resolved reference to a TypeScript implementation. * @property {Hook} [js] Pipeline hook for .js file outputs. @@ -259,9 +285,9 @@ function resolvePathOptions(options) { * @property {boolean} [force] Force recompilation (no up-to-date check). * @property {boolean} [inProcess] Indicates whether to run gulp-typescript in-process or out-of-process (default). */ -function resolveProjectOptions(options = {}) { +function resolveCompileOptions(options = {}) { const paths = resolvePathOptions(options); - const typescript = resolveTypeScript(options.typescript || "default", paths); + const typescript = resolveTypeScript(options.typescript, paths); return { paths, typescript, @@ -274,13 +300,13 @@ function resolveProjectOptions(options = {}) { } /** - * @param {ResolvedProjectOptions} left - * @param {ResolvedProjectOptions} right - * @returns {ResolvedProjectOptions} + * @param {ResolvedCompileOptions} left + * @param {ResolvedCompileOptions} right + * @returns {ResolvedCompileOptions} */ -function mergeProjectOptions(left, right) { +function mergeCompileOptions(left, right) { if (left.typescript !== right.typescript) throw new Error("Cannot merge project options targeting different TypeScript packages"); - if (tryReuseProjectOptions(left, right)) return left; + if (tryReuseCompileOptions(left, right)) return left; return { paths: left.paths, typescript: left.typescript, @@ -293,10 +319,10 @@ function mergeProjectOptions(left, right) { } /** - * @param {ResolvedProjectOptions} left - * @param {ResolvedProjectOptions} right + * @param {ResolvedCompileOptions} left + * @param {ResolvedCompileOptions} right */ -function tryReuseProjectOptions(left, right) { +function tryReuseCompileOptions(left, right) { return left === right || left.js === (right.js || left.js) && left.dts === (right.dts || left.dts) @@ -309,11 +335,13 @@ function tryReuseProjectOptions(left, right) { * @param {ResolvedProjectSpec} projectSpec * @param {ResolvedPathOptions} paths * @returns {UnqualifiedProjectName} - * + * * @typedef {string & {_isUnqualifiedProjectName:never}} UnqualifiedProjectName */ function getUnqualifiedProjectName(projectSpec, paths) { - return /**@type {UnqualifiedProjectName}*/(normalizeSlashes(path.relative(paths.base, projectSpec))); + let projectName = path.relative(paths.base, projectSpec); + if (path.basename(projectName) === "tsconfig.json") projectName = path.dirname(projectName); + return /**@type {UnqualifiedProjectName}*/(normalizeSlashes(projectName)); } /** @@ -321,16 +349,16 @@ function getUnqualifiedProjectName(projectSpec, paths) { * @param {ResolvedPathOptions} paths * @param {ResolvedTypeScript} typescript * @returns {QualifiedProjectName} - * + * * @typedef {string & {_isQualifiedProjectName:never}} QualifiedProjectName */ function getQualifiedProjectName(projectName, paths, typescript) { return /**@type {QualifiedProjectName}*/(projectName + getTaskNameSuffix(typescript, paths)); } -/** +/** * @typedef {import("../../lib/typescript").ParseConfigFileHost} ParseConfigFileHost - * @type {ParseConfigFileHost} + * @type {ParseConfigFileHost} */ const parseConfigFileHost = { useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, @@ -342,11 +370,11 @@ const parseConfigFileHost = { }; /** - * @param {string} [cwd] + * @param {AbsolutePath} [cwd] * @returns {ParseConfigFileHost} */ function getParseConfigFileHost(cwd) { - if (!cwd || cwd === process.cwd()) return parseConfigFileHost; + if (!cwd || cwd === defaultPaths.cwd) return parseConfigFileHost; return { useCaseSensitiveFileNames: parseConfigFileHost.useCaseSensitiveFileNames, fileExists: parseConfigFileHost.fileExists, @@ -363,14 +391,21 @@ function getParseConfigFileHost(cwd) { * @returns {ProjectGraph} * * @typedef ProjectGraph + * @property {ResolvedPathOptions} paths * @property {ResolvedProjectSpec} projectSpec The fully qualified path to the tsconfig.json of the project * @property {UnqualifiedProjectName} projectName The relative project name, excluding any TypeScript suffix. - * @property {string} projectDirectory The fully qualified path to the project directory. + * @property {AbsolutePath} projectDirectory The fully qualified path to the project directory. * @property {ParsedCommandLine} project The parsed tsconfig.json file. * @property {ProjectGraphReference[]} references An array of project references. + * @property {Set} referrers An array of referring projects. + * @property {Set} inputs A set of compilation inputs. + * @property {Set} outputs A set of compilation outputs. * @property {Map} configurations TypeScript-specific configurations for the project. * @property {boolean} cleanTaskCreated A value indicating whether a `clean:` task has been created for this project (not dependent on TypeScript version). - * + * @property {boolean} watcherCreated A value indicating whether a watcher has been created for this project. + * @property {boolean} isRoot The project graph is a root project reference. + * @property {Set} [allWatchers] Tasks to execute when the compilation has completed after being triggered by a watcher. + * * @typedef ProjectGraphReference * @property {ProjectGraph} source The referring project. * @property {ProjectGraph} target The referenced project. @@ -378,15 +413,22 @@ function getParseConfigFileHost(cwd) { function getOrCreateProjectGraph(projectSpec, paths) { let projectGraph = projectGraphCache.get(projectSpec); if (!projectGraph) { - const project = ts.getParsedCommandLineOfConfigFile(projectSpec, {}, getParseConfigFileHost(paths.cwd)); + const project = parseProject(projectSpec, paths); + const projectDirectory = parentDirectory(projectSpec); projectGraph = { + paths, projectSpec, projectName: getUnqualifiedProjectName(projectSpec, paths), - projectDirectory: path.dirname(projectSpec), + projectDirectory, project, references: [], + referrers: new Set(), + inputs: new Set(project.fileNames.map(file => resolvePath(projectDirectory, file))), + outputs: new Set(ts.getAllProjectOutputs(project).map(file => resolvePath(projectDirectory, file))), configurations: new Map(), - cleanTaskCreated: false + cleanTaskCreated: false, + watcherCreated: false, + isRoot: false }; projectGraphCache.set(projectSpec, projectGraph); if (project.projectReferences) { @@ -395,21 +437,80 @@ function getOrCreateProjectGraph(projectSpec, paths) { const referencedProject = getOrCreateProjectGraph(resolvedProjectSpec, paths); const reference = { source: projectGraph, target: referencedProject }; projectGraph.references.push(reference); + referencedProject.referrers.add(projectGraph); } } } return projectGraph; } +/** + * @param {ResolvedPathOptions} paths + */ +function createParseProject(paths) { + /** + * @param {string} configFilePath + */ + function getProject(configFilePath) { + const projectSpec = resolveProjectSpec(configFilePath, paths, /*referrer*/ undefined); + const projectGraph = getOrCreateProjectGraph(projectSpec, defaultPaths); + return projectGraph && projectGraph.project; + } + return getProject; +} + /** * @param {ProjectGraph} projectGraph - * @param {ResolvedProjectOptions} resolvedOptions + * @param {ParsedCommandLine} parsedProject + */ +function updateProjectGraph(projectGraph, parsedProject) { + projectGraph.project = parsedProject; + projectGraph.inputs = new Set(projectGraph.project.fileNames.map(file => resolvePath(projectGraph.projectDirectory, file))); + projectGraph.outputs = new Set(ts.getAllProjectOutputs(projectGraph.project).map(file => resolvePath(projectGraph.projectDirectory, file))); + + // Update project references. + const oldReferences = new Set(projectGraph.references.map(ref => ref.target)); + projectGraph.references = []; + if (projectGraph.project.projectReferences) { + for (const projectReference of projectGraph.project.projectReferences) { + const resolvedProjectSpec = resolveProjectSpec(projectReference.path, projectGraph.paths, projectGraph); + const referencedProject = getOrCreateProjectGraph(resolvedProjectSpec, projectGraph.paths); + const reference = { source: projectGraph, target: referencedProject }; + projectGraph.references.push(reference); + referencedProject.referrers.add(projectGraph); + oldReferences.delete(referencedProject); + } + } + + // Remove project references that have been removed from the project + for (const referencedProject of oldReferences) { + referencedProject.referrers.delete(projectGraph); + // If there are no more references to this project and the project was not directly requested, + // remove it from the cache. + if (referencedProject.referrers.size === 0 && !referencedProject.isRoot) { + projectGraphCache.delete(referencedProject.projectSpec); + } + } +} + +/** + * @param {ResolvedProjectSpec} projectSpec + * @param {ResolvedPathOptions} paths + */ +function parseProject(projectSpec, paths) { + return ts.getParsedCommandLineOfConfigFile(projectSpec, {}, getParseConfigFileHost(paths.cwd)); +} + +/** + * @param {ProjectGraph} projectGraph + * @param {ResolvedCompileOptions} resolvedOptions * @returns {ProjectGraphConfiguration} * * @typedef ProjectGraphConfiguration * @property {QualifiedProjectName} projectName - * @property {ResolvedProjectOptions} resolvedOptions - * @property {boolean} compileTaskCreated + * @property {ResolvedCompileOptions} resolvedOptions + * @property {boolean} compileTaskCreated A value indicating whether a `compile:` task has been created for this project. + * @property {Set} [watchers] Tasks to execute when the compilation has completed after being triggered by a watcher. */ function getOrCreateProjectGraphConfiguration(projectGraph, resolvedOptions) { let projectGraphConfig = projectGraph.configurations.get(resolvedOptions.typescript.typescript); @@ -424,18 +525,56 @@ function getOrCreateProjectGraphConfiguration(projectGraph, resolvedOptions) { return projectGraphConfig; } +/** + * Resolves a series of path steps as a normalized, canonical, and absolute path. + * @param {AbsolutePath} basePath + * @param {...string} paths + * @returns {AbsolutePath} + * + * @typedef {string & {_isResolvedPath:never}} AbsolutePath + */ +function resolvePath(basePath, ...paths) { + return /**@type {AbsolutePath}*/(normalizeSlashes(path.resolve(basePath, ...paths))); +} + +/** + * @param {AbsolutePath} from + * @param {AbsolutePath} to + * @returns {Path} + * + * @typedef {string & {_isRelativePath:never}} RelativePath + * @typedef {RelativePath | AbsolutePath} Path + */ +function relativePath(from, to) { + let relativePath = normalizeSlashes(path.relative(from, to)); + if (!relativePath) relativePath = "."; + if (path.isAbsolute(relativePath)) return /**@type {AbsolutePath}*/(relativePath); + if (relativePath.charAt(0) !== ".") relativePath = "./" + relativePath; + return /**@type {RelativePath}*/(relativePath); +} + +/** + * @param {AbsolutePath} file + * @returns {AbsolutePath} + */ +function parentDirectory(file) { + const dirname = path.dirname(file); + if (!dirname || dirname === file) return file; + return /**@type {AbsolutePath}*/(normalizeSlashes(dirname)); +} + /** * @param {string} projectSpec * @param {ResolvedPathOptions} paths * @param {ProjectGraph | undefined} referrer * @returns {ResolvedProjectSpec} - * - * @typedef {string & {_isResolvedProjectSpec: never}} ResolvedProjectSpec + * + * @typedef {AbsolutePath & {_isResolvedProjectSpec: never}} ResolvedProjectSpec */ function resolveProjectSpec(projectSpec, paths, referrer) { - projectSpec = path.resolve(paths.cwd, referrer && referrer.projectDirectory || "", projectSpec); - if (!ts.sys.fileExists(projectSpec)) projectSpec = path.join(projectSpec, "tsconfig.json"); - return /**@type {ResolvedProjectSpec}*/(normalizeSlashes(projectSpec)); + let projectPath = resolvePath(paths.cwd, referrer && referrer.projectDirectory || "", projectSpec); + if (!ts.sys.fileExists(projectPath)) projectPath = resolvePath(paths.cwd, projectPath, "tsconfig.json"); + return /**@type {ResolvedProjectSpec}*/(normalizeSlashes(projectPath)); } /** @@ -443,24 +582,24 @@ function resolveProjectSpec(projectSpec, paths, referrer) { * @param {ResolvedPathOptions} paths */ function resolveDestPath(projectGraph, paths) { - /** @type {string} */ + /** @type {AbsolutePath} */ let destPath = projectGraph.projectDirectory; if (projectGraph.project.options.outDir) { - destPath = path.resolve(paths.cwd, destPath, projectGraph.project.options.outDir); + destPath = resolvePath(paths.cwd, destPath, projectGraph.project.options.outDir); } else if (projectGraph.project.options.outFile || projectGraph.project.options.out) { - destPath = path.dirname(path.resolve(paths.cwd, destPath, projectGraph.project.options.outFile || projectGraph.project.options.out)); + destPath = parentDirectory(resolvePath(paths.cwd, destPath, projectGraph.project.options.outFile || projectGraph.project.options.out)); } - return normalizeSlashes(path.relative(paths.base, destPath)); + return relativePath(paths.base, destPath); } /** * @param {ProjectGraph} projectGraph - * @param {ResolvedProjectOptions} options + * @param {ResolvedCompileOptions} options */ function ensureCompileTask(projectGraph, options) { const projectGraphConfig = getOrCreateProjectGraphConfiguration(projectGraph, options); - projectGraphConfig.resolvedOptions = options = mergeProjectOptions(options, options); + projectGraphConfig.resolvedOptions = options = mergeCompileOptions(options, options); if (!projectGraphConfig.compileTaskCreated) { const deps = makeProjectReferenceCompileTasks(projectGraph, options.typescript, options.paths); compilationGulp.task(compileTaskName(projectGraph, options.typescript), deps, () => { @@ -473,7 +612,7 @@ function ensureCompileTask(projectGraph, options) { ? tsc.createProject(configFilePath, { typescript: require(options.typescript.typescript) }) : tsc_oop.createProject(configFilePath, {}, { typescript: options.typescript.typescript }); const stream = project.src() - .pipe(gulpif(!options.force, upToDate(projectGraph.project, { verbose: options.verbose }))) + .pipe(gulpif(!options.force, upToDate(projectGraph.project, { verbose: options.verbose, parseProject: createParseProject(options.paths) }))) .pipe(gulpif(sourceMap || inlineSourceMap, sourcemaps.init())) .pipe(project()); const js = (options.js ? options.js(stream.js) : stream.js) @@ -528,7 +667,300 @@ function makeProjectReferenceCleanTasks(projectGraph) { } /** - * @param {ProjectGraph} projectGraph + * @param {ProjectGraph} projectGraph + * @param {ResolvedCompileOptions} options + * @param {string[]} [tasks] + * @param {(err?: any) => void} [callback] + * + * @typedef Watcher + * @property {string[]} [tasks] + * @property {(err?: any) => void} [callback] + * + * @typedef WatcherRegistration + * @property {() => void} end + */ +function ensureWatcher(projectGraph, options, tasks, callback) { + ensureCompileTask(projectGraph, options); + if (!projectGraph.watcherCreated) { + projectGraph.watcherCreated = true; + makeProjectReferenceWatchers(projectGraph, options.typescript, options.paths); + createWatcher(projectGraph, options, () => { + for (const config of projectGraph.configurations.values()) { + const taskName = compileTaskName(projectGraph, config.resolvedOptions.typescript); + const task = compilationGulp.tasks[taskName]; + if (!task) continue; + possiblyTriggerRecompilation(config, task); + } + }); + } + if ((tasks && tasks.length) || callback) { + const projectGraphConfig = getOrCreateProjectGraphConfiguration(projectGraph, options); + if (!projectGraphConfig.watchers) projectGraphConfig.watchers = new Set(); + if (!projectGraph.allWatchers) projectGraph.allWatchers = new Set(); + + /** @type {Watcher} */ + const watcher = { tasks, callback }; + projectGraphConfig.watchers.add(watcher); + projectGraph.allWatchers.add(watcher); + + /** @type {WatcherRegistration} */ + const registration = { + end() { + projectGraphConfig.watchers.delete(watcher); + projectGraph.allWatchers.delete(watcher); + } + }; + return registration; + } +} + +/** + * @param {ProjectGraphConfiguration} config + * @param {import("orchestrator").Task} task + */ +function possiblyTriggerRecompilation(config, task) { + // if any of the task's dependencies are still running, wait until they are complete. + for (const dep of task.dep) { + if (compilationGulp.tasks[dep].running) { + setTimeout(possiblyTriggerRecompilation, 50, config, task); + return; + } + } + + triggerRecompilation(task, config); +} + +/** + * @param {import("orchestrator").Task} task + * @param {ProjectGraphConfiguration} config + */ +function triggerRecompilation(task, config) { + compilationGulp._resetTask(task); + if (config.watchers && config.watchers.size) { + compilationGulp.fork().start(task.name, () => { + /** @type {Set} */ + const taskNames = new Set(); + /** @type {((err?: any) => void)[]} */ + const callbacks = []; + for (const { tasks, callback } of config.watchers) { + if (tasks) for (const task of tasks) taskNames.add(task); + if (callback) callbacks.push(callback); + } + if (taskNames.size) { + gulp.start([...taskNames], error => { + for (const callback of callbacks) callback(error); + }); + } + else { + for (const callback of callbacks) callback(); + } + }); + } + else { + compilationGulp.fork(/*verbose*/ true).start(task.name); + } +} + +/** + * @param {ProjectGraph} projectGraph + * @param {ResolvedTypeScript} typescript + * @param {ResolvedPathOptions} paths + */ +function makeProjectReferenceWatchers(projectGraph, typescript, paths) { + for (const { target } of projectGraph.references) { + ensureWatcher(target, { paths, typescript }); + } +} + +/** + * @param {ProjectGraph} projectGraph + * @param {ResolvedCompileOptions} options + * @param {() => void} callback + */ +function createWatcher(projectGraph, options, callback) { + let projectRemoved = false; + let patterns = collectWatcherPatterns(projectGraph.projectSpec, projectGraph.project, projectGraph); + let watcher = /**@type {GulpWatcher}*/ (gulp.watch(patterns, { cwd: projectGraph.projectDirectory }, onWatchEvent)); + + /** + * @param {WatchEvent} event + */ + function onWatchEvent(event) { + const file = resolvePath(options.paths.cwd, event.path); + if (file === projectGraph.projectSpec) { + onProjectWatchEvent(event); + } + else { + onInputOrOutputChanged(file); + } + } + + /** + * @param {WatchEvent} event + */ + function onProjectWatchEvent(event) { + if (event.type === "renamed" || event.type === "deleted") { + onProjectRenamedOrDeleted(); + } + else { + onProjectCreatedOrModified(); + } + } + + function onProjectRenamedOrDeleted() { + // stop listening for file changes and wait for the project to be created again + projectRemoved = true; + watcher.end(); + watcher = /**@type {GulpWatcher}*/ (gulp.watch([projectGraph.projectSpec], onWatchEvent)); + } + + function onProjectCreatedOrModified() { + const newParsedProject = parseProject(projectGraph.projectSpec, options.paths); + const newPatterns = collectWatcherPatterns(projectGraph.projectSpec, newParsedProject, projectGraph); + if (projectRemoved || !sameValues(patterns, newPatterns)) { + projectRemoved = false; + watcher.end(); + updateProjectGraph(projectGraph, newParsedProject); + // Ensure we catch up with any added projects + for (const config of projectGraph.configurations.values()) { + if (config.watchers) { + makeProjectReferenceWatchers(projectGraph, config.resolvedOptions.typescript, config.resolvedOptions.paths); + } + } + patterns = newPatterns; + watcher = /**@type {GulpWatcher}*/ (gulp.watch(patterns, onWatchEvent)); + } + onProjectInvalidated(); + } + + function onProjectInvalidated() { + callback(); + } + + /** + * @param {AbsolutePath} file + */ + function onInputOrOutputChanged(file) { + if (projectGraph.inputs.has(file) || + projectGraph.references.some(ref => ref.target.outputs.has(file))) { + onProjectInvalidated(); + } + } +} + +/** + * @param {ResolvedProjectSpec} projectSpec + * @param {ParsedCommandLine} parsedProject + * @param {ProjectGraph} projectGraph + */ +function collectWatcherPatterns(projectSpec, parsedProject, projectGraph) { + const configFileSpecs = parsedProject.configFileSpecs; + + // NOTE: we do not currently handle files from `/// ` tags + const patterns = /**@type {string[]} */([]); + + // Add the project contents. + if (configFileSpecs) { + addIncludeSpecs(patterns, configFileSpecs.validatedIncludeSpecs); + addExcludeSpecs(patterns, configFileSpecs.validatedExcludeSpecs); + addIncludeSpecs(patterns, configFileSpecs.filesSpecs); + } + else { + addWildcardDirectories(patterns, parsedProject.wildcardDirectories); + addIncludeSpecs(patterns, parsedProject.fileNames); + } + + // Add the project itself. + addIncludeSpec(patterns, projectSpec); + + // TODO: Add the project base. + // addExtendsSpec(patterns, project.raw && project.raw.extends); + + // Add project reference outputs. + addProjectReferences(patterns, parsedProject.projectReferences); + + return patterns; + + /** + * @param {string[]} patterns + * @param {string | undefined} includeSpec + */ + function addIncludeSpec(patterns, includeSpec) { + if (!includeSpec) return; + patterns.push(includeSpec); + } + + /** + * @param {string[]} patterns + * @param {ReadonlyArray | undefined} includeSpecs + */ + function addIncludeSpecs(patterns, includeSpecs) { + if (!includeSpecs) return; + for (const includeSpec of includeSpecs) { + addIncludeSpec(patterns, includeSpec); + } + } + + /** + * @param {string[]} patterns + * @param {string | undefined} excludeSpec + */ + function addExcludeSpec(patterns, excludeSpec) { + if (!excludeSpec) return; + patterns.push("!" + excludeSpec); + } + + /** + * @param {string[]} patterns + * @param {ReadonlyArray | undefined} excludeSpecs + */ + function addExcludeSpecs(patterns, excludeSpecs) { + if (!excludeSpecs) return; + for (const excludeSpec of excludeSpecs) { + addExcludeSpec(patterns, excludeSpec); + } + } + + /** + * @param {string[]} patterns + * @param {ts.MapLike | undefined} wildcardDirectories + */ + function addWildcardDirectories(patterns, wildcardDirectories) { + if (!wildcardDirectories) return; + for (const dirname of Object.keys(wildcardDirectories)) { + const flags = wildcardDirectories[dirname]; + patterns.push(path.join(dirname, flags & ts.WatchDirectoryFlags.Recursive ? "**" : "", "*")); + } + } + + // TODO: Add the project base + // /** + // * @param {string[]} patterns + // * @param {string | undefined} base + // */ + // function addExtendsSpec(patterns, base) { + // if (!base) return; + // addIncludeSpec(patterns, base); + // } + + /** + * @param {string[]} patterns + * @param {ReadonlyArray} projectReferences + */ + function addProjectReferences(patterns, projectReferences) { + if (!projectReferences) return; + for (const projectReference of projectReferences) { + const resolvedProjectSpec = resolveProjectSpec(projectReference.path, projectGraph.paths, projectGraph); + const referencedProject = getOrCreateProjectGraph(resolvedProjectSpec, projectGraph.paths); + for (const output of referencedProject.outputs) { + patterns.push(output); + } + } + } +} + +/** + * @param {ProjectGraph} projectGraph * @param {ResolvedTypeScript} typescript */ function compileTaskName(projectGraph, typescript) { @@ -536,7 +968,7 @@ function compileTaskName(projectGraph, typescript) { } /** - * @param {ProjectGraph} projectGraph + * @param {ProjectGraph} projectGraph */ function cleanTaskName(projectGraph) { return `clean:${projectGraph.projectName}`; @@ -558,7 +990,32 @@ function isPath(moduleSpec) { } /** - * @typedef {import("../../lib/typescript").ParsedCommandLine & { options: CompilerOptions }} ParsedCommandLine + * @template T + * @param {ReadonlyArray} left + * @param {ReadonlyArray} right + */ +function sameValues(left, right) { + if (left === right) return true; + if (left.length !== right.length) return false; + for (let i = 0; i < left.length; i++) { + if (left[i] !== right[i]) return false; + } + return true; +} + +/** + * @typedef {import("../../lib/typescript").ParsedCommandLine & { options: CompilerOptions, configFileSpecs?: ConfigFileSpecs }} ParsedCommandLine * @typedef {import("../../lib/typescript").CompilerOptions & { configFilePath?: string }} CompilerOptions + * @typedef {import("../../lib/typescript").ProjectReference} ProjectReference + * @typedef {import("gulp").WatchEvent} WatchEvent + * @typedef {import("gulp").WatchCallback} WatchCallback + * @typedef {NodeJS.EventEmitter & { end(): void, add(files: string | string[], done?: () => void): void, remove(file: string): void }} GulpWatcher + * + * @typedef ConfigFileSpecs + * @property {ReadonlyArray | undefined} filesSpecs + * @property {ReadonlyArray | undefined} referenceSpecs + * @property {ReadonlyArray | undefined} validatedIncludeSpecs + * @property {ReadonlyArray | undefined} validatedExcludeSpecs + * @property {ts.MapLike} wildcardDirectories */ void 0; \ No newline at end of file diff --git a/scripts/build/rm.js b/scripts/build/rm.js index 616eaef4cff..0922954b49d 100644 --- a/scripts/build/rm.js +++ b/scripts/build/rm.js @@ -35,32 +35,28 @@ function rm(dest, opts) { */ write(file, _, cb) { if (failed) return; - if (Vinyl.isVinyl(file)) { - const basePath = typeof dest === "string" ? path.resolve(cwd, dest) : - typeof dest === "function" ? path.resolve(cwd, dest(file)) : - file.base; - const filePath = path.resolve(basePath, file.relative); - file.cwd = cwd; - file.base = basePath; - file.path = filePath; - const entry = { - file, - deleted: false, - cb, - promise: del(file.path).then(() => { - entry.deleted = true; - processDeleted(); - }, err => { - failed = true; - pending.length = 0; - cb(err); - }) - }; - pending.push(entry); - } - else { - cb(new Error("Only Vinyl files are supported.")); - } + if (typeof file === "string" || Buffer.isBuffer(file)) return cb(new Error("Only Vinyl files are supported.")); + const basePath = typeof dest === "string" ? path.resolve(cwd, dest) : + typeof dest === "function" ? path.resolve(cwd, dest(file)) : + file.base; + const filePath = path.resolve(basePath, file.relative); + file.cwd = cwd; + file.base = basePath; + file.path = filePath; + const entry = { + file, + deleted: false, + cb, + promise: del(file.path).then(() => { + entry.deleted = true; + processDeleted(); + }, err => { + failed = true; + pending.length = 0; + cb(err); + }) + }; + pending.push(entry); }, final(cb) { processDeleted(); diff --git a/scripts/build/tests.js b/scripts/build/tests.js index 60b72e64cdd..36615149d70 100644 --- a/scripts/build/tests.js +++ b/scripts/build/tests.js @@ -27,14 +27,16 @@ exports.localTest262Baseline = "internal/baselines/test262/local"; */ function runConsoleTests(runJs, defaultReporter, runInParallel) { let testTimeout = cmdLineOptions.timeout; + let tests = cmdLineOptions.tests; const lintFlag = cmdLineOptions.lint; const debug = cmdLineOptions.debug; const inspect = cmdLineOptions.inspect; - const tests = cmdLineOptions.tests; const runners = cmdLineOptions.runners; const light = cmdLineOptions.light; const stackTraceLimit = cmdLineOptions.stackTraceLimit; const testConfigFile = "test.config"; + const failed = cmdLineOptions.failed; + const keepFailed = cmdLineOptions.keepFailed || failed; return cleanTestDirs() .then(() => { if (fs.existsSync(testConfigFile)) { @@ -59,8 +61,8 @@ function runConsoleTests(runJs, defaultReporter, runInParallel) { testTimeout = 400000; } - if (tests || runners || light || testTimeout || taskConfigsFolder) { - writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, testTimeout); + if (tests || runners || light || testTimeout || taskConfigsFolder || keepFailed) { + writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, testTimeout, keepFailed); } const colors = cmdLineOptions.colors; @@ -75,7 +77,8 @@ function runConsoleTests(runJs, defaultReporter, runInParallel) { // timeout normally isn"t necessary but Travis-CI has been timing out on compiler baselines occasionally // default timeout is 2sec which really should be enough, but maybe we just need a small amount longer if (!runInParallel) { - args.push("-R", reporter); + args.push("-R", "scripts/failed-tests"); + args.push("-O", '"reporter=' + reporter + (keepFailed ? ",keepFailed=true" : "") + '"'); if (tests) { args.push("-g", `"${tests}"`); } @@ -103,7 +106,12 @@ function runConsoleTests(runJs, defaultReporter, runInParallel) { args.push(runJs); } setNodeEnvToDevelopment(); - return exec(host, [runJs]); + if (failed) { + return exec(host, ["scripts/run-failed-tests.js"].concat(args)); + } + else { + return exec(host, args); + } }) .then(({ exitCode }) => { if (exitCode !== 0) return finish(undefined, exitCode); @@ -148,8 +156,9 @@ exports.cleanTestDirs = cleanTestDirs; * @param {string | number} [workerCount] * @param {string} [stackTraceLimit] * @param {string | number} [timeout] + * @param {boolean} [keepFailed] */ -function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, timeout) { +function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, timeout, keepFailed) { const testConfigContents = JSON.stringify({ test: tests ? [tests] : undefined, runner: runners ? runners.split(",") : undefined, @@ -159,6 +168,7 @@ function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCou taskConfigsFolder, noColor: !cmdLineOptions.colors, timeout, + keepFailed }); log.info("Running tests with config: " + testConfigContents); fs.writeFileSync("test.config", testConfigContents); diff --git a/scripts/build/upToDate.js b/scripts/build/upToDate.js index 5f23726ed85..f92fd6b503a 100644 --- a/scripts/build/upToDate.js +++ b/scripts/build/upToDate.js @@ -15,6 +15,7 @@ const Vinyl = require("vinyl"); * * @typedef UpToDateOptions * @property {boolean} [verbose] + * @property {(configFilePath: string) => ParsedCommandLine | undefined} [parseProject] */ function upToDate(parsedProject, options) { /** @type {File[]} */ @@ -31,7 +32,8 @@ function upToDate(parsedProject, options) { }, getModifiedTime(fileName) { return getStat(fileName).mtime; - } + }, + parseConfigFile: options && options.parseProject }; const duplex = new Duplex({ objectMode: true, @@ -39,10 +41,9 @@ function upToDate(parsedProject, options) { * @param {string|Buffer|File} file */ write(file, _, cb) { - if (Vinyl.isVinyl(file)) { - inputs.push(file); - inputMap.set(path.resolve(file.path), file); - } + if (typeof file === "string" || Buffer.isBuffer(file)) return cb(new Error("Only Vinyl files are supported.")); + inputs.push(file); + inputMap.set(path.resolve(file.path), file); cb(); }, final(cb) { @@ -77,7 +78,6 @@ function upToDate(parsedProject, options) { module.exports = exports = upToDate; /** - * * @param {DiagnosticMessage} message * @param {...string} args */ diff --git a/scripts/failed-tests.d.ts b/scripts/failed-tests.d.ts new file mode 100644 index 00000000000..e82c0b178e8 --- /dev/null +++ b/scripts/failed-tests.d.ts @@ -0,0 +1,22 @@ +import Mocha = require("mocha"); + +export = FailedTestsReporter; + +declare class FailedTestsReporter extends Mocha.reporters.Base { + passes: Mocha.Test[]; + failures: Mocha.Test[]; + reporterOptions: FailedTestsReporter.ReporterOptions; + reporter?: Mocha.reporters.Base; + constructor(runner: Mocha.Runner, options?: { reporterOptions?: FailedTestsReporter.ReporterOptions }); + static writeFailures(file: string, passes: ReadonlyArray, failures: ReadonlyArray, keepFailed: boolean, done: (err?: NodeJS.ErrnoException) => void): void; + done(failures: number, fn?: (failures: number) => void): void; +} + +declare namespace FailedTestsReporter { + interface ReporterOptions { + file?: string; + keepFailed?: boolean; + reporter?: string | Mocha.ReporterConstructor; + reporterOptions?: any; + } +} \ No newline at end of file diff --git a/scripts/failed-tests.js b/scripts/failed-tests.js new file mode 100644 index 00000000000..ca26c432948 --- /dev/null +++ b/scripts/failed-tests.js @@ -0,0 +1,117 @@ +// @ts-check +const Mocha = require("mocha"); +const path = require("path"); +const fs = require("fs"); +const os = require("os"); + +/** + * .failed-tests reporter + * + * @typedef {Object} ReporterOptions + * @property {string} [file] + * @property {boolean} [keepFailed] + * @property {string|Mocha.ReporterConstructor} [reporter] + * @property {*} [reporterOptions] + */ +class FailedTestsReporter extends Mocha.reporters.Base { + /** + * @param {Mocha.Runner} runner + * @param {{ reporterOptions?: ReporterOptions }} [options] + */ + constructor(runner, options) { + super(runner, options); + if (!runner) return; + + const reporterOptions = this.reporterOptions = options.reporterOptions || {}; + if (reporterOptions.file === undefined) reporterOptions.file = ".failed-tests"; + if (reporterOptions.keepFailed === undefined) reporterOptions.keepFailed = false; + if (reporterOptions.reporter) { + /** @type {Mocha.ReporterConstructor} */ + let reporter; + if (typeof reporterOptions.reporter === "function") { + reporter = reporterOptions.reporter; + } + else if (Mocha.reporters[reporterOptions.reporter]) { + reporter = Mocha.reporters[reporterOptions.reporter]; + } + else { + try { + reporter = require(reporterOptions.reporter); + } + catch (_) { + reporter = require(path.resolve(process.cwd(), reporterOptions.reporter)); + } + } + + const newOptions = Object.assign({}, options, { reporterOptions: reporterOptions.reporterOptions || {} }); + this.reporter = new reporter(runner, newOptions); + } + + /** @type {Mocha.Test[]} */ + this.passes = []; + + /** @type {Mocha.Test[]} */ + this.failures = []; + + runner.on("pass", test => this.passes.push(test)); + runner.on("fail", test => this.failures.push(test)); + } + + /** + * @param {string} file + * @param {ReadonlyArray} passes + * @param {ReadonlyArray} failures + * @param {boolean} keepFailed + * @param {(err?: NodeJS.ErrnoException) => void} done + */ + static writeFailures(file, passes, failures, keepFailed, done) { + const failingTests = new Set(fs.existsSync(file) ? readTests() : undefined); + if (failingTests.size > 0) { + for (const test of passes) { + const title = test.fullTitle().trim(); + if (title) failingTests.delete(title); + } + } + for (const test of failures) { + const title = test.fullTitle().trim(); + if (title) failingTests.add(title); + } + if (failingTests.size > 0) { + const failed = Array.from(failingTests).join(os.EOL); + fs.writeFile(file, failed, "utf8", done); + } + else if (!keepFailed) { + fs.unlink(file, done); + } + else { + done(); + } + + function readTests() { + return fs.readFileSync(file, "utf8") + .split(/\r?\n/g) + .map(line => line.trim()) + .filter(line => line.length > 0); + } + } + + /** + * @param {number} failures + * @param {(failures: number) => void} [fn] + */ + done(failures, fn) { + FailedTestsReporter.writeFailures(this.reporterOptions.file, this.passes, this.failures, this.reporterOptions.keepFailed || this.stats.tests === 0, (err) => { + const reporter = this.reporter; + if (reporter && reporter.done) { + reporter.done(failures, fn); + } + else if (fn) { + fn(failures); + } + + if (err) console.error(err); + }); + } +} + +module.exports = FailedTestsReporter; \ No newline at end of file diff --git a/scripts/produceLKG.ts b/scripts/produceLKG.ts index 65802520ee3..a49b4fcbe49 100644 --- a/scripts/produceLKG.ts +++ b/scripts/produceLKG.ts @@ -17,7 +17,6 @@ async function produceLKG() { await copyLocalizedDiagnostics(); await buildProtocol(); await copyScriptOutputs(); - await buildTsc(); await copyDeclarationOutputs(); await writeGitAttributes(); } @@ -53,7 +52,7 @@ async function buildProtocol() { async function copyScriptOutputs() { await copyWithCopyright("cancellationToken.js"); - await copyWithCopyright("tsc.js"); + await copyWithCopyright("tsc.release.js", "tsc.js"); await copyWithCopyright("tsserver.js"); await copyWithCopyright("typescript.js"); await copyWithCopyright("typescriptServices.js"); @@ -61,10 +60,6 @@ async function copyScriptOutputs() { await copyWithCopyright("watchGuard.js"); } -async function buildTsc() { - await exec(path.join(source, "tsc.js"), [`-b -f ${path.join(root, "src/tsc/tsconfig.release.json")}`]); -} - async function copyDeclarationOutputs() { await copyWithCopyright("tsserverlibrary.d.ts"); await copyWithCopyright("typescript.d.ts"); @@ -75,9 +70,9 @@ async function writeGitAttributes() { await fs.writeFile(path.join(dest, ".gitattributes"), `* text eol=lf`, "utf-8"); } -async function copyWithCopyright(fileName: string) { +async function copyWithCopyright(fileName: string, destName = fileName) { const content = await fs.readFile(path.join(source, fileName), "utf-8"); - await fs.writeFile(path.join(dest, fileName), copyright + "\n" + content); + await fs.writeFile(path.join(dest, destName), copyright + "\n" + content); } async function copyFromBuiltLocal(fileName: string) { diff --git a/scripts/run-failed-tests.js b/scripts/run-failed-tests.js new file mode 100644 index 00000000000..876c4a34eec --- /dev/null +++ b/scripts/run-failed-tests.js @@ -0,0 +1,92 @@ +const spawn = require('child_process').spawn; +const os = require("os"); +const fs = require("fs"); +const path = require("path"); + +let grep; +try { + const failedTests = fs.readFileSync(".failed-tests", "utf8"); + grep = failedTests + .split(/\r?\n/g) + .map(test => test.trim()) + .filter(test => test.length > 0) + .map(escapeRegExp); +} +catch (e) { + grep = []; +} + +let args = []; +let waitForGrepValue = false; +let grepIndex = -1; +process.argv.slice(2).forEach((arg, index) => { + const [flag, value] = arg.split('='); + if (flag === "g" || flag === "grep") { + grepIndex = index - 1; + waitForGrepValue = arg !== flag; + if (!waitForGrepValue) grep.push(value.replace(/^"|"$/g, "")); + return; + } + if (waitForGrepValue) { + grep.push(arg.replace(/^"|"$/g, "")); + waitForGrepValue = false; + return; + } + args.push(arg); +}); + +let mocha = "./node_modules/mocha/bin/mocha"; +let grepOption; +let grepOptionValue; +let grepFile; +if (grep.length) { + grepOption = "--grep"; + grepOptionValue = grep.join("|"); + if (grepOptionValue.length > 20) { + grepFile = path.resolve(os.tmpdir(), ".failed-tests.opts"); + fs.writeFileSync(grepFile, `--grep ${grepOptionValue}`, "utf8"); + grepOption = "--opts"; + grepOptionValue = grepFile; + mocha = "./node_modules/mocha/bin/_mocha"; + } +} + +if (grepOption) { + if (grepIndex >= 0) { + args.splice(grepIndex, 0, grepOption, grepOptionValue); + } + else { + args.push(grepOption, grepOptionValue); + } +} + +args.unshift(path.resolve(mocha)); + +console.log(args.join(" ")); +const proc = spawn(process.execPath, args, { + stdio: 'inherit' +}); +proc.on('exit', (code, signal) => { + process.on('exit', () => { + if (grepFile) { + fs.unlinkSync(grepFile); + } + + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code); + } + }); +}); + +process.on('SIGINT', () => { + proc.kill('SIGINT'); + proc.kill('SIGTERM'); +}); + +function escapeRegExp(pattern) { + return pattern + .replace(/[^-\w\d\s]/g, match => "\\" + match) + .replace(/\s/g, "\\s"); +} \ No newline at end of file diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e47a28359d5..27e1b8117f4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4650,15 +4650,15 @@ namespace ts { let jsDocType: Type | undefined; for (const declaration of symbol.declarations) { let declarationInConstructor = false; - const expression = declaration.kind === SyntaxKind.BinaryExpression ? declaration : - declaration.kind === SyntaxKind.PropertyAccessExpression ? cast(declaration.parent, isBinaryExpression) : + const expression = isBinaryExpression(declaration) ? declaration : + isPropertyAccessExpression(declaration) ? isBinaryExpression(declaration.parent) ? declaration.parent : declaration : undefined; if (!expression) { return errorType; } - const special = getSpecialPropertyAssignmentKind(expression); + const special = isPropertyAccessExpression(expression) ? getSpecialPropertyAccessKind(expression) : getSpecialPropertyAssignmentKind(expression); if (special === SpecialPropertyAssignmentKind.ThisProperty) { const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false); // Properties defined in a constructor (or base constructor, or javascript constructor function) don't get undefined added. @@ -4687,7 +4687,7 @@ namespace ts { errorNextVariableOrPropertyDeclarationMustHaveSameType(jsDocType, declaration, declarationType); } } - else if (!jsDocType) { + else if (!jsDocType && isBinaryExpression(expression)) { // If we don't have an explicit JSDoc type, get the type from the expression. let type = getWidenedLiteralType(checkExpressionCached(expression.right)); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2ea1b32a1e6..ff905db8dbf 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1029,23 +1029,6 @@ namespace ts { return array.slice().sort(comparer); } - export function best(iter: Iterator, isBetter: (a: T, b: T) => boolean): T | undefined { - const x = iter.next(); - if (x.done) { - return undefined; - } - let best = x.value; - while (true) { - const { value, done } = iter.next(); - if (done) { - return best; - } - if (isBetter(value, best)) { - best = value; - } - } - } - export function arrayIterator(array: ReadonlyArray): Iterator { let i = 0; return { next: () => { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index a4f7f8b82e5..f269d0be373 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4419,12 +4419,20 @@ "category": "Message", "code": 95060 }, - "Add missing enum member '{0}'": { + "Convert default export to named export": { "category": "Message", "code": 95061 }, - "Add all missing enum members": { + "Convert named export to default export": { "category": "Message", "code": 95062 + }, + "Add missing enum member '{0}'": { + "category": "Message", + "code": 95063 + }, + "Add all missing enum members": { + "category": "Message", + "code": 95064 } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ea0ec323798..99d5b1b663f 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -864,13 +864,9 @@ namespace ts { } function addJSDocComment(node: T): T { - const comments = getJSDocCommentRanges(node, sourceFile.text); - if (comments) { - for (const comment of comments) { - node.jsDoc = append(node.jsDoc, JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); - } - } - + Debug.assert(!node.jsDoc); // Should only be called once per node + const jsDoc = mapDefined(getJSDocCommentRanges(node, sourceFile.text), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); + if (jsDoc.length) node.jsDoc = jsDoc; return node; } @@ -6334,19 +6330,18 @@ namespace ts { Debug.assert(start <= end); Debug.assert(end <= content.length); + // Check for /** (JSDoc opening part) + if (!isJSDocLikeText(content, start)) { + return undefined; + } + let tags: JSDocTag[]; let tagsPos: number; let tagsEnd: number; const comments: string[] = []; - let result: JSDoc | undefined; - - // Check for /** (JSDoc opening part) - if (!isJSDocLikeText(content, start)) { - return result; - } // + 3 for leading /**, - 5 in total for /** */ - scanner.scanRange(start + 3, length - 5, () => { + return scanner.scanRange(start + 3, length - 5, () => { // Initially we can parse out a tag. We also have seen a starting asterisk. // This is so that /** * @type */ doesn't parse. let state = JSDocState.SawAsterisk; @@ -6432,11 +6427,9 @@ namespace ts { } removeLeadingNewlines(comments); removeTrailingNewlines(comments); - result = createJSDocComment(); + return createJSDocComment(); }); - return result; - function removeLeadingNewlines(comments: string[]) { while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { comments.shift(); @@ -6574,6 +6567,16 @@ namespace ts { indent += whitespace.length; } break; + case SyntaxKind.OpenBraceToken: + state = JSDocState.SavingComments; + if (lookAhead(() => nextJSDocToken() === SyntaxKind.AtToken && tokenIsIdentifierOrKeyword(nextJSDocToken()) && scanner.getTokenText() === "link")) { + pushComment(scanner.getTokenText()); + nextJSDocToken(); + pushComment(scanner.getTokenText()); + nextJSDocToken(); + } + pushComment(scanner.getTokenText()); + break; case SyntaxKind.AsteriskToken: if (state === JSDocState.BeginningOfLine) { // leading asterisks start recording on the *next* (non-whitespace) token diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 2c62e44fadf..9c933b44d01 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1887,6 +1887,14 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } const lhs = expr.left; + if (isEntityNameExpression(lhs.expression) && lhs.name.escapedText === "prototype" && isObjectLiteralExpression(getInitializerOfBinaryExpression(expr))) { + // F.prototype = { ... } + return SpecialPropertyAssignmentKind.Prototype; + } + return getSpecialPropertyAccessKind(lhs); + } + + export function getSpecialPropertyAccessKind(lhs: PropertyAccessExpression): SpecialPropertyAssignmentKind { if (lhs.expression.kind === SyntaxKind.ThisKeyword) { return SpecialPropertyAssignmentKind.ThisProperty; } @@ -1895,11 +1903,7 @@ namespace ts { return SpecialPropertyAssignmentKind.ModuleExports; } else if (isEntityNameExpression(lhs.expression)) { - if (lhs.name.escapedText === "prototype" && isObjectLiteralExpression(getInitializerOfBinaryExpression(expr))) { - // F.prototype = { ... } - return SpecialPropertyAssignmentKind.Prototype; - } - else if (isPrototypeAccess(lhs.expression)) { + if (isPrototypeAccess(lhs.expression)) { // F.G....prototype.x = expr return SpecialPropertyAssignmentKind.PrototypeProperty; } @@ -5844,7 +5848,7 @@ namespace ts { // Keywords /* @internal */ - export function isModifierKind(token: SyntaxKind): boolean { + export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] { switch (token) { case SyntaxKind.AbstractKeyword: case SyntaxKind.AsyncKeyword: diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 177d98c37ba..50bf29339c6 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -451,6 +451,12 @@ namespace FourSlash { this.selectionEnd = end.position; } + public selectAllInFile(fileName: string) { + this.openFile(fileName); + this.goToPosition(0); + this.selectionEnd = this.activeFile.content.length; + } + public selectRange(range: Range): void { this.goToRangeStart(range); this.selectionEnd = range.end; @@ -850,7 +856,7 @@ namespace FourSlash { private verifyCompletionsWorker(options: FourSlashInterface.VerifyCompletionsOptions): void { const actualCompletions = this.getCompletionListAtCaret({ ...options.preferences, triggerCharacter: options.triggerCharacter })!; if (!actualCompletions) { - if (options.exact === undefined) return; + if ("exact" in options && options.exact === undefined) return; this.raiseError(`No completions at position '${this.currentCaretPosition}'.`); } @@ -1382,17 +1388,8 @@ Actual: ${stringify(fullActual)}`); } } - public verifyRenameLocations(startRanges: ArrayOrSingle, options: Range[] | { findInStrings?: boolean, findInComments?: boolean, ranges: Range[] }) { - let findInStrings: boolean, findInComments: boolean, ranges: Range[]; - if (ts.isArray(options)) { - findInStrings = findInComments = false; - ranges = options; - } - else { - findInStrings = !!options.findInStrings; - findInComments = !!options.findInComments; - ranges = options.ranges; - } + public verifyRenameLocations(startRanges: ArrayOrSingle, options: ReadonlyArray | { findInStrings?: boolean, findInComments?: boolean, ranges: ReadonlyArray }) { + const { findInStrings = false, findInComments = false, ranges = this.getRanges() } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options } : options; for (const startRange of toArray(startRanges)) { this.goToRangeStart(startRange); @@ -1403,30 +1400,12 @@ Actual: ${stringify(fullActual)}`); break; } - let references = this.languageService.findRenameLocations( + const references = this.languageService.findRenameLocations( this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments); - ranges = ranges || this.getRanges(); - - if (!references) { - if (ranges.length !== 0) { - this.raiseError(`Expected ${ranges.length} rename locations; got none.`); - } - return; - } - - if (ranges.length !== references.length) { - this.raiseError("Rename location count does not match result.\n\nExpected: " + stringify(ranges) + "\n\nActual:" + stringify(references)); - } - - ranges = ranges.sort((r1, r2) => r1.pos - r2.pos); - references = references.sort((r1, r2) => r1.textSpan.start - r2.textSpan.start); - - ts.zipWith(references, ranges, (reference, range) => { - if (reference.textSpan.start !== range.pos || ts.textSpanEnd(reference.textSpan) !== range.end) { - this.raiseError("Rename location results do not match.\n\nExpected: " + stringify(ranges) + "\n\nActual:" + stringify(references)); - } - }); + const sort = (locations: ReadonlyArray | undefined) => + locations && ts.sort(locations, (r1, r2) => ts.compareStringsCaseSensitive(r1.fileName, r2.fileName) || r1.textSpan.start - r2.textSpan.start); + assert.deepEqual(sort(references), sort(ranges.map((r): ts.RenameLocation => ({ fileName: r.fileName, textSpan: ts.createTextSpanFromRange(r) })))); } } @@ -3090,9 +3069,22 @@ Actual: ${stringify(fullActual)}`); this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); } - const { renamePosition, newContent } = parseNewContent(); + let renameFilename: string | undefined; + let renamePosition: number | undefined; - this.verifyCurrentFileContent(newContent); + const newFileContents = typeof newContentWithRenameMarker === "string" ? { [this.activeFile.fileName]: newContentWithRenameMarker } : newContentWithRenameMarker; + for (const fileName in newFileContents) { + const { renamePosition: rp, newContent } = TestState.parseNewContent(newFileContents[fileName]); + if (renamePosition === undefined) { + renameFilename = fileName; + renamePosition = rp; + } + else { + ts.Debug.assert(rp === undefined); + } + this.verifyFileContent(fileName, newContent); + + } if (renamePosition === undefined) { if (editInfo.renameLocation !== undefined) { @@ -3100,22 +3092,21 @@ Actual: ${stringify(fullActual)}`); } } else { - // TODO: test editInfo.renameFilename value - assert.isDefined(editInfo.renameFilename); + this.assertObjectsEqual(editInfo.renameFilename, renameFilename); if (renamePosition !== editInfo.renameLocation) { this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`); } } + } - function parseNewContent(): { renamePosition: number | undefined, newContent: string } { - const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/"); - if (renamePosition === -1) { - return { renamePosition: undefined, newContent: newContentWithRenameMarker }; - } - else { - const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length); - return { renamePosition, newContent }; - } + private static parseNewContent(newContentWithRenameMarker: string): { readonly renamePosition: number | undefined, readonly newContent: string } { + const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/"); + if (renamePosition === -1) { + return { renamePosition: undefined, newContent: newContentWithRenameMarker }; + } + else { + const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length); + return { renamePosition, newContent }; } } @@ -3966,6 +3957,10 @@ namespace FourSlashInterface { this.state.select(startMarker, endMarker); } + public selectAllInFile(fileName: string) { + this.state.selectAllInFile(fileName); + } + public selectRange(range: FourSlash.Range): void { this.state.selectRange(range); } @@ -4736,7 +4731,7 @@ namespace FourSlashInterface { refactorName: string; actionName: string; actionDescription: string; - newContent: string; + newContent: NewFileContent; } export type ExpectedCompletionEntry = string | { @@ -4798,9 +4793,11 @@ namespace FourSlashInterface { filesToSearch?: ReadonlyArray; } + export type NewFileContent = string | { readonly [filename: string]: string }; + export interface NewContentOptions { // Exactly one of these should be defined. - newFileContent?: string | { readonly [filename: string]: string }; + newFileContent?: NewFileContent; newRangeContent?: string; } diff --git a/src/lib/es2015.collection.d.ts b/src/lib/es2015.collection.d.ts index 06a3473076f..fd49ca1f25b 100644 --- a/src/lib/es2015.collection.d.ts +++ b/src/lib/es2015.collection.d.ts @@ -30,8 +30,7 @@ interface WeakMap { } interface WeakMapConstructor { - new (): WeakMap; - new (entries?: ReadonlyArray<[K, V]> | null): WeakMap; + new (entries?: ReadonlyArray<[K, V]> | null): WeakMap; readonly prototype: WeakMap; } declare var WeakMap: WeakMapConstructor; diff --git a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl index e3d5959f0c7..98bdf52817b 100644 --- a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -487,7 +487,7 @@ - + @@ -496,7 +496,7 @@ - + diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d703f991cd3..0801c42405c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -692,6 +692,13 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } + /* @internal */ + forEachProject(cb: (project: Project) => void) { + for (const p of this.inferredProjects) cb(p); + this.configuredProjects.forEach(cb); + this.externalProjects.forEach(cb); + } + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { return ensureProject ? this.ensureDefaultProjectForFile(fileName) : this.tryGetDefaultProjectForFile(fileName); } @@ -750,6 +757,14 @@ namespace ts.server { return info && info.getPreferences() || this.hostConfiguration.preferences; } + getHostFormatCodeOptions(): FormatCodeSettings { + return this.hostConfiguration.formatCodeOptions; + } + + getHostPreferences(): UserPreferences { + return this.hostConfiguration.preferences; + } + private onSourceFileChanged(fileName: string, eventKind: FileWatcherEventKind, path: Path) { const info = this.getScriptInfoForPath(path); if (!info) { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 49fb54a6b10..5e159954956 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -627,9 +627,8 @@ namespace ts.server.protocol { arguments: GetEditsForFileRenameRequestArgs; } - // Note: The file from FileRequestArgs is just any file in the project. - // We will generate code changes for every file in that project, so the choice is arbitrary. - export interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { + /** Note: Paths may also be directories. */ + export interface GetEditsForFileRenameRequestArgs { readonly oldFilePath: string; readonly newFilePath: string; } diff --git a/src/server/session.ts b/src/server/session.ts index a70f240188b..550575ca2e8 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1142,7 +1142,7 @@ namespace ts.server { return this.getPosition(args, scriptInfo); } - private getFileAndProject(args: protocol.FileRequestArgs): { file: NormalizedPath, project: Project } { + private getFileAndProject(args: protocol.FileRequestArgs): FileAndProject { return this.getFileAndProjectWorker(args.file, args.projectFileName); } @@ -1738,9 +1738,23 @@ namespace ts.server { } private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { - const { file, project } = this.getFileAndProject(args); - const changes = project.getLanguageService().getEditsForFileRename(toNormalizedPath(args.oldFilePath), toNormalizedPath(args.newFilePath), this.getFormatOptions(file), this.getPreferences(file)); - return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; + const oldPath = toNormalizedPath(args.oldFilePath); + const newPath = toNormalizedPath(args.newFilePath); + const formatOptions = this.getHostFormatOptions(); + const preferences = this.getHostPreferences(); + + const changes: (protocol.FileCodeEdits | FileTextChanges)[] = []; + this.projectService.forEachProject(project => { + if (project.isOrphan() || !project.languageServiceEnabled) return; + for (const fileTextChanges of project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences)) { + // Subsequent projects may make conflicting edits to the same file -- just go with the first. + if (!changes.some(f => f.fileName === fileTextChanges.fileName)) { + changes.push(simplifiedResult ? this.mapTextChangeToCodeEdit(project, fileTextChanges) : fileTextChanges); + } + } + }); + + return changes as ReadonlyArray | ReadonlyArray; } private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray | undefined { @@ -1810,10 +1824,12 @@ namespace ts.server { } private mapTextChangesToCodeEdits(project: Project, textChanges: ReadonlyArray): protocol.FileCodeEdits[] { - return textChanges.map(change => { - const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName)); - return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path)); - }); + return textChanges.map(change => this.mapTextChangeToCodeEdit(project, change)); + } + + private mapTextChangeToCodeEdit(project: Project, change: FileTextChanges): protocol.FileCodeEdits { + const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName)); + return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path)); } private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { @@ -2303,6 +2319,19 @@ namespace ts.server { private getPreferences(file: NormalizedPath): UserPreferences { return this.projectService.getPreferences(file); } + + private getHostFormatOptions(): FormatCodeSettings { + return this.projectService.getHostFormatCodeOptions(); + } + + private getHostPreferences(): UserPreferences { + return this.projectService.getHostPreferences(); + } + } + + interface FileAndProject { + readonly file: NormalizedPath; + readonly project: Project; } function mapTextChangesToCodeEdits(textChanges: FileTextChanges, sourceFile: SourceFile | undefined): protocol.FileCodeEdits { diff --git a/src/services/completions.ts b/src/services/completions.ts index 21d704661b2..59c280ae4fc 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -961,8 +961,7 @@ namespace ts.Completions { break; case SyntaxKind.BinaryExpression: - if (!((parent as BinaryExpression).left.flags & NodeFlags.ThisNodeHasError)) { - // It has a left-hand side, so we're not in an opening JSX tag. + if (!binaryExpressionMayBeOpenTag(parent as BinaryExpression)) { break; } // falls through @@ -2256,7 +2255,7 @@ namespace ts.Completions { return isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; case "<": // Opening JSX tag - return contextToken.kind === SyntaxKind.LessThanToken && contextToken.parent.kind !== SyntaxKind.BinaryExpression; + return contextToken.kind === SyntaxKind.LessThanToken && (!isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent)); case "/": return isStringLiteralLike(contextToken) ? !!tryGetImportFromModuleSpecifier(contextToken) @@ -2266,6 +2265,10 @@ namespace ts.Completions { } } + function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean { + return nodeIsMissing(left); + } + function isStringLiteralOrTemplate(node: Node): node is StringLiteralLike | TemplateExpression | TaggedTemplateExpression { switch (node.kind) { case SyntaxKind.StringLiteral: diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 458b12c2ada..6976cd2ea20 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -187,15 +187,8 @@ namespace ts.DocumentHighlights { }); } - function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): Node[] { - const modifierFlag = modifierToFlag(modifier); - return mapDefined(getNodesToSearchForModifier(declaration, modifierFlag), node => { - if (getModifierFlags(node) & modifierFlag) { - const mod = find(node.modifiers!, m => m.kind === modifier); - Debug.assert(!!mod); - return mod; - } - }); + function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] { + return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier)); } function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): ReadonlyArray | undefined { diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 0fbcd5cd6e9..043856e4ab1 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -590,6 +590,30 @@ namespace ts.FindAllReferences.Core { } } + export function eachExportReference( + sourceFiles: ReadonlyArray, + checker: TypeChecker, + cancellationToken: CancellationToken | undefined, + exportSymbol: Symbol, + exportingModuleSymbol: Symbol, + exportName: string, + isDefaultExport: boolean, + cb: (ref: Identifier) => void, + ): void { + const importTracker = createImportTracker(sourceFiles, arrayToSet(sourceFiles, f => f.fileName), checker, cancellationToken); + const { importSearches, indirectUsers } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ExportKind.Default : ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false); + for (const [importLocation] of importSearches) { + cb(importLocation); + } + for (const indirectUser of indirectUsers) { + for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) { + if (isIdentifier(node) && checker.getSymbolAtLocation(node) === exportSymbol) { + cb(node); + } + } + } + } + function shouldAddSingleReference(singleRef: Identifier | StringLiteral, state: State): boolean { if (!hasMatchingMeaning(singleRef, state)) return false; if (!state.options.isForRename) return true; diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index d91dc63d92e..7060ffad612 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -99,7 +99,7 @@ namespace ts.GoToDefinition { */ function symbolMatchesSignature(s: Symbol, calledDeclaration: SignatureDeclaration) { return s === calledDeclaration.symbol || s === calledDeclaration.symbol.parent || - isVariableDeclaration(calledDeclaration.parent) && s === calledDeclaration.parent.symbol; + !isCallLikeExpression(calledDeclaration.parent) && s === calledDeclaration.parent.symbol; } export function getReferenceAtPosition(sourceFile: SourceFile, position: number, program: Program): { fileName: string, file: SourceFile } | undefined { diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 7861f28402a..77b3c02c80b 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -12,7 +12,7 @@ namespace ts.FindAllReferences { export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult; /** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */ - export function createImportTracker(sourceFiles: ReadonlyArray, sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken): ImportTracker { + export function createImportTracker(sourceFiles: ReadonlyArray, sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker { const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken); return (exportSymbol, exportInfo, isForRename) => { const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken); @@ -43,7 +43,7 @@ namespace ts.FindAllReferences { allDirectImports: Map, { exportingModuleSymbol, exportKind }: ExportInfo, checker: TypeChecker, - cancellationToken: CancellationToken + cancellationToken: CancellationToken | undefined, ): { directImports: Importer[], indirectUsers: ReadonlyArray } { const markSeenDirectImport = nodeSeenTracker(); const markSeenIndirectUser = nodeSeenTracker(); @@ -80,7 +80,7 @@ namespace ts.FindAllReferences { continue; } - cancellationToken.throwIfCancellationRequested(); + if (cancellationToken) cancellationToken.throwIfCancellationRequested(); switch (direct.kind) { case SyntaxKind.CallExpression: @@ -363,11 +363,11 @@ namespace ts.FindAllReferences { } /** Returns a map from a module symbol Id to all import statements that directly reference the module. */ - function getDirectImportsMap(sourceFiles: ReadonlyArray, checker: TypeChecker, cancellationToken: CancellationToken): Map { + function getDirectImportsMap(sourceFiles: ReadonlyArray, checker: TypeChecker, cancellationToken: CancellationToken | undefined): Map { const map = createMap(); for (const sourceFile of sourceFiles) { - cancellationToken.throwIfCancellationRequested(); + if (cancellationToken) cancellationToken.throwIfCancellationRequested(); forEachImport(sourceFile, (importDecl, moduleSpecifier) => { const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); if (moduleSymbol) { diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index bfd371d1154..136ac7b09eb 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -263,11 +263,7 @@ namespace ts.JsDoc { return { newText: singleLineResult, caretOffset: 3 }; } - const posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position); - const lineStart = sourceFile.getLineStarts()[posLineAndChar.line]; - - // replace non-whitespace characters in prefix with spaces. - const indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character).replace(/\S/i, () => " "); + const indentationStr = getIndentationStringAtPosition(sourceFile, position); // A doc comment consists of the following // * The opening comment line @@ -276,8 +272,7 @@ namespace ts.JsDoc { // * TODO: other tags. // * the closing comment line // * if the caret was directly in front of the object, then we add an extra line and indentation. - const preamble = "/**" + newLine + - indentationStr + " * "; + const preamble = "/**" + newLine + indentationStr + " * "; const result = preamble + newLine + parameterDocComments(parameters, hasJavaScriptFileExtension(sourceFile.fileName), indentationStr, newLine) + @@ -287,6 +282,14 @@ namespace ts.JsDoc { return { newText: result, caretOffset: preamble.length }; } + function getIndentationStringAtPosition(sourceFile: SourceFile, position: number): string { + const { text } = sourceFile; + const lineStart = getLineStartPositionForPosition(position, sourceFile); + let pos = lineStart; + for (; pos <= position && isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++); + return text.slice(lineStart, pos); + } + function parameterDocComments(parameters: ReadonlyArray, isJavaScriptFile: boolean, indentationStr: string, newLine: string): string { return parameters.map(({ name, dotDotDotToken }, i) => { const paramName = name.kind === SyntaxKind.Identifier ? name.text : "param" + i; @@ -303,6 +306,7 @@ namespace ts.JsDoc { for (let commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) { switch (commentOwner.kind) { case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: case SyntaxKind.MethodDeclaration: case SyntaxKind.Constructor: case SyntaxKind.MethodSignature: diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts new file mode 100644 index 00000000000..72c72e719b7 --- /dev/null +++ b/src/services/refactors/convertExport.ts @@ -0,0 +1,212 @@ +/* @internal */ +namespace ts.refactor { + const refactorName = "Convert export"; + const actionNameDefaultToNamed = "Convert default export to named export"; + const actionNameNamedToDefault = "Convert named export to default export"; + registerRefactor(refactorName, { + getAvailableActions(context): ApplicableRefactorInfo[] | undefined { + const info = getInfo(context); + if (!info) return undefined; + const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; + const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; + return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.assertDefined(getInfo(context)), t, context.cancellationToken)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + }, + }); + + // If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. + type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement; + interface Info { + readonly exportNode: ExportToConvert; + readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. + readonly wasDefault: boolean; + readonly exportingModuleSymbol: Symbol; + } + + function getInfo(context: RefactorContext): Info | undefined { + const { file } = context; + const span = getRefactorContextSpan(context); + const token = getTokenAtPosition(file, span.start, /*includeJsDocComment*/ false); + const exportNode = getParentNodeInSpan(token, file, span); + if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { + return undefined; + } + + const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; + + const flags = getModifierFlags(exportNode); + const wasDefault = !!(flags & ModifierFlags.Default); + // If source file already has a default export, don't offer refactor. + if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { + return undefined; + } + + switch (exportNode.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ModuleDeclaration: { + const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; + return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined; + } + case SyntaxKind.VariableStatement: { + const vs = exportNode as VariableStatement; + // Must be `export const x = something;`. + if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { + return undefined; + } + const decl = first(vs.declarationList.declarations); + if (!decl.initializer) return undefined; + Debug.assert(!wasDefault); + return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined; + } + default: + return undefined; + } + } + + function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { + changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); + changeImports(program, info, changes, cancellationToken); + } + + function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void { + if (wasDefault) { + changes.deleteNode(exportingSourceFile, Debug.assertDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword))); + } + else { + const exportKeyword = Debug.assertDefined(findModifier(exportNode, SyntaxKind.ExportKeyword)); + switch (exportNode.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + changes.insertNodeAfter(exportingSourceFile, exportKeyword, createToken(SyntaxKind.DefaultKeyword)); + break; + case SyntaxKind.VariableStatement: + // If 'x' isn't used in this file, `export const x = 0;` --> `export default 0;` + if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile)) { + // We checked in `getInfo` that an initializer exists. + changes.replaceNode(exportingSourceFile, exportNode, createExportDefault(Debug.assertDefined(first(exportNode.declarationList.declarations).initializer))); + break; + } + // falls through + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ModuleDeclaration: + // `export type T = number;` -> `type T = number; export default T;` + changes.deleteModifier(exportingSourceFile, exportKeyword); + changes.insertNodeAfter(exportingSourceFile, exportNode, createExportDefault(createIdentifier(exportName.text))); + break; + default: + Debug.assertNever(exportNode); + } + } + } + + function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { + const checker = program.getTypeChecker(); + const exportSymbol = Debug.assertDefined(checker.getSymbolAtLocation(exportName)); + FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { + const importingSourceFile = ref.getSourceFile(); + if (wasDefault) { + changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); + } + else { + changeNamedToDefaultImport(importingSourceFile, ref, changes); + } + }); + } + + function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker, exportName: string): void { + const { parent } = ref; + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + // `a.default` --> `a.foo` + changes.replaceNode(importingSourceFile, ref, createIdentifier(exportName)); + break; + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: { + const spec = parent as ImportSpecifier | ExportSpecifier; + // `default as foo` --> `foo`, `default as bar` --> `foo as bar` + changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); + break; + } + case SyntaxKind.ImportClause: { + const clause = parent as ImportClause; + Debug.assert(clause.name === ref); + const spec = makeImportSpecifier(exportName, ref.text); + const { namedBindings } = clause; + if (!namedBindings) { + // `import foo from "./a";` --> `import { foo } from "./a";` + changes.replaceNode(importingSourceFile, ref, createNamedImports([spec])); + } + else if (namedBindings.kind === SyntaxKind.NamespaceImport) { + // `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";` + changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); + const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double; + const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); + changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); + } + else { + // `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";` + changes.deleteNode(importingSourceFile, ref); + changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec); + } + break; + } + default: + Debug.failBadSyntaxKind(parent); + } + } + + function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker): void { + const { parent } = ref; + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + // `a.foo` --> `a.default` + changes.replaceNode(importingSourceFile, ref, createIdentifier("default")); + break; + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: { + const spec = parent as ImportSpecifier | ExportSpecifier; + if (spec.kind === SyntaxKind.ImportSpecifier) { + // `import { foo } from "./a";` --> `import foo from "./a";` + // `import { foo as bar } from "./a";` --> `import bar from "./a";` + const defaultImport = createIdentifier(spec.name.text); + if (spec.parent.elements.length === 1) { + changes.replaceNode(importingSourceFile, spec.parent, defaultImport); + } + else { + changes.deleteNodeInList(importingSourceFile, spec); + changes.insertNodeBefore(importingSourceFile, spec.parent, defaultImport); + } + } + else { + // `export { foo } from "./a";` --> `export { default as foo } from "./a";` + // `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";` + // `export { foo as default } from "./a";` --> `export { default } from "./a";` + // (Because `export foo from "./a";` isn't valid syntax.) + changes.replaceNode(importingSourceFile, spec, makeExportSpecifier("default", spec.name.text)); + } + break; + } + default: + Debug.failBadSyntaxKind(parent); + } + + } + + function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier { + return createImportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); + } + + function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier { + return createExportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); + } +} diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index bdd3e7f4a96..cfdd9baffde 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -1,5 +1,5 @@ /* @internal */ -namespace ts.refactor.generateGetAccessorAndSetAccessor { +namespace ts.refactor { const refactorName = "Convert import"; const actionNameNamespaceToNamed = "Convert namespace import to named imports"; const actionNameNamedToNamespace = "Convert named imports to namespace import"; diff --git a/src/services/services.ts b/src/services/services.ts index 67241270e9e..29b3a4988bd 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1697,7 +1697,8 @@ namespace ts { } function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined { - return getReferences(fileName, position, { findInStrings, findInComments, isForRename: true }); + const refs = getReferences(fileName, position, { findInStrings, findInComments, isForRename: true }); + return refs && refs.map(({ fileName, textSpan }): RenameLocation => ({ fileName, textSpan })); } function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined { diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index a5eed1418ae..3001f5cf44e 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -241,6 +241,10 @@ namespace ts.textChanges { return this; } + public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void { + this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) }); + } + public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = {}) { const startPosition = getAdjustedStartPosition(sourceFile, startNode, options, Position.FullStart); const endPosition = getAdjustedEndPosition(sourceFile, endNode, options); @@ -397,6 +401,9 @@ namespace ts.textChanges { else if (isParameter(before)) { return {}; } + else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) { + return { suffix: ", " }; + } return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it } @@ -465,6 +472,10 @@ namespace ts.textChanges { this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); } + public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray, newNode: Node): void { + this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " }); + } + public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray): void { const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes)); this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); @@ -502,6 +513,9 @@ namespace ts.textChanges { case SyntaxKind.PropertyAssignment: return { suffix: "," + this.newLineCharacter }; + case SyntaxKind.ExportKeyword: + return { prefix: " " }; + case SyntaxKind.Parameter: return {}; diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 9318bbe4469..6e1495d78a2 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -71,6 +71,7 @@ "codefixes/useDefaultImport.ts", "codefixes/fixAddModuleReferTypeMissingTypeof.ts", "codefixes/convertToMappedObjectType.ts", + "refactors/convertExport.ts", "refactors/convertImport.ts", "refactors/extractSymbol.ts", "refactors/generateGetAccessorAndSetAccessor.ts", diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 481d20e91bd..eb710b28dc6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1281,13 +1281,17 @@ namespace ts { export const enum QuotePreference { Single, Double } + export function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference { + return isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single; + } + export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { if (preferences.quotePreference) { return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; } else { - const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); - return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile) ? QuotePreference.Single : QuotePreference.Double; + const firstModuleSpecifier = sourceFile.imports && find(sourceFile.imports, isStringLiteral); + return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double; } } @@ -1384,6 +1388,10 @@ namespace ts { node.getEnd() <= textSpanEnd(span); } + export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined { + return node.modifiers && find(node.modifiers, m => m.kind === kind); + } + /* @internal */ export function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement): void { const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax); diff --git a/src/testRunner/parallel/host.ts b/src/testRunner/parallel/host.ts index 87d193656c3..988e181852a 100644 --- a/src/testRunner/parallel/host.ts +++ b/src/testRunner/parallel/host.ts @@ -16,6 +16,10 @@ namespace Harness.Parallel.Host { const { fork } = require("child_process") as typeof import("child_process"); const { statSync } = require("fs") as typeof import("fs"); + // NOTE: paths for module and types for FailedTestReporter _do not_ line up due to our use of --outFile for run.js + // tslint:disable-next-line:variable-name + const FailedTestReporter = require(path.resolve(__dirname, "../../scripts/failed-tests")) as typeof import("../../../scripts/failed-tests"); + const perfData = readSavedPerfData(configOption); const newTasks: Task[] = []; let tasks: Task[] = []; @@ -54,7 +58,7 @@ namespace Harness.Parallel.Host { interface Worker { process: import("child_process").ChildProcess; accumulatedOutput: string; - currentTasks?: {file: string}[]; + currentTasks?: { file: string }[]; timer?: any; } @@ -115,7 +119,7 @@ namespace Harness.Parallel.Host { update(index: number, percentComplete: number, color: string, title: string | undefined, titleColor?: string) { percentComplete = minMax(percentComplete, 0, 1); - const progressBar = this._progressBars[index] || (this._progressBars[index] = { }); + const progressBar = this._progressBars[index] || (this._progressBars[index] = {}); const width = this._options.width; const n = Math.floor(width * percentComplete); const i = width - n; @@ -177,7 +181,7 @@ namespace Harness.Parallel.Host { return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; } - function readSavedPerfData(target?: string): {[testHash: string]: number} | undefined { + function readSavedPerfData(target?: string): { [testHash: string]: number } | undefined { const perfDataContents = IO.readFile(perfdataFileName(target)); if (perfDataContents) { return JSON.parse(perfDataContents); @@ -189,7 +193,7 @@ namespace Harness.Parallel.Host { return `tsrunner-${runner}://${test}`; } - function startDelayed(perfData: {[testHash: string]: number} | undefined, totalCost: number) { + function startDelayed(perfData: { [testHash: string]: number } | undefined, totalCost: number) { console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : ".")); console.log("Discovering runner-based tests..."); const discoverStart = +(new Date()); @@ -247,7 +251,7 @@ namespace Harness.Parallel.Host { const progressUpdateInterval = 1 / progressBars._options.width; let nextProgress = progressUpdateInterval; - const newPerfData: {[testHash: string]: number} = {}; + const newPerfData: { [testHash: string]: number } = {}; const workers: Worker[] = []; let closedWorkers = 0; @@ -531,10 +535,26 @@ namespace Harness.Parallel.Host { patchStats(consoleReporter.stats); let xunitReporter: import("mocha").reporters.XUnit | undefined; - if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser && process.env.CI === "true") { - xunitReporter = new Mocha.reporters.XUnit(replayRunner, { reporterOptions: { suiteName: "Tests", output: "./TEST-results.xml" } }); - patchStats(xunitReporter.stats); - xunitReporter.write(`\n`); + let failedTestReporter: import("../../../scripts/failed-tests") | undefined; + if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser) { + if (process.env.CI === "true") { + xunitReporter = new Mocha.reporters.XUnit(replayRunner, { + reporterOptions: { + suiteName: "Tests", + output: "./TEST-results.xml" + } + }); + patchStats(xunitReporter.stats); + xunitReporter.write(`\n`); + } + else { + failedTestReporter = new FailedTestReporter(replayRunner, { + reporterOptions: { + file: path.resolve(".failed-tests"), + keepFailed + } + }); + } } const savedUseColors = Base.useColors; @@ -551,6 +571,9 @@ namespace Harness.Parallel.Host { if (xunitReporter) { xunitReporter.done(errorResults.length, failures => process.exit(failures)); } + else if (failedTestReporter) { + failedTestReporter.done(errorResults.length, failures => process.exit(failures)); + } else { process.exit(errorResults.length); } diff --git a/src/testRunner/runner.ts b/src/testRunner/runner.ts index d66360458a8..6e430b08734 100644 --- a/src/testRunner/runner.ts +++ b/src/testRunner/runner.ts @@ -62,6 +62,7 @@ let workerCount: number; let runUnitTests: boolean | undefined; let stackTraceLimit: number | "full" | undefined; let noColors = false; +let keepFailed = false; interface TestConfig { light?: boolean; @@ -74,6 +75,7 @@ interface TestConfig { runUnitTests?: boolean; noColors?: boolean; timeout?: number; + keepFailed?: boolean; } interface TaskSet { @@ -102,6 +104,9 @@ function handleTestConfig() { if (testConfig.noColors !== undefined) { noColors = testConfig.noColors; } + if (testConfig.keepFailed) { + keepFailed = true; + } if (testConfig.stackTraceLimit === "full") { (Error).stackTraceLimit = Infinity; diff --git a/src/testRunner/unittests/jsDocParsing.ts b/src/testRunner/unittests/jsDocParsing.ts index 9a97cdc1505..6eecad9983b 100644 --- a/src/testRunner/unittests/jsDocParsing.ts +++ b/src/testRunner/unittests/jsDocParsing.ts @@ -308,6 +308,12 @@ namespace ts { * @param {object} o Doc doc * @param {string} o.f Doc for f */`); + parsesCorrectly("", +`/** + * {@link first link} + * Inside {@link link text} thing + * @see {@link second link text} and {@link Foo|a foo} as well. + */`); }); }); describe("getFirstToken", () => { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index aad546c0ffe..2345cf216a0 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -421,6 +421,18 @@ namespace ts.projectSystem { return createTextSpan(start, substring.length); } + function protocolTextSpanFromSubstring(str: string, substring: string): protocol.TextSpan { + const start = str.indexOf(substring); + Debug.assert(start !== -1); + const lineStarts = computeLineStarts(str); + const toLocation = (pos: number) => lineAndCharacterToLocation(computeLineAndCharacterOfPosition(lineStarts, pos)); + return { start: toLocation(start), end: toLocation(start + substring.length) }; + } + + function lineAndCharacterToLocation(lc: LineAndCharacter): protocol.Location { + return { line: lc.line + 1, offset: lc.character + 1 }; + } + /** * Test server cancellation token used to mock host token cancellation requests. * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls @@ -464,14 +476,13 @@ namespace ts.projectSystem { } } - export function makeSessionRequest(command: string, args: T) { - const newRequest: protocol.Request = { + export function makeSessionRequest(command: string, args: T): protocol.Request { + return { seq: 0, type: "request", command, arguments: args }; - return newRequest; } export function openFilesForSession(files: ReadonlyArray, session: server.Session) { @@ -8682,6 +8693,49 @@ export const x = 10;` }], }]); }); + + it("works with multiple projects", () => { + const aUserTs: File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: "{}", + }; + const bUserTs: File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aUserTs, bUserTs], session); + + const renameRequest = makeSessionRequest(CommandNames.GetEditsForFileRename, { + oldFilePath: "/a/old.ts", + newFilePath: "/a/new.ts", + }); + const response = session.executeCommand(renameRequest).response as protocol.GetEditsForFileRenameResponse["body"]; + assert.deepEqual(response, [ + { + fileName: aUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); }); describe("tsserverProjectSystem document registry in project service", () => { diff --git a/src/tsc/tsconfig.release.json b/src/tsc/tsconfig.release.json index ca18e30277d..edf73f17f18 100644 --- a/src/tsc/tsconfig.release.json +++ b/src/tsc/tsconfig.release.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig-base", "compilerOptions": { - "outFile": "../../lib/tsc.js", + "outFile": "../../built/local/tsc.release.js", "stripInternal": true, "preserveConstEnums": false, "declaration": false, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly..json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly..json new file mode 100644 index 00000000000..c694d240371 --- /dev/null +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly..json @@ -0,0 +1,28 @@ +{ + "kind": "JSDocComment", + "pos": 0, + "end": 127, + "tags": { + "0": { + "kind": "JSDocTag", + "pos": 63, + "end": 68, + "atToken": { + "kind": "AtToken", + "pos": 63, + "end": 64 + }, + "tagName": { + "kind": "Identifier", + "pos": 64, + "end": 67, + "escapedText": "see" + }, + "comment": "{@link second link text} and {@link Foo|a foo} as well." + }, + "length": 1, + "pos": 63, + "end": 68 + }, + "comment": "{@link first link}\nInside {@link link text} thing" +} \ No newline at end of file diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5998d1dcc14..8f9077cf203 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -225,7 +225,6 @@ declare namespace ts { * Returns a new sorted array. */ function sort(array: ReadonlyArray, comparer: Comparer): T[]; - function best(iter: Iterator, isBetter: (a: T, b: T) => boolean): T | undefined; function arrayIterator(array: ReadonlyArray): Iterator; /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. @@ -5916,6 +5915,8 @@ declare namespace ts { Remove_braces_from_arrow_function: DiagnosticMessage; Add_missing_enum_member_0: DiagnosticMessage; Add_all_missing_enum_members: DiagnosticMessage; + Convert_default_export_to_named_export: DiagnosticMessage; + Convert_named_export_to_default_export: DiagnosticMessage; }; } declare namespace ts { @@ -6263,6 +6264,7 @@ declare namespace ts { function isExportsIdentifier(node: Node): boolean; function isModuleExportsPropertyAccessExpression(node: Node): boolean; function getSpecialPropertyAssignmentKind(expr: BinaryExpression): SpecialPropertyAssignmentKind; + function getSpecialPropertyAccessKind(lhs: PropertyAccessExpression): SpecialPropertyAssignmentKind; function getInitializerOfBinaryExpression(expr: BinaryExpression): Expression; function isPrototypePropertyAssignment(node: Node): boolean; function isSpecialPropertyDeclaration(expr: PropertyAccessExpression): boolean; @@ -6950,7 +6952,7 @@ declare namespace ts { function isTemplateMiddleOrTemplateTail(node: Node): node is TemplateMiddle | TemplateTail; function isStringTextContainingNode(node: Node): node is StringLiteral | TemplateLiteralToken; function isGeneratedIdentifier(node: Node): node is GeneratedIdentifier; - function isModifierKind(token: SyntaxKind): boolean; + function isModifierKind(token: SyntaxKind): token is Modifier["kind"]; function isParameterPropertyModifier(kind: SyntaxKind): boolean; function isClassMemberModifier(idToken: SyntaxKind): boolean; function isModifier(node: Node): node is Modifier; @@ -10784,6 +10786,7 @@ declare namespace ts { Single = 0, Double = 1 } + function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference; function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference; function symbolNameNoDefault(symbol: Symbol): string | undefined; function symbolEscapedNameNoDefault(symbol: Symbol): __String | undefined; @@ -10808,6 +10811,7 @@ declare namespace ts { some(pred: (node: Node) => boolean): boolean; } function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined; + function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined; function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement): void; } declare namespace ts { @@ -10985,7 +10989,7 @@ declare namespace ts.FindAllReferences { } type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult; /** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */ - function createImportTracker(sourceFiles: ReadonlyArray, sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken): ImportTracker; + function createImportTracker(sourceFiles: ReadonlyArray, sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker; /** Info about an exported symbol to perform recursive search on. */ interface ExportInfo { exportingModuleSymbol: Symbol; @@ -11088,6 +11092,7 @@ declare namespace ts.FindAllReferences { declare namespace ts.FindAllReferences.Core { /** Core find-all-references algorithm. Handles special cases before delegating to `getReferencedSymbolsForSymbol`. */ function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: ReadonlyArray, cancellationToken: CancellationToken, options?: Options, sourceFilesSet?: ReadonlyMap): SymbolAndEntries[] | undefined; + function eachExportReference(sourceFiles: ReadonlyArray, checker: TypeChecker, cancellationToken: CancellationToken | undefined, exportSymbol: Symbol, exportingModuleSymbol: Symbol, exportName: string, isDefaultExport: boolean, cb: (ref: Identifier) => void): void; /** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */ function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean; function eachSymbolReferenceInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined; @@ -11473,6 +11478,7 @@ declare namespace ts.textChanges { deleteRange(sourceFile: SourceFile, range: TextRange): this; /** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */ deleteNode(sourceFile: SourceFile, node: Node, options?: ConfigurableStartEnd): this; + deleteModifier(sourceFile: SourceFile, modifier: Modifier): void; deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options?: ConfigurableStartEnd): this; deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options?: ConfigurableStartEnd): void; deleteNodeInList(sourceFile: SourceFile, node: Node): this; @@ -11504,6 +11510,7 @@ declare namespace ts.textChanges { private getInsertNodeAtClassStartPrefixSuffix; insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void; insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void; + insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray, newNode: Node): void; insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray): void; private insertNodeAfterWorker; private getInsertNodeAfterOptions; @@ -11661,7 +11668,9 @@ declare namespace ts.codefix { } declare namespace ts.codefix { } -declare namespace ts.refactor.generateGetAccessorAndSetAccessor { +declare namespace ts.refactor { +} +declare namespace ts.refactor { } declare namespace ts.refactor.extractSymbol { /** @@ -12458,7 +12467,7 @@ declare namespace ts.server.protocol { command: CommandTypes.GetEditsForFileRename; arguments: GetEditsForFileRenameRequestArgs; } - interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { + interface GetEditsForFileRenameRequestArgs { readonly oldFilePath: string; readonly newFilePath: string; } @@ -13830,6 +13839,7 @@ declare namespace ts.server { private delayUpdateProjectGraphs; setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void; findProject(projectName: string): Project | undefined; + forEachProject(cb: (project: Project) => void): void; getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined; tryGetDefaultProjectForFile(fileName: NormalizedPath): Project | undefined; ensureDefaultProjectForFile(fileName: NormalizedPath): Project; @@ -13838,6 +13848,8 @@ declare namespace ts.server { private ensureProjectStructuresUptoDate; getFormatCodeOptions(file: NormalizedPath): FormatCodeSettings; getPreferences(file: NormalizedPath): UserPreferences; + getHostFormatCodeOptions(): FormatCodeSettings; + getHostPreferences(): UserPreferences; private onSourceFileChanged; private handleDeletedFile; watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject): FileWatcher; @@ -14068,6 +14080,7 @@ declare namespace ts.server { private mapCodeAction; private mapCodeFixAction; private mapTextChangesToCodeEdits; + private mapTextChangeToCodeEdit; private convertTextChangeToCodeEdit; private getBraceMatching; private getDiagnosticsForProject; @@ -14084,6 +14097,8 @@ declare namespace ts.server { onMessage(message: string): void; private getFormatOptions; private getPreferences; + private getHostFormatOptions; + private getHostPreferences; } interface HandlerResponse { response?: {}; diff --git a/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.symbols b/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.symbols new file mode 100644 index 00000000000..00e906ea4ad --- /dev/null +++ b/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.symbols @@ -0,0 +1,16 @@ +=== /a.js === +function C() { this.x = false; }; +>C : Symbol(C, Decl(a.js, 0, 0)) +>x : Symbol(C.x, Decl(a.js, 0, 14), Decl(a.js, 0, 33)) + +/** @type {number} */ +C.prototype.x; +>C.prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) +>C : Symbol(C, Decl(a.js, 0, 0)) +>prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) + +new C().x; +>new C().x : Symbol(C.x, Decl(a.js, 0, 14), Decl(a.js, 0, 33)) +>C : Symbol(C, Decl(a.js, 0, 0)) +>x : Symbol(C.x, Decl(a.js, 0, 14), Decl(a.js, 0, 33)) + diff --git a/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.types b/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.types new file mode 100644 index 00000000000..6756c07f231 --- /dev/null +++ b/tests/baselines/reference/jsdocPrototypePropertyAccessWithType.types @@ -0,0 +1,23 @@ +=== /a.js === +function C() { this.x = false; }; +>C : typeof C +>this.x = false : false +>this.x : any +>this : any +>x : any +>false : false + +/** @type {number} */ +C.prototype.x; +>C.prototype.x : any +>C.prototype : any +>C : typeof C +>prototype : any +>x : any + +new C().x; +>new C().x : number +>new C() : C +>C : typeof C +>x : number + diff --git a/tests/cases/conformance/jsdoc/jsdocPrototypePropertyAccessWithType.ts b/tests/cases/conformance/jsdoc/jsdocPrototypePropertyAccessWithType.ts new file mode 100644 index 00000000000..b9e3c2719be --- /dev/null +++ b/tests/cases/conformance/jsdoc/jsdocPrototypePropertyAccessWithType.ts @@ -0,0 +1,9 @@ +// @allowJs: true +// @checkJs: true +// @noEmit: true + +// @Filename: /a.js +function C() { this.x = false; }; +/** @type {number} */ +C.prototype.x; +new C().x; diff --git a/tests/cases/fourslash/completionsTriggerCharacter.ts b/tests/cases/fourslash/completionsTriggerCharacter.ts index ea1abfdb3e4..959785a68c1 100644 --- a/tests/cases/fourslash/completionsTriggerCharacter.ts +++ b/tests/cases/fourslash/completionsTriggerCharacter.ts @@ -13,10 +13,12 @@ ////whatever // @Filename: /a.tsx -////declare namespace JSX { -//// interface Element {} -//// interface IntrinsicElements { -//// div: {}; +////declare global { +//// namespace JSX { +//// interface Element {} +//// interface IntrinsicElements { +//// div: {}; +//// } //// } ////} ////const ctr = + +/////*above*/ +////const x = /*next*/ function f(p) {} + +for (const marker of test.markerNames()) { + verify.docCommentTemplateAt(marker, 8, +`/** + * + * @param p + */`); +} diff --git a/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts b/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts index 5121957a718..029d685e3a0 100644 --- a/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts +++ b/tests/cases/fourslash/docCommentTemplateObjectLiteralMethods01.ts @@ -12,8 +12,7 @@ const multiLineOffset = 12; //// [1 + 2 + 3 + Math.rand()](x: number, y: string, z = true) { } ////} -verify.docCommentTemplateAt("0", singleLineOffset, - "/** */"); +verify.docCommentTemplateAt("0", singleLineOffset, "/** */"); verify.docCommentTemplateAt("1", multiLineOffset, `/** @@ -21,4 +20,4 @@ verify.docCommentTemplateAt("1", multiLineOffset, * @param x * @param y * @param z - */`); \ No newline at end of file + */`); diff --git a/tests/cases/fourslash/docCommentTemplateRegex.ts b/tests/cases/fourslash/docCommentTemplateRegex.ts index 685c1ca5aef..108ecc25430 100644 --- a/tests/cases/fourslash/docCommentTemplateRegex.ts +++ b/tests/cases/fourslash/docCommentTemplateRegex.ts @@ -1,6 +1,5 @@ /// -// @Filename: regex.ts ////var regex = /*0*///*1*/asdf/*2*/ /*3*///*4*/; for (const marker of test.markers()) { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 89e1948ce77..0753ded25c9 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -139,6 +139,7 @@ declare namespace FourSlashInterface { file(name: string, content?: string, scriptKindName?: string): any; select(startMarker: string, endMarker: string): void; selectRange(range: Range): void; + selectAllInFile(fileName: string): void; } class verifyNegatable { private negative; @@ -178,7 +179,7 @@ declare namespace FourSlashInterface { isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void; codeFix(options: { description: string, - newFileContent?: string | { readonly [fileName: string]: string }, + newFileContent?: NewFileContent, newRangeContent?: string, errorCode?: number, index?: number, @@ -336,7 +337,7 @@ declare namespace FourSlashInterface { getEditsForFileRename(options: { oldPath: string; newPath: string; - newFileContents: { [fileName: string]: string }; + newFileContents: { readonly [fileName: string]: string }; }): void; moveToNewFile(options: { readonly newFileContents: { readonly [fileName: string]: string }; @@ -357,7 +358,7 @@ declare namespace FourSlashInterface { enableFormatting(): void; disableFormatting(): void; - applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: string }): void; + applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: NewFileContent }): void; } class debug { printCurrentParameterHelp(): void; @@ -573,6 +574,7 @@ declare namespace FourSlashInterface { } type ArrayOrSingle = T | ReadonlyArray; + type NewFileContent = string | { readonly [fileName: string]: string }; } declare function verifyOperationIsCancelled(f: any): void; declare var test: FourSlashInterface.test_; diff --git a/tests/cases/fourslash/goToDefinitionSignatureAlias.ts b/tests/cases/fourslash/goToDefinitionSignatureAlias.ts index 3657ebfd0d6..01880be9a33 100644 --- a/tests/cases/fourslash/goToDefinitionSignatureAlias.ts +++ b/tests/cases/fourslash/goToDefinitionSignatureAlias.ts @@ -14,6 +14,9 @@ ////[|/*useI*/i|](); ////[|/*useJ*/j|](); +////const o = { m: /*m*/() => 0 }; +////o.[|/*useM*/m|](); + verify.goToDefinition({ useF: "f", useG: ["g", "f"], @@ -21,4 +24,5 @@ verify.goToDefinition({ useI: "i", useJ: ["j", "i"], + useM: "m", }); diff --git a/tests/cases/fourslash/refactorConvertExport_ambientModule.ts b/tests/cases/fourslash/refactorConvertExport_ambientModule.ts new file mode 100644 index 00000000000..a0e39c7a251 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertExport_ambientModule.ts @@ -0,0 +1,25 @@ +/// + +// @Filename: /foo.ts +////declare module "foo" { +//// /*a*/export default function foo(): void;/*b*/ +////} + +// @Filename: /b.ts +////import foo from "foo"; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert export", + actionName: "Convert default export to named export", + actionDescription: "Convert default export to named export", + newContent: { + "/foo.ts": +`declare module "foo" { + export function foo(): void; +}`, + + "/b.ts": +`import { foo } from "foo";`, +}, +}); diff --git a/tests/cases/fourslash/refactorConvertExport_defaultToNamed.ts b/tests/cases/fourslash/refactorConvertExport_defaultToNamed.ts new file mode 100644 index 00000000000..7cf5e5ae03f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertExport_defaultToNamed.ts @@ -0,0 +1,42 @@ +/// + +// @Filename: /a.ts +/////*a*/export default function f() {}/*b*/ + +// @Filename: /b.ts +////import f from "./a"; +////import { default as f } from "./a"; +////import { default as g } from "./a"; +////import f, * as a from "./a"; +//// +////export { default } from "./a"; +////export { default as f } from "./a"; +////export { default as i } from "./a"; +//// +////import * as a from "./a"; +////a.default(); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert export", + actionName: "Convert default export to named export", + actionDescription: "Convert default export to named export", + newContent: { + "/a.ts": +`export function f() {}`, + + "/b.ts": +`import { f } from "./a"; +import { f } from "./a"; +import { f as g } from "./a"; +import * as a from "./a"; +import { f } from "./a"; + +export { f as default } from "./a"; +export { f } from "./a"; +export { f as i } from "./a"; + +import * as a from "./a"; +a.f();`, +}, +}); diff --git a/tests/cases/fourslash/refactorConvertExport_exportNodeKinds.ts b/tests/cases/fourslash/refactorConvertExport_exportNodeKinds.ts new file mode 100644 index 00000000000..4e73f8e7c9f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertExport_exportNodeKinds.ts @@ -0,0 +1,79 @@ +/// + +// @Filename: /fn.ts +////export function f() {} + +// @Filename: /cls.ts +////export class C {} + +// @Filename: /interface.ts +////export interface I {} + +// @Filename: /enum.ts +////export const enum E {} + +// @Filename: /namespace.ts +////export namespace N {} + +// @Filename: /type.ts +////export type T = number; + +// @Filename: /var_unused.ts +////export const x = 0; + +// @Filename: /var_unused_noInitializer.ts +////export const x; + +// @Filename: /var_used.ts +////export const x = 0; +////x; + +const tests: { [fileName: string]: string | undefined } = { + fn: `export default function f() {}`, + + cls: `export default class C {}`, + + interface: `export default interface I {}`, + + enum: +`const enum E {} +export default E; +`, + + namespace: +`namespace N {} + +export default N; +`, + + type: +`type T = number; +export default T; +`, + + var_unused: `export default 0;`, + + var_unused_noInitializer: undefined, + + var_used: +`const x = 0; +export default x; +x;`, +}; + +for (const name in tests) { + const newContent = tests[name]; + const fileName = `/${name}.ts`; + goTo.selectAllInFile(fileName); + if (newContent === undefined) { + verify.refactorsAvailable([]); + } + else { + edit.applyRefactor({ + refactorName: "Convert export", + actionName: "Convert named export to default export", + actionDescription: "Convert named export to default export", + newContent: { [fileName]: newContent }, + }); + } +} diff --git a/tests/cases/fourslash/refactorConvertExport_namedToDefault.ts b/tests/cases/fourslash/refactorConvertExport_namedToDefault.ts new file mode 100644 index 00000000000..4e179d3c793 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertExport_namedToDefault.ts @@ -0,0 +1,39 @@ +/// + +// @Filename: /a.ts +/////*a*/export function f() {}/*b*/ + +// @Filename: /b.ts +////import { f } from "./a"; +////import { f as g } from "./a"; +////import { f, other } from "./a"; +//// +////export { f } from "./a"; +////export { f as i } from "./a"; +////export { f as default } from "./a"; +//// +////import * as a from "./a"; +////a.f(); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert export", + actionName: "Convert named export to default export", + actionDescription: "Convert named export to default export", + newContent: { + "/a.ts": +`export default function f() {}`, + + "/b.ts": +`import f from "./a"; +import g from "./a"; +import f, { other } from "./a"; + +export { default as f } from "./a"; +export { default as i } from "./a"; +export { default } from "./a"; + +import * as a from "./a"; +a.default();`, +}, +}); diff --git a/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts b/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts new file mode 100644 index 00000000000..1e9faf5bd5c --- /dev/null +++ b/tests/cases/fourslash/refactorConvertExport_namedToDefault_alreadyHasDefault.ts @@ -0,0 +1,8 @@ +/// + +// @Filename: /a.ts +/////*a*/export function f() {}/*b*/ +////export default function g() {} + +goTo.select("a", "b"); +verify.refactorsAvailable([]); diff --git a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter index 40bdb4eadab..6b9706810b5 160000 --- a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter +++ b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter @@ -1 +1 @@ -Subproject commit 40bdb4eadabc9fbed7d83e3f26817a931c0763b6 +Subproject commit 6b9706810b55af326a93b9aa59cb17815a30bb32