Skip to content

Commit 320e17f

Browse files
authored
"Annotate" exported object to fix named / namespace imports of our API in Node ESM (#57133)
1 parent 6d458e8 commit 320e17f

File tree

4 files changed

+50
-29
lines changed

4 files changed

+50
-29
lines changed

Herebyfile.mjs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
CancelToken,
44
} from "@esfx/canceltoken";
5+
import assert from "assert";
56
import chalk from "chalk";
67
import chokidar from "chokidar";
78
import esbuild from "esbuild";
@@ -46,7 +47,7 @@ import {
4647
void 0;
4748

4849
const copyrightFilename = "./scripts/CopyrightNotice.txt";
49-
const copyright = memoize(async () => {
50+
const getCopyrightHeader = memoize(async () => {
5051
const contents = await fs.promises.readFile(copyrightFilename, "utf-8");
5152
return contents.replace(/\r\n/g, "\n");
5253
});
@@ -76,7 +77,7 @@ export const generateLibs = task({
7677
run: async () => {
7778
await fs.promises.mkdir("./built/local", { recursive: true });
7879
for (const lib of libs()) {
79-
let output = await copyright();
80+
let output = await getCopyrightHeader();
8081

8182
for (const source of lib.sources) {
8283
const contents = await fs.promises.readFile(source, "utf-8");
@@ -187,10 +188,13 @@ async function runDtsBundler(entrypoint, output) {
187188
*/
188189
function createBundler(entrypoint, outfile, taskOptions = {}) {
189190
const getOptions = memoize(async () => {
191+
const copyright = await getCopyrightHeader();
192+
const banner = taskOptions.exportIsTsObject ? "var ts = {}; ((module) => {" : "";
193+
190194
/** @type {esbuild.BuildOptions} */
191195
const options = {
192196
entryPoints: [entrypoint],
193-
banner: { js: await copyright() },
197+
banner: { js: copyright + banner },
194198
bundle: true,
195199
outfile,
196200
platform: "node",
@@ -205,12 +209,10 @@ function createBundler(entrypoint, outfile, taskOptions = {}) {
205209
};
206210

207211
if (taskOptions.exportIsTsObject) {
208-
// We use an IIFE so we can inject the footer, and so that "ts" is global if not loaded as a module.
209-
options.format = "iife";
210-
// Name the variable ts, matching our old big bundle and so we can use the code below.
211-
options.globalName = "ts";
212-
// If we are in a CJS context, export the ts namespace.
213-
options.footer = { js: `\nif (typeof module !== "undefined" && module.exports) { module.exports = ts; }` };
212+
// Monaco bundles us as ESM by wrapping our code with something that defines module.exports
213+
// but then does not use it, instead using the `ts` variable. Ensure that if we think we're CJS
214+
// that we still set `ts` to the module.exports object.
215+
options.footer = { js: `})(typeof module !== "undefined" && module.exports ? module : { exports: ts });\nif (typeof module !== "undefined" && module.exports) { ts = module.exports; }` };
214216

215217
// esbuild converts calls to "require" to "__require"; this function
216218
// calls the real require if it exists, or throws if it does not (rather than
@@ -227,13 +229,25 @@ function createBundler(entrypoint, outfile, taskOptions = {}) {
227229
const fakeName = "Q".repeat(require.length);
228230
const fakeNameRegExp = new RegExp(fakeName, "g");
229231
options.define = { [require]: fakeName };
232+
233+
// For historical reasons, TypeScript does not set __esModule. Hack esbuild's __toCommonJS to be a noop.
234+
// We reference `__copyProps` to ensure the final bundle doesn't have any unreferenced code.
235+
const toCommonJsRegExp = /var __toCommonJS .*/;
236+
const toCommonJsRegExpReplacement = "var __toCommonJS = (mod) => (__copyProps, mod); // Modified helper to skip setting __esModule.";
237+
230238
options.plugins = [
231239
{
232-
name: "fix-require",
240+
name: "post-process",
233241
setup: build => {
234242
build.onEnd(async () => {
235243
let contents = await fs.promises.readFile(outfile, "utf-8");
236244
contents = contents.replace(fakeNameRegExp, require);
245+
let matches = 0;
246+
contents = contents.replace(toCommonJsRegExp, () => {
247+
matches++;
248+
return toCommonJsRegExpReplacement;
249+
});
250+
assert(matches === 1, "Expected exactly one match for __toCommonJS");
237251
await fs.promises.writeFile(outfile, contents);
238252
});
239253
},
@@ -450,7 +464,7 @@ export = ts;
450464
* @param {string} contents
451465
*/
452466
async function fileContentsWithCopyright(contents) {
453-
return await copyright() + contents.trim().replace(/\r\n/g, "\n") + "\n";
467+
return await getCopyrightHeader() + contents.trim().replace(/\r\n/g, "\n") + "\n";
454468
}
455469

456470
const lssl = task({

scripts/checkModuleFormat.mjs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,37 @@ import {
55
__importDefault,
66
__importStar,
77
} from "tslib";
8+
import {
9+
pathToFileURL,
10+
} from "url";
811

912
// This script tests that TypeScript's CJS API is structured
1013
// as expected. It calls "require" as though it were in CWD,
1114
// so it can be tested on a separate install of TypeScript.
1215

1316
const require = createRequire(process.cwd() + "/index.js");
17+
const typescript = process.argv[2];
18+
const resolvedTypeScript = pathToFileURL(require.resolve(typescript)).toString();
1419

15-
console.log(`Testing ${process.argv[2]}...`);
16-
const ts = require(process.argv[2]);
20+
console.log(`Testing ${typescript}...`);
1721

1822
// See: https://github.com/microsoft/TypeScript/pull/51474#issuecomment-1310871623
19-
/** @type {[fn: (() => any), shouldSucceed: boolean][]} */
23+
/** @type {[fn: (() => Promise<any>), shouldSucceed: boolean][]} */
2024
const fns = [
21-
[() => ts.version, true],
22-
[() => ts.default.version, false],
23-
[() => __importDefault(ts).version, false],
24-
[() => __importDefault(ts).default.version, true],
25-
[() => __importStar(ts).version, true],
26-
[() => __importStar(ts).default.version, true],
25+
[() => require(typescript).version, true],
26+
[() => require(typescript).default.version, false],
27+
[() => __importDefault(require(typescript)).version, false],
28+
[() => __importDefault(require(typescript)).default.version, true],
29+
[() => __importStar(require(typescript)).version, true],
30+
[() => __importStar(require(typescript)).default.version, true],
31+
[async () => (await import(resolvedTypeScript)).version, true],
32+
[async () => (await import(resolvedTypeScript)).default.version, true],
2733
];
2834

2935
for (const [fn, shouldSucceed] of fns) {
3036
let success = false;
3137
try {
32-
success = !!fn();
38+
success = !!(await fn());
3339
}
3440
catch {
3541
// Ignore
@@ -43,4 +49,10 @@ for (const [fn, shouldSucceed] of fns) {
4349
process.exitCode = 1;
4450
}
4551
}
46-
console.log("ok");
52+
53+
if (process.exitCode) {
54+
console.log("fail");
55+
}
56+
else {
57+
console.log("ok");
58+
}

src/compiler/core.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,12 +2743,8 @@ export function skipWhile<T, U extends T>(array: readonly T[] | undefined, predi
27432743
export function isNodeLikeSystem(): boolean {
27442744
// This is defined here rather than in sys.ts to prevent a cycle from its
27452745
// use in performanceCore.ts.
2746-
//
2747-
// We don't use the presence of `require` to check if we are in Node;
2748-
// when bundled using esbuild, this function will be rewritten to `__require`
2749-
// and definitely exist.
27502746
return typeof process !== "undefined"
27512747
&& !!process.nextTick
27522748
&& !(process as any).browser
2753-
&& typeof module === "object";
2749+
&& typeof require !== "undefined";
27542750
}

src/typescript/typescript.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Debug,
33
LogLevel,
44
} from "./_namespaces/ts";
5-
import * as ts from "./_namespaces/ts";
65

76
// enable deprecation logging
87
declare const console: any;
@@ -23,4 +22,4 @@ if (typeof console !== "undefined") {
2322
};
2423
}
2524

26-
export = ts;
25+
export * from "./_namespaces/ts";

0 commit comments

Comments
 (0)