TypeScript: Emit does not preserve empty lines

Hi,

TS Version: 1.1

Given

function foo() {

    var x = 10;

    var y = 11;
}

We used to get

function foo() {
    var x = 10;

    var y = 11;
}

In the new compiler the line break is missing

function foo() {
    var x = 10;
    var y = 11;
}

(Both compilers removed the first empty line, but the new compiler has gone a step further.)

This can affect the experience when debugging JavaScript in the browser.

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Reactions: 79
  • Comments: 45 (9 by maintainers)

Commits related to this issue

Most upvoted comments

The crowd wants preserveWhitespace: true/false @ORESoftware ++

This shouldn’t be that hard, shouldn’t there just be an option in tsconfig.json

preserveWhitespace: true/false

?

I would also like this for a similar reason as @timjmartel - when developers see the emitted JS looks nice they are less resistant to adopt. Preserving (at least vertical) whitespace makes the code look less like it was generated by a machine and more like idomatic JS code written by a human.

If our team ever decided to abandon TS and instead continue with the transpiled JS it would be much easier to adopt the emitted JS sources if they had human-friendly whitespace.

Reason this is important is that TypeScript is supposed to “degrade gracefully” to JS. Right now it can’t preserve new lines making the JS a bit dense to read, especially if you write TypeScript, but you are supposed to deliver JS to some place else.

I don’t think using esformatter solves the whole problem. Sure, it can automatically insert blank lines around functions etc. But for me, blank lines within functions are even more crucial. We use blank lines like paragraphs in prose: to group individual thoughts.

Those blank lines within functions help to communicate the structure of the function. Without them, I find that readability suffers.

As an engineering director exploring moving our javascript development community to typescript the new line capability would really help. One of the primary appeal of typescript was maintaining the readability and structure of the code created in typescript generatedJavascript.

Great to see comments are kept in the recent update and addition of the “–removeComments" command line directive.

I would also like to see this implemented, with a few specific reasons in mind:

  • Consistency with general design philosophy per NoelAbrahams’ comment above
  • Ciantic’s comment about needing to deliver JS elsewhere
  • Being able to cleanly compare JS generated through TS and JS that existed before, where demonstrating a close match is important to adoption
  • Not having long lines requiring horizontal scrolling in the emitted code, where the source included line breaks to make things neat and beautiful and meeting linter/style specs (also commented on above)
  • Being able to continue working with transpiled JS if all these overhead costs of TypeScript become too much; anticipating difficulty here creates lock-in costs that raise barriers to adoption in the first place.

This PR, discussed in the 3.9 release notes, show code actions preserving newlines; why can’t the emitted code do something similar?

I was part way into trying to fix this in the compiler itself when my teammate @emadum reminded me that tsc can preserve comments. Here’s a little gulp pipeline that seems to do a fairly decent job of preserving newlines:

const gulp = require('gulp');
const ts = require('gulp-typescript');
const through = require('through2');

function preserveNewlines() {
  return through.obj(function(file, encoding, callback) {
    const data = file.contents.toString('utf8');
    const fixedUp = data.replace(/\n\n/g, '\n/** THIS_IS_A_NEWLINE **/');
    file.contents = Buffer.from(fixedUp, 'utf8');
    callback(null, file);
  });
}

function restoreNewlines() {
  return through.obj(function(file, encoding, callback) {
    const data = file.contents.toString('utf8');
    const fixedUp = data.replace(/\/\*\* THIS_IS_A_NEWLINE \*\*\//g, '\n');
    file.contents = Buffer.from(fixedUp, 'utf8');
    callback(null, file);
  });
}

gulp.task('default', function () {
  return gulp.src('src/**/*.ts')
    .pipe(preserveNewlines())
    .pipe(ts({
      removeComments: false
    }))
    .pipe(restoreNewlines())
    .pipe(gulp.dest('lib'));
});

I think a more clever comment would prevent some false positives, but it seems to work well for us today. I’ve only tested this on a relatively small file, but the performance overhead there was minimal.

hth

Adding some background info… The reason the new compiler removes all blank lines is that this is the only thing we can do consistently. Blank line preservation is always going to be a hit or miss thing when it comes to constructs that are subject to rewriting, e.g. class and module declarations. We face exactly the same issues with comment preservation. So, while I’m sympathetic to resolving this, it’s not an easy thing to do.

@NoelAbrahams I’m curious what issues you see when debugging?

I just realized. One good reason to not keep whitespace is that it will really help prevent you from accidentally editing the .js instead of the .ts. I guess one thing to do would be to write out the .js files as readonly/execute only. So maybe this is a non-issue.

That being said, I don’t think tsc writes out .js files as readonly/execute only, is there a way to configure tsc to do this?

I’m changing this to Won’t Fix. Reasons:

  • This has obviously not been a key blocker for anyone
  • Of all the “keep trivia” problems we have, this is surely the lowest-priority among them
  • At this point, many alternative transpilers with different configurable behaviors around comments/whitespace exist; if you need truly need newline preservation, you can use one of those
  • Performance remains top-of-mind for us and anything that makes emit slower needs to be carrying a lot more value than newline preservation.

@ahejlsberg @NoelAbrahams There is one issue that exists with debugging in the browser that is slightly related to this conversation. When using setter/getter chains (like with jquery) or chaining promises, the new line feeds are lost during translation. That being said, it is a huge pain point when working with Arrow functions.

As an example:

(<any> x).a('#test')
    .b('test')
    .c(() => 'foo')
    .d(() => 'bar')
    .e(() => 5)
    .f(() => 6);

Becomes:

x.a('#test').b('test').c(function () { return 'foo'; }).d(function () { return 'bar'; }).e(function () { return 5; }).f(function () { return 6; });

Using Chrome and sourceMaps, the breakpoints are still skipped.

http://www.typescriptlang.org/Playground#src=(<any> x).a(‘%23test’) .b(‘test’) .c(() %3D> ‘foo’)%0A%09.d(()%20%3D%3E%20’bar’)%0A%09.e(()%20%3D%3E%205)%0A%09.f(()%20%3D%3E%206)%3B

I also run into this issue. I came up with a workaround by creating a diff patch and revert whitespace changes in the patch. jsdiff allows you to create a structured patch object and manipulate it as you wish.

import * as diff from 'diff';

const patch =
      diff.parsePatch(diff.createPatch('file', oldText, newText, '', ''));
const hunks = patch[0].hunks;
for (let i = 0; i < hunks.length; ++i) {
  let lineOffset = 0;
  const hunk = hunks[i];
  hunk.lines = hunk.lines.map(line => {
    if (line === '-') {
      lineOffset++;
      return ' ';
    }
    return line;
  });
  hunk.newLines += lineOffset;
  for (let j = i + 1; j < hunks.length; ++j) {
    hunks[j].newStart += lineOffset;
  }
}
return diff.applyPatch(oldText, patch);

With this workaround, you can preserve all the line breaks from the original file.

+1 preserveWhitespace: true/false

Temporary hack

Use esformatter to add linebreaks.

With the following configuration file:

{
  "lineBreak": {
    "before": {
      "FunctionDeclaration": ">=2",
      "FunctionDeclarationOpeningBrace": 0,
      "FunctionDeclarationClosingBrace": 1,
      "MethodDefinition": ">=2",
      "ClassDeclaration": ">=2"
    },
    "after": {
      "FunctionDeclaration": ">=2",
      "FunctionDeclarationOpeningBrace": 1,
      "MethodDefinitionClosingBrace": ">=2",
      "ClassClosingBrace": ">=2"
    }
  }
}

At this point, many alternative transpilers with different configurable behaviors around comments/whitespace exist

Does anyone know of a good option for this?

It depends on your exact needs. There are several suggestions in this issue and a list of other transpilers is as follows. Note that this list hasn’t been sanitized for the specific requirement:

Are there any updates for almost 10 years? I would like to be able to configure preserving new lines

@mbroadst

I ended up using your idea as a base, and eventually expanded upon it until it became an npm module: https://www.npmjs.com/package/gulp-preserve-typescript-whitespace

I credited your post in the Readme, hopefully you don’t mind 😃