镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Flip files from CRLF to LF - #64046

Open
Jake Bailey (jakebailey) wants to merge 14 commits into
microsoft:mainfrom
jakebailey:no-more-crlf
Open

Jake Bailey (jakebailey) wants to merge 14 commits into
microsoft:mainfrom
jakebailey:no-more-crlf

Conversation

@jakebailey

@jakebailey Jake Bailey (jakebailey) commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

Historically, all files in the TS repo were CRLF, including our actual main code in src. This is extra annoying now-a-days where agents, tools, etc, tend to screw up CRLF, git diff --check hates it, etc. But changing that is hard.

But, now, we're in Go, and I made sure the repo always enforced LF for .go files, which means the only remaining CRLF source we have are the public API code (new) and then misc files like scripts, lib.d.ts.

I think we can safely switch over.

A follow up can add a .git-blame-ignore entry, which does appear to work in my testing.

After this, the only files with CRLF are:

  • tsc/testdata/** - input test files, but definitely output test files as we default to CRLF
  • tsc/internal/locale/lcl/** - dead in Redo localization for onboarding #63987, though it's possible that the loc team will break this in the files they push to us; it won't matter too much. .gitattributes will probably be enough to keep this working.

Copilot AI balanced review requested due to automatic review settings August 26, 2026 23:20
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Aug 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Converts repository text files from CRLF to LF and configures Git to retain LF endings.

Changes:

  • Adds repository-wide LF normalization with fixture exclusions.
  • Converts scripts, API sources, tests, and documentation to LF.
  • Regenerates bundled libraries and affected call-hierarchy baselines.

Reviewed changes

Copilot reviewed 10 out of 293 changed files in this pull request and generated no comments.

Show a summary per file
File Description
.gitattributes Configures LF normalization and exclusions.
.github/pull_request_template.md Converts line endings.
README.md Converts line endings.
tools/scripts/link-hooks.mjs Converts line endings.
tools/scripts/tsc/generate.ts Converts line endings.
tsc/internal/lsp/lsproto/_generate/fetchModel.mts Converts line endings.
tsc/internal/format/README.md Converts line endings.
tsc/internal/bundled/README.md Converts line endings.
tsc/internal/bundled/generate.go Changes bundled-lib header validation.
tsc/internal/bundled/libs/lib.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.decorators.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.decorators.legacy.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es5.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es6.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.collection.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.core.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.generator.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.iterable.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.proxy.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.reflect.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.symbol.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2015.symbol.wellknown.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2016.array.include.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2016.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2016.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2016.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.arraybuffer.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.date.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.object.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.sharedmemory.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2017.typedarrays.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.asyncgenerator.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.asynciterable.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2018.regexp.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.array.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.object.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2019.symbol.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.bigint.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.date.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.number.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.sharedmemory.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2020.symbol.wellknown.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2021.weakref.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.array.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.error.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.object.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.regexp.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2022.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2023.array.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2023.collection.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2023.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2023.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2023.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.arraybuffer.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.collection.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.object.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.regexp.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.sharedmemory.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2024.string.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.collection.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.float16.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.iterator.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.promise.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.es2025.regexp.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.array.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.collection.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.date.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.decorators.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.disposable.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.error.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.full.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.intl.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.sharedmemory.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.temporal.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.esnext.typedarrays.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.scripthost.d.ts Converts LF and removes a blank line.
tsc/internal/bundled/libs/lib.webworker.importscripts.d.ts Converts LF and removes a blank line.
tsc/testdata/baselines/reference/fourslash/callHierarchy/callHierarchyInPropDeclarationOfExportedDefaultClass1.callHierarchy.txt Updates shifted lib line numbers.
tsc/testdata/baselines/reference/fourslash/callHierarchy/callHierarchyIncomingCallsNoCrashArrayPush.callHierarchy.txt Updates shifted lib line numbers.
tsc/testdata/baselines/reference/fourslash/callHierarchy/callHierarchyIncomingCallsObjectLiteralMethodInExpressionComputedProperty.callHierarchy.txt Updates shifted lib line numbers.
tsc/testdata/baselines/reference/fourslash/callHierarchy/callHierarchyIncomingCallsObjectLiteralMethodInIdentifierComputedProperty.callHierarchy.txt Updates shifted lib line numbers.
tsc/testdata/baselines/reference/fourslash/callHierarchy/callHierarchyIncomingCallsObjectLiteralMethodInStringLiteralComputedProperty.callHierarchy.txt Updates shifted lib line numbers.
packages/vscode-typescript/test/index.test.ts Converts line endings.
packages/vscode-typescript/test/contentMapperContributions.test.ts Converts line endings.
packages/vscode-typescript/src/workspaceSymbolMiddleware.ts Converts line endings.
packages/vscode-typescript/src/vscode.proposed.multiDocumentHighlightProvider.d.ts Converts line endings.
packages/vscode-typescript/src/vscode.proposed.editorHoverVerbosityLevel.d.ts Converts line endings.
packages/vscode-typescript/src/tsdkPackage.ts Converts line endings.
packages/vscode-typescript/src/statusBar.ts Converts line endings.
packages/vscode-typescript/src/managedFileContext.ts Converts line endings.
packages/vscode-typescript/src/languageFeatures/util/dependentRegistration.ts Converts line endings.
packages/vscode-typescript/src/languageFeatures/hover.ts Converts line endings.
packages/vscode-typescript/src/languageFeatures/documentHighlight.ts Converts line endings.
packages/vscode-typescript/src/experimentationService.ts Converts line endings.
packages/typescript/test/wtf8.test.ts Converts line endings.
packages/typescript/test/version.test.ts Converts line endings.
packages/typescript/test/types.d.ts Converts line endings.
packages/typescript/test/api.bench.ts Converts line endings.
packages/typescript/src/internal/utils.ts Converts line endings.
packages/typescript/src/ast/index.ts Converts line endings.
packages/typescript/src/api/options.ts Converts line endings.
packages/typescript/src/api/node/wtf8.ts Converts line endings.
packages/typescript/src/api/node/protocol.ts Converts line endings.
packages/typescript/src/enums/typePredicateKind.ts Converts line endings.
packages/typescript/src/enums/typePredicateKind.enum.ts Converts line endings.
packages/typescript/src/enums/typeFormatFlags.ts Converts line endings.
packages/typescript/src/enums/typeFormatFlags.enum.ts Converts line endings.
packages/typescript/src/enums/tokenFlags.ts Converts line endings.
packages/typescript/src/enums/tokenFlags.enum.ts Converts line endings.
packages/typescript/src/enums/spanMapKind.ts Converts line endings.
packages/typescript/src/enums/spanMapKind.enum.ts Converts line endings.
packages/typescript/src/enums/spanMapFidelity.ts Converts line endings.
packages/typescript/src/enums/spanMapFidelity.enum.ts Converts line endings.
packages/typescript/src/enums/spanMapFeature.ts Converts line endings.
packages/typescript/src/enums/spanMapFeature.enum.ts Converts line endings.
packages/typescript/src/enums/signatureKind.ts Converts line endings.
packages/typescript/src/enums/signatureKind.enum.ts Converts line endings.
packages/typescript/src/enums/signatureFlags.ts Converts line endings.
packages/typescript/src/enums/signatureFlags.enum.ts Converts line endings.
packages/typescript/src/enums/scriptTarget.ts Converts line endings.
packages/typescript/src/enums/scriptTarget.enum.ts Converts line endings.
packages/typescript/src/enums/scriptKind.ts Converts line endings.
packages/typescript/src/enums/scriptKind.enum.ts Converts line endings.
packages/typescript/src/enums/regularExpressionFlags.ts Converts line endings.
packages/typescript/src/enums/regularExpressionFlags.enum.ts Converts line endings.
packages/typescript/src/enums/outerExpressionKinds.ts Converts line endings.
packages/typescript/src/enums/outerExpressionKinds.enum.ts Converts line endings.
packages/typescript/src/enums/objectFlags.enum.ts Converts line endings.
packages/typescript/src/enums/nodeFlags.enum.ts Converts line endings.
packages/typescript/src/enums/nodeBuilderFlags.enum.ts Converts line endings.
packages/typescript/src/enums/newLineKind.ts Converts line endings.
packages/typescript/src/enums/newLineKind.enum.ts Converts line endings.
packages/typescript/src/enums/moduleResolutionKind.ts Converts line endings.
packages/typescript/src/enums/moduleResolutionKind.enum.ts Converts line endings.
packages/typescript/src/enums/moduleKind.ts Converts line endings.
packages/typescript/src/enums/moduleKind.enum.ts Converts line endings.
packages/typescript/src/enums/moduleDetectionKind.ts Converts line endings.
packages/typescript/src/enums/moduleDetectionKind.enum.ts Converts line endings.
packages/typescript/src/enums/modifierFlags.enum.ts Converts line endings.
packages/typescript/src/enums/languageVariant.ts Converts line endings.
packages/typescript/src/enums/languageVariant.enum.ts Converts line endings.
packages/typescript/src/enums/jsxEmit.ts Converts line endings.
packages/typescript/src/enums/jsxEmit.enum.ts Converts line endings.
packages/typescript/src/enums/internalSymbolName.ts Converts line endings.
packages/typescript/src/enums/internalSymbolName.enum.ts Converts line endings.
packages/typescript/src/enums/emitOnly.ts Converts line endings.
packages/typescript/src/enums/emitOnly.enum.ts Converts line endings.
packages/typescript/src/enums/elementFlags.ts Converts line endings.
packages/typescript/src/enums/elementFlags.enum.ts Converts line endings.
packages/typescript/src/enums/diagnosticDirectivePolicy.ts Converts line endings.
packages/typescript/src/enums/diagnosticDirectivePolicy.enum.ts Converts line endings.
packages/typescript/src/enums/diagnosticCategory.ts Converts line endings.
packages/typescript/src/enums/diagnosticCategory.enum.ts Converts line endings.
packages/typescript/src/enums/completionItemKind.ts Converts line endings.
packages/typescript/src/enums/completionItemKind.enum.ts Converts line endings.
packages/typescript/src/enums/commentDirectiveType.ts Converts line endings.
packages/typescript/src/enums/commentDirectiveType.enum.ts Converts line endings.
packages/typescript/src/enums/checkFlags.ts Converts line endings.
packages/typescript/src/enums/checkFlags.enum.ts Converts line endings.
packages/typescript/lib/version.d.cts Converts line endings.
packages/typescript/lib/version.cjs Converts line endings.
packages/typescript/lib/tsc.js Converts line endings.
packages/typescript/lib/getExePath.d.ts Converts line endings.
Suppressed comments (1)

tsc/internal/bundled/generate.go:94

  • This normalization removes one blank line from every bundled lib instead of only changing its line endings. That shifts all public lib source positions (as shown by the accompanying call-hierarchy baseline changes), which is an observable and unrelated content change for a line-ending-only PR. Please preserve the existing three-newline boundary after CopyrightNotice.txt, restore the deleted blank lines in the libs, and revert the resulting baseline offsets.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@jakebailey
Jake Bailey (jakebailey) force-pushed the no-more-crlf branch 2 times, most recently from 7f952fa to ebc567d Compare August 27, 2026 00:22

@weswigham Wesley Wigham (weswigham) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will we need any changes to local gitconfigs to make this work on windows machines or is the local .gitattributes sufficient?

@github-project-automation github-project-automation Bot moved this from Not started to Needs merge in PR Backlog Aug 27, 2026
@jakebailey

Copy link
Copy Markdown
Member Author

Nope, gitattributes does it all

@jakebailey
Jake Bailey (jakebailey) force-pushed the no-more-crlf branch 2 times, most recently from 78aa9a8 to 4a1d35e Compare August 27, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 297 changed files in this pull request and generated no new comments.

@jakebailey

Copy link
Copy Markdown
Member Author

Unfortunately, I checked this, and it seems like while the gitattributes enforce files that get pulled and how the files are stored, if you make a CRLF file locally, it won't actually auto fix it for you, you'll just be operating on a file with CRLF while committing files that use LF. I'm not sure how bad that is. We'd just need to get everyone to actually adopt that VS Code setting.

Otherwise it's at least checked in CI. Perhaps I need to add a hereby task which can flip the files?

@weswigham Wesley Wigham (weswigham) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Man, we need to just quickly push this through

[git-generate]
node <<'NODE'
const fs = require("node:fs");
const path = ".gitattributes";
const from = `* -text
*.go text eol=lf`;
const to = `
* text=auto eol=lf

# Preserve line-ending-sensitive fixtures exactly.
tsc/testdata/** -text

# TODO: Remove this exception when this directory is converted or removed.
tools/loc/** -text`;
const text = fs.readFileSync(path, "utf8");
const index = text.indexOf(from);
if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
    throw new Error(`expected exactly one match in ${path}`);
}
fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
NODE
[git-generate]
git add --renormalize .
[git-generate]
node <<'NODE'
const fs = require("node:fs");

function replaceOnce(path, from, to) {
    const text = fs.readFileSync(path, "utf8");
    const index = text.indexOf(from);
    if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
        throw new Error(`expected exactly one match in ${path}`);
    }
    fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
}

replaceOnce(".dprint.jsonc", '"newLineKind": "auto"', '"newLineKind": "lf"');
replaceOnce(".dprint.jsonc", '"newLineKind": "crlf"', '"newLineKind": "lf"');
const path = ".dprint.jsonc";
const text = fs.readFileSync(path, "utf8");
const pattern = /^        "tsc\/internal\/bundled\/source\/\*\*",\r?\n/gm;
const matches = text.match(pattern);
if (matches?.length !== 1) {
    throw new Error(`expected exactly one obsolete exclusion in ${path}`);
}
fs.writeFileSync(path, text.replace(pattern, ""));
NODE
npm ci --no-audit --no-fund

npx dprint fmt
[git-generate]
node <<'NODE'
const fs = require("node:fs");

function replaceOnce(path, from, to) {
    const text = fs.readFileSync(path, "utf8");
    const index = text.indexOf(from);
    if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
        throw new Error(`expected exactly one match in ${path}`);
    }
    fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
}

replaceOnce(
    "tsc/internal/bundled/generate.go",
    `\tif bytes.ContainsRune(copyright, '\\r') || !bytes.HasSuffix(copyright, []byte("\\n\\n")) {
\t\tlog.Fatalf("%s must use LF line endings and end with a blank line", copyrightNotice)
\t}
\theader := append(copyright, '\\n')`,
    `\theader := append(bytes.TrimRight(copyright, "\\n"), '\\n', '\\n')`,
);
replaceOnce(
    "tsc/internal/bundled/generate.go",
    `\t\tif bytes.ContainsRune(content, '\\r') {
\t\t\tlog.Fatalf("%s must use LF line endings", path)
\t\t}
\t\tif !bytes.HasPrefix(content, header) {
\t\t\tlog.Fatalf("%s must start with %s followed by a blank line", path, copyrightNotice)
\t\t}
\t\tif !bytes.HasSuffix(content, []byte("\\n")) {
\t\t\tlog.Fatalf("%s must end with a newline", path)
\t\t}`,
    `\t\tif !bytes.HasPrefix(content, header) {
\t\t\tlog.Fatalf("%s must start with %s", path, copyrightNotice)
\t\t}`,
);
NODE
[git-generate]
node <<'NODE'
const fs = require("node:fs");
const path = ".dprint.jsonc";
const from = '        "tsc/internal/bundled/libs/**",';
const to = `        "tsc/internal/bundled/libs/lib.dom.d.ts",
        "tsc/internal/bundled/libs/lib.dom.asynciterable.d.ts",
        "tsc/internal/bundled/libs/lib.dom.iterable.d.ts",
        "tsc/internal/bundled/libs/lib.webworker.d.ts",
        "tsc/internal/bundled/libs/lib.webworker.asynciterable.d.ts",
        "tsc/internal/bundled/libs/lib.webworker.iterable.d.ts",`;
const text = fs.readFileSync(path, "utf8");
const index = text.indexOf(from);
if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
    throw new Error(`expected exactly one match in ${path}`);
}
fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
NODE

npm ci --no-audit --no-fund

npx dprint fmt

node <<'NODE'
const fs = require("node:fs");
fs.rmSync("tsc/testdata/baselines/local", { recursive: true, force: true });
NODE
npx hereby test || true
npx hereby baseline-accept
node <<'NODE'
const fs = require("node:fs");
fs.rmSync("tsc/testdata/baselines/local", { recursive: true, force: true });
NODE
[git-generate]
node <<'NODE'
const fs = require("node:fs");
const path = "Herebyfile.mjs";
const text = fs.readFileSync(path, "utf8");
const marker = `export const checkFormat = task({
    name: "check:format",
    description: "Checks that the repo is formatted.",
    run: async () => {`;
const markerIndex = text.indexOf(marker);
if (markerIndex < 0 || text.indexOf(marker, markerIndex + marker.length) >= 0) {
    throw new Error(`expected exactly one checkFormat task in ${path}`);
}
const bodyStart = markerIndex + marker.length;
const bodyEnd = text.indexOf("\n    },\n});", bodyStart);
if (bodyEnd < 0) {
    throw new Error(`could not find checkFormat task body in ${path}`);
}
const body = text.slice(bodyStart, bodyEnd);
if (!body.includes("dprint") || body.includes("checkLineEndings")) {
    throw new Error(`unexpected checkFormat task body in ${path}`);
}
const replacement = `${marker}${body}
        await checkLineEndings();
    },
});

async function checkLineEndings() {
    const exclusions = [
        ":(exclude)tsc/testdata/**",
        // TODO: Remove this exclusion when this directory is converted or removed.
        ":(exclude)tools/loc/**",
    ];
    const result = xSync(
        "git",
        ["grep", "--cached", "-Il", "\\r", "--", ".", ...exclusions],
        { throwOnError: false, nodeOptions: { stdio: "pipe" } },
    );
    if (result.exitCode === 1) {
        return;
    }
    if (result.exitCode !== 0) {
        throw new Error(\`Failed to check line endings:\\n\${result.stderr}\`);
    }
    throw new Error(\`Files must use LF line endings:\\n\${result.stdout}\`);
}`;
fs.writeFileSync(path, text.slice(0, markerIndex) + replacement + text.slice(bodyEnd + "\n    },\n});".length));
NODE
[git-generate]
node <<'NODE'
const fs = require("node:fs");

const files = [
    "tsc/testdata/tests/cases/compiler/collisionCodeGenModuleWithUnicodeNames.ts",
    "tsc/testdata/tests/cases/compiler/instanceofOperator.ts",
    "tsc/testdata/tests/cases/compiler/promiseTest.ts",
    "tsc/testdata/tests/cases/compiler/targetTypeBaseCalls.ts",
    "tsc/testdata/tests/cases/compiler/unicodeIdentifierNames.ts",
];

for (const file of files) {
    const data = fs.readFileSync(file);
    if (data[0] !== 0xff || data[1] !== 0xfe) {
        throw new Error(`${file} does not start with a UTF-16LE BOM`);
    }
    const text = data.toString("utf16le", 2).replaceAll("\r\n", "\n");
    fs.writeFileSync(file, text);
}
NODE
[git-generate]
node <<'NODE'
const fs = require("node:fs");
const path = ".vscode/settings.template.json";
let text = fs.readFileSync(path, "utf8");
const from = `{
    "[typescript][typescriptreact][javascript][javascriptreact][json][jsonc][yaml][github-actions-workflow]":`;
const to = `{
    "files.eol": "\\n",
    "[typescript][typescriptreact][javascript][javascriptreact][json][jsonc][yaml][github-actions-workflow]":`;
if (!text.includes(from) || text.indexOf(from) !== text.lastIndexOf(from)) {
    throw new Error(`expected exactly one insertion point in ${path}`);
}
text = text.replace(from, to);
const scoped = `    "[go][go.mod][go.sum][go.work]": {
        "files.eol": "\\n",`;
const unscoped = `    "[go][go.mod][go.sum][go.work]": {`;
if (!text.includes(scoped) || text.indexOf(scoped) !== text.lastIndexOf(scoped)) {
    throw new Error(`expected exactly one scoped EOL setting in ${path}`);
}
fs.writeFileSync(path, text.replace(scoped, unscoped));
NODE
[git-generate]
node <<'NODE'
const fs = require("node:fs");

fs.writeFileSync(".editorconfig", `root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[tsc/testdata/**]
end_of_line = unset
charset = unset
trim_trailing_whitespace = unset
insert_final_newline = unset

[tools/loc/**]
end_of_line = unset
charset = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
`);

const path = "Herebyfile.mjs";
let text = fs.readFileSync(path, "utf8");

const formatMarker = `async function runFormat() {`;
const formatStart = text.indexOf(formatMarker);
if (formatStart < 0 || text.indexOf(formatMarker, formatStart + formatMarker.length) >= 0) {
    throw new Error(`expected exactly one runFormat function in ${path}`);
}
const formatBodyStart = formatStart + formatMarker.length;
const formatBodyEnd = text.indexOf("\n}", formatBodyStart);
if (formatBodyEnd < 0) {
    throw new Error(`could not find runFormat body in ${path}`);
}
const formatBody = text.slice(formatBodyStart, formatBodyEnd);
if (!formatBody.includes("dprint") || formatBody.includes("fixLineEndings")) {
    throw new Error(`unexpected runFormat body in ${path}`);
}
const formatTo = `${formatMarker}${formatBody}
    await fixLineEndings();
}

const lineEndingExclusions = [
    ":(exclude)tsc/testdata/**",
    // TODO: Remove this exclusion when this directory is converted or removed.
    ":(exclude)tools/loc/**",
];

export const fixLineEndingsTask = task({
    name: "fix:line-endings",
    description: "Converts tracked files that Git expects to use LF.",
    run: fixLineEndings,
});

async function fixLineEndings() {
    const result = await runOutput("git", ["ls-files", "--eol", "-z", "--", ".", ...lineEndingExclusions]);
    const files = [];
    for (const record of result.stdout.split("\\0")) {
        if (!record) {
            continue;
        }
        const separator = record.indexOf("\\t");
        assert(separator !== -1);
        const eolInfo = record.slice(0, separator);
        if (!/\\bw\\/(?:crlf|mixed)\\b/.test(eolInfo) || !eolInfo.includes("eol=lf")) {
            continue;
        }
        files.push(record.slice(separator + 1));
    }

    await Promise.all(files.map(async file => {
        const contents = await fs.promises.readFile(file);
        const normalized = Buffer.from(contents.toString("latin1").replaceAll("\\r\\n", "\\n"), "latin1");
        await fs.promises.writeFile(file, normalized);
    }));

    if (files.length) {
        console.log(\`Converted \${files.length} file(s) to LF.\`);
    }
}`;

text = text.slice(0, formatStart) + formatTo + text.slice(formatBodyEnd + 2);

const checkMarker = `async function checkLineEndings() {`;
const checkStart = text.indexOf(checkMarker);
if (checkStart < 0 || text.indexOf(checkMarker, checkStart + checkMarker.length) >= 0) {
    throw new Error(`expected exactly one checkLineEndings function in ${path}`);
}
const exclusionsSpread = "...exclusions";
const exclusionsIndex = text.indexOf(exclusionsSpread, checkStart);
if (exclusionsIndex < 0 || text.indexOf(exclusionsSpread, exclusionsIndex + exclusionsSpread.length) >= 0) {
    throw new Error(`expected exactly one line-ending exclusion reference in ${path}`);
}
text = text.slice(0, exclusionsIndex)
    + "...lineEndingExclusions"
    + text.slice(exclusionsIndex + exclusionsSpread.length);

fs.writeFileSync(path, text);
NODE
Localization updates must not reintroduce CRLF into the repository.
Preserve the existing JSON formatting and keep the canonical-format
checks aligned with the importer's LF output.

[git-generate]
node <<'NODE'
const fs = require("node:fs");

function replaceOnce(path, from, to) {
    const text = fs.readFileSync(path, "utf8");
    const index = text.indexOf(from);
    if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
        throw new Error(`expected exactly one match in ${path}`);
    }
    fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
}

replaceOnce(
    "tools/cmd/import-localization/main.go",
    '\t' + String.raw`data = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))` + '\n',
    "",
);

const importerTest = "tools/cmd/import-localization/main_test.go";
const importerText = fs.readFileSync(importerTest, "utf8");
const expectedStart = importerText.indexOf('\tassert.Equal(t, string(data), "{\\r\\n"+');
const expectedEnd = importerText.indexOf('\n\tassert.NilError(t, importLocalization', expectedStart);
if (expectedStart < 0 || expectedEnd < 0) {
    throw new Error(`could not find expected importer output in ${importerTest}`);
}
const expected = importerText.slice(expectedStart, expectedEnd);
replaceOnce(importerTest, expected, expected.replaceAll("\\r\\n", "\\n"));

const diagnosticsTest = "tsc/internal/diagnostics/diagnostics_test.go";
replaceOnce(diagnosticsTest,
    '\t' + String.raw`expected = bytes.ReplaceAll(expected, []byte("\n"), []byte("\r\n"))` + '\n',
    "");
const diagnosticsText = fs.readFileSync(diagnosticsTest, "utf8");
const fixtureStart = diagnosticsText.indexOf('func TestLocaleFilesIgnoreStaleDiagnostics(');
const fixtureEnd = diagnosticsText.indexOf('\nfunc TestGenerateLocalizations(', fixtureStart);
if (fixtureStart < 0 || fixtureEnd < 0) {
    throw new Error(`could not find stale diagnostic fixtures in ${diagnosticsTest}`);
}
const fixtures = diagnosticsText.slice(fixtureStart, fixtureEnd);
replaceOnce(diagnosticsTest, fixtures, fixtures.replaceAll("\\r\\n", "\\n"));

fs.appendFileSync("tsc/internal/diagnostics/loc/README.md",
    "\nThe importer writes sorted JSON with LF line endings and two-space indentation.\n");
NODE

npx dprint fmt tools/cmd/import-localization/main.go tools/cmd/import-localization/main_test.go tsc/internal/diagnostics/diagnostics_test.go tsc/internal/diagnostics/loc/README.md
Keep imported translations consistent with the repository's final-newline
convention without changing their two-space indentation.

[git-generate]
node <<'NODE'
const fs = require("node:fs");

function replaceOnce(path, from, to) {
    const text = fs.readFileSync(path, "utf8");
    const index = text.indexOf(from);
    if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
        throw new Error(`expected exactly one match in ${path}`);
    }
    fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
}

replaceOnce("tools/cmd/import-localization/main.go",
    "\tif err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {",
    "\tdata = append(data, '\\n')\n\tif err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {");
replaceOnce("tools/cmd/import-localization/main_test.go",
    '\t\t"}")',
    '\t\t"}\\n")');
replaceOnce("tsc/internal/diagnostics/diagnostics_test.go",
    '\tassert.Equal(t, string(data), string(expected), "handback must use sorted keys and canonical formatting")',
    '\texpected = append(expected, \'\\n\')\n\tassert.Equal(t, string(data), string(expected), "handback must use sorted keys and canonical formatting")');
replaceOnce("tsc/internal/diagnostics/diagnostics_test.go",
    '\t\t"}")',
    '\t\t"}\\n")');
replaceOnce("tsc/internal/diagnostics/diagnostics_test.go",
    String.raw`validateLocaleFile(t, []byte("{\n  \"Removed_diagnostic_99999\": \"Stale translation.\"\n}"), nil)`,
    String.raw`validateLocaleFile(t, []byte("{\n  \"Removed_diagnostic_99999\": \"Stale translation.\"\n}\n"), nil)`);
replaceOnce("tsc/internal/diagnostics/loc/README.md",
    "The importer writes sorted JSON with LF line endings and two-space indentation.",
    "The importer writes sorted JSON with LF line endings, two-space indentation,\nand a final newline.");

const directory = "tsc/internal/diagnostics/loc";
const files = fs.readdirSync(directory).filter(file => file.endsWith(".generated.json"));
if (!files.length) {
    throw new Error("expected localization JSON files");
}
for (const file of files) {
    const path = `${directory}/${file}`;
    const text = fs.readFileSync(path, "utf8");
    if (!text.endsWith("}")) {
        throw new Error(`expected JSON without a final newline in ${path}`);
    }
    fs.appendFileSync(path, "\n");
}
NODE

npx dprint fmt tools/cmd/import-localization/main.go tools/cmd/import-localization/main_test.go tsc/internal/diagnostics/diagnostics_test.go tsc/internal/diagnostics/loc/README.md
Generated source should use the repository's line endings without relying
on the formatter to convert intermediate CRLF output. Check the raw output
so formatting cannot hide regressions.

[git-generate]
node <<'NODE'
const fs = require("node:fs");
const generator = "tools/scripts/tsc/generate-ts-ast.ts";
let text = fs.readFileSync(generator, "utf8");
const eol = String.raw`    const eol = "\r\n";`;
if (text.split(eol).length !== 2) {
    throw new Error("expected exactly one AST line-ending constant");
}
text = text.replace(eol, String.raw`    const eol = "\n";`);
const conversion = String.raw`    let result = out.join("\n");
    result = result.replace(/\n/g, "\r\n");
    return result;`;
if (text.split(conversion).length !== 4) {
    throw new Error("expected exactly three AST CRLF conversions");
}
fs.writeFileSync(generator, text.replaceAll(conversion, String.raw`    return out.join("\n");`));

const test = "tools/scripts/gen/generatedFile.test.mts";
const tests = fs.readFileSync(test, "utf8");
const marker = 'test("AST generation forwards force to schema generators and the kind stringer", async () => {';
if (tests.split(marker).length !== 2) {
    throw new Error("expected exactly one AST generation test");
}
const regression = String.raw`test("TypeScript AST generation emits LF before formatting", async context => {
    const writes = context.mock.method(GeneratedFile.prototype, "write");
    const { default: generate } = await import("../tsc/generate-ts-ast.ts");
    generate(true);
    assert.equal(writes.mock.callCount(), 4);
    for (const call of writes.mock.calls) {
        assert.equal(call.arguments[0].includes("\r"), false, "unformatted output must use LF");
    }
});

`;
fs.writeFileSync(test, tests.replace(marker, regression + marker));
NODE

npm ci --no-audit --no-fund
npx dprint fmt tools/scripts/tsc/generate-ts-ast.ts tools/scripts/gen/generatedFile.test.mts
Keep the directory documentation focused on ownership and generated
artifacts rather than serialization details enforced by tests.

[git-generate]
node <<'NODE'
const fs = require("node:fs");
const path = "tsc/internal/diagnostics/loc/README.md";
const from = "\nThe importer writes sorted JSON with LF line endings, two-space indentation,\nand a final newline.\n";
const text = fs.readFileSync(path, "utf8");
if (text.split(from).length !== 2) {
    throw new Error(`expected exactly one serialization paragraph in ${path}`);
}
fs.writeFileSync(path, text.replace(from, ""));
NODE
Only the OneLoc LCL files need their existing line endings preserved.
Other files under tools/loc should follow the repository's LF rules.

[git-generate]
node <<'NODE'
const fs = require("node:fs");

function replaceOnce(path, from, to) {
    const text = fs.readFileSync(path, "utf8");
    const index = text.indexOf(from);
    if (index < 0 || text.indexOf(from, index + from.length) >= 0) {
        throw new Error(`expected exactly one match in ${path}`);
    }
    fs.writeFileSync(path, text.slice(0, index) + to + text.slice(index + from.length));
}

replaceOnce(".gitattributes",
    "# TODO: Remove this exception when this directory is converted or removed.\ntools/loc/** -text",
    "tools/loc/**/*.lcl -text");
replaceOnce(".editorconfig", "[tools/loc/**]", "[tools/loc/**/*.lcl]");
replaceOnce("Herebyfile.mjs",
    '    // TODO: Remove this exclusion when this directory is converted or removed.\n    ":(exclude)tools/loc/**",',
    '    ":(exclude)tools/loc/**/*.lcl",');
replaceOnce("Herebyfile.mjs",
    `    const exclusions = [
        ":(exclude)tsc/testdata/**",
        // TODO: Remove this exclusion when this directory is converted or removed.
        ":(exclude)tools/loc/**",
    ];
`,
    "");
NODE

npm ci --no-audit --no-fund
npx dprint fmt Herebyfile.mjs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Needs merge

Development

Successfully merging this pull request may close these issues.

4 participants