Skip to content

Commit

Permalink
"Annotate" exported object to fix named / namespace imports of our AP…
Browse files Browse the repository at this point in the history
…I in Node ESM (#57133)
  • Loading branch information
jakebailey committed Mar 4, 2024
1 parent 6d458e8 commit 320e17f
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 29 deletions.
36 changes: 25 additions & 11 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
CancelToken,
} from "@esfx/canceltoken";
import assert from "assert";
import chalk from "chalk";
import chokidar from "chokidar";
import esbuild from "esbuild";
Expand Down Expand Up @@ -46,7 +47,7 @@ import {
void 0;

const copyrightFilename = "./scripts/CopyrightNotice.txt";
const copyright = memoize(async () => {
const getCopyrightHeader = memoize(async () => {
const contents = await fs.promises.readFile(copyrightFilename, "utf-8");
return contents.replace(/\r\n/g, "\n");
});
Expand Down Expand Up @@ -76,7 +77,7 @@ export const generateLibs = task({
run: async () => {
await fs.promises.mkdir("./built/local", { recursive: true });
for (const lib of libs()) {
let output = await copyright();
let output = await getCopyrightHeader();

for (const source of lib.sources) {
const contents = await fs.promises.readFile(source, "utf-8");
Expand Down Expand Up @@ -187,10 +188,13 @@ async function runDtsBundler(entrypoint, output) {
*/
function createBundler(entrypoint, outfile, taskOptions = {}) {
const getOptions = memoize(async () => {
const copyright = await getCopyrightHeader();
const banner = taskOptions.exportIsTsObject ? "var ts = {}; ((module) => {" : "";

/** @type {esbuild.BuildOptions} */
const options = {
entryPoints: [entrypoint],
banner: { js: await copyright() },
banner: { js: copyright + banner },
bundle: true,
outfile,
platform: "node",
Expand All @@ -205,12 +209,10 @@ function createBundler(entrypoint, outfile, taskOptions = {}) {
};

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

// esbuild converts calls to "require" to "__require"; this function
// calls the real require if it exists, or throws if it does not (rather than
Expand All @@ -227,13 +229,25 @@ function createBundler(entrypoint, outfile, taskOptions = {}) {
const fakeName = "Q".repeat(require.length);
const fakeNameRegExp = new RegExp(fakeName, "g");
options.define = { [require]: fakeName };

// For historical reasons, TypeScript does not set __esModule. Hack esbuild's __toCommonJS to be a noop.
// We reference `__copyProps` to ensure the final bundle doesn't have any unreferenced code.
const toCommonJsRegExp = /var __toCommonJS .*/;
const toCommonJsRegExpReplacement = "var __toCommonJS = (mod) => (__copyProps, mod); // Modified helper to skip setting __esModule.";

options.plugins = [
{
name: "fix-require",
name: "post-process",
setup: build => {
build.onEnd(async () => {
let contents = await fs.promises.readFile(outfile, "utf-8");
contents = contents.replace(fakeNameRegExp, require);
let matches = 0;
contents = contents.replace(toCommonJsRegExp, () => {
matches++;
return toCommonJsRegExpReplacement;
});
assert(matches === 1, "Expected exactly one match for __toCommonJS");
await fs.promises.writeFile(outfile, contents);
});
},
Expand Down Expand Up @@ -450,7 +464,7 @@ export = ts;
* @param {string} contents
*/
async function fileContentsWithCopyright(contents) {
return await copyright() + contents.trim().replace(/\r\n/g, "\n") + "\n";
return await getCopyrightHeader() + contents.trim().replace(/\r\n/g, "\n") + "\n";
}

const lssl = task({
Expand Down
34 changes: 23 additions & 11 deletions scripts/checkModuleFormat.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,37 @@ import {
__importDefault,
__importStar,
} from "tslib";
import {
pathToFileURL,
} from "url";

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

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

console.log(`Testing ${process.argv[2]}...`);
const ts = require(process.argv[2]);
console.log(`Testing ${typescript}...`);

// See: https://github.com/microsoft/TypeScript/pull/51474#issuecomment-1310871623
/** @type {[fn: (() => any), shouldSucceed: boolean][]} */
/** @type {[fn: (() => Promise<any>), shouldSucceed: boolean][]} */
const fns = [
[() => ts.version, true],
[() => ts.default.version, false],
[() => __importDefault(ts).version, false],
[() => __importDefault(ts).default.version, true],
[() => __importStar(ts).version, true],
[() => __importStar(ts).default.version, true],
[() => require(typescript).version, true],
[() => require(typescript).default.version, false],
[() => __importDefault(require(typescript)).version, false],
[() => __importDefault(require(typescript)).default.version, true],
[() => __importStar(require(typescript)).version, true],
[() => __importStar(require(typescript)).default.version, true],
[async () => (await import(resolvedTypeScript)).version, true],
[async () => (await import(resolvedTypeScript)).default.version, true],
];

for (const [fn, shouldSucceed] of fns) {
let success = false;
try {
success = !!fn();
success = !!(await fn());
}
catch {
// Ignore
Expand All @@ -43,4 +49,10 @@ for (const [fn, shouldSucceed] of fns) {
process.exitCode = 1;
}
}
console.log("ok");

if (process.exitCode) {
console.log("fail");
}
else {
console.log("ok");
}
6 changes: 1 addition & 5 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2743,12 +2743,8 @@ export function skipWhile<T, U extends T>(array: readonly T[] | undefined, predi
export function isNodeLikeSystem(): boolean {
// This is defined here rather than in sys.ts to prevent a cycle from its
// use in performanceCore.ts.
//
// We don't use the presence of `require` to check if we are in Node;
// when bundled using esbuild, this function will be rewritten to `__require`
// and definitely exist.
return typeof process !== "undefined"
&& !!process.nextTick
&& !(process as any).browser
&& typeof module === "object";
&& typeof require !== "undefined";
}
3 changes: 1 addition & 2 deletions src/typescript/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Debug,
LogLevel,
} from "./_namespaces/ts";
import * as ts from "./_namespaces/ts";

// enable deprecation logging
declare const console: any;
Expand All @@ -23,4 +22,4 @@ if (typeof console !== "undefined") {
};
}

export = ts;
export * from "./_namespaces/ts";

0 comments on commit 320e17f

Please sign in to comment.