Skip to content

Commit 6b6f122

Browse files
committed
Add dts bundling
1 parent f588ed7 commit 6b6f122

File tree

11 files changed

+7487
-7657
lines changed

11 files changed

+7487
-7657
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ scripts/configurePrerelease.js
4747
scripts/configureLanguageServiceBuild.js
4848
scripts/open-user-pr.js
4949
scripts/open-cherry-pick-pr.js
50+
scripts/dtsBundler.js
5051
scripts/processDiagnosticMessages.d.ts
5152
scripts/processDiagnosticMessages.js
5253
scripts/produceLKG.js

Gulpfile.js

+34-6
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ const buildSrc = () => buildProject("src");
126126

127127
task("build-src", series(preSrc, buildSrc));
128128

129+
/**
130+
* @param {string} entrypoint
131+
* @param {string} output
132+
*/
133+
async function runDtsBundler(entrypoint, output) {
134+
// Want to preserve @internal? Pass `--stripInternal=false` when running the dts task.
135+
await exec(process.execPath, [
136+
"./scripts/dtsBundler.js",
137+
"--entrypoint",
138+
entrypoint,
139+
"--output",
140+
output,
141+
`--stripInternal=${cmdLineOptions.stripInternal}`,
142+
]);
143+
}
144+
129145
/** @type {string | undefined} */
130146
let copyrightHeader;
131147
function getCopyrightHeader() {
@@ -268,6 +284,7 @@ const esbuildServices = esbuildTask("./src/typescript/typescript.ts", "./built/l
268284
const writeServicesCJSShim = () => writeCJSReexport("./built/local/typescript/typescript.js", "./built/local/typescript.js");
269285
const buildServicesProject = () => buildProject("src/typescript");
270286

287+
// TODO(jakebailey): rename this; no longer "services".
271288
const buildServices = () => {
272289
if (cmdLineOptions.bundle) return esbuildServices.build();
273290
writeServicesCJSShim();
@@ -297,6 +314,9 @@ task("watch-services").flags = {
297314
" --built": "Compile using the built version of the compiler."
298315
};
299316

317+
const dtsServices = () => runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
318+
task("dts-services", series(preBuild, buildServicesProject, dtsServices));
319+
task("dts-services").description = "Builds typescript.d.ts";
300320

301321
const esbuildServer = esbuildTask("./src/tsserver/server.ts", "./built/local/tsserver.js", /* exportIsTsObject */ true);
302322
const writeServerCJSShim = () => writeCJSReexport("./built/local/tsserver/server.js", "./built/local/tsserver.js");
@@ -348,10 +368,11 @@ task("watch-min").flags = {
348368
const esbuildLssl = esbuildTask("./src/tsserverlibrary/tsserverlibrary.ts", "./built/local/tsserverlibrary.js", /* exportIsTsObject */ true);
349369
const writeLsslCJSShim = () => writeCJSReexport("./built/local/tsserverlibrary/tsserverlibrary.js", "./built/local/tsserverlibrary.js");
350370

371+
const buildLsslProject = () => buildProject("src/tsserverlibrary");
351372
const buildLssl = () => {
352373
if (cmdLineOptions.bundle) return esbuildLssl.build();
353374
writeLsslCJSShim();
354-
return buildProject("src/tsserverlibrary");
375+
return buildLsslProject();
355376
};
356377
task("lssl", series(preBuild, buildLssl));
357378
task("lssl").description = "Builds language service server library";
@@ -375,6 +396,14 @@ task("watch-lssl").flags = {
375396
" --built": "Compile using the built version of the compiler."
376397
};
377398

399+
const dtsLssl = () => runDtsBundler("./built/local/tsserverlibrary/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.d.ts");
400+
task("dts-lssl", series(preBuild, buildLsslProject, dtsLssl));
401+
task("dts-lssl").description = "Builds tsserverlibrary.d.ts";
402+
403+
// TODO(jakebailey): this is probably not efficient, but, gulp.
404+
const dts = series(preBuild, parallel(buildServicesProject, buildLsslProject), parallel(dtsServices, dtsLssl));
405+
task("dts", dts);
406+
378407
const testRunner = "./built/local/run.js";
379408
const esbuildTests = esbuildTask("./src/testRunner/_namespaces/Harness.ts", testRunner);
380409
const writeTestsCJSShim = () => writeCJSReexport("./built/local/testRunner/runner.js", testRunner);
@@ -488,7 +517,7 @@ const buildOtherOutputs = parallel(buildCancellationToken, buildTypingsInstaller
488517
task("other-outputs", series(preBuild, buildOtherOutputs));
489518
task("other-outputs").description = "Builds miscelaneous scripts and documents distributed with the LKG";
490519

491-
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs)));
520+
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts)));
492521
task("local").description = "Builds the full compiler and services";
493522
task("local").flags = {
494523
" --built": "Compile using the built version of the compiler."
@@ -504,7 +533,7 @@ const preTest = parallel(buildTsc, buildTests, buildServices, buildLssl);
504533
preTest.displayName = "preTest";
505534

506535
const runTests = () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ false);
507-
task("runtests", series(preBuild, preTest, runTests));
536+
task("runtests", series(preBuild, preTest, dts, runTests));
508537
task("runtests").description = "Runs the tests using the built run.js file.";
509538
task("runtests").flags = {
510539
"-t --tests=<regex>": "Pattern for tests to run.",
@@ -523,7 +552,7 @@ task("runtests").flags = {
523552
};
524553

525554
const runTestsParallel = () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false);
526-
task("runtests-parallel", series(preBuild, preTest, runTestsParallel));
555+
task("runtests-parallel", series(preBuild, preTest, dts, runTestsParallel));
527556
task("runtests-parallel").description = "Runs all the tests in parallel using the built run.js file.";
528557
task("runtests-parallel").flags = {
529558
" --light": "Run tests in light mode (fewer verifications, but tests run faster).",
@@ -613,8 +642,7 @@ const produceLKG = async () => {
613642
}
614643
};
615644

616-
// TODO(jakebailey): dependencies on dts
617-
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs), produceLKG));
645+
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts), produceLKG));
618646
task("LKG").description = "Makes a new LKG out of the built js files";
619647
task("LKG").flags = {
620648
" --built": "Compile using the built version of the compiler.",

scripts/build/options.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const ci = ["1", "true"].includes(process.env.CI);
66

77
/** @type {CommandLineOptions} */
88
module.exports = minimist(process.argv.slice(2), {
9-
boolean: ["dirty", "light", "colors", "lkg", "soft", "fix", "failed", "keepFailed", "force", "built", "ci", "bundle"],
9+
boolean: ["dirty", "light", "colors", "lkg", "soft", "fix", "failed", "keepFailed", "force", "built", "ci", "bundle", "stripInternal"],
1010
string: ["browser", "tests", "break", "host", "reporter", "stackTraceLimit", "timeout", "shards", "shardId"],
1111
alias: {
1212
/* eslint-disable quote-props */
@@ -41,7 +41,8 @@ module.exports = minimist(process.argv.slice(2), {
4141
dirty: false,
4242
built: false,
4343
ci,
44-
bundle: true
44+
bundle: true,
45+
stripInternal: true,
4546
}
4647
});
4748

@@ -74,5 +75,6 @@ if (module.exports.built) {
7475
* @property {string} shards
7576
* @property {string} shardId
7677
* @property {string} break
78+
* @property {boolean} stripInternal
7779
*/
7880
void 0;

scripts/dtsBundler.ts

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* WARNING: this is a very, very rudimentary d.ts bundler; it only works
3+
* in the TS project thanks to our history using namespaces, which has
4+
* prevented us from duplicating names across files, and allows us to
5+
* bundle as namespaces again, even though the project is modules.
6+
*/
7+
8+
import * as assert from "assert";
9+
import * as fs from "fs";
10+
import * as path from "path";
11+
import * as minimist from "minimist";
12+
import * as ts from "../lib/typescript";
13+
14+
const options = minimist(process.argv.slice(2), {
15+
boolean: ["stripInternal"],
16+
string: ["project", "entrypoint", "output"],
17+
default: {
18+
stripInternal: true,
19+
},
20+
});
21+
22+
const stripInternal = !!options.stripInternal;
23+
const entrypoint = options.entrypoint;
24+
const output = options.output;
25+
26+
27+
assert(typeof entrypoint === "string" && entrypoint);
28+
assert(typeof output === "string" && output);
29+
30+
console.log(`Bundling ${entrypoint} to ${output}`);
31+
32+
const newLineKind = ts.NewLineKind.LineFeed;
33+
const newLine = newLineKind === ts.NewLineKind.LineFeed ? "\n" : "\r\n";
34+
35+
function isDeclarationStatement(node: ts.Node): node is ts.DeclarationStatement {
36+
return (ts as any).isDeclarationStatement(node);
37+
}
38+
39+
function isInternalDeclaration(node: ts.Node): boolean {
40+
return (ts as any).isInternalDeclaration(node, node.getSourceFile());
41+
}
42+
43+
function getParentVariableStatement(node: ts.VariableDeclaration): ts.VariableStatement {
44+
const declarationList = node.parent as ts.VariableDeclarationList;
45+
assert(ts.isVariableDeclarationList(declarationList), `expected VariableDeclarationList at ${nodeToLocation(node)}`);
46+
assert(declarationList.declarations.length === 1, `expected VariableDeclarationList of length 1 at ${nodeToLocation(node)}`);
47+
const variableStatement = declarationList.parent;
48+
assert(ts.isVariableStatement(variableStatement), `expected VariableStatement at ${nodeToLocation(node)}`);
49+
return variableStatement;
50+
}
51+
52+
function getDeclarationStatement(node: ts.Declaration): ts.Statement | undefined {
53+
if (ts.isVariableDeclaration(node)) {
54+
return getParentVariableStatement(node);
55+
}
56+
else if (isDeclarationStatement(node)) {
57+
return node;
58+
}
59+
return undefined;
60+
}
61+
62+
const nullTransformationContext: ts.TransformationContext = (ts as any).nullTransformationContext;
63+
64+
const program = ts.createProgram([entrypoint], { target: ts.ScriptTarget.ES5 });
65+
66+
const typeChecker = program.getTypeChecker();
67+
68+
const sourceFile = program.getSourceFile(entrypoint);
69+
assert(sourceFile);
70+
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
71+
assert(moduleSymbol);
72+
73+
const printer = ts.createPrinter({ newLine: newLineKind });
74+
75+
76+
const lines: string[] = [];
77+
const indent = " ";
78+
let currentIndent = "";
79+
80+
function increaseIndent() {
81+
currentIndent += indent;
82+
}
83+
84+
function decreaseIndent() {
85+
currentIndent = currentIndent.slice(indent.length);
86+
}
87+
88+
function write(s: string) {
89+
if (!s) {
90+
lines.push("");
91+
}
92+
else {
93+
lines.push(...s.split(/\r?\n/).filter(line => line).map(line => (currentIndent + line).trimEnd()));
94+
}
95+
}
96+
97+
const containsPublicAPICache = new Map<ts.Symbol, boolean>();
98+
99+
function containsPublicAPI(symbol: ts.Symbol): boolean {
100+
const cached = containsPublicAPICache.get(symbol);
101+
if (cached !== undefined) {
102+
return cached;
103+
}
104+
105+
const result = containsPublicAPIWorker();
106+
containsPublicAPICache.set(symbol, result);
107+
return result;
108+
109+
function containsPublicAPIWorker(): boolean {
110+
if (!symbol.declarations?.length) {
111+
return false;
112+
}
113+
114+
if (symbol.flags & ts.SymbolFlags.Alias) {
115+
const resolved = typeChecker.getAliasedSymbol(symbol);
116+
return containsPublicAPI(resolved);
117+
}
118+
119+
if (symbol.flags & ts.SymbolFlags.ValueModule) {
120+
for (const me of typeChecker.getExportsOfModule(symbol)) {
121+
if (containsPublicAPI(me)) {
122+
return true;
123+
}
124+
}
125+
return false;
126+
}
127+
128+
for (const decl of symbol.declarations) {
129+
const statement = getDeclarationStatement(decl);
130+
if (statement && !isInternalDeclaration(statement)) {
131+
return true;
132+
}
133+
}
134+
135+
return false;
136+
}
137+
}
138+
139+
function nodeToLocation(decl: ts.Node): string {
140+
const sourceFile = decl.getSourceFile();
141+
const lc = sourceFile.getLineAndCharacterOfPosition(decl.pos);
142+
return `${sourceFile.fileName}:${lc.line}:${lc.character}`;
143+
}
144+
145+
function emitAsNamespace(name: string, moduleSymbol: ts.Symbol) {
146+
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule);
147+
148+
if (name === "ts") {
149+
// We will write `export = ts` at the end.
150+
write(`declare namespace ${name} {`);
151+
}
152+
else {
153+
// No export modifier; we are already in the namespace.
154+
write(`namespace ${name} {`);
155+
}
156+
increaseIndent();
157+
158+
const moduleExports = typeChecker.getExportsOfModule(moduleSymbol);
159+
for (const me of moduleExports) {
160+
if (stripInternal && !containsPublicAPI(me)) {
161+
continue;
162+
}
163+
164+
assert(me.declarations?.length);
165+
166+
if (me.flags & ts.SymbolFlags.Alias) {
167+
const resolved = typeChecker.getAliasedSymbol(me);
168+
emitAsNamespace(me.name, resolved);
169+
continue;
170+
}
171+
172+
for (const decl of me.declarations) {
173+
let statement = getDeclarationStatement(decl);
174+
175+
if (!statement) {
176+
throw new Error(`Unhandled declaration for ${me.name} at ${nodeToLocation(decl)}`);
177+
}
178+
179+
// Ignore an internal declaration.
180+
if (stripInternal && isInternalDeclaration(statement)) {
181+
continue;
182+
}
183+
184+
// Remove internal components and declare/const keywords.
185+
statement = ts.visitEachChild(statement, (node) => {
186+
if (stripInternal && isInternalDeclaration(node)) {
187+
return undefined;
188+
}
189+
190+
switch (node.kind) {
191+
case ts.SyntaxKind.DeclareKeyword: // No need to emit this in d.ts files.
192+
case ts.SyntaxKind.ConstKeyword: // Remove const from const enums.
193+
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
194+
return undefined;
195+
}
196+
197+
return node;
198+
}, nullTransformationContext);
199+
200+
write(printer.printNode(ts.EmitHint.Unspecified, statement, decl.getSourceFile()));
201+
}
202+
}
203+
204+
decreaseIndent();
205+
write(`}`);
206+
}
207+
208+
emitAsNamespace("ts", moduleSymbol);
209+
210+
write("export = ts;");
211+
212+
const copyrightNotice = fs.readFileSync(path.join(__dirname, "..", "CopyrightNotice.txt"), "utf-8");
213+
const outputContents = copyrightNotice + lines.join(newLine);
214+
215+
if (stripInternal && outputContents.includes("@internal")) {
216+
console.error("Output includes untrimmed @internal nodes!");
217+
}
218+
219+
fs.writeFileSync(output, outputContents);

scripts/produceLKG.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as glob from "glob";
77
const root = path.join(__dirname, "..");
88
const source = path.join(root, "built/local");
99
const dest = path.join(root, "lib");
10-
const copyright = fs.readFileSync(path.join(__dirname, "../CopyrightNotice.txt"), "utf-8");
1110

1211
async function produceLKG() {
1312
console.log(`Building LKG from ${source} to ${dest}`);
@@ -74,19 +73,14 @@ async function copyScriptOutputs() {
7473
}
7574

7675
async function copyDeclarationOutputs() {
77-
await copyWithCopyright("tsserverlibrary.d.ts");
78-
await copyWithCopyright("typescript.d.ts");
76+
await copyFromBuiltLocal("tsserverlibrary.d.ts");
77+
await copyFromBuiltLocal("typescript.d.ts");
7978
}
8079

8180
async function writeGitAttributes() {
8281
await fs.writeFile(path.join(dest, ".gitattributes"), `* text eol=lf`, "utf-8");
8382
}
8483

85-
async function copyWithCopyright(fileName: string, destName = fileName) {
86-
const content = await fs.readFile(path.join(source, fileName), "utf-8");
87-
await fs.writeFile(path.join(dest, destName), copyright + "\n" + content);
88-
}
89-
9084
async function copyFromBuiltLocal(fileName: string) {
9185
await fs.copy(path.join(source, fileName), path.join(dest, fileName));
9286
}

0 commit comments

Comments
 (0)