Skip to content

Commit 983a8e1

Browse files
committed
Make things work in ESM with polyfill
1 parent 40176ce commit 983a8e1

34 files changed

+249
-230
lines changed

Herebyfile.mjs

Lines changed: 32 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @ts-check
22
import { CancelToken } from "@esfx/canceltoken";
3-
import assert from "assert";
43
import chalk from "chalk";
54
import chokidar from "chokidar";
65
import esbuild from "esbuild";
@@ -172,25 +171,22 @@ async function runDtsBundler(entrypoint, output) {
172171
* @param {BundlerTaskOptions} [taskOptions]
173172
*
174173
* @typedef BundlerTaskOptions
175-
* @property {boolean} [exportIsTsObject]
176174
* @property {boolean} [treeShaking]
177175
* @property {boolean} [usePublicAPI]
178176
* @property {() => void} [onWatchRebuild]
179177
*/
180178
function createBundler(entrypoint, outfile, taskOptions = {}) {
181179
const getOptions = memoize(async () => {
182180
const copyright = await getCopyrightHeader();
183-
const banner = taskOptions.exportIsTsObject ? "var ts = {}; ((module) => {" : "";
184-
185181
/** @type {esbuild.BuildOptions} */
186182
const options = {
187183
entryPoints: [entrypoint],
188-
banner: { js: copyright + banner },
184+
banner: { js: copyright },
189185
bundle: true,
190186
outfile,
191187
platform: "node",
192188
target: ["es2020", "node14.17"],
193-
format: "cjs",
189+
format: "esm",
194190
sourcemap: "linked",
195191
sourcesContent: false,
196192
treeShaking: taskOptions.treeShaking,
@@ -200,66 +196,17 @@ function createBundler(entrypoint, outfile, taskOptions = {}) {
200196
};
201197

202198
if (taskOptions.usePublicAPI) {
203-
options.external = ["./typescript.js"];
204199
options.plugins = options.plugins || [];
205200
options.plugins.push({
206-
name: "remap-typescript-to-require",
201+
name: "remap-typescript-to-public-api",
207202
setup(build) {
208-
build.onLoad({ filter: /src[\\/]typescript[\\/]typescript\.ts$/ }, () => {
209-
return { contents: `export * from "./typescript.js"` };
203+
build.onResolve({ filter: /^(?:\.\.[\\/])*typescript[\\/]typescript\.js$/ }, () => {
204+
return { path: "./typescript.js", external: true };
210205
});
211206
},
212207
});
213208
}
214209

215-
if (taskOptions.exportIsTsObject) {
216-
// Monaco bundles us as ESM by wrapping our code with something that defines module.exports
217-
// but then does not use it, instead using the `ts` variable. Ensure that if we think we're CJS
218-
// that we still set `ts` to the module.exports object.
219-
options.footer = { js: `})(typeof module !== "undefined" && module.exports ? module : { exports: ts });\nif (typeof module !== "undefined" && module.exports) { ts = module.exports; }` };
220-
221-
// esbuild converts calls to "require" to "__require"; this function
222-
// calls the real require if it exists, or throws if it does not (rather than
223-
// throwing an error like "require not defined"). But, since we want typescript
224-
// to be consumable by other bundlers, we need to convert these calls back to
225-
// require so our imports are visible again.
226-
//
227-
// To fix this, we redefine "require" to a name we're unlikely to use with the
228-
// same length as "require", then replace it back to "require" after bundling,
229-
// ensuring that source maps still work.
230-
//
231-
// See: https://github.com/evanw/esbuild/issues/1905
232-
const require = "require";
233-
const fakeName = "Q".repeat(require.length);
234-
const fakeNameRegExp = new RegExp(fakeName, "g");
235-
options.define = { [require]: fakeName };
236-
237-
// For historical reasons, TypeScript does not set __esModule. Hack esbuild's __toCommonJS to be a noop.
238-
// We reference `__copyProps` to ensure the final bundle doesn't have any unreferenced code.
239-
const toCommonJsRegExp = /var __toCommonJS .*/;
240-
const toCommonJsRegExpReplacement = "var __toCommonJS = (mod) => (__copyProps, mod); // Modified helper to skip setting __esModule.";
241-
242-
options.plugins = options.plugins || [];
243-
options.plugins.push(
244-
{
245-
name: "post-process",
246-
setup: build => {
247-
build.onEnd(async () => {
248-
let contents = await fs.promises.readFile(outfile, "utf-8");
249-
contents = contents.replace(fakeNameRegExp, require);
250-
let matches = 0;
251-
contents = contents.replace(toCommonJsRegExp, () => {
252-
matches++;
253-
return toCommonJsRegExpReplacement;
254-
});
255-
assert(matches === 1, "Expected exactly one match for __toCommonJS");
256-
await fs.promises.writeFile(outfile, contents);
257-
});
258-
},
259-
},
260-
);
261-
}
262-
263210
return options;
264211
});
265212

@@ -304,6 +251,7 @@ let printedWatchWarning = false;
304251
* @param {string} options.builtEntrypoint
305252
* @param {string} options.output
306253
* @param {Task[]} [options.mainDeps]
254+
* @param {boolean} [options.reexportDefault]
307255
* @param {BundlerTaskOptions} [options.bundlerOptions]
308256
*/
309257
function entrypointBuildTask(options) {
@@ -324,22 +272,33 @@ function entrypointBuildTask(options) {
324272
});
325273

326274
/**
327-
* Writes a CJS module that reexports another CJS file. E.g. given
275+
* Writes a module that reexports another file. E.g. given
328276
* `options.builtEntrypoint = "./built/local/tsc/tsc.js"` and
329277
* `options.output = "./built/local/tsc.js"`, this will create a file
330278
* named "./built/local/tsc.js" containing:
331279
*
332280
* ```
333-
* module.exports = require("./tsc/tsc.js")
281+
* export * from "./tsc/tsc.js";
334282
* ```
335283
*/
336284
const shim = task({
337285
name: `shim-${options.name}`,
338286
run: async () => {
339287
const outDir = path.dirname(options.output);
340288
await fs.promises.mkdir(outDir, { recursive: true });
341-
const moduleSpecifier = path.relative(outDir, options.builtEntrypoint);
342-
await fs.promises.writeFile(options.output, `module.exports = require("./${moduleSpecifier.replace(/[\\/]/g, "/")}")`);
289+
const moduleSpecifier = path.relative(outDir, options.builtEntrypoint).replace(/[\\/]/g, "/");
290+
const lines = [
291+
`export * from "./${moduleSpecifier}";`,
292+
];
293+
294+
if (options.reexportDefault) {
295+
lines.push(
296+
`import _default from "./${moduleSpecifier}";`,
297+
`export default _default;`,
298+
);
299+
}
300+
301+
await fs.promises.writeFile(options.output, lines.join("\n") + "\n");
343302
},
344303
});
345304

@@ -404,7 +363,7 @@ const { main: services, build: buildServices, watch: watchServices } = entrypoin
404363
builtEntrypoint: "./built/local/typescript/typescript.js",
405364
output: "./built/local/typescript.js",
406365
mainDeps: [generateLibs],
407-
bundlerOptions: { exportIsTsObject: true },
366+
reexportDefault: true,
408367
});
409368
export { services, watchServices };
410369

@@ -445,25 +404,22 @@ export const watchMin = task({
445404
dependencies: [watchTsc, watchTsserver],
446405
});
447406

448-
// This is technically not enough to make tsserverlibrary loadable in the
449-
// browser, but it's unlikely that anyone has actually been doing that.
450407
const lsslJs = `
451-
if (typeof module !== "undefined" && module.exports) {
452-
module.exports = require("./typescript.js");
453-
}
454-
else {
455-
throw new Error("tsserverlibrary requires CommonJS; use typescript.js instead");
456-
}
408+
import ts from "./typescript.js";
409+
export * from "./typescript.js";
410+
export default ts;
457411
`;
458412

459413
const lsslDts = `
460-
import ts = require("./typescript.js");
461-
export = ts;
414+
import ts from "./typescript.js";
415+
export * from "./typescript.js";
416+
export default ts;
462417
`;
463418

464419
const lsslDtsInternal = `
465-
import ts = require("./typescript.internal.js");
466-
export = ts;
420+
import ts from "./typescript.internal.js";
421+
export * from "./typescript.internal.js";
422+
export default ts;
467423
`;
468424

469425
/**
@@ -504,7 +460,7 @@ const { main: tests, watch: watchTests } = entrypointBuildTask({
504460
description: "Builds the test infrastructure",
505461
buildDeps: [generateDiagnostics],
506462
project: "src/testRunner",
507-
srcEntrypoint: "./src/testRunner/_namespaces/Harness.ts",
463+
srcEntrypoint: "./src/testRunner/runner.ts",
508464
builtEntrypoint: "./built/local/testRunner/runner.js",
509465
output: testRunner,
510466
mainDeps: [generateLibs],

bin/tsc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/usr/bin/env node
2-
require('../lib/tsc.js')
2+
import '../lib/tsc.js';

bin/tsserver

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/usr/bin/env node
2-
require('../lib/tsserver.js')
2+
import '../lib/tsserver.js';

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@esfx/canceltoken": "^1.0.0",
4444
"@octokit/rest": "^20.1.1",
4545
"@types/chai": "^4.3.16",
46+
"@types/diff": "^5.2.1",
4647
"@types/microsoft__typescript-etw": "^0.1.3",
4748
"@types/minimist": "^1.2.5",
4849
"@types/mocha": "^10.0.6",

patchProcessGetBuiltin.cjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const _module = require("module");
2+
3+
/** @type {(name: string) => any} */
4+
function getBuiltinModule(name) {
5+
if (!_module.isBuiltin(name)) return undefined;
6+
return require(name);
7+
}
8+
9+
process.getBuiltinModule = getBuiltinModule;

scripts/browserIntegrationTest.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ for (const browserType of browsers) {
2828

2929
await page.setContent(`
3030
<html>
31-
<script>${readFileSync(join("built", "local", "typescript.js"), "utf8")}</script>
31+
<script type="module">${readFileSync(join("built", "local", "typescript.js"), "utf8")}</script>
3232
</html>
3333
`);
3434

scripts/checkModuleFormat.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ console.log(`Testing ${typescript}...`);
1919
/** @type {[fn: (() => Promise<any>), shouldSucceed: boolean][]} */
2020
const fns = [
2121
[() => require(typescript).version, true],
22-
[() => require(typescript).default.version, false],
22+
[() => require(typescript).default.version, true],
2323
[() => __importDefault(require(typescript)).version, false],
2424
[() => __importDefault(require(typescript)).default.version, true],
2525
[() => __importStar(require(typescript)).version, true],

scripts/dtsBundler.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ function isSelfReference(reference, symbol) {
357357
* @param {ts.Symbol} moduleSymbol
358358
*/
359359
function emitAsNamespace(name, parent, moduleSymbol, needExportModifier) {
360+
if (name === "default") return;
361+
360362
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule, "moduleSymbol is not a module");
361363

362364
const fullName = parent ? `${parent}.${name}` : name;
@@ -465,6 +467,7 @@ function emitAsNamespace(name, parent, moduleSymbol, needExportModifier) {
465467

466468
emitAsNamespace("ts", "", moduleSymbol, /*needExportModifier*/ false);
467469

470+
// TODO(jakebailey): require(ESM) - fix this
468471
write("export = ts;", WriteTarget.Both);
469472

470473
const copyrightNotice = fs.readFileSync(path.join(__dirname, "CopyrightNotice.txt"), "utf-8");

src/.eslintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
{ "name": "clearInterval" },
1515
{ "name": "setImmediate" },
1616
{ "name": "clearImmediate" },
17-
{ "name": "performance" }
17+
{ "name": "performance" },
18+
{ "name": "require" },
19+
{ "name": "__dirname" },
20+
{ "name": "__filename" }
1821
]
1922
},
2023
"overrides": [

src/cancellationToken/cancellationToken.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function pipeExists(name: string): boolean {
1717
return fs.existsSync(name);
1818
}
1919

20-
function createCancellationToken(args: string[]): ServerCancellationToken {
20+
export function createCancellationToken(args: string[]): ServerCancellationToken {
2121
let cancellationPipeName: string | undefined;
2222
for (let i = 0; i < args.length - 1; i++) {
2323
if (args[i] === "--cancellationPipeName") {
@@ -66,4 +66,3 @@ function createCancellationToken(args: string[]): ServerCancellationToken {
6666
};
6767
}
6868
}
69-
export = createCancellationToken;

src/compiler/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2730,5 +2730,5 @@ export function isNodeLikeSystem(): boolean {
27302730
return typeof process !== "undefined"
27312731
&& !!process.nextTick
27322732
&& !(process as any).browser
2733-
&& typeof require !== "undefined";
2733+
&& typeof (process as any).getBuiltinModule === "function";
27342734
}

src/compiler/nodeGetBuiltinModule.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function nodeCreateRequire(path: string): NodeJS.Require {
2+
const mod = nodeGetBuiltinModule("module") as typeof import("module") | undefined;
3+
if (!mod) throw new Error("missing node:module");
4+
return mod.createRequire(path);
5+
}
6+
7+
function nodeGetBuiltinModule(moduleName: string): unknown {
8+
if (typeof process === "undefined" || typeof (process as any).getBuiltinModule !== "function") {
9+
throw new Error("process.getBuiltinModule is not supported in this environment.");
10+
}
11+
return (process as any).getBuiltinModule(moduleName);
12+
}

src/compiler/perfLogger.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { nodeCreateRequire } from "./nodeGetBuiltinModule.js";
2+
13
/** @internal */
24
export interface PerfLogger {
35
logEvent(msg: string): void;
@@ -27,6 +29,7 @@ export interface PerfLogger {
2729
let etwModule: typeof import("@microsoft/typescript-etw") | undefined;
2830
try {
2931
const etwModulePath = process.env.TS_ETW_MODULE_PATH ?? "./node_modules/@microsoft/typescript-etw";
32+
const require = nodeCreateRequire(import.meta.url);
3033

3134
// require() will throw an exception if the module is not found
3235
// It may also return undefined if not installed properly

src/compiler/performanceCore.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isNodeLikeSystem } from "./_namespaces/ts.js";
2+
import { nodeCreateRequire } from "./nodeGetBuiltinModule.js";
23

34
// The following definitions provide the minimum compatible support for the Web Performance User Timings API
45
// between browsers and NodeJS:
@@ -31,7 +32,8 @@ function tryGetPerformance() {
3132
if (isNodeLikeSystem()) {
3233
try {
3334
// By default, only write native events when generating a cpu profile or using the v8 profiler.
34-
const { performance } = require("perf_hooks") as typeof import("perf_hooks");
35+
const require = nodeCreateRequire(import.meta.url);
36+
const { performance } = require("perf_hooks");
3537
return {
3638
shouldWriteNativeEvents: false,
3739
performance,

src/compiler/sys.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
WatchOptions,
4848
writeFileEnsuringDirectories,
4949
} from "./_namespaces/ts.js";
50+
import { nodeCreateRequire } from "./nodeGetBuiltinModule.js";
5051

5152
declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any;
5253
declare function clearTimeout(handle: any): void;
@@ -1466,9 +1467,15 @@ export let sys: System = (() => {
14661467
const byteOrderMarkIndicator = "\uFEFF";
14671468

14681469
function getNodeSystem(): System {
1470+
// TODO(jakebailey): Only use createRequire for sys.require.
1471+
const require = nodeCreateRequire(import.meta.url);
1472+
const _path: typeof import("path") = require("path");
1473+
const _url: typeof import("url") = require("url");
1474+
const __filename = _url.fileURLToPath(new URL(import.meta.url));
1475+
const __dirname = _path.dirname(__filename);
1476+
14691477
const nativePattern = /^native |^\([^)]+\)$|^(internal[\\/]|[a-zA-Z0-9_\s]+(\.js)?$)/;
14701478
const _fs: typeof import("fs") = require("fs");
1471-
const _path: typeof import("path") = require("path");
14721479
const _os = require("os");
14731480
// crypto can be absent on reduced node installations
14741481
let _crypto: typeof import("crypto") | undefined;

0 commit comments

Comments
 (0)