Skip to content

Commit

Permalink
feat: use babel when transpiling to es5 instead of swc until it reach…
Browse files Browse the repository at this point in the history
…es maturity
  • Loading branch information
wessberg committed Sep 26, 2021
1 parent 2e85c75 commit e7a02c8
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 267 deletions.
235 changes: 18 additions & 217 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
},
"dependencies": {
"@babel/core": "7.15.5",
"@babel/plugin-transform-block-scoping": "7.15.3",
"@babel/preset-env": "7.15.6",
"@babel/template": "7.15.4",
"@formatjs/intl-datetimeformat": "4.2.3",
"@formatjs/intl-displaynames": "5.2.3",
"@formatjs/intl-getcanonicallocales": "1.7.3",
Expand Down
4 changes: 3 additions & 1 deletion src/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export function booleanize(str: string | undefined): boolean {
switch (str.toLowerCase().trim()) {
case "true":
case "yes":
case "y":
case "1":
return true;
case "false":
case "no":
case "n":
case "0":
case null:
return false;
Expand All @@ -39,7 +41,7 @@ export function ensureArray<T>(item: T | T[]): T[] {
/**
* Returns a new object with all of the keys uppercased
*/
export function uppercaseKeys<T extends object>(obj: T): UppercaseKeys<T> {
export function uppercaseKeys<T extends Record<PropertyKey, unknown>>(obj: T): UppercaseKeys<T> {
const newObject = {} as UppercaseKeys<T>;
const entries = Object.entries(obj) as [keyof T & string, T[keyof T]][];
entries.forEach(([key, value]) => {
Expand Down
2 changes: 2 additions & 0 deletions src/build/build-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface BuildOptions {
context: PolyfillContext;
features: PolyfillFeature[];
featuresRequested: PolyfillFeature[];
browserslist: string[];
module: boolean;
ecmaVersion: EcmaVersion;
paths: string[];
sourcemap?: boolean;
Expand Down
101 changes: 62 additions & 39 deletions src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,10 @@ import {build as esbuild} from "esbuild";
import {tmpdir} from "os";
import {join, normalize} from "crosspath";
import {unlinkSync, writeFileSync} from "fs";
import {transform} from "@swc/core";
import {REGENERATOR_SOURCE, REGENERATOR_SOURCE_MINIFIED} from "../constant/regenerator-source";
import {transform as swc} from "@swc/core";
import {transformAsync as babel} from "@babel/core";
import {generateRandomHash} from "../api/util";

const swcBug1461MatchA = /var regeneratorRuntime\d?\s*=\s*require\(["'`]regenerator-runtime["'`]\);/;
const swcBug1461MatchB = /import\s+regeneratorRuntime\d?\s+from\s*["'`]regenerator-runtime["'`];/;
const swcBug1461MatchReference = /regeneratorRuntime\d\./g;

/**
* TODO: Remove this when https://github.com/swc-project/swc/issues/1461 has been resolved
*/
function workAroundSwcBug1461(str: string, minify = false): string {
return str
.replace(swcBug1461MatchA, minify ? REGENERATOR_SOURCE_MINIFIED : REGENERATOR_SOURCE)
.replace(swcBug1461MatchReference, "regeneratorRuntime.")
.replace(swcBug1461MatchB, minify ? REGENERATOR_SOURCE_MINIFIED : REGENERATOR_SOURCE)
.replace(swcBug1461MatchReference, "regeneratorRuntime.");
}
import pluginTransformInlineRegenerator from "./plugin/babel/plugin-transform-inline-regenerator";

function stringifyPolyfillFeature(feature: PolyfillFeature): string {
const metaEntries = Object.entries(feature.meta);
Expand All @@ -36,7 +22,7 @@ function stringifyPolyfillFeature(feature: PolyfillFeature): string {
return `${feature.name}${metaEntriesText.length === 0 ? "" : ` (${metaEntriesText})`}`;
}

export async function build({paths, features, ecmaVersion, context, sourcemap = false, minify = false}: BuildOptions): Promise<BuildResult> {
export async function build({paths, features, browserslist, ecmaVersion, context, module, sourcemap = false, minify = false}: BuildOptions): Promise<BuildResult> {
const entryText = paths.map(path => `import "${normalize(path)}";`).join("\n");

// Generate the intro text
Expand All @@ -56,19 +42,28 @@ export async function build({paths, features, ecmaVersion, context, sourcemap =
// esbuild only supports transforming down to es2015
const canUseOnlyEsbuild = ecmaVersion !== "es3" && ecmaVersion !== "es5";

// swc is still a bit too unstable for production use
const canUseSwc = false;

// With babel, input sourcemaps are so slow that it actually leads to the JavaScript heap being exceeded and very slow transpilation times.
const canUseSourcemaps = sourcemap && (canUseOnlyEsbuild || canUseSwc);

const format = module ? "esm" : "iife";

try {
const result = await esbuild({
format,
banner: {
js: banner
},
write: false,
format: "esm",

outfile: virtualOutputFileName,
platform: context === "node" ? "node" : "browser",
bundle: true,
target: "esnext",
mainFields: context === "node" ? ["module", "es2015", "main"] : ["browser", "module", "es2015", "main"],
sourcemap: sourcemap ? (canUseOnlyEsbuild ? "inline" : true) : false,
sourcemap: canUseSourcemaps ? (canUseOnlyEsbuild ? "inline" : true) : false,
entryPoints: [tempInputFileLocation],
...(canUseOnlyEsbuild
? {
Expand All @@ -86,27 +81,55 @@ export async function build({paths, features, ecmaVersion, context, sourcemap =
}

let code = Buffer.from(codeOutputFile.contents).toString("utf8");
let map = mapOutputFile == null ? undefined : Buffer.from(mapOutputFile.contents).toString("utf8");
// We might need to apply transformations in a separate step using swc if the target is ES5 or below
if (!canUseOnlyEsbuild) {
// swc doesn't support es2020 as a target
if (ecmaVersion === "es2020") {
ecmaVersion = "es2019";
}
const map = mapOutputFile == null ? undefined : Buffer.from(mapOutputFile.contents).toString("utf8");

({code} = await transform(code, {
sourceMaps: sourcemap ? "inline" : false,
inputSourceMap: map,
minify,
filename: virtualOutputFileName,
jsc: {
target: ecmaVersion
if (!canUseOnlyEsbuild) {
if (canUseSwc) {
({code} = await swc(code, {
minify,
sourceMaps: sourcemap ? "inline" : false,
inputSourceMap: map,
isModule: format === "esm",
...(format === "esm"
? {
module: {
type: "es6" as "amd"
}
}
: {}),
filename: virtualOutputFileName,
jsc: {
target: ecmaVersion
}
}));
} else {
const babelResult =
(await babel(code, {
compact: minify,
minified: minify,
comments: !minify,
sourceMaps: canUseSourcemaps ? "inline" : false,
// Unfortunately, input sourcemaps are quite slow in Babel and sometimes exceed the JavaScript heap
inputSourceMap: false as unknown as undefined, // map == null ? undefined : JSON.parse(map),
sourceType: format === "esm" ? "module" : "script",
filename: virtualOutputFileName,
plugins: [pluginTransformInlineRegenerator],
presets: [
[
"@babel/preset-env",
{
// core-js breaks when typeof are transformed into the _typeof helper, so we will have to turn off this plugin.
// this is a good thing anyway since polyfills should be treated as close to their original definitions as possible
exclude: ["transform-typeof-symbol"],
targets: browserslist,
modules: format === "esm" ? false : "auto"
}
]
]
})) ?? {};
if (babelResult.code != null) {
code = babelResult.code;
}
}));

// TODO: Remove this when https://github.com/swc-project/swc/issues/1461 has been resolved
if (swcBug1461MatchA.test(code) || swcBug1461MatchB.test(code)) {
code = workAroundSwcBug1461(code, minify);
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/build/plugin/babel/plugin-transform-inline-regenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type {PluginItem, types, NodePath} from "@babel/core";
import template from "@babel/template";
import { REGENERATOR_SOURCE } from "../../../constant/regenerator-source";

const REGENERATOR_TEMPLATE = template(REGENERATOR_SOURCE)();

export default function (): PluginItem {
let hasInlinedRegeneratorRuntime = false;
let root: NodePath<types.Program>;

return {
name: "transform-inline-regenerator",
visitor: {
Program(path) {
root = path;
},
MemberExpression(path) {
if (hasInlinedRegeneratorRuntime || path.node.object.type !== "Identifier" || path.node.object.name !== "regeneratorRuntime") {
return;
}

hasInlinedRegeneratorRuntime = true;
root.unshiftContainer("body", REGENERATOR_TEMPLATE);
}
}
};
}
1 change: 0 additions & 1 deletion src/constant/regenerator-source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const REGENERATOR_SOURCE_MINIFIED = `var regeneratorRuntime=function(){var t,r=Object.prototype,e=r.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},o=n.iterator||"@@iterator",i=n.asyncIterator||"@@asyncIterator",a=n.toStringTag||"@@toStringTag";function c(t,r,e,n){var o=r&&r.prototype instanceof y?r:y,i=Object.create(o.prototype),a=new k(n||[]);return i._invoke=function(t,r,e){var n=h;return function(o,i){if(n===l)throw new Error("Generator is already running");if(n===s){if("throw"===o)throw i;return G()}for(e.method=o,e.arg=i;;){var a=e.delegate;if(a){var c=_(a,e);if(c){if(c===p)continue;return c}}if("next"===e.method)e.sent=e._sent=e.arg;else if("throw"===e.method){if(n===h)throw n=s,e.arg;e.dispatchException(e.arg)}else"return"===e.method&&e.abrupt("return",e.arg);n=l;var y=u(t,r,e);if("normal"===y.type){if(n=e.done?s:f,y.arg===p)continue;return{value:y.arg,done:e.done}}"throw"===y.type&&(n=s,e.method="throw",e.arg=y.arg)}}}(t,e,a),i}function u(t,r,e){try{return{type:"normal",arg:t.call(r,e)}}catch(t){return{type:"throw",arg:t}}}var h="suspendedStart",f="suspendedYield",l="executing",s="completed",p={};function y(){}function v(){}function d(){}var g={};g[o]=function(){return this};var m=Object.getPrototypeOf,w=m&&m(m(N([])));w&&w!==r&&e.call(w,o)&&(g=w);var L=d.prototype=y.prototype=Object.create(g);function x(t){["next","throw","return"].forEach((function(r){t[r]=function(t){return this._invoke(r,t)}}))}function E(t){var r="function"==typeof t&&t.constructor;return!!r&&(r===v||"GeneratorFunction"===(r.displayName||r.name))}function b(t,r){function n(o,i,a,c){var h=u(t[o],t,i);if("throw"!==h.type){var f=h.arg,l=f.value;return l&&"object"==typeof l&&e.call(l,"__await")?r.resolve(l.__await).then((function(t){n("next",t,a,c)}),(function(t){n("throw",t,a,c)})):r.resolve(l).then((function(t){f.value=t,a(f)}),(function(t){return n("throw",t,a,c)}))}c(h.arg)}var o;this._invoke=function(t,e){function i(){return new r((function(r,o){n(t,e,r,o)}))}return o=o?o.then(i,i):i()}}function _(r,e){var n=r.iterator[e.method];if(n===t){if(e.delegate=null,"throw"===e.method){if(r.iterator.return&&(e.method="return",e.arg=t,_(r,e),"throw"===e.method))return p;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return p}var o=u(n,r.iterator,e.arg);if("throw"===o.type)return e.method="throw",e.arg=o.arg,e.delegate=null,p;var i=o.arg;return i?i.done?(e[r.resultName]=i.value,e.next=r.nextLoc,"return"!==e.method&&(e.method="next",e.arg=t),e.delegate=null,p):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,p)}function O(t){var r={tryLoc:t[0]};1 in t&&(r.catchLoc=t[1]),2 in t&&(r.finallyLoc=t[2],r.afterLoc=t[3]),this.tryEntries.push(r)}function j(t){var r=t.completion||{};r.type="normal",delete r.arg,t.completion=r}function k(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(O,this),this.reset(!0)}function N(r){if(r){var n=r[o];if(n)return n.call(r);if("function"==typeof r.next)return r;if(!isNaN(r.length)){var i=-1,a=function n(){for(;++i<r.length;)if(e.call(r,i))return n.value=r[i],n.done=!1,n;return n.value=t,n.done=!0,n};return a.next=a}}return{next:G}}function G(){return{value:t,done:!0}}return v.prototype=L.constructor=d,d.constructor=v,d[a]=v.displayName="GeneratorFunction",x(b.prototype),b.prototype[i]=function(){return this},x(L),L[a]="Generator",L[o]=function(){return this},L.toString=function(){return"[object Generator]"},k.prototype={constructor:k,reset:function(r){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(j),!r)for(var n in this)"t"===n.charAt(0)&&e.call(this,n)&&!isNaN(+n.slice(1))&&(this[n]=t)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(r){if(this.done)throw r;var n=this;function o(e,o){return c.type="throw",c.arg=r,n.next=e,o&&(n.method="next",n.arg=t),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=e.call(a,"catchLoc"),h=e.call(a,"finallyLoc");if(u&&h){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(u){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!h)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,r){for(var n=this.tryEntries.length-1;n>=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&e.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=r&&r<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=r,i?(this.method="next",this.next=i.finallyLoc,p):this.complete(a)},complete:function(t,r){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&r&&(this.next=r),p},finish:function(t){for(var r=this.tryEntries.length-1;r>=0;--r){var e=this.tryEntries[r];if(e.finallyLoc===t)return this.complete(e.completion,e.afterLoc),j(e),p}},catch:function(t){for(var r=this.tryEntries.length-1;r>=0;--r){var e=this.tryEntries[r];if(e.tryLoc===t){var n=e.completion;if("throw"===n.type){var o=n.arg;j(e)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(r,e,n){return this.delegate={iterator:N(r),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=t),p}},{wrap:c,isGeneratorFunction:E,AsyncIterator:b,mark:function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,d):(t.__proto__=d,a in t||(t[a]="GeneratorFunction")),t.prototype=Object.create(L),t},awrap:function(t){return{__await:t}},async:function(t,r,e,n,o){void 0===o&&(o=Promise);var i=new b(c(t,r,e,n),o);return E(r)?i:i.next().then((function(t){return t.done?t.value:i.next()}))},keys:function(t){var r=[];for(var e in t)r.push(e);return r.reverse(),function e(){for(;r.length;){var n=r.pop();if(n in t)return e.value=n,e.done=!1,e}return e.done=!0,e}},values:N}}();`;
export const REGENERATOR_SOURCE = `var regeneratorRuntime = (function() {
/**
* Copyright (c) 2014-present, Facebook, Inc.
Expand Down
3 changes: 3 additions & 0 deletions src/polyfill/polyfill-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {PolyfillContext} from "./polyfill-context";
import {EcmaVersion} from "../util/type/ecma-version";

export interface PolyfillRequest {
// If true, the output will be in ESM-format
module: boolean;
userAgent: string | undefined;
browserslist: string[];
ecmaVersion: EcmaVersion;
encoding?: ContentEncodingKind;
features: Set<PolyfillFeatureInput>;
Expand Down
7 changes: 5 additions & 2 deletions src/service/polyfill-builder/polyfill-builder-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ function normalizeLocale(locale: string): string {

function selectMetaPaths<Meta extends Exclude<IPolyfillDictEntryBase["meta"], undefined>>(value: Meta[keyof Meta], context: PolyfillContext): string[] {
if (typeof value === "string") return [value];
if (Array.isArray(value)) return value as string[];
return ensureArray((value as any)[context]);
if (Array.isArray(value)) return value;
return ensureArray((value as never)[context]);
}

/**
Expand Down Expand Up @@ -188,6 +188,7 @@ export class PolyfillBuilderService implements IPolyfillBuilderService {
requestedLocales.map(async requestedLocale => {
// Resolve the absolute path
for (const localeDir of selectMetaPaths(meta.localeDir, request.context)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const locale = new (Intl as any).Locale(requestedLocale);
let addedLocale = false;
for (const currentLocale of new Set([locale.baseName, normalizeLocale(locale.baseName), locale.language, normalizeLocale(locale.language)])) {
Expand Down Expand Up @@ -215,7 +216,9 @@ export class PolyfillBuilderService implements IPolyfillBuilderService {
const {brotli, zlib, raw} = await build({
context: request.context,
userAgent: request.userAgent,
browserslist: request.browserslist,
ecmaVersion: request.ecmaVersion,
module: request.module,
featuresRequested: [...request.features],
paths: [...new Set(paths)],
features: [...polyfillSet],
Expand Down
15 changes: 10 additions & 5 deletions src/util/polyfill/polyfill-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {polyfillRawForceName} from "../../polyfill/polyfill-raw-force-name";
import {polyfillOptionValueSeparator} from "../../polyfill/polyfill-option-value-separator";
import {createHash} from "crypto";
import {constant} from "../../constant/constant";
import {generateBrowserslistFromUseragent, getAppropriateEcmaVersionForBrowserslist, userAgentSupportsFeatures} from "browserslist-generator";
import {generateBrowserslistFromUseragent, browsersWithSupportForEcmaVersion, getAppropriateEcmaVersionForBrowserslist, userAgentSupportsFeatures} from "browserslist-generator";
import {truncate} from "@wessberg/stringutil";
import {IPolyfillLibraryDictEntry, IPolyfillLocalDictEntry} from "../../polyfill/polyfill-dict";
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import toposort from "toposort";
import {POLYFILL_CONTEXTS, PolyfillContext} from "../../polyfill/polyfill-context";
Expand Down Expand Up @@ -185,8 +185,10 @@ export function getPolyfillRequestFromUrl(url: URL, userAgent: string | undefine
const contextRaw = url.searchParams.get("context") as PolyfillContext;
const sourcemapRaw = url.searchParams.get("sourcemap");
const minifyRaw = url.searchParams.get("minify");
const moduleRaw = url.searchParams.get("module");
const sourcemap = sourcemapRaw === "" || (sourcemapRaw != null && booleanize(sourcemapRaw));
const minify = minifyRaw === "" || (minifyRaw != null && booleanize(minifyRaw));
const module = moduleRaw === "" || (moduleRaw != null && booleanize(moduleRaw));
const context: PolyfillContext = contextRaw == null || !POLYFILL_CONTEXTS.includes(contextRaw) ? "window" : contextRaw;

// Prepare a Set of features
Expand Down Expand Up @@ -234,14 +236,17 @@ export function getPolyfillRequestFromUrl(url: URL, userAgent: string | undefine
}

// Return the IPolyfillRequest
return {
const browserslist = userAgent == null ? browsersWithSupportForEcmaVersion("es5") : generateBrowserslistFromUseragent(userAgent);
return {
userAgent,
ecmaVersion: userAgent == null ? "es5" : getAppropriateEcmaVersionForBrowserslist(generateBrowserslistFromUseragent(userAgent)),
browserslist,
ecmaVersion: getAppropriateEcmaVersionForBrowserslist(browserslist),
encoding,
features: featureSet,
context,
sourcemap,
minify
minify,
module
};
}

Expand Down
2 changes: 1 addition & 1 deletion test/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ test("Generates SourceMap if required. #1", async t => {
const result = await sendRequest({
path: `${constant.endpoint.polyfill}?features=form-data&sourcemap`,
headers: {
"User-Agent": ie("11")
"User-Agent": chrome("70")
}
});

Expand Down

0 comments on commit e7a02c8

Please sign in to comment.