Skip to content

Commit d051afa

Browse files
committed
Add dts bundling
1 parent d46fefa commit d051afa

File tree

13 files changed

+7500
-7660
lines changed

13 files changed

+7500
-7660
lines changed

Diff for: .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

Diff for: Gulpfile.js

+37-7
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ const buildSrc = () => buildProject("src");
127127
// But, if we are bundling, we are running only d.ts emit, so maybe this is fast?
128128
task("build-src", series(preSrc, buildSrc));
129129

130+
/**
131+
* @param {string} entrypoint
132+
* @param {string} output
133+
*/
134+
async function runDtsBundler(entrypoint, output) {
135+
// Want to preserve @internal? Pass `--stripInternal=false` when running the dts task.
136+
await exec(process.execPath, [
137+
"./scripts/dtsBundler.js",
138+
"--entrypoint",
139+
entrypoint,
140+
"--output",
141+
output,
142+
`--stripInternal=${cmdLineOptions.stripInternal}`,
143+
]);
144+
}
145+
130146
/** @type {string | undefined} */
131147
let copyrightHeader;
132148
function getCopyrightHeader() {
@@ -252,10 +268,13 @@ const preBuild = cmdLineOptions.lkg ? lkgPreBuild : localPreBuild;
252268
const esbuildServices = esbuildTask("./src/typescript/typescript.ts", "./built/local/typescript.js", /* exportIsTsObject */ true, /* performanceMatters */ true);
253269

254270
// TODO(jakebailey): rename this; no longer "services".
271+
272+
const buildServicesProject = () => buildProject("src/typescript");
273+
255274
const buildServices = () => {
256275
if (cmdLineOptions.bundle) return esbuildServices.build();
257276
writeCJSReexport("./built/local/typescript/typescript.js", "./built/local/typescript.js");
258-
return buildProject("src/typescript");
277+
return buildServicesProject();
259278
};
260279

261280
task("services", series(preBuild, buildServices));
@@ -278,6 +297,9 @@ task("watch-services").flags = {
278297
" --built": "Compile using the built version of the compiler."
279298
};
280299

300+
const dtsServices = () => runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
301+
task("dts-services", series(preBuild, buildServicesProject, dtsServices));
302+
task("dts-services").description = "Builds typescript.d.ts";
281303

282304
const esbuildServer = esbuildTask("./src/tsserver/server.ts", "./built/local/tsserver.js", /* exportIsTsObject */ true, /* performanceMatters */ true);
283305

@@ -324,10 +346,11 @@ task("watch-min").flags = {
324346

325347
const esbuildLssl = esbuildTask("./src/tsserverlibrary/tsserverlibrary.ts", "./built/local/tsserverlibrary.js", /* exportIsTsObject */ true, /* performanceMatters */ true);
326348

349+
const buildLsslProject = () => buildProject("src/tsserverlibrary");
327350
const buildLssl = () => {
328351
if (cmdLineOptions.bundle) return esbuildLssl.build();
329352
writeCJSReexport("./built/local/tsserverlibrary/tsserverlibrary.js", "./built/local/tsserverlibrary.js");
330-
return buildProject("src/tsserverlibrary");
353+
return buildLsslProject();
331354
};
332355
task("lssl", series(preBuild, buildLssl));
333356
task("lssl").description = "Builds language service server library";
@@ -349,6 +372,14 @@ task("watch-lssl").flags = {
349372
" --built": "Compile using the built version of the compiler."
350373
};
351374

375+
const dtsLssl = () => runDtsBundler("./built/local/tsserverlibrary/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.d.ts");
376+
task("dts-lssl", series(preBuild, buildLsslProject, dtsLssl));
377+
task("dts-lssl").description = "Builds tsserverlibrary.d.ts";
378+
379+
// TODO(jakebailey): this is probably not efficient, but, gulp.
380+
const dts = series(preBuild, parallel(buildServicesProject, buildLsslProject), parallel(dtsServices, dtsLssl));
381+
task("dts", dts);
382+
352383
const testRunner = "./built/local/run.js";
353384
const esbuildTests = esbuildTask("./src/testRunner/_namespaces/Harness.ts", testRunner);
354385

@@ -459,7 +490,7 @@ const buildOtherOutputs = parallel(buildCancellationToken, buildTypingsInstaller
459490
task("other-outputs", series(preBuild, buildOtherOutputs));
460491
task("other-outputs").description = "Builds miscelaneous scripts and documents distributed with the LKG";
461492

462-
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs)));
493+
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts)));
463494
task("local").description = "Builds the full compiler and services";
464495
task("local").flags = {
465496
" --built": "Compile using the built version of the compiler."
@@ -475,7 +506,7 @@ const preTest = parallel(buildTsc, buildTests, buildServices, buildLssl);
475506
preTest.displayName = "preTest";
476507

477508
const runTests = () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ false);
478-
task("runtests", series(preBuild, preTest, runTests));
509+
task("runtests", series(preBuild, preTest, dts, runTests));
479510
task("runtests").description = "Runs the tests using the built run.js file.";
480511
task("runtests").flags = {
481512
"-t --tests=<regex>": "Pattern for tests to run.",
@@ -494,7 +525,7 @@ task("runtests").flags = {
494525
};
495526

496527
const runTestsParallel = () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false);
497-
task("runtests-parallel", series(preBuild, preTest, runTestsParallel));
528+
task("runtests-parallel", series(preBuild, preTest, dts, runTestsParallel));
498529
task("runtests-parallel").description = "Runs all the tests in parallel using the built run.js file.";
499530
task("runtests-parallel").flags = {
500531
" --light": "Run tests in light mode (fewer verifications, but tests run faster).",
@@ -598,8 +629,7 @@ const produceLKG = async () => {
598629
}
599630
};
600631

601-
// TODO(jakebailey): dependencies on dts
602-
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs), produceLKG));
632+
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts), produceLKG));
603633
task("LKG").description = "Makes a new LKG out of the built js files";
604634
task("LKG").flags = {
605635
" --built": "Compile using the built version of the compiler.",

Diff for: 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;

Diff for: scripts/dtsBundler.ts

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
// TODO(jakebailey): I can't seem to figure out how to load the real tsconfig, so, this will have to do.
65+
// But, why don't we get trailing commas?
66+
const program = ts.createProgram([entrypoint], { target: ts.ScriptTarget.ES5 });
67+
68+
const typeChecker = program.getTypeChecker();
69+
70+
const sourceFile = program.getSourceFile(entrypoint);
71+
assert(sourceFile);
72+
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
73+
assert(moduleSymbol);
74+
75+
const printer = ts.createPrinter({ newLine: newLineKind });
76+
77+
78+
const lines: string[] = [];
79+
const indent = " ";
80+
let currentIndent = "";
81+
82+
function increaseIndent() {
83+
currentIndent += indent;
84+
}
85+
86+
function decreaseIndent() {
87+
currentIndent = currentIndent.slice(indent.length);
88+
}
89+
90+
function write(s: string) {
91+
if (!s) {
92+
lines.push("");
93+
}
94+
else {
95+
lines.push(...s.split(/\r?\n/).filter(line => line).map(line => (currentIndent + line).trimEnd()));
96+
}
97+
}
98+
99+
const containsPublicAPICache = new Map<ts.Symbol, boolean>();
100+
101+
function containsPublicAPI(symbol: ts.Symbol): boolean {
102+
const cached = containsPublicAPICache.get(symbol);
103+
if (cached !== undefined) {
104+
return cached;
105+
}
106+
107+
const result = containsPublicAPIWorker();
108+
containsPublicAPICache.set(symbol, result);
109+
return result;
110+
111+
function containsPublicAPIWorker(): boolean {
112+
if (!symbol.declarations?.length) {
113+
return false;
114+
}
115+
116+
if (symbol.flags & ts.SymbolFlags.Alias) {
117+
const resolved = typeChecker.getAliasedSymbol(symbol);
118+
return containsPublicAPI(resolved);
119+
}
120+
121+
if (symbol.flags & ts.SymbolFlags.ValueModule) {
122+
for (const me of typeChecker.getExportsOfModule(symbol)) {
123+
if (containsPublicAPI(me)) {
124+
return true;
125+
}
126+
}
127+
return false;
128+
}
129+
130+
for (const decl of symbol.declarations) {
131+
const statement = getDeclarationStatement(decl);
132+
if (statement && !isInternalDeclaration(statement)) {
133+
return true;
134+
}
135+
}
136+
137+
return false;
138+
}
139+
}
140+
141+
function nodeToLocation(decl: ts.Node): string {
142+
const sourceFile = decl.getSourceFile();
143+
const lc = sourceFile.getLineAndCharacterOfPosition(decl.pos);
144+
return `${sourceFile.fileName}:${lc.line}:${lc.character}`;
145+
}
146+
147+
function emitAsNamespace(name: string, moduleSymbol: ts.Symbol) {
148+
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule);
149+
150+
if (name === "ts") {
151+
// We will write `export = ts` at the end.
152+
write(`declare namespace ${name} {`);
153+
}
154+
else {
155+
// No export modifier; we are already in the namespace.
156+
write(`namespace ${name} {`);
157+
}
158+
increaseIndent();
159+
160+
const moduleExports = typeChecker.getExportsOfModule(moduleSymbol);
161+
for (const me of moduleExports) {
162+
if (stripInternal && !containsPublicAPI(me)) {
163+
continue;
164+
}
165+
166+
assert(me.declarations?.length);
167+
168+
if (me.flags & ts.SymbolFlags.Alias) {
169+
const resolved = typeChecker.getAliasedSymbol(me);
170+
emitAsNamespace(me.name, resolved);
171+
continue;
172+
}
173+
174+
for (const decl of me.declarations) {
175+
let statement = getDeclarationStatement(decl);
176+
177+
if (!statement) {
178+
throw new Error(`Unhandled declaration for ${me.name} at ${nodeToLocation(decl)}`);
179+
}
180+
181+
// Ignore an internal declaration.
182+
if (stripInternal && isInternalDeclaration(statement)) {
183+
continue;
184+
}
185+
186+
// Remove internal components and declare/const keywords.
187+
statement = ts.visitEachChild(statement, (node) => {
188+
if (stripInternal && isInternalDeclaration(node)) {
189+
return undefined;
190+
}
191+
192+
switch (node.kind) {
193+
case ts.SyntaxKind.DeclareKeyword: // No need to emit this in d.ts files.
194+
case ts.SyntaxKind.ConstKeyword: // Remove const from const enums.
195+
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
196+
return undefined;
197+
}
198+
199+
return node;
200+
}, nullTransformationContext);
201+
202+
write(printer.printNode(ts.EmitHint.Unspecified, statement, decl.getSourceFile()));
203+
}
204+
}
205+
206+
decreaseIndent();
207+
write(`}`);
208+
}
209+
210+
emitAsNamespace("ts", moduleSymbol);
211+
212+
write("export = ts;");
213+
214+
const copyrightNotice = fs.readFileSync(path.join(__dirname, "..", "CopyrightNotice.txt"), "utf-8");
215+
const outputContents = copyrightNotice + lines.join(newLine);
216+
217+
if (stripInternal && outputContents.includes("@internal")) {
218+
console.error("Output includes untrimmed @internal nodes!");
219+
}
220+
221+
fs.writeFileSync(output, outputContents);

0 commit comments

Comments
 (0)