Skip to content

Commit

Permalink
🏗 Share dependencies in bento.js (#36432)
Browse files Browse the repository at this point in the history
Partial for #36421

`bento-*.js` files now use shared modules from a global `BENTO`. 

Shared modules are listed on `shared-bento-symbols.js`.

- We generate `bento.js` to import and provide the dependencies based on this list. We also generate `bento-shared.js` to use the dependencies from the global.

- `bento-*.js` binaries use `module-resolver` so that the listed imports are replaced with `bento-shared.js`.

Also adds the flag `--bento_runtime_only`.
  • Loading branch information
alanorozco committed Dec 9, 2021
1 parent b6124cb commit faf69c4
Show file tree
Hide file tree
Showing 24 changed files with 480 additions and 36 deletions.
2 changes: 2 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const babelTransforms = new Map([
['babel-jest', 'getEmptyConfig'],
['post-closure', 'getPostClosureConfig'],
['pre-closure', 'getPreClosureConfig'],
['bento-element-minified', 'getBentoElementMinifiedConfig'],
['bento-element-unminified', 'getBentoElementUnminifiedConfig'],
['test', 'getTestConfig'],
['unminified', 'getUnminifiedConfig'],
['minified', 'getMinifiedConfig'],
Expand Down
70 changes: 70 additions & 0 deletions build-system/babel-config/bento-element-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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,
};
4 changes: 3 additions & 1 deletion build-system/compile/bundles.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ exports.jsBundles = {
minifiedDestDir: './build/',
},
'bento.js': {
srcDir: './src/',
// This file is generated, so we find its source in the build/ dir
// See compileBentoRuntime() and generateBentoRuntimeEntrypoint()
srcDir: 'build/',
srcFilename: 'bento.js',
destDir: './dist',
minifiedDestDir: './dist',
Expand Down
14 changes: 14 additions & 0 deletions build-system/compile/generate/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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},
],
},
],
}
92 changes: 92 additions & 0 deletions build-system/compile/generate/bento.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @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<string, string[]>} packageSymbols
* @return {string}
*/
function generateBentoRuntimeEntrypoint(
packageSymbols = getSharedBentoSymbols()
) {
assertNoDupes(Object.values(packageSymbols).flat());
return dedent(`
import {dict} from '#core/types/object';
import {isEsm} from '#core/mode';
import {install as installCustomElements} from '#polyfills/custom-elements';
${Object.entries(packageSymbols)
.map(
([name, symbols]) => `import {${symbols.join(', ')}} from '${name}';`
)
.join('\n')}
if (!isEsm()) {
installCustomElements(self, class {});
}
const bento = self.BENTO || [];
bento['_'] = dict({
${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<string, string[]>} 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,
};
102 changes: 102 additions & 0 deletions build-system/compile/generate/shared-bento-symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @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 {relative} = require('path');

// 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: ['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?.map(({id}) => id.name) ?? [])
);
// @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<string, string[]>}
*/
function getSharedBentoSymbols() {
if (!sharedBentoSymbols) {
const backToRoot = relative(__dirname, process.cwd());
const entries = packages.map((pkg) => {
const filepath = require.resolve(`${backToRoot}/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;
}

module.exports = {getExportedSymbols, getSharedBentoSymbols};
61 changes: 61 additions & 0 deletions build-system/compile/generate/test/bento.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 {dict} from '#core/types/object';
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['_'] = dict({
// #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');
`)
);
});

0 comments on commit faf69c4

Please sign in to comment.