From b6877bdb6739203c2266f32e57823bb44b3f06c6 Mon Sep 17 00:00:00 2001 From: Alan Orozco Date: Fri, 11 Feb 2022 11:03:10 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=20Use=20ESM=20to=20import=20peer?= =?UTF-8?q?=20Bento=20binaries=20(#37586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On `module` builds, use browser's `import` statement to share Bento dependencies like `bento.mjs`. On `nomodule` builds, we can't use `import`. Instead, we use babel to output to a format similar to AMD. This change also simplifies some aspects: - Prefers using `{remapDependencies}` for esbuild rather than `module-resolver` on babel. - Removes `bento` compile wrapper. - Removes generators. Uses `src/bento.js` directly, and the intermediate `bento-shared.js` module is no longer needed. --- babel.config.js | 3 +- .../babel-config/bento-element-config.js | 70 ---------- build-system/babel-config/minified-config.js | 2 +- .../babel-config/nomodule-loader-config.js | 10 ++ .../babel-config/unminified-config.js | 2 +- .../define-template.js | 59 ++++++++ .../babel-plugin-nomodule-loader/index.js | 125 +++++++++++++++++ .../transform/export-default/input.mjs | 1 + .../transform/export-default/options.json | 5 + .../transform/export-default/output.js | 42 ++++++ .../fixtures/transform/export-from/input.mjs | 1 + .../transform/export-from/options.json | 5 + .../fixtures/transform/export-from/output.js | 43 ++++++ .../transform/import-default/input.mjs | 2 + .../transform/import-default/options.json | 5 + .../transform/import-default/output.js | 39 ++++++ .../transform/imports-and-exports/input.mjs | 3 + .../imports-and-exports/options.json | 5 + .../transform/imports-and-exports/output.js | 46 +++++++ .../fixtures/transform/no-transform/input.mjs | 1 + .../transform/no-transform/options.json | 5 + .../fixtures/transform/no-transform/output.js | 3 + .../fixtures/transform/only-exports/input.mjs | 2 + .../transform/only-exports/options.json | 5 + .../fixtures/transform/only-exports/output.js | 45 ++++++ .../transform/single-import-name/input.mjs | 3 + .../transform/single-import-name/options.json | 5 + .../transform/single-import-name/output.js | 42 ++++++ .../test/index.js | 3 + build-system/common/esbuild-babel.js | 7 +- build-system/common/once.js | 20 +++ build-system/compile/bento-remap.js | 128 +++++++++++++++++ build-system/compile/bundles.config.js | 17 +-- build-system/compile/compile-wrappers.js | 2 - build-system/compile/generate/OWNERS | 14 -- build-system/compile/generate/bento.js | 117 ---------------- .../compile/generate/shared-bento-symbols.js | 116 ---------------- .../compile/generate/test/bento.test.js | 60 -------- .../test/shared-bento-symbols.test.js | 47 ------- build-system/tasks/extension-helpers.js | 31 ++--- build-system/tasks/helpers.js | 129 ++++++++---------- build-system/tasks/sourcemaps.js | 48 ++----- src/bento/bento.js | 20 +++ 43 files changed, 756 insertions(+), 582 deletions(-) delete mode 100644 build-system/babel-config/bento-element-config.js create mode 100644 build-system/babel-config/nomodule-loader-config.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/define-template.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/index.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/input.mjs create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/options.json create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/output.js create mode 100644 build-system/babel-plugins/babel-plugin-nomodule-loader/test/index.js create mode 100644 build-system/common/once.js create mode 100644 build-system/compile/bento-remap.js delete mode 100644 build-system/compile/generate/OWNERS delete mode 100644 build-system/compile/generate/bento.js delete mode 100644 build-system/compile/generate/shared-bento-symbols.js delete mode 100644 build-system/compile/generate/test/bento.test.js delete mode 100644 build-system/compile/generate/test/shared-bento-symbols.test.js create mode 100644 src/bento/bento.js diff --git a/babel.config.js b/babel.config.js index 7e094af38ea1..e67f6e62358d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -19,8 +19,7 @@ const {log} = require('./build-system/common/logging'); */ const babelTransforms = new Map([ ['babel-jest', 'getEmptyConfig'], - ['bento-element-minified', 'getBentoElementMinifiedConfig'], - ['bento-element-unminified', 'getBentoElementUnminifiedConfig'], + ['nomodule-loader', 'getNoModuleLoaderConfig'], ['test', 'getTestConfig'], ['unminified', 'getUnminifiedConfig'], ['minified', 'getMinifiedConfig'], diff --git a/build-system/babel-config/bento-element-config.js b/build-system/babel-config/bento-element-config.js deleted file mode 100644 index bc0f8ad05034..000000000000 --- a/build-system/babel-config/bento-element-config.js +++ /dev/null @@ -1,70 +0,0 @@ -const { - getSharedBentoSymbols, -} = require('../compile/generate/shared-bento-symbols'); -const {generateIntermediatePackage} = require('../compile/generate/bento'); -const {getMinifiedConfig} = require('./minified-config'); -const {getUnminifiedConfig} = require('./unminified-config'); -const {outputFileSync} = require('fs-extra'); - -let modulePath; - -/** - * @param {{[name: string]: string[]}} packages - * @return {string} - */ -function writeIntermediatePackage(packages) { - if (!modulePath) { - // Don't remove the `./` - modulePath = './build/bento-shared.js'; - outputFileSync(modulePath, generateIntermediatePackage(packages)); - } - return modulePath; -} - -/** - * @param {{[name: string]: string[]}} packages - * @return {[string, {root: string[], alias: {[alias: string]: string}}, string]} - */ -function getModuleResolver(packages) { - const modulePath = writeIntermediatePackage(packages); - const alias = Object.fromEntries( - Object.entries(packages).map(([name]) => [`^${name}$`, modulePath]) - ); - return [ - 'module-resolver', - {root: ['.'], alias}, - // Unique name because "module-resolver" is used elsewhere and babel will - // throw a duplicate name error. - 'module-resolver-bento-shared', - ]; -} - -/** - * @param {!Object} config - * @return {Object} - */ -function withModuleResolver(config) { - return { - ...config, - plugins: [getModuleResolver(getSharedBentoSymbols()), ...config.plugins], - }; -} - -/** - * @return {!Object} - */ -function getBentoElementUnminifiedConfig() { - return withModuleResolver(getUnminifiedConfig()); -} - -/** - * @return {!Object} - */ -function getBentoElementMinifiedConfig() { - return withModuleResolver(getMinifiedConfig()); -} - -module.exports = { - getBentoElementUnminifiedConfig, - getBentoElementMinifiedConfig, -}; diff --git a/build-system/babel-config/minified-config.js b/build-system/babel-config/minified-config.js index f8d4718d6886..33646df4d44e 100644 --- a/build-system/babel-config/minified-config.js +++ b/build-system/babel-config/minified-config.js @@ -77,7 +77,7 @@ function getMinifiedConfig() { return { compact: false, plugins, - sourceMaps: true, + sourceMaps: 'both', presets: [presetTypescript, presetEnv], retainLines: true, assumptions: { diff --git a/build-system/babel-config/nomodule-loader-config.js b/build-system/babel-config/nomodule-loader-config.js new file mode 100644 index 000000000000..0c38ee3579f7 --- /dev/null +++ b/build-system/babel-config/nomodule-loader-config.js @@ -0,0 +1,10 @@ +/** @return {{[string: string]: any}} */ +function getNoModuleLoaderConfig() { + return { + plugins: ['./build-system/babel-plugins/babel-plugin-nomodule-loader'], + }; +} + +module.exports = { + getNoModuleLoaderConfig, +}; diff --git a/build-system/babel-config/unminified-config.js b/build-system/babel-config/unminified-config.js index cf1d43915dbe..93b7309ec997 100644 --- a/build-system/babel-config/unminified-config.js +++ b/build-system/babel-config/unminified-config.js @@ -55,7 +55,7 @@ function getUnminifiedConfig() { compact: false, plugins: unminifiedPlugins, presets: unminifiedPresets, - sourceMaps: true, + sourceMaps: 'both', assumptions: { constantSuper: true, noClassCalls: true, diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/define-template.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/define-template.js new file mode 100644 index 000000000000..760e57380ae0 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/define-template.js @@ -0,0 +1,59 @@ +// @ts-nocheck +/* eslint-disable no-var */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/* global __MODULE_NAME__ */ +/* global __HAS_EXPORTS__ */ +/* global __IMPORT_NAMES__ */ +/* global __SINGLE_IMPORT_NO_EXPORTS__ */ +/* global __ONLY_EXPORTS__ */ + +/** + * An async module loader similar to define() in AMD. + * Our implementation varies in that it can be compiled down to a minimal form + * based on the module's needs, rather than bundling a full implementation. + */ +(function defineish(defineCallback) { + // self.BENTO maps module names to callbacks to execute with their contents. + // interface ModuleCallbacks { + // [name: string]: ((Object) => void)[], + // } + var callbacks = (self.BENTO = self.BENTO || {}); + var exec = __HAS_EXPORTS__ + ? function (_exports) { + defineCallback.apply(null, arguments); + var name = __MODULE_NAME__; + var awaiting = (callbacks[name] = callbacks[name] || []); + while (awaiting.length) { + awaiting.pop()(_exports); + } + awaiting.push = function (callback) { + callback(_exports); + }; + } + : defineCallback; + // The most common cases are ONLY_EXPORTS and SINGLE_IMPORT_NO_EXPORTS. + // We provide them with single-purpose implementations whose output is + // significantly smaller than the worst case. + if (__ONLY_EXPORTS__) { + exec({}); + } else if (__SINGLE_IMPORT_NO_EXPORTS__) { + var name = __SINGLE_IMPORT_NO_EXPORTS__; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + // Fallback general purpose implementation. + Promise.all( + __IMPORT_NAMES__.map(function (name) { + // exports is identified as the number 0 + if (__HAS_EXPORTS__ && name === 0) { + return {}; + } + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + }) + ).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (__CALLBACK_ARGS__) {}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/index.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/index.js new file mode 100644 index 000000000000..f1c3ba4abac0 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/index.js @@ -0,0 +1,125 @@ +/** + * @fileoverview + * Transforms ESM import statements into an async loader meant for `nomodule` + * builds. + */ + +const { + buildNamespaceInitStatements, + ensureStatementsHoisted, + hasExports, + isModule, + rewriteModuleStatementsAndPrepareHeader, +} = require('@babel/helper-module-transforms'); +const {readFileSync} = require('fs'); +const {join: pathJoin, posix, relative} = require('path'); + +let wrapperTemplate; + +module.exports = function (babel) { + const {template, types: t} = babel; + + const pathToModuleName = (filename) => + filename.replace(/^(\.\/)?dist\//, '').replace(/(\.max)?\.m?js$/, ''); + + const resolveModuleName = (filename, source) => + pathToModuleName(posix.join(posix.dirname(filename), source)); + + /** + * @param {Object} replacements + * @return {babel.Node} + */ + function buildWrapper(replacements) { + if (!wrapperTemplate) { + const templateSource = readFileSync( + pathJoin(__dirname, 'define-template.js'), + 'utf8' + ); + wrapperTemplate = template(templateSource, { + placeholderPattern: /^__[A-Z0-9_]+__$/, + }); + } + return wrapperTemplate(replacements); + } + + /** + * @param {babel.NodePath} path + * @param {babel.Node} wrapper + */ + function injectWrapper(path, wrapper) { + const {body, directives} = path.node; + path.node.directives = []; + path.node.body = []; + const wrapperPath = path.pushContainer('body', wrapper)[0]; + const callback = wrapperPath + .get('expression.arguments') + // @ts-ignore + .filter((arg) => arg.isFunctionExpression())[0] + .get('body'); + callback.pushContainer('directives', directives); + callback.pushContainer('body', body); + } + + return { + name: 'nomodule-loader', + visitor: { + Program: { + enter(path, state) { + // We can stop since this should be the last transform step. + // See nomodule-loader-config.js + path.stop(); + + if (!isModule(path)) { + throw new Error(); + } + const loose = true; + const noInterop = true; + const {headers, meta} = rewriteModuleStatementsAndPrepareHeader( + path, + {loose, noInterop} + ); + + const filename = relative(process.cwd(), state.filename); + const importNames = []; + const callbackArgs = []; + const metaHasExports = hasExports(meta); + if (metaHasExports) { + // exports is identified as the number 0 + importNames.push(t.numericLiteral(0)); + callbackArgs.push(t.identifier(meta.exportName)); + } + for (const [source, metadata] of meta.source) { + importNames.push( + t.stringLiteral(resolveModuleName(filename, source)) + ); + callbackArgs.push(t.identifier(metadata.name)); + headers.push( + ...buildNamespaceInitStatements(meta, metadata, loose) + ); + } + if (importNames.length < 1) { + return; + } + ensureStatementsHoisted(headers); + path.unshiftContainer('body', headers); + injectWrapper( + path, + buildWrapper({ + __MODULE_NAME__: t.stringLiteral(pathToModuleName(filename)), + __HAS_EXPORTS__: t.booleanLiteral(metaHasExports), + __ONLY_EXPORTS__: t.booleanLiteral( + metaHasExports && importNames.length === 1 + ), + __IMPORT_NAMES__: t.arrayExpression(importNames), + __SINGLE_IMPORT_NO_EXPORTS__: + importNames.length === 1 && !metaHasExports + ? t.cloneNode(importNames[0]) + : t.nullLiteral(), + __CALLBACK_ARGS__: callbackArgs, + }) + ); + }, + }, + }, + }; +}; diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/input.mjs new file mode 100644 index 000000000000..9385f7349188 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/input.mjs @@ -0,0 +1 @@ +export default function fn() {} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/output.js new file mode 100644 index 000000000000..b93af0d76598 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/output.js @@ -0,0 +1,42 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = true ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-default/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (true) { + exec({}); + } else if (null) { + var name = null; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all([0].map(function (name) { + if (true && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_exports) { + "use strict"; + + _exports.__esModule = true; + _exports.default = fn; + + function fn() {} +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/input.mjs new file mode 100644 index 000000000000..2f81f2527127 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/input.mjs @@ -0,0 +1 @@ +export {x, y, z} from './abc'; diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/output.js new file mode 100644 index 000000000000..94dc3e894dfe --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/output.js @@ -0,0 +1,43 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = true ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (false) { + exec({}); + } else if (null) { + var name = null; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all([0, "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/export-from/abc"].map(function (name) { + if (true && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_exports, _abc) { + "use strict"; + + _exports.__esModule = true; + _exports.z = _exports.y = _exports.x = void 0; + _exports.x = _abc.x; + _exports.y = _abc.y; + _exports.z = _abc.z; +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/input.mjs new file mode 100644 index 000000000000..70e7fa2cda07 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/input.mjs @@ -0,0 +1,2 @@ +import x from 'x'; +console.log(x); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/output.js new file mode 100644 index 000000000000..dfbc4e1eb1ac --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/output.js @@ -0,0 +1,39 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = false ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (false) { + exec({}); + } else if ("build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/x") { + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/x"; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all(["build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/import-default/x"].map(function (name) { + if (false && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_x) { + "use strict"; + + console.log(_x.default); +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/input.mjs new file mode 100644 index 000000000000..76861af0119d --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/input.mjs @@ -0,0 +1,3 @@ +import {foo, bar} from './foo'; + +export const baz = {foo, bar, baz: 'baz'}; diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/output.js new file mode 100644 index 000000000000..0a95557176a9 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/output.js @@ -0,0 +1,46 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = true ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (false) { + exec({}); + } else if (null) { + var name = null; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all([0, "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/imports-and-exports/foo"].map(function (name) { + if (true && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_exports, _foo) { + "use strict"; + + _exports.__esModule = true; + _exports.baz = void 0; + const baz = { + foo: _foo.foo, + bar: _foo.bar, + baz: 'baz' + }; + _exports.baz = baz; +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/input.mjs new file mode 100644 index 000000000000..2d847b16e7d5 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/input.mjs @@ -0,0 +1 @@ +function x() {} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/output.js new file mode 100644 index 000000000000..2f817152daa8 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/no-transform/output.js @@ -0,0 +1,3 @@ +"use strict"; + +function x() {} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/input.mjs new file mode 100644 index 000000000000..eee8cd5a00a7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/input.mjs @@ -0,0 +1,2 @@ +export const x = 'y'; +export function y() {} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/output.js new file mode 100644 index 000000000000..05c8140a1004 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/output.js @@ -0,0 +1,45 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = true ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/only-exports/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (true) { + exec({}); + } else if (null) { + var name = null; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all([0].map(function (name) { + if (true && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_exports) { + "use strict"; + + _exports.__esModule = true; + _exports.y = y; + _exports.x = void 0; + const x = 'y'; + _exports.x = x; + + function y() {} +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/input.mjs b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/input.mjs new file mode 100644 index 000000000000..be8d6e914b2c --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/input.mjs @@ -0,0 +1,3 @@ +import {foo, bar} from './foo'; + +console.log({foo, bar}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/options.json b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/options.json new file mode 100644 index 000000000000..3ebeb6374116 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "../../../.." + ] +} diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/output.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/output.js new file mode 100644 index 000000000000..33ec139133d7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/output.js @@ -0,0 +1,42 @@ +(function defineish(defineCallback) { + var callbacks = self.BENTO = self.BENTO || {}; + var exec = false ? function (_exports) { + defineCallback.apply(null, arguments); + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/input"; + var awaiting = callbacks[name] = callbacks[name] || []; + + while (awaiting.length) { + awaiting.pop()(_exports); + } + + awaiting.push = function (callback) { + callback(_exports); + }; + } : defineCallback; + + if (false) { + exec({}); + } else if ("build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/foo") { + var name = "build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/foo"; + (callbacks[name] = callbacks[name] || []).push(exec); + } else { + Promise.all(["build-system/babel-plugins/babel-plugin-nomodule-loader/test/fixtures/transform/single-import-name/foo"].map(function (name) { + if (false && name === 0) { + return {}; + } + + return new Promise(function (resolve) { + (callbacks[name] = callbacks[name] || []).push(resolve); + }); + })).then(function (modules) { + exec.apply(null, modules); + }); + } +})(function (_foo) { + "use strict"; + + console.log({ + foo: _foo.foo, + bar: _foo.bar + }); +}); diff --git a/build-system/babel-plugins/babel-plugin-nomodule-loader/test/index.js b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/index.js new file mode 100644 index 000000000000..23251129fae9 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-nomodule-loader/test/index.js @@ -0,0 +1,3 @@ +const runner = require('@babel/helper-plugin-test-runner').default; + +runner(__dirname); diff --git a/build-system/common/esbuild-babel.js b/build-system/common/esbuild-babel.js index 5709fbb76557..d9410bdaab2b 100644 --- a/build-system/common/esbuild-babel.js +++ b/build-system/common/esbuild-babel.js @@ -7,7 +7,6 @@ const {TransformCache, batchedRead, md5} = require('./transform-cache'); * @typedef {{ * filename: string, * code: string, - * map: *, * }} */ let CacheMessageDef; @@ -26,14 +25,13 @@ let transformCache; * @param {{ * preSetup?: function():void, * postLoad?: function():void, - * babelMaps?: Map, * }} callbacks * @return {!Object} */ function getEsbuildBabelPlugin( callerName, enableCache, - {preSetup = () => {}, postLoad = () => {}, babelMaps} = {} + {preSetup = () => {}, postLoad = () => {}} = {} ) { /** * @param {string} filename @@ -59,7 +57,7 @@ function getEsbuildBabelPlugin( .then((result) => { const {code, map} = /** @type {!babel.BabelFileResult} */ (result); debug('post-babel', filename, code, map); - return {filename, code: code || '', map}; + return {filename, code: code || ''}; }); if (enableCache) { @@ -99,7 +97,6 @@ function getEsbuildBabelPlugin( rehash, getFileBabelOptions(babelOptions, filename) ); - babelMaps?.set(filename, transformed.map); return {contents: transformed.code}; } ); diff --git a/build-system/common/once.js b/build-system/common/once.js new file mode 100644 index 000000000000..86a857df7b4b --- /dev/null +++ b/build-system/common/once.js @@ -0,0 +1,20 @@ +/** + * @param {() => T} fn + * @return {() => T} + * @template T + */ +function once(fn) { + let evaluated = false; + let value; + return () => { + if (!evaluated) { + evaluated = true; + value = fn(); + } + return value; + }; +} + +module.exports = { + once, +}; diff --git a/build-system/compile/bento-remap.js b/build-system/compile/bento-remap.js new file mode 100644 index 000000000000..6e9efb174a22 --- /dev/null +++ b/build-system/compile/bento-remap.js @@ -0,0 +1,128 @@ +const babelTypes = require('@babel/types'); +const {parse} = require('@babel/parser'); +const {resolvePath} = require('../babel-config/import-resolver'); +const {lstatSync, readFileSync} = require('fs-extra'); +const {once} = require('../common/once'); +const {bentoBundles} = require('./bundles.config'); +const glob = require('globby'); +const {getNameWithoutComponentPrefix} = require('../tasks/bento-helpers'); + +/** + * @param {string} path + * @return {?string} + */ +function resolveExactModuleFile(path) { + let unaliased = resolvePath(path); + try { + if (lstatSync(unaliased).isDirectory()) { + unaliased += '/index'; + } + } catch { + // lstat fails if not directory + } + const result = glob.sync(`${unaliased}.{js,jsx,ts,tsx}`); + if (result.length !== 1) { + return null; + } + return result[0]; +} + +/** + * @param {string} filename + * @return {string[]} + */ +function getExportAll(filename) { + const source = readFileSync(filename, 'utf8'); + const tree = parse(source, { + sourceType: 'module', + plugins: ['typescript', 'jsx', 'exportDefaultFrom'], + }); + const modules = []; + for (const node of tree.program.body) { + if (babelTypes.isExportAllDeclaration(node)) { + modules.push(node.source.value); + } else if (babelTypes.isExportDefaultDeclaration(node)) { + throw new Error(`${filename} should not "export default"`); + } else if (babelTypes.isExportNamedDeclaration(node)) { + throw new Error(`${filename} should not export named symbol`); + } + } + return modules; +} + +/** @typedef {{source: string, cdn?: string, npm?: string}} */ +let MappingEntryDef; + +/** @return {MappingEntryDef[]}}} */ +const getAllRemappings = once(() => { + // IMPORTANT: cdn mappings must start with ./dist/ + + // Determine modules to import from shared bento.mjs. + const coreBentoModules = getExportAll('src/bento/bento.js'); + const coreBentoRemappings = coreBentoModules.map((source) => ({ + source, + cdn: '../bento.mjs', + npm: '@bentoproject/core', + })); + + // Allow component cross-dependency + const componentRemappings = bentoBundles.map(({name, version}) => ({ + source: `./src/bento/components/${name}/${version}/${name}`, + cdn: `./${name}-${version}.mjs`, + npm: `@bentoproject/${getNameWithoutComponentPrefix(name)}`, + })); + + return /** @type {MappingEntryDef[]} */ ( + [...coreBentoRemappings, ...componentRemappings] + .map(({cdn, npm, source}) => { + const resolved = resolveExactModuleFile(source); + if (resolved) { + return {source: resolved, cdn, npm}; + } + }) + .filter(Boolean) + ); +}); + +/** + * @param {'npm'|'cdn'} type + * @return {{[string: string]: string}} + */ +function getRemappings(type) { + return /** @type {{[string: string]: string}} */ ( + Object.fromEntries( + getAllRemappings() + .filter((mapping) => mapping[type]) + .map((mapping) => [mapping.source, mapping[type]]) + ) + ); +} + +/** + * Remaps imports from source to externals. + * @param {string} isMinified + * @return {{[string: string]: string}} + */ +function getRemapBentoDependencies(isMinified) { + const remappings = getRemappings('cdn'); + if (isMinified) { + return remappings; + } + return Object.fromEntries( + Object.entries(isMinified).map(([source, cdn]) => [ + source, + cdn.replace('.mjs', '.max.mjs'), + ]) + ); +} + +/** + * Remaps imports from source to externals. + * @return {{[string: string]: string}} + */ +const getRemapBentoNpmDependencies = once(() => getRemappings('npm')); + +module.exports = { + getRemapBentoDependencies, + getRemapBentoNpmDependencies, +}; diff --git a/build-system/compile/bundles.config.js b/build-system/compile/bundles.config.js index 81ef709bd051..09145bfad032 100644 --- a/build-system/compile/bundles.config.js +++ b/build-system/compile/bundles.config.js @@ -18,9 +18,7 @@ exports.jsBundles = { minifiedDestDir: './build/', }, 'bento.js': { - // This file is generated, so we find its source in the build/ dir - // See compileBentoRuntime() and generateBentoRuntimeEntrypoint() - srcDir: 'build/', + srcDir: './src/bento', srcFilename: 'bento.js', destDir: './dist', minifiedDestDir: './dist', @@ -32,19 +30,6 @@ exports.jsBundles = { aliasName: 'custom-elements-polyfill.js', }, }, - 'bento.core.js': { - // This file is generated, so we find its source in the build/ dir - // See compileBentoCore() and generateBentoCoreEntrypoint() - srcDir: 'build/', - srcFilename: 'bento.shared.js', - destDir: './src/bento/core/dist', - minifiedDestDir: './src/bento/core/dist', - options: { - includePolyfills: false, - toName: 'bento.core.max.js', - minifiedName: 'bento.core.js', - }, - }, 'alp.max.js': { srcDir: './ads/alp/', srcFilename: 'install-alp.js', diff --git a/build-system/compile/compile-wrappers.js b/build-system/compile/compile-wrappers.js index 48bf6899a413..7ae8bb182903 100644 --- a/build-system/compile/compile-wrappers.js +++ b/build-system/compile/compile-wrappers.js @@ -83,6 +83,4 @@ function extensionPayload(name, version, isModule, loadPriority) { ); } -exports.bento = '(self.BENTO=self.BENTO||[]).push(function(){<%= contents %>})'; - exports.none = '<%= contents %>'; diff --git a/build-system/compile/generate/OWNERS b/build-system/compile/generate/OWNERS deleted file mode 100644 index ecb6a5e4e348..000000000000 --- a/build-system/compile/generate/OWNERS +++ /dev/null @@ -1,14 +0,0 @@ -// For an explanation of the OWNERS rules and syntax, see: -// https://github.com/ampproject/amp-github-apps/blob/main/owners/OWNERS.example - -{ - rules: [ - { - owners: [ - {name: 'ampproject/wg-infra'}, - {name: 'ampproject/wg-bento'}, - {name: 'alanorozco', notify: true}, - ], - }, - ], -} diff --git a/build-system/compile/generate/bento.js b/build-system/compile/generate/bento.js deleted file mode 100644 index ec46115fa7d5..000000000000 --- a/build-system/compile/generate/bento.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @fileoverview - * Compile-time generators of entry-points for Bento-related binaries. - */ - -// TODO(alanorozco): Move generators of extension-related `defineElement()` -// into this file. Add tests for them, now that we have tests here. - -const dedent = require('dedent'); -const {getSharedBentoSymbols} = require('./shared-bento-symbols'); - -/** - * @param {Object} [packageSymbols] - * @return {string} - */ -function generateBentoRuntimeEntrypoint(packageSymbols) { - packageSymbols = packageSymbols || getSharedBentoSymbols(); - - assertNoDupes(Object.values(packageSymbols).flat()); - return dedent(` - import {isEsm} from '#core/mode'; - import {install as installCustomElements} from '#polyfills/custom-elements'; - - ${generateEsImportExports(packageSymbols)} - - if (!isEsm()) { - installCustomElements(self, class {}); - } - - const bento = self.BENTO || []; - - bento['_'] = { - ${Object.entries(packageSymbols) - .map(([name, symbols]) => [ - `// ${name}`, - ...symbols.map((symbol) => `'${symbol}': ${symbol},`), - ]) - .flat() - .join('\n')} - }; - - bento.push = (fn) => { - fn(); - }; - - self.BENTO = bento; - - for (const fn of bento) { - bento.push(fn); - } - `); -} - -/** - * @param {Object} [packageSymbols] - * @return {string} - */ -function generateBentoCoreEntrypoint(packageSymbols) { - packageSymbols = packageSymbols || getSharedBentoSymbols(); - - assertNoDupes(Object.values(packageSymbols).flat()); - return dedent(` - ${generateEsImportExports(packageSymbols, true)} - `); -} - -/** - * - * @param {Object} packageSymbols - * @param {boolean} generateExport - * @return {string} - */ -function generateEsImportExports(packageSymbols, generateExport = false) { - const esImportOrExportStatement = generateExport ? 'export' : 'import'; - return Object.entries(packageSymbols) - .map( - ([name, symbols]) => - `${esImportOrExportStatement} {${symbols.join(', ')}} from '${name}';` - ) - .join('\n'); -} - -/** - * @param {Object} packageSymbols - * @return {string} - */ -function generateIntermediatePackage(packageSymbols = getSharedBentoSymbols()) { - assertNoDupes(Object.values(packageSymbols).flat()); - return [ - "const _ = (name) => self.BENTO['_'][name];", - ...Object.entries(packageSymbols).map(([name, symbols]) => [ - `// ${name}`, - ...symbols.map( - (symbol) => `export const ${symbol} = /*#__PURE__*/ _('${symbol}');` - ), - ]), - ] - .flat() - .join('\n'); -} - -/** - * @param {string[]} symbols - */ -function assertNoDupes(symbols) { - if (Array.from(new Set(symbols)).length !== symbols.length) { - throw new Error( - 'Shred symbols should not duplicate names, even if they come from different packages.' - ); - } -} - -module.exports = { - generateBentoRuntimeEntrypoint, - generateIntermediatePackage, - generateBentoCoreEntrypoint, -}; diff --git a/build-system/compile/generate/shared-bento-symbols.js b/build-system/compile/generate/shared-bento-symbols.js deleted file mode 100644 index 8f31bf42fbb4..000000000000 --- a/build-system/compile/generate/shared-bento-symbols.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @fileoverview - * These are the packages, and their exports that are included in `bento.js` - * Extension `bento-*.js` binaries will use these exports as provided by - * `bento.js` from the `BENTO` global. - * - * We specify each export explicitly by name. - * Unlisted imports will be bundled with each binary. - */ - -const types = require('@babel/types'); -const {parse} = require('@babel/parser'); -const {readFileSync} = require('fs-extra'); -const {resolvePath} = require('../../babel-config/import-resolver'); - -// These must be aliased from `src/`, e.g. `#preact` to `src/preact`. -// See tsconfig.json for the list of aliases. -const packages = [ - 'core/context', - 'preact', - 'preact/base-element', - 'preact/compat', - 'preact/component', - 'preact/context', - 'preact/slot', -]; - -/** - * @param {string} source - * @return {string[]} - */ -function getExportedSymbols(source) { - const tree = parse(source, { - sourceType: 'module', - plugins: ['typescript', 'jsx', 'exportDefaultFrom'], - }); - const symbols = []; - for (const node of tree.program.body) { - if (types.isExportAllDeclaration(node)) { - throw new Error('Should not "export *"'); - } - if (types.isExportDefaultDeclaration(node)) { - throw new Error('Should not "export default"'); - } - if (!types.isExportNamedDeclaration(node)) { - continue; - } - symbols.push( - // @ts-ignore - ...(node.declaration?.declarations - ?.filter((node) => !types.isTypeScript(node)) - .map(({id}) => id.name) ?? []) - ); - if (!types.isTypeScript(node.declaration)) { - // @ts-ignore - symbols.push(node.declaration?.id?.name); - } - symbols.push( - ...node.specifiers.map((node) => { - if (types.isExportDefaultSpecifier(node)) { - throw new Error('Should not export from a default import'); - } - if (types.isExportNamespaceSpecifier(node)) { - throw new Error('Should not export a namespace'); - } - const {exported, local} = node; - if (types.isStringLiteral(exported)) { - throw new Error('Should not export symbol as string'); - } - if (local.name !== exported.name) { - throw new Error( - `Exported name "${exported.name}" should match local name "${local.name}"` - ); - } - return exported.name; - }) - ); - } - return symbols.filter(Boolean); -} - -let sharedBentoSymbols; - -/** - * @return {Object} - */ -function getSharedBentoSymbols() { - if (!sharedBentoSymbols) { - const entries = packages.map((pkg) => { - const filepath = resolvePath(`src/${pkg}`); - try { - const source = readFileSync(filepath, 'utf8'); - const symbols = getExportedSymbols(source); - return [`#${pkg}`, symbols]; - } catch (e) { - e.message = `${filepath}: ${e.message}`; - throw e; - } - }); - sharedBentoSymbols = Object.fromEntries(entries); - } - return sharedBentoSymbols; -} - -/** - * @return {Array} - */ -function getSharedBentoModules() { - return packages.map((pkg) => resolvePath(`src/${pkg}`)); -} - -module.exports = { - getExportedSymbols, - getSharedBentoSymbols, - getSharedBentoModules, -}; diff --git a/build-system/compile/generate/test/bento.test.js b/build-system/compile/generate/test/bento.test.js deleted file mode 100644 index 27f65f7f26a0..000000000000 --- a/build-system/compile/generate/test/bento.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const dedent = require('dedent'); -const test = require('ava'); -const { - generateBentoRuntimeEntrypoint, - generateIntermediatePackage, -} = require('../bento'); - -test('generateBentoRuntimeEntrypoint', (t) => { - t.is( - generateBentoRuntimeEntrypoint({ - '#foo': ['bar', 'baz'], - '#baz/bar': ['car'], - }), - dedent(` - import {isEsm} from '#core/mode'; - import {install as installCustomElements} from '#polyfills/custom-elements'; - - import {bar, baz} from '#foo'; - import {car} from '#baz/bar'; - - if (!isEsm()) { - installCustomElements(self, class {}); - } - - const bento = self.BENTO || []; - - bento['_'] = { - // #foo - 'bar': bar, - 'baz': baz, - // #baz/bar - 'car': car, - }; - - bento.push = (fn) => { - fn(); - }; - - self.BENTO = bento; - - for (const fn of bento) { - bento.push(fn); - } - `) - ); -}); - -test('generateIntermediatePackage', (t) => { - t.is( - generateIntermediatePackage({x: ['foo', 'bar'], y: ['baz']}), - dedent(` - const _ = (name) => self.BENTO['_'][name]; - // x - export const foo = /*#__PURE__*/ _('foo'); - export const bar = /*#__PURE__*/ _('bar'); - // y - export const baz = /*#__PURE__*/ _('baz'); - `) - ); -}); diff --git a/build-system/compile/generate/test/shared-bento-symbols.test.js b/build-system/compile/generate/test/shared-bento-symbols.test.js deleted file mode 100644 index 7ce730e403ac..000000000000 --- a/build-system/compile/generate/test/shared-bento-symbols.test.js +++ /dev/null @@ -1,47 +0,0 @@ -const dedent = require('dedent'); -const test = require('ava'); -const {getExportedSymbols} = require('../shared-bento-symbols'); - -test('getExportedSymbols', (t) => { - t.deepEqual( - getExportedSymbols( - dedent(` - export const foo = 'foo'; - export function bar() {} - export class Baz {} - export {qux} from './qux'; - `) - ), - ['foo', 'bar', 'Baz', 'qux'] - ); -}); - -test('getExportedSymbols fails with export *', (t) => { - t.throws(() => getExportedSymbols("export * from './qux';"), { - message: /export */, - }); -}); - -test('getExportedSymbols fails with export default', (t) => { - t.throws(() => getExportedSymbols("export default 'foo';"), { - message: /export default/, - }); -}); - -test('getExportedSymbols fails with export from default', (t) => { - t.throws(() => getExportedSymbols("export foo from 'foo';"), { - message: /export from a default import/, - }); -}); - -test('getExportedSymbols fails with exported namespace', (t) => { - t.throws(() => getExportedSymbols("export * as foo from 'foo';"), { - message: /export a namespace/, - }); -}); - -test('getExportedSymbols fails with exported x as y', (t) => { - t.throws(() => getExportedSymbols("export {x as y} from 'x';"), { - message: /should match local name/, - }); -}); diff --git a/build-system/tasks/extension-helpers.js b/build-system/tasks/extension-helpers.js index cd185977e73e..ebb55a745efc 100644 --- a/build-system/tasks/extension-helpers.js +++ b/build-system/tasks/extension-helpers.js @@ -39,8 +39,9 @@ const {renameSelectorsToBentoTagNames} = require('./css/bento-css'); const {TransformCache, batchedRead} = require('../common/transform-cache'); const {watch} = require('chokidar'); const { - getSharedBentoModules, -} = require('../compile/generate/shared-bento-symbols'); + getRemapBentoDependencies, + getRemapBentoNpmDependencies, +} = require('../compile/bento-remap'); const legacyLatestVersions = json5.parse( fs.readFileSync( @@ -665,8 +666,8 @@ async function buildNpmBinaries(extDir, name, options) { }; if (options.useBentoCore) { // remap all shared modules to the @bentoproject/core package and declare as external - npm.bento.remap = getSharedBentoPackageRemap(); - npm.bento.external = ['@bentoproject/core']; + npm.bento.remap = getRemapBentoNpmDependencies(); + npm.bento.external = Object.values(npm.bento.remap); } } const binaries = Object.values(npm); @@ -701,19 +702,6 @@ function buildBinaries(extDir, binaries, options) { return Promise.all(promises); } -/** - * Creates configuration to remap shared bento modules - * Returns an Object of the form: `{'path/to/shared/module': '@bentoproject/core'}`. - * Uses require.resolve() to normalize directories to the appropriate index file - * @return {Object} - */ -function getSharedBentoPackageRemap() { - const remap = Object.fromEntries( - getSharedBentoModules().map((p) => [p, '@bentoproject/core']) - ); - return remap; -} - /** * @param {string} dir * @param {string} name @@ -721,12 +709,13 @@ function getSharedBentoPackageRemap() { * @return {!Promise} */ async function buildBentoExtensionJs(dir, name, options) { + const remapDependencies = getRemapBentoDependencies(options.minify); await buildExtensionJs(dir, name, { ...options, - wrapper: 'bento', - babelCaller: options.minify - ? 'bento-element-minified' - : 'bento-element-unminified', + externalDependencies: Object.values(remapDependencies), + remapDependencies, + wrapper: 'none', + outputFormat: argv.esm ? 'esm' : 'nomodule-loader', filename: await getBentoBuildFilename(dir, name, 'standalone', options), }); } diff --git a/build-system/tasks/helpers.js b/build-system/tasks/helpers.js index 77d503f3ba66..fa20bee58716 100644 --- a/build-system/tasks/helpers.js +++ b/build-system/tasks/helpers.js @@ -12,10 +12,6 @@ const { VERSION: internalRuntimeVersion, } = require('../compile/internal-version'); const {cyan, green, red} = require('kleur/colors'); -const { - generateBentoCoreEntrypoint, - generateBentoRuntimeEntrypoint, -} = require('../compile/generate/bento'); const {getAmpConfigForFile} = require('./prepend-global'); const {getEsbuildBabelPlugin} = require('../common/esbuild-babel'); const {massageSourcemaps} = require('./sourcemaps'); @@ -25,6 +21,7 @@ const {log, logLocalDev} = require('../common/logging'); const {thirdPartyFrames} = require('../test-configs/config'); const {watch} = require('chokidar'); const {resolvePath} = require('../babel-config/import-resolver'); +const babel = require('@babel/core'); /** * Tasks that should print the `--nobuild` help text. @@ -132,56 +129,22 @@ async function compileCoreRuntime(options) { * @return {Promise} */ async function compileBentoRuntimeAndCore(options) { - await Promise.all([compileBentoRuntime(options), compileBentoCore(options)]); -} - -/** - * @param {!Object} options - * @return {Promise} - */ -async function compileBentoRuntime(options) { - const {srcDir, srcFilename} = jsBundles['bento.js']; - const filename = `${srcDir}/${srcFilename}`; - const fileSource = generateBentoRuntimeEntrypoint(); - await fs.outputFile(filename, fileSource); - await doBuildJs(jsBundles, 'bento.js', options); -} - -/** - * @typedef {{ - * minifiedName?: string; - * toName?: string; - * outputFormat?: string; - * esbuild?: boolean; - * minify?: boolean; - * watch?: boolean; - * onWatchBuild?: *; - * wrapper?: string; - * babelCaller?: string; - * remapDependencies?: Object; - * externalDependencies?: Array - * }} CompileBentoCoreOptions - */ - -/** - * @param {CompileBentoCoreOptions} options - * @return {Promise} - */ -async function compileBentoCore(options) { - const {options: bundleOpts, srcDir, srcFilename} = jsBundles['bento.core.js']; - const {minifiedName, toName} = bundleOpts; - const filename = `${srcDir}/${srcFilename}`; - const fileSource = generateBentoCoreEntrypoint(); - await fs.outputFile(filename, fileSource); - - const esm = argv.esm || argv.sxg || false; - await doBuildJs(jsBundles, 'bento.core.js', { - ...options, - toName: maybeToNpmEsmName(toName), - minifiedName: maybeToNpmEsmName(minifiedName), - - outputFormat: esm ? 'esm' : 'cjs', - }); + const bundleName = 'bento.js'; + const {srcDir, srcFilename} = jsBundles[bundleName]; + await Promise.all([ + // cdn + doBuildJs(jsBundles, bundleName, { + ...options, + outputFormat: argv.esm ? 'esm' : 'nomodule-loader', + }), + // npm + compileJs(srcDir, srcFilename, 'src/bento/core/dist', { + ...options, + toName: maybeToNpmEsmName('bento.core.max.js'), + minifiedName: maybeToNpmEsmName('bento.core.js'), + outputFormat: argv.esm ? 'esm' : 'cjs', + }), + ]); } /** @@ -362,11 +325,9 @@ async function esbuildCompile(srcDir, srcFilename, destDir, options) { const babelCaller = options.babelCaller ?? (options.minify ? 'minified' : 'unminified'); - const babelMaps = new Map(); const babelPlugin = getEsbuildBabelPlugin( babelCaller, - /* enableCache */ true, - {babelMaps} + /* enableCache */ true ); const plugins = [babelPlugin]; @@ -391,7 +352,12 @@ async function esbuildCompile(srcDir, srcFilename, destDir, options) { outfile: destFile, define: experimentDefines, plugins, - format: options.outputFormat, + // When using `nomodule-loader` we build as ESM in order to preserve + // import statements. These are transformed in a post-build Babel step. + format: + options.outputFormat === 'nomodule-loader' + ? 'esm' + : options.outputFormat, banner, footer, // For es5 builds, ensure esbuild-injected code is transpiled. @@ -405,20 +371,43 @@ async function esbuildCompile(srcDir, srcFilename, destDir, options) { } else { result = await result.rebuild(); } - let code = result.outputFiles.find(({path}) => !path.endsWith('.map')).text; - let map = result.outputFiles.find(({path}) => path.endsWith('.map')).text; + + const {outputFiles} = result; + + let code = outputFiles.find(({path}) => !path.endsWith('.map')).text; + let map = JSON.parse( + result.outputFiles.find(({path}) => path.endsWith('.map')).text + ); + const mapChain = [map]; + + if (options.outputFormat === 'nomodule-loader') { + const result = await babel.transformAsync(code, { + caller: {name: 'nomodule-loader'}, + filename: destFile, + sourceRoot: path.dirname(destFile), + sourceMaps: true, + }); + if (!result) { + throw new Error('failed to babel'); + } + code = result.code; + mapChain.unshift(result.map); + } if (options.minify) { - const {code: minified, map: minifiedMap} = await minify(code); - code = minified; - map = await massageSourcemaps([minifiedMap, map], babelMaps, options); - } else { - map = await massageSourcemaps([map], babelMaps, options); + const result = await minify(code, { + // toplevel clobbers the global namespace when with nomodule-loader + toplevel: options.outputFormat !== 'nomodule-loader', + }); + code = result.code; + mapChain.unshift(result.map); } + map = massageSourcemaps(mapChain, options); + const sourceMapComment = `\n//# sourceMappingURL=${destFilename}.map`; await Promise.all([ - fs.outputFile(destFile, code), - fs.outputFile(`${destFile}.map`, map), + fs.outputFile(destFile, `${code}\n${sourceMapComment}`), + fs.outputJson(`${destFile}.map`, map), ]); await finishBundle(destDir, destFilename, options, startTime); @@ -525,9 +514,10 @@ const mangleIdentifier = { * Minify the code with Terser. Only used by the ESBuild. * * @param {string} code + * @param {terser.MinifyOptions} options * @return {!Promise<{code: string, map: *, error?: Error}>} */ -async function minify(code) { +async function minify(code, options = {}) { /* eslint-disable local/camelcase */ const terserOptions = { mangle: { @@ -549,6 +539,7 @@ async function minify(code) { toplevel: true, module: !!argv.esm, nameCache: argv.nomanglecache ? undefined : nameCache, + ...options, }; /* eslint-enable local/camelcase */ @@ -763,8 +754,6 @@ async function getDependencies(entryPoint, options) { module.exports = { bootstrapThirdPartyFrames, compileAllJs, - compileBentoCore, - compileBentoRuntime, compileBentoRuntimeAndCore, compileCoreRuntime, compileJs, diff --git a/build-system/tasks/sourcemaps.js b/build-system/tasks/sourcemaps.js index 59dc45e4e051..7d52a3e41b09 100644 --- a/build-system/tasks/sourcemaps.js +++ b/build-system/tasks/sourcemaps.js @@ -9,49 +9,17 @@ const Remapping = require('@ampproject/remapping'); const remapping = /** @type {*} */ (Remapping); /** - * @param {!Array} sourcemaps - * @param {Map} babelMaps + * @param {Array} mapChain * @param {*} options - * @return {string} + * @return {Object} */ -function massageSourcemaps(sourcemaps, babelMaps, options) { - const root = process.cwd(); - const remapped = remapping( - sourcemaps, - (f) => { - if (f.includes('__SOURCE__')) { - return null; - } - const file = path.join(root, f); - // The Babel tranformed file and the original file have the same path, - // which makes it difficult to distinguish during remapping's load phase. - // We perform some manual path mangling to destingish the babel files - // (which have a sourcemap) from the actual source file by pretending the - // source file exists in the '__SOURCE__' root directory. - const map = babelMaps.get(file); - if (!map) { - throw new Error(`failed to find sourcemap for babel file "${f}"`); - } - return { - ...map, - sourceRoot: path.posix.join('/__SOURCE__/', path.dirname(f)), - }; - }, - !argv.full_sourcemaps - ); - - remapped.sources = remapped.sources.map((source) => { - if (source?.startsWith('/__SOURCE__/')) { - return source.slice('/__SOURCE__/'.length); - } - return source; - }); - remapped.sourceRoot = getSourceRoot(options); - if (remapped.file) { - remapped.file = path.basename(remapped.file); +function massageSourcemaps(mapChain, options) { + const map = remapping(mapChain, () => null, !argv.full_sourcemaps); + map.sourceRoot = getSourceRoot(options); + if (map.file) { + map.file = path.basename(map.file); } - - return remapped.toString(); + return map; } /** diff --git a/src/bento/bento.js b/src/bento/bento.js new file mode 100644 index 000000000000..c91d20f80609 --- /dev/null +++ b/src/bento/bento.js @@ -0,0 +1,20 @@ +/** + * @fileoverview + * Entry point for `bento.js` + */ + +import {isEsm} from '#core/mode'; + +import {install as installCustomElements} from '#polyfills/custom-elements'; + +if (!isEsm()) { + installCustomElements(self, class {}); +} + +export * from '#core/context'; +export * from '#preact'; +export * from '#preact/base-element'; +export * from '#preact/compat'; +export * from '#preact/component'; +export * from '#preact/context'; +export * from '#preact/slot';