Skip to content

Commit d40a564

Browse files
committed
Make things work in ESM with polyfill
1 parent 0a03122 commit d40a564

34 files changed

+250
-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": [

0 commit comments

Comments
 (0)