Skip to content

Commit 316dc88

Browse files
committed
Add dts bundling
1 parent 708c6b0 commit 316dc88

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+7872
-7791
lines changed

Gulpfile.mjs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,20 @@ const buildSrc = () => buildProject("src");
125125

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

128+
/**
129+
* @param {string} entrypoint
130+
* @param {string} output
131+
*/
132+
async function runDtsBundler(entrypoint, output) {
133+
await exec(process.execPath, [
134+
"./scripts/dtsBundler.mjs",
135+
"--entrypoint",
136+
entrypoint,
137+
"--output",
138+
output,
139+
]);
140+
}
141+
128142
/** @type {string | undefined} */
129143
let copyrightHeader;
130144
function getCopyrightHeader() {
@@ -271,6 +285,7 @@ const esbuildServices = esbuildTask("./src/typescript/typescript.ts", "./built/l
271285
const writeServicesCJSShim = () => writeCJSReexport("./built/local/typescript/typescript.js", "./built/local/typescript.js");
272286
const buildServicesProject = () => buildProject("src/typescript");
273287

288+
// TODO(jakebailey): rename this; no longer "services".
274289
const buildServices = () => {
275290
if (cmdLineOptions.bundle) return esbuildServices.build();
276291
writeServicesCJSShim();
@@ -300,6 +315,9 @@ task("watch-services").flags = {
300315
" --built": "Compile using the built version of the compiler."
301316
};
302317

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

304322
const esbuildServer = esbuildTask("./src/tsserver/server.ts", "./built/local/tsserver.js", /* exportIsTsObject */ true);
305323
const writeServerCJSShim = () => writeCJSReexport("./built/local/tsserver/server.js", "./built/local/tsserver.js");
@@ -351,10 +369,11 @@ task("watch-min").flags = {
351369
const esbuildLssl = esbuildTask("./src/tsserverlibrary/tsserverlibrary.ts", "./built/local/tsserverlibrary.js", /* exportIsTsObject */ true);
352370
const writeLsslCJSShim = () => writeCJSReexport("./built/local/tsserverlibrary/tsserverlibrary.js", "./built/local/tsserverlibrary.js");
353371

372+
const buildLsslProject = () => buildProject("src/tsserverlibrary");
354373
const buildLssl = () => {
355374
if (cmdLineOptions.bundle) return esbuildLssl.build();
356375
writeLsslCJSShim();
357-
return buildProject("src/tsserverlibrary");
376+
return buildLsslProject();
358377
};
359378
task("lssl", series(preBuild, buildLssl));
360379
task("lssl").description = "Builds language service server library";
@@ -378,6 +397,14 @@ task("watch-lssl").flags = {
378397
" --built": "Compile using the built version of the compiler."
379398
};
380399

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

485-
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs)));
512+
task("local", series(preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts)));
486513
task("local").description = "Builds the full compiler and services";
487514
task("local").flags = {
488515
" --built": "Compile using the built version of the compiler."
@@ -498,7 +525,7 @@ const preTest = parallel(buildTsc, buildTests, buildServices, buildLssl);
498525
preTest.displayName = "preTest";
499526

500527
const runTests = () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ false);
501-
task("runtests", series(preBuild, preTest, runTests));
528+
task("runtests", series(preBuild, preTest, dts, runTests));
502529
task("runtests").description = "Runs the tests using the built run.js file.";
503530
task("runtests").flags = {
504531
"-t --tests=<regex>": "Pattern for tests to run.",
@@ -517,7 +544,7 @@ task("runtests").flags = {
517544
};
518545

519546
const runTestsParallel = () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1, /*watchMode*/ false);
520-
task("runtests-parallel", series(preBuild, preTest, runTestsParallel));
547+
task("runtests-parallel", series(preBuild, preTest, dts, runTestsParallel));
521548
task("runtests-parallel").description = "Runs all the tests in parallel using the built run.js file.";
522549
task("runtests-parallel").flags = {
523550
" --light": "Run tests in light mode (fewer verifications, but tests run faster).",
@@ -603,8 +630,7 @@ const produceLKG = async () => {
603630
}
604631
};
605632

606-
// TODO(jakebailey): dependencies on dts
607-
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs), produceLKG));
633+
task("LKG", series(lkgPreBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, dts), produceLKG));
608634
task("LKG").description = "Makes a new LKG out of the built js files";
609635
task("LKG").flags = {
610636
" --built": "Compile using the built version of the compiler.",

scripts/dtsBundler.mjs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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 assert from "assert";
9+
import fs from "fs";
10+
import path from "path";
11+
import minimist from "minimist";
12+
import url from "url";
13+
import ts from "../lib/typescript.js";
14+
15+
const __filename = url.fileURLToPath(new URL(import.meta.url));
16+
const __dirname = path.dirname(__filename);
17+
18+
19+
const dotDts = ".d.ts";
20+
21+
const options = minimist(process.argv.slice(2), {
22+
string: ["project", "entrypoint", "output"],
23+
});
24+
25+
const entrypoint = options.entrypoint;
26+
const output = options.output;
27+
28+
assert(typeof entrypoint === "string" && entrypoint);
29+
assert(typeof output === "string" && output);
30+
assert(output.endsWith(dotDts));
31+
32+
const internalOutput = output.substring(0, output.length - dotDts.length) + ".internal" + dotDts;
33+
34+
console.log(`Bundling ${entrypoint} to ${output} and ${internalOutput}`);
35+
36+
const newLineKind = ts.NewLineKind.LineFeed;
37+
const newLine = newLineKind === ts.NewLineKind.LineFeed ? "\n" : "\r\n";
38+
39+
/** @type {(node: ts.Node) => node is ts.DeclarationStatement} */
40+
function isDeclarationStatement(node) {
41+
return /** @type {any} */ (ts).isDeclarationStatement(node);
42+
}
43+
44+
/** @type {(node: ts.Node) => boolean} */
45+
function isInternalDeclaration(node) {
46+
return /** @type {any} */ (ts).isInternalDeclaration(node, node.getSourceFile());
47+
}
48+
49+
/**
50+
*
51+
* @param {ts.VariableDeclaration} node
52+
* @returns {ts.VariableStatement}
53+
*/
54+
function getParentVariableStatement(node) {
55+
const declarationList = node.parent;
56+
assert(ts.isVariableDeclarationList(declarationList), `expected VariableDeclarationList at ${nodeToLocation(node)}`);
57+
assert(declarationList.declarations.length === 1, `expected VariableDeclarationList of length 1 at ${nodeToLocation(node)}`);
58+
const variableStatement = declarationList.parent;
59+
assert(ts.isVariableStatement(variableStatement), `expected VariableStatement at ${nodeToLocation(node)}`);
60+
return variableStatement;
61+
}
62+
63+
/**
64+
*
65+
* @param {ts.Declaration} node
66+
* @returns {ts.Statement | undefined}
67+
*/
68+
function getDeclarationStatement(node) {
69+
if (ts.isVariableDeclaration(node)) {
70+
return getParentVariableStatement(node);
71+
}
72+
else if (isDeclarationStatement(node)) {
73+
return node;
74+
}
75+
return undefined;
76+
}
77+
78+
/** @type {ts.TransformationContext} */
79+
const nullTransformationContext = /** @type {any} */ (ts).nullTransformationContext;
80+
81+
const program = ts.createProgram([entrypoint], { target: ts.ScriptTarget.ES5 });
82+
83+
const typeChecker = program.getTypeChecker();
84+
85+
const sourceFile = program.getSourceFile(entrypoint);
86+
assert(sourceFile);
87+
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
88+
assert(moduleSymbol);
89+
90+
const printer = ts.createPrinter({ newLine: newLineKind });
91+
92+
/** @type {string[]} */
93+
const publicLines = [];
94+
/** @type {string[]} */
95+
const internalLines = [];
96+
97+
const indent = " ";
98+
let currentIndent = "";
99+
100+
function increaseIndent() {
101+
currentIndent += indent;
102+
}
103+
104+
function decreaseIndent() {
105+
currentIndent = currentIndent.slice(indent.length);
106+
}
107+
108+
/**
109+
* @enum {number}
110+
*/
111+
const WriteTarget = {
112+
Public: 1 << 0,
113+
Internal: 1 << 1,
114+
Both: (1 << 0) | (1 << 1),
115+
}
116+
117+
/**
118+
* @param {string} s
119+
* @param {WriteTarget} target
120+
*/
121+
function write(s, target) {
122+
if (!target) {
123+
return;
124+
}
125+
126+
const toPush = !s ? [""] : s.split(/\r?\n/).filter(line => line).map(line => (currentIndent + line).trimEnd());
127+
128+
if (target & WriteTarget.Public) {
129+
publicLines.push(...toPush);
130+
}
131+
if (target & WriteTarget.Internal) {
132+
internalLines.push(...toPush);
133+
}
134+
}
135+
136+
/**
137+
* @param {ts.Node} node
138+
* @param {ts.SourceFile} sourceFile
139+
* @param {WriteTarget} target
140+
*/
141+
function writeNode(node, sourceFile, target) {
142+
write(printer.printNode(ts.EmitHint.Unspecified, node, sourceFile), target);
143+
}
144+
145+
/** @type {Map<ts.Symbol, boolean>} */
146+
const containsPublicAPICache = new Map();
147+
148+
/**
149+
* @param {ts.Symbol} symbol
150+
* @returns {boolean}
151+
*/
152+
function containsPublicAPI(symbol) {
153+
const cached = containsPublicAPICache.get(symbol);
154+
if (cached !== undefined) {
155+
return cached;
156+
}
157+
158+
const result = containsPublicAPIWorker();
159+
containsPublicAPICache.set(symbol, result);
160+
return result;
161+
162+
function containsPublicAPIWorker() {
163+
if (!symbol.declarations?.length) {
164+
return false;
165+
}
166+
167+
if (symbol.flags & ts.SymbolFlags.Alias) {
168+
const resolved = typeChecker.getAliasedSymbol(symbol);
169+
return containsPublicAPI(resolved);
170+
}
171+
172+
// Namespace barrel; actual namespaces are checked below.
173+
if (symbol.flags & ts.SymbolFlags.ValueModule && symbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile) {
174+
for (const me of typeChecker.getExportsOfModule(symbol)) {
175+
if (containsPublicAPI(me)) {
176+
return true;
177+
}
178+
}
179+
return false;
180+
}
181+
182+
for (const decl of symbol.declarations) {
183+
const statement = getDeclarationStatement(decl);
184+
if (statement && !isInternalDeclaration(statement)) {
185+
return true;
186+
}
187+
}
188+
189+
return false;
190+
}
191+
}
192+
193+
/**
194+
* @param {ts.Node} decl
195+
*/
196+
function nodeToLocation(decl) {
197+
const sourceFile = decl.getSourceFile();
198+
const lc = sourceFile.getLineAndCharacterOfPosition(decl.pos);
199+
return `${sourceFile.fileName}:${lc.line}:${lc.character}`;
200+
}
201+
202+
/**
203+
* @param {ts.Node} node
204+
* @returns {ts.Node | undefined}
205+
*/
206+
function removeDeclareConstExport(node) {
207+
switch (node.kind) {
208+
case ts.SyntaxKind.DeclareKeyword: // No need to emit this in d.ts files.
209+
case ts.SyntaxKind.ConstKeyword: // Remove const from const enums.
210+
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
211+
return undefined;
212+
}
213+
return node;
214+
}
215+
216+
/**
217+
* @param {string} name
218+
* @param {ts.Symbol} moduleSymbol
219+
*/
220+
function emitAsNamespace(name, moduleSymbol) {
221+
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule);
222+
223+
const target = containsPublicAPI(moduleSymbol) ? WriteTarget.Both : WriteTarget.Internal;
224+
225+
if (name === "ts") {
226+
// We will write `export = ts` at the end.
227+
write(`declare namespace ${name} {`, target);
228+
}
229+
else {
230+
// No export modifier; we are already in the namespace.
231+
write(`namespace ${name} {`, target);
232+
}
233+
increaseIndent();
234+
235+
const moduleExports = typeChecker.getExportsOfModule(moduleSymbol);
236+
for (const me of moduleExports) {
237+
assert(me.declarations?.length);
238+
239+
if (me.flags & ts.SymbolFlags.Alias) {
240+
const resolved = typeChecker.getAliasedSymbol(me);
241+
emitAsNamespace(me.name, resolved);
242+
continue;
243+
}
244+
245+
for (const decl of me.declarations) {
246+
const statement = getDeclarationStatement(decl);
247+
const sourceFile = decl.getSourceFile();
248+
249+
if (!statement) {
250+
throw new Error(`Unhandled declaration for ${me.name} at ${nodeToLocation(decl)}`);
251+
}
252+
253+
const isInternal = isInternalDeclaration(statement);
254+
if (!isInternal) {
255+
const publicStatement = ts.visitEachChild(statement, (node) => {
256+
// No @internal comments in the public API.
257+
if (isInternalDeclaration(node)) {
258+
return undefined;
259+
}
260+
return removeDeclareConstExport(node);
261+
}, nullTransformationContext);
262+
263+
writeNode(publicStatement, sourceFile, WriteTarget.Public);
264+
}
265+
266+
const internalStatement = ts.visitEachChild(statement, removeDeclareConstExport, nullTransformationContext);
267+
268+
writeNode(internalStatement, sourceFile, WriteTarget.Internal);
269+
}
270+
}
271+
272+
decreaseIndent();
273+
write(`}`, target);
274+
}
275+
276+
emitAsNamespace("ts", moduleSymbol);
277+
278+
write("export = ts;", WriteTarget.Both);
279+
280+
const copyrightNotice = fs.readFileSync(path.join(__dirname, "..", "CopyrightNotice.txt"), "utf-8");
281+
const publicContents = copyrightNotice + publicLines.join(newLine);
282+
const internalContents = copyrightNotice + internalLines.join(newLine);
283+
284+
if (publicContents.includes("@internal")) {
285+
console.error("Output includes untrimmed @internal nodes!");
286+
}
287+
288+
fs.writeFileSync(output, publicContents);
289+
fs.writeFileSync(internalOutput, internalContents);

0 commit comments

Comments
 (0)