Skip to content

Commit 7fed31f

Browse files
committed
Add dts bundling
1 parent fd53551 commit 7fed31f

File tree

13 files changed

+7507
-7667
lines changed

13 files changed

+7507
-7667
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
@@ -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() {
@@ -265,10 +281,13 @@ const preBuild = cmdLineOptions.lkg ? lkgPreBuild : localPreBuild;
265281
const esbuildServices = esbuildTask("./src/typescript/typescript.ts", "./built/local/typescript.js", /* exportIsTsObject */ true, /* performanceMatters */ true);
266282

267283
// TODO(jakebailey): rename this; no longer "services".
284+
285+
const buildServicesProject = () => buildProject("src/typescript");
286+
268287
const buildServices = () => {
269288
if (cmdLineOptions.bundle) return esbuildServices.build();
270289
writeCJSReexport("./built/local/typescript/typescript.js", "./built/local/typescript.js");
271-
return buildProject("src/typescript");
290+
return buildServicesProject();
272291
};
273292

274293
task("services", series(preBuild, buildServices));
@@ -291,6 +310,9 @@ task("watch-services").flags = {
291310
" --built": "Compile using the built version of the compiler."
292311
};
293312

313+
const dtsServices = () => runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
314+
task("dts-services", series(preBuild, buildServicesProject, dtsServices));
315+
task("dts-services").description = "Builds typescript.d.ts";
294316

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

@@ -337,10 +359,11 @@ task("watch-min").flags = {
337359

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

362+
const buildLsslProject = () => buildProject("src/tsserverlibrary");
340363
const buildLssl = () => {
341364
if (cmdLineOptions.bundle) return esbuildLssl.build();
342365
writeCJSReexport("./built/local/tsserverlibrary/tsserverlibrary.js", "./built/local/tsserverlibrary.js");
343-
return buildProject("src/tsserverlibrary");
366+
return buildLsslProject();
344367
};
345368
task("lssl", series(preBuild, buildLssl));
346369
task("lssl").description = "Builds language service server library";
@@ -362,6 +385,14 @@ task("watch-lssl").flags = {
362385
" --built": "Compile using the built version of the compiler."
363386
};
364387

388+
const dtsLssl = () => runDtsBundler("./built/local/tsserverlibrary/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.d.ts");
389+
task("dts-lssl", series(preBuild, buildLsslProject, dtsLssl));
390+
task("dts-lssl").description = "Builds tsserverlibrary.d.ts";
391+
392+
// TODO(jakebailey): this is probably not efficient, but, gulp.
393+
const dts = series(preBuild, parallel(buildServicesProject, buildLsslProject), parallel(dtsServices, dtsLssl));
394+
task("dts", dts);
395+
365396
const testRunner = "./built/local/run.js";
366397
const esbuildTests = esbuildTask("./src/testRunner/_namespaces/Harness.ts", testRunner);
367398

@@ -472,7 +503,7 @@ const buildOtherOutputs = parallel(buildCancellationToken, buildTypingsInstaller
472503
task("other-outputs", series(preBuild, buildOtherOutputs));
473504
task("other-outputs").description = "Builds miscelaneous scripts and documents distributed with the LKG";
474505

475-
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs)));
506+
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts)));
476507
task("local").description = "Builds the full compiler and services";
477508
task("local").flags = {
478509
" --built": "Compile using the built version of the compiler."
@@ -488,7 +519,7 @@ const preTest = parallel(buildTsc, buildTests, buildServices, buildLssl);
488519
preTest.displayName = "preTest";
489520

490521
const runTests = () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ false);
491-
task("runtests", series(preBuild, preTest, runTests));
522+
task("runtests", series(preBuild, preTest, dts, runTests));
492523
task("runtests").description = "Runs the tests using the built run.js file.";
493524
task("runtests").flags = {
494525
"-t --tests=<regex>": "Pattern for tests to run.",
@@ -507,7 +538,7 @@ task("runtests").flags = {
507538
};
508539

509540
const runTestsParallel = () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false);
510-
task("runtests-parallel", series(preBuild, preTest, runTestsParallel));
541+
task("runtests-parallel", series(preBuild, preTest, dts, runTestsParallel));
511542
task("runtests-parallel").description = "Runs all the tests in parallel using the built run.js file.";
512543
task("runtests-parallel").flags = {
513544
" --light": "Run tests in light mode (fewer verifications, but tests run faster).",
@@ -594,8 +625,7 @@ const produceLKG = async () => {
594625
}
595626
};
596627

597-
// TODO(jakebailey): dependencies on dts
598-
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs), produceLKG));
628+
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts), produceLKG));
599629
task("LKG").description = "Makes a new LKG out of the built js files";
600630
task("LKG").flags = {
601631
" --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)