Commit bcf7ef0a authored by Robert Knight's avatar Robert Knight

Replace Browserify with Rollup

Replace Browserify with Rollup as a module bundler. This brings several
advantages:

- It is a more modern bundler that has better support in the ecosystem,
  more active maintenence, fewer dependencies and is easier to write plugins for.
  The core team also maintain most of the plugins that we need.

- It has native support for modern platform features like ES modules,
  including dynamic import for lazy-loading of functionality.

- It generates smaller bundles due to its ability to do tree shaking and scope hoisting.

The new bundles are generated as ES modules rather than UMD / IIFE bundles.
Our target browsers now all support ES modules and this will enable us to use
native static/dynamic imports for code splitting or lazy-loading of
functionality in future.

The initial Rollup build generates one bundle per application (boot script,
annotator, sidebar). This simplifies the build process and minimizes production
bundle size, at the cost of making incremental updates during development
slightly slower.

Build performance is currently similar to or a little slower than Browserify.
We can optimize this by adding caching of transforms (mainly Babel) and
pre-building of a vendor bundle containing large dependencies.

As part of changing the bundler this also changes how the code is packaged
for tests.  We now do the bundling ourselves rather than using a Karma
integration. This makes it much easier to inspect the processed code and fix
problems. It also makes it easier to optimize the bundle building in future.
parent 910b3da5
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
] ]
], ],
"plugins": ["inject-args"], "plugins": ["inject-args"],
"ignore": ["**/vendor/*"],
"env": { "env": {
"development": { "development": {
"presets": [ "presets": [
...@@ -26,9 +25,7 @@ ...@@ -26,9 +25,7 @@
{ {
"development": true, "development": true,
"runtime": "automatic", "runtime": "automatic",
// Use `preact/compat/jsx-dev-runtime` which is an alias for `preact/jsx-runtime`. "importSource": "preact"
// See https://github.com/preactjs/preact/issues/2974.
"importSource": "preact/compat"
} }
] ]
] ]
......
...@@ -16,5 +16,12 @@ ...@@ -16,5 +16,12 @@
// Replaced by TypeScript's static checking. // Replaced by TypeScript's static checking.
"react/prop-types": "off" "react/prop-types": "off"
},
"overrides": [
{
"files": "rollup*.js",
"env": { "node": true },
"parserOptions": { "sourceType": "module" }
} }
]
} }
...@@ -2,29 +2,28 @@ ...@@ -2,29 +2,28 @@
'use strict'; 'use strict';
const { mkdirSync, readdirSync } = require('fs'); const { mkdirSync, writeFileSync } = require('fs');
const path = require('path'); const path = require('path');
const changed = require('gulp-changed'); const changed = require('gulp-changed');
const commander = require('commander'); const commander = require('commander');
const log = require('fancy-log'); const log = require('fancy-log');
const glob = require('glob');
const gulp = require('gulp'); const gulp = require('gulp');
const replace = require('gulp-replace'); const replace = require('gulp-replace');
const rename = require('gulp-rename'); const rename = require('gulp-rename');
const rollup = require('rollup');
const loadConfigFile = require('rollup/dist/loadConfigFile');
const through = require('through2'); const through = require('through2');
const createBundle = require('./scripts/gulp/create-bundle');
const createStyleBundle = require('./scripts/gulp/create-style-bundle'); const createStyleBundle = require('./scripts/gulp/create-style-bundle');
const manifest = require('./scripts/gulp/manifest'); const manifest = require('./scripts/gulp/manifest');
const serveDev = require('./dev-server/serve-dev'); const serveDev = require('./dev-server/serve-dev');
const servePackage = require('./dev-server/serve-package'); const servePackage = require('./dev-server/serve-package');
const vendorBundles = require('./scripts/gulp/vendor-bundles');
const IS_PRODUCTION_BUILD = process.env.NODE_ENV === 'production'; const IS_PRODUCTION_BUILD = process.env.NODE_ENV === 'production';
const SCRIPT_DIR = 'build/scripts';
const STYLE_DIR = 'build/styles'; const STYLE_DIR = 'build/styles';
const FONTS_DIR = 'build/fonts'; const FONTS_DIR = 'build/fonts';
const IMAGES_DIR = 'build/images';
function parseCommandLine() { function parseCommandLine() {
commander commander
...@@ -58,108 +57,51 @@ function parseCommandLine() { ...@@ -58,108 +57,51 @@ function parseCommandLine() {
const karmaOptions = parseCommandLine(); const karmaOptions = parseCommandLine();
/** A list of all modules included in vendor bundles. */ async function buildJS(rollupConfig) {
const vendorModules = Object.keys(vendorBundles.bundles).reduce((deps, key) => { const { options, warnings } = await loadConfigFile(
return deps.concat(vendorBundles.bundles[key]); require.resolve(rollupConfig)
}, []);
// Builds the bundles containing vendor JS code
gulp.task('build-vendor-js', () => {
const finished = [];
Object.keys(vendorBundles.bundles).forEach(name => {
finished.push(
createBundle({
name,
require: vendorBundles.bundles[name],
minify: IS_PRODUCTION_BUILD,
path: SCRIPT_DIR,
noParse: vendorBundles.noParseModules,
})
); );
}); warnings.flush();
return Promise.all(finished);
});
const appBundleBaseConfig = { await Promise.all(
path: SCRIPT_DIR, options.map(async inputs => {
external: vendorModules, const bundle = await rollup.rollup(inputs);
minify: IS_PRODUCTION_BUILD, await Promise.all(inputs.output.map(output => bundle.write(output)));
noParse: vendorBundles.noParseModules,
};
const appBundles = [
{
// The entry point for both the Hypothesis client and the sidebar
// application. This is responsible for loading the rest of the assets needed
// by the client.
name: 'boot',
entry: './src/boot/index',
transforms: ['babel'],
},
{
// The sidebar application for displaying and editing annotations.
name: 'sidebar',
transforms: ['babel'],
entry: './src/sidebar/index',
},
{
// The annotation layer which handles displaying highlights, presenting
// annotation tools on the page and instantiating the sidebar application.
name: 'annotator',
entry: './src/annotator/index',
transforms: ['babel'],
},
{
// A web app to assist with testing UI components.
name: 'ui-playground',
entry: './dev-server/ui-playground/index',
transforms: ['babel'],
},
];
// Polyfill bundles. Polyfills are grouped into "sets" (one bundle per set)
// based on major ECMAScript version or DOM API. Some large polyfills
// (eg. for String.prototype.normalize) are additionally separated out into
// their own bundles.
//
// To add a new polyfill:
// - Add the relevant dependencies to the project
// - Create an entry point in `src/boot/polyfills/{set}` and a feature
// detection function in `src/boot/polyfills/index.js`
// - Add the polyfill set name to the required dependencies for the parts of
// the client that need it in `src/boot/boot.js`
const polyfillBundles = readdirSync('./src/boot/polyfills/')
.filter(name => name.endsWith('.js') && name !== 'index.js')
.map(name => name.replace(/\.js$/, ''))
.map(set => ({
name: `polyfills-${set}`,
entry: `./src/boot/polyfills/${set}`,
transforms: ['babel'],
}));
const appBundleConfigs = appBundles.concat(polyfillBundles).map(config => {
return Object.assign({}, appBundleBaseConfig, config);
});
gulp.task(
'build-js',
gulp.parallel('build-vendor-js', () => {
return Promise.all(
appBundleConfigs.map(config => {
return createBundle(config);
}) })
); );
}) }
);
gulp.task( async function watchJS(rollupConfig) {
'watch-js', const { options, warnings } = await loadConfigFile(
gulp.series('build-vendor-js', function watchJS() { require.resolve(rollupConfig)
appBundleConfigs.forEach(config => { );
createBundle(config, { watch: true }); warnings.flush();
const watcher = rollup.watch(options);
return new Promise(resolve => {
watcher.on('event', event => {
switch (event.code) {
case 'START':
log('JS build starting...');
break;
case 'BUNDLE_END':
event.result.close();
break;
case 'ERROR':
log('JS build error', event.error);
break;
case 'END':
log('JS build completed.');
resolve(); // Resolve once the initial build completes.
break;
}
}); });
}) });
); }
gulp.task('build-js', () => buildJS('./rollup.config.js'));
gulp.task('watch-js', () => watchJS('./rollup.config.js'));
const cssBundles = [ const cssBundles = [
// Hypothesis client // Hypothesis client
...@@ -215,23 +157,8 @@ gulp.task( ...@@ -215,23 +157,8 @@ gulp.task(
}) })
); );
const imageFiles = 'src/images/**/*';
gulp.task('build-images', () => {
return gulp
.src(imageFiles)
.pipe(changed(IMAGES_DIR))
.pipe(gulp.dest(IMAGES_DIR));
});
gulp.task(
'watch-images',
gulp.series('build-images', function watchImages() {
gulp.watch(imageFiles, gulp.task('build-images'));
})
);
const MANIFEST_SOURCE_FILES = const MANIFEST_SOURCE_FILES =
'build/@(fonts|images|scripts|styles)/*.@(js|css|woff|jpg|png|svg)'; 'build/@(fonts|scripts|styles)/*.@(js|css|woff|jpg|png|svg)';
let isFirstBuild = true; let isFirstBuild = true;
...@@ -320,12 +247,7 @@ gulp.task('serve-test-pages', () => { ...@@ -320,12 +247,7 @@ gulp.task('serve-test-pages', () => {
serveDev(3002, { clientUrl: `//{current_host}:3001/hypothesis` }); serveDev(3002, { clientUrl: `//{current_host}:3001/hypothesis` });
}); });
const buildAssets = gulp.parallel( const buildAssets = gulp.parallel('build-js', 'build-css', 'build-fonts');
'build-js',
'build-css',
'build-fonts',
'build-images'
);
gulp.task('build', gulp.series(buildAssets, generateManifest)); gulp.task('build', gulp.series(buildAssets, generateManifest));
gulp.task( gulp.task(
...@@ -336,34 +258,57 @@ gulp.task( ...@@ -336,34 +258,57 @@ gulp.task(
'watch-js', 'watch-js',
'watch-css', 'watch-css',
'watch-fonts', 'watch-fonts',
'watch-images',
'watch-manifest' 'watch-manifest'
) )
); );
function runKarma(done) { async function buildAndRunTests() {
const { grep, singleRun } = karmaOptions;
const testFiles = [
'src/sidebar/test/bootstrap.js',
...glob
.sync('src/**/*-test.js')
.filter(path => (grep ? path.match(grep) : true)),
];
const testSource = testFiles
.map(path => `import "../../${path}";`)
.join('\n');
writeFileSync('build/scripts/test-inputs.js', testSource);
log(`Building test bundle... (${testFiles.length} files)`);
if (singleRun) {
await buildJS('./rollup-tests.config.js');
} else {
await watchJS('./rollup-tests.config.js');
}
log('Starting Karma...');
return new Promise(resolve => {
const karma = require('karma'); const karma = require('karma');
new karma.Server( new karma.Server(
karma.config.parseConfig( karma.config.parseConfig(
path.resolve(__dirname, './src/karma.config.js'), path.resolve(__dirname, './src/karma.config.js'),
karmaOptions { singleRun }
), ),
done resolve
).start(); ).start();
process.on('SIGINT', () => { process.on('SIGINT', () => {
// Give Karma a chance to handle SIGINT and cleanup, but forcibly // Give Karma a chance to handle SIGINT and cleanup, but forcibly
// exit if it takes too long. // exit if it takes too long.
setTimeout(() => { setTimeout(() => {
done(); resolve();
process.exit(1); process.exit(1);
}, 5000); }, 5000);
}); });
});
} }
// Unit and integration testing tasks. // Unit and integration testing tasks.
// Some (eg. a11y) tests rely on CSS bundles, so build these first. //
gulp.task( // Some (eg. a11y) tests rely on CSS bundles. We assume that JS will always take
'test', // londer to build than CSS, so build in parallel.
gulp.series('build-css', done => runKarma(done)) gulp.task('test', gulp.parallel('build-css', buildAndRunTests));
);
...@@ -14,6 +14,13 @@ ...@@ -14,6 +14,13 @@
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@hypothesis/frontend-shared": "3.13.0", "@hypothesis/frontend-shared": "3.13.0",
"@octokit/rest": "^18.0.0", "@octokit/rest": "^18.0.0",
"@rollup/plugin-alias": "^3.1.2",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-virtual": "^2.0.3",
"@sentry/browser": "^6.0.2", "@sentry/browser": "^6.0.2",
"approx-string-match": "^1.1.0", "approx-string-match": "^1.1.0",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
...@@ -21,11 +28,8 @@ ...@@ -21,11 +28,8 @@
"axe-core": "^4.0.0", "axe-core": "^4.0.0",
"babel-plugin-inject-args": "^1.0.0", "babel-plugin-inject-args": "^1.0.0",
"babel-plugin-istanbul": "^6.0.0", "babel-plugin-istanbul": "^6.0.0",
"babel-plugin-mockable-imports": "^1.5.1", "babel-plugin-mockable-imports": "^1.8.0",
"babel-plugin-transform-async-to-promises": "^0.8.6", "babel-plugin-transform-async-to-promises": "^0.8.6",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"browserify-versionify": "^1.0.6",
"chai": "^4.1.2", "chai": "^4.1.2",
"chance": "^1.0.13", "chance": "^1.0.13",
"classnames": "^2.2.4", "classnames": "^2.2.4",
...@@ -45,20 +49,16 @@ ...@@ -45,20 +49,16 @@
"eslint-plugin-mocha": "^9.0.0", "eslint-plugin-mocha": "^9.0.0",
"eslint-plugin-react": "^7.12.4", "eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^4.0.4", "eslint-plugin-react-hooks": "^4.0.4",
"exorcist": "^2.0.0",
"express": "^4.14.1", "express": "^4.14.1",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fetch-mock": "9", "fetch-mock": "9",
"focus-visible": "^5.0.0", "focus-visible": "^5.0.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-changed": "^4.0.0", "gulp-changed": "^4.0.0",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0", "gulp-replace": "^1.0.0",
"gulp-sourcemaps": "^3.0.0",
"hammerjs": "^2.0.4", "hammerjs": "^2.0.4",
"karma": "^6.0.1", "karma": "^6.0.1",
"karma-browserify": "^8.0.0",
"karma-chai": "^0.1.0", "karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0", "karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "^3.0.2", "karma-coverage-istanbul-reporter": "^3.0.2",
...@@ -67,7 +67,6 @@ ...@@ -67,7 +67,6 @@
"karma-sinon": "^1.0.5", "karma-sinon": "^1.0.5",
"katex": "^0.13.0", "katex": "^0.13.0",
"lodash.debounce": "^4.0.3", "lodash.debounce": "^4.0.3",
"loose-envify": "^1.4.0",
"mocha": "9.1.2", "mocha": "9.1.2",
"mustache": "^4.0.1", "mustache": "^4.0.1",
"mustache-express": "^1.3.0", "mustache-express": "^1.3.0",
...@@ -81,44 +80,25 @@ ...@@ -81,44 +80,25 @@
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"retry": "^0.13.1", "retry": "^0.13.1",
"rollup": "^2.47.0",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.23.0", "sass": "^1.23.0",
"scroll-into-view": "^1.8.2", "scroll-into-view": "^1.8.2",
"shallowequal": "^1.1.0", "shallowequal": "^1.1.0",
"showdown": "^1.6.4", "showdown": "^1.6.4",
"sinon": "^11.1.1", "sinon": "^11.1.1",
"stringify": "^5.1.0",
"terser": "^5.0.0",
"through2": "^4.0.1", "through2": "^4.0.1",
"tiny-emitter": "^2.0.2", "tiny-emitter": "^2.0.2",
"typescript": "^4.0.2", "typescript": "^4.0.2",
"vinyl": "^2.2.0", "vinyl": "^2.2.0",
"watchify": "^4.0.0",
"wrap-text": "^1.0.7" "wrap-text": "^1.0.7"
}, },
"browserslist": "chrome 70, firefox 70, safari 11.1", "browserslist": "chrome 70, firefox 70, safari 11.1",
"browserify": {
"transform": [
"browserify-versionify",
[
"stringify",
{
"appliesTo": {
"includeExtensions": [
".html",
".svg"
]
}
}
]
]
},
"prettier": { "prettier": {
"arrowParens": "avoid", "arrowParens": "avoid",
"singleQuote": true "singleQuote": true
}, },
"browser": {
"fetch-mock": "./node_modules/fetch-mock/cjs/client.js"
},
"main": "./build/boot.js", "main": "./build/boot.js",
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production gulp build", "build": "cross-env NODE_ENV=production gulp build",
......
import alias from '@rollup/plugin-alias';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { string } from 'rollup-plugin-string';
import virtual from '@rollup/plugin-virtual';
export default {
input: 'build/scripts/test-inputs.js', // Input file generated by gulp task
output: {
file: 'build/scripts/tests.bundle.js',
format: 'iife',
name: 'testsBundle', // This just exists to suppress a build warning.
},
treeshake: false,
plugins: [
// Replace some problematic dependencies which are imported but not actually
// used with stubs.
virtual({
// Enzyme dependency used in its "Static Rendering" mode, which we don't use.
'cheerio/lib/utils': '',
cheerio: '',
// Node modules that are not available in the browser.
crypto: '',
util: '',
}),
alias({
entries: [
{
// This is needed because of Babel configuration used by
// @hypothesis/frontend-shared. It can be removed once that is fixed.
find: 'preact/compat/jsx-dev-runtime',
replacement: 'preact/jsx-dev-runtime',
},
],
}),
replace({
preventAssignment: true,
values: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
__VERSION__: require('./package.json').version,
},
}),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: [
[
'@babel/preset-react',
{
// Turn off the `development` setting in tests to prevent warnings
// about `this`. See https://github.com/babel/babel/issues/9149.
development: false,
runtime: 'automatic',
importSource: 'preact',
},
],
],
plugins: [
'mockable-imports',
[
'babel-plugin-istanbul',
{
exclude: ['**/test/**/*.js', '**/test-util/**'],
},
],
],
}),
nodeResolve({ browser: true, preferBuiltins: false }),
commonjs(),
string({
include: '**/*.{html,svg}',
}),
json(),
],
};
import alias from '@rollup/plugin-alias';
import { babel } from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { string } from 'rollup-plugin-string';
import { terser } from 'rollup-plugin-terser';
const isProd = process.env.NODE_ENV === 'production';
const prodPlugins = [];
const prodAliases = [];
if (isProd) {
prodPlugins.push(terser());
// Exclude 'preact/debug' from production builds.
prodAliases.push({
find: 'preact/debug',
replacement: 'preact',
});
}
function bundleConfig(name, entryFile) {
return {
input: {
[name]: entryFile,
},
output: {
dir: 'build/scripts/',
format: 'es',
chunkFileNames: `${name}-[name].bundle.js`,
entryFileNames: '[name].bundle.js',
sourcemap: true,
},
preserveEntrySignatures: false,
treeshake: isProd,
plugins: [
alias({
entries: [
{
// This is needed because of Babel configuration used by
// @hypothesis/frontend-shared. It can be removed once that is fixed.
find: 'preact/compat/jsx-dev-runtime',
replacement: 'preact/jsx-dev-runtime',
},
...prodAliases,
],
}),
replace({
preventAssignment: true,
values: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
__VERSION__: require('./package.json').version,
},
}),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
}),
nodeResolve(),
commonjs(),
string({
include: '**/*.svg',
}),
...prodPlugins,
],
};
}
export default [
bundleConfig('annotator', 'src/annotator/index.js'),
bundleConfig('boot', 'src/boot/index.js'),
bundleConfig('sidebar', 'src/sidebar/index.js'),
bundleConfig('ui-playground', 'dev-server/ui-playground/index.js'),
];
/**
* Shared functions for creating JS code bundles using Browserify.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const babelify = require('babelify');
const browserify = require('browserify');
const exorcist = require('exorcist');
const log = require('fancy-log');
const envify = require('loose-envify/custom');
const watchify = require('watchify');
const minifyStream = require('./minify-stream');
function streamFinished(stream) {
return new Promise((resolve, reject) => {
stream.on('finish', resolve);
stream.on('error', reject);
});
}
function waitForever() {
return new Promise(() => {});
}
/**
* Generates a JavaScript application or library bundle and source maps
* for debugging.
*
* @param {BundleOptions} config - Configuration information for this bundle,
* specifying the name of the bundle, what
* modules to include and which code
* transformations to apply.
* @param {BuildOptions} buildOpts
* @return {Promise} Promise for when the bundle is fully written
* if opts.watch is false or a promise that
* waits forever otherwise.
*/
module.exports = function createBundle(config, buildOpts) {
fs.mkdirSync(config.path, { recursive: true });
buildOpts = buildOpts || { watch: false };
// Use a custom name for the "require" function that bundles use to export
// and import modules from other bundles. This avoids conflicts with eg.
// pages that use RequireJS.
const externalRequireName = 'hypothesisRequire';
const bundleOpts = {
debug: true,
// Browserify will try to detect and automatically provide
// browser implementations of Node modules.
//
// This can bloat the bundle hugely if implementations for large
// modules like 'Buffer' or 'crypto' are inadvertently pulled in.
// Here we explicitly whitelist the builtins that can be used.
//
// In particular 'Buffer' is excluded from the list of automatically
// detected variables.
//
// See node_modules/browserify/lib/builtins.js to find out which
// modules provide the implementations of these.
builtins: ['console', '_process'],
externalRequireName,
// Map of global variable names to functions returning _source code_ for
// the values of those globals.
insertGlobalVars: {
// Workaround for Hammer.JS on pages that use RequireJS. Hammer doesn't
// set `module.exports` if a global variable called `define` exists.
define: () => 'undefined',
// The Browserify polyfill for the `Buffer` global is large and
// unnecessary, but can get pulled into the bundle by modules that can
// optionally use it if present.
Buffer: undefined,
// Override the default stub for the `global` var which defaults to
// the `global`, `self` and `window` globals in that order.
//
// This can break on web pages which provide their own definition of
// `global` or `self` that is not an alias for `window`. See
// https://github.com/hypothesis/h/issues/2723 and
// https://github.com/hypothesis/client/issues/277
global: function () {
return 'window';
},
},
};
if (buildOpts.watch) {
bundleOpts.cache = {};
bundleOpts.packageCache = {};
}
// Specify modules that Browserify should not parse.
// The 'noParse' array must contain full file paths,
// not module names.
bundleOpts.noParse = (config.noParse || []).map(id => {
// If package.json specifies a custom entry point for the module for
// use in the browser, resolve that.
const packageConfig = require('../../package.json');
if (packageConfig.browser && packageConfig.browser[id]) {
return require.resolve('../../' + packageConfig.browser[id]);
} else {
return require.resolve(id);
}
});
// Override the "prelude" script which Browserify adds at the start of
// generated bundles to look for the custom require name set by previous bundles
// on the page instead of the default "require".
const defaultPreludePath = require.resolve('browser-pack/prelude');
const prelude = fs
.readFileSync(defaultPreludePath)
.toString()
.replace(
/typeof require == "function" && require/g,
`typeof ${externalRequireName} == "function" && ${externalRequireName};`
);
if (!prelude.includes(externalRequireName)) {
throw new Error(
'Failed to modify prelude to use custom name for "require" function'
);
}
bundleOpts.prelude = prelude;
const name = config.name;
const bundleFileName = name + '.bundle.js';
const bundlePath = config.path + '/' + bundleFileName;
const sourcemapPath = bundlePath + '.map';
const bundle = browserify([], bundleOpts);
(config.require || []).forEach(req => {
// When another bundle uses 'bundle.external(<module path>)',
// the module path is rewritten relative to the
// base directory and a '/' prefix is added, so
// if the other bundle contains "require('./dir/module')",
// then Browserify will generate "require('/dir/module')".
//
// In the bundle which provides './dir/module', we
// therefore need to expose the module as '/dir/module'.
if (req[0] === '.') {
bundle.require(req, { expose: req.slice(1) });
} else if (req[0] === '/') {
// If the require path is absolute, the same rules as
// above apply but the path needs to be relative to
// the root of the repository
const repoRootPath = path.join(__dirname, '../../');
const relativePath = path.relative(
path.resolve(repoRootPath),
path.resolve(req)
);
bundle.require(req, { expose: '/' + relativePath });
} else {
// this is a package under node_modules/, no
// rewriting required.
bundle.require(req);
}
});
bundle.add(config.entry || []);
bundle.external(config.external || []);
(config.transforms || []).forEach(transform => {
if (transform === 'babel') {
bundle.transform(babelify);
}
});
// Include or disable debugging checks in our code and dependencies by
// replacing references to `process.env.NODE_ENV`.
bundle.transform(
envify({
NODE_ENV: process.env.NODE_ENV || 'development',
}),
{
// Ideally packages should configure this transform in their package.json
// file if they need it, but not all of them do.
global: true,
}
);
if (config.minify) {
bundle.transform({ global: true }, minifyStream);
}
function build() {
const output = fs.createWriteStream(bundlePath);
let stream = bundle.bundle();
stream.on('error', err => {
log('Build error', err.toString());
});
if (config.minify) {
stream = stream.pipe(minifyStream());
}
stream = stream.pipe(exorcist(sourcemapPath)).pipe(output);
return streamFinished(stream);
}
if (buildOpts.watch) {
bundle.plugin(watchify);
bundle.on('update', ids => {
const start = Date.now();
log('Source files changed', ids);
build()
.then(() => {
log('Updated %s (%d ms)', bundleFileName, Date.now() - start);
})
.catch(err => {
console.error('Building updated bundle failed:', err);
});
});
build()
.then(() => {
log('Built ' + bundleFileName);
})
.catch(err => {
console.error('Error building bundle:', err);
});
return waitForever();
} else {
return build();
}
};
'use strict';
const { Transform } = require('stream');
const terser = require('terser');
/**
* Return a Node `Transform` stream that minifies JavaScript input.
*
* This is designed to be used both to process individual modules as a Browserify
* transform, and also to be applied to the output of the whole bundle to
* compress the module infrastructure that Browserify adds.
*
* @example
* browserify(['src/app.js'])
* .transform({ global: true }, minifyStream) // Minify individual modules
* .pipe(minifyStream()) // Minify the code added by Browserify
* .pipe(output);
*/
function minifyStream() {
return new Transform({
transform(data, encoding, callback) {
if (!this.chunks) {
this.chunks = [];
}
this.chunks.push(data);
callback();
},
async flush(callback) {
const code = Buffer.concat(this.chunks).toString();
// See https://github.com/terser/terser#minify-options-structure
const options = {
// See https://github.com/hypothesis/client/issues/2664.
safari10: true,
};
// If the code we're minifying has a sourcemap then generate one for the
// minified output, otherwise skip it.
if (code.includes('sourceMappingURL=data:')) {
options.sourceMap = {
content: 'inline',
url: 'inline',
};
}
const minifyResult = await terser.minify(code, options);
this.push(minifyResult.code);
callback();
},
});
}
module.exports = minifyStream;
'use strict';
/**
* Defines a set of vendor bundles which are
* libraries of 3rd-party code referenced by
* one or more bundles of the Hypothesis client/frontend.
*/
module.exports = {
bundles: {
katex: ['katex'],
sentry: ['@sentry/browser'],
showdown: ['showdown'],
},
// List of modules to exclude from parsing for require() statements.
//
// Modules may be excluded from parsing for two reasons:
//
// 1. The module is large and contains no require statements,
// so skipping parsing speeds up the build process.
// 2. The module is itself a compiled Browserify bundle containing
// internal require() statements, which should not be processed
// when including the bundle in another project.
noParseModules: [],
};
/* global process */
/** /**
* @typedef {import('../types/annotator').HypothesisWindow} HypothesisWindow * @typedef {import('../types/annotator').HypothesisWindow} HypothesisWindow
*/ */
...@@ -8,9 +6,7 @@ ...@@ -8,9 +6,7 @@
import 'focus-visible'; import 'focus-visible';
// Enable debug checks for Preact components. // Enable debug checks for Preact components.
if (process.env.NODE_ENV !== 'production') { import 'preact/debug';
require('preact/debug');
}
// Load icons. // Load icons.
import { registerIcons } from '@hypothesis/frontend-shared'; import { registerIcons } from '@hypothesis/frontend-shared';
......
...@@ -60,7 +60,7 @@ function injectStylesheet(doc, href) { ...@@ -60,7 +60,7 @@ function injectStylesheet(doc, href) {
*/ */
function injectScript(doc, src) { function injectScript(doc, src) {
const script = doc.createElement('script'); const script = doc.createElement('script');
script.type = 'text/javascript'; script.type = 'module';
script.src = src; script.src = src;
// Set 'async' to false to maintain execution order of scripts. // Set 'async' to false to maintain execution order of scripts.
...@@ -170,7 +170,6 @@ export function bootHypothesisClient(doc, config) { ...@@ -170,7 +170,6 @@ export function bootHypothesisClient(doc, config) {
// Vendor code and polyfills // Vendor code and polyfills
...polyfills, ...polyfills,
// Main entry point for the client
'scripts/annotator.bundle.js', 'scripts/annotator.bundle.js',
'styles/annotator.css', 'styles/annotator.css',
...@@ -194,12 +193,6 @@ export function bootSidebarApp(doc, config) { ...@@ -194,12 +193,6 @@ export function bootSidebarApp(doc, config) {
injectAssets(doc, config, [ injectAssets(doc, config, [
...polyfills, ...polyfills,
// Vendor code required by sidebar.bundle.js
'scripts/sentry.bundle.js',
'scripts/katex.bundle.js',
'scripts/showdown.bundle.js',
// The sidebar app
'scripts/sidebar.bundle.js', 'scripts/sidebar.bundle.js',
'styles/katex.min.css', 'styles/katex.min.css',
......
...@@ -140,9 +140,6 @@ describe('bootstrap', () => { ...@@ -140,9 +140,6 @@ describe('bootstrap', () => {
it('loads assets for the sidebar application', () => { it('loads assets for the sidebar application', () => {
runBoot('sidebar'); runBoot('sidebar');
const expectedAssets = [ const expectedAssets = [
'scripts/katex.bundle.1234.js',
'scripts/sentry.bundle.1234.js',
'scripts/showdown.bundle.1234.js',
'scripts/sidebar.bundle.1234.js', 'scripts/sidebar.bundle.1234.js',
'styles/katex.min.1234.css', 'styles/katex.min.1234.css',
'styles/sidebar.1234.css', 'styles/sidebar.1234.css',
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
/* global __dirname */ /* global __dirname */
const path = require('path'); const path = require('path');
const envify = require('loose-envify/custom');
const glob = require('glob'); const glob = require('glob');
let chromeFlags = []; let chromeFlags = [];
...@@ -48,25 +47,15 @@ module.exports = function (config) { ...@@ -48,25 +47,15 @@ module.exports = function (config) {
// frameworks to use // frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['browserify', 'mocha', 'chai', 'sinon'], frameworks: ['mocha', 'chai', 'sinon'],
// list of files / patterns to load in the browser // list of files / patterns to load in the browser
files: [ files: [
// Test setup
'./sidebar/test/bootstrap.js',
// Empty HTML file to assist with some tests // Empty HTML file to assist with some tests
{ pattern: './annotator/test/empty.html', watched: false }, { pattern: './annotator/test/empty.html', watched: false },
// Test modules. // Test bundles.
...testFiles.map(pattern => ({ '../build/scripts/tests.bundle.js',
pattern,
// Disable watching because karma-browserify handles this.
watched: false,
type: 'js',
})),
// CSS bundles, relied upon by accessibility tests (eg. for color-contrast // CSS bundles, relied upon by accessibility tests (eg. for color-contrast
// checks). // checks).
...@@ -81,35 +70,6 @@ module.exports = function (config) { ...@@ -81,35 +70,6 @@ module.exports = function (config) {
// preprocess matching files before serving them to the browser // preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'./boot/polyfills/*.js': ['browserify'],
'./sidebar/test/bootstrap.js': ['browserify'],
'**/*-test.js': ['browserify'],
'**/*-it.js': ['browserify'],
},
browserify: {
debug: true,
transform: [
[
'babelify',
{
extensions: ['.js'],
plugins: [
'mockable-imports',
[
'babel-plugin-istanbul',
{
exclude: ['**/test/**/*.js', '**/test-util/**'],
},
],
],
},
],
// Enable debugging checks in libraries that use `NODE_ENV` guards.
[envify({ NODE_ENV: 'development' }), { global: true }],
],
},
mochaReporter: { mochaReporter: {
// Display a helpful diff when comparing complex objects // Display a helpful diff when comparing complex objects
......
...@@ -8,13 +8,20 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; ...@@ -8,13 +8,20 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import MenuKeyboardNavigation from './MenuKeyboardNavigation'; import MenuKeyboardNavigation from './MenuKeyboardNavigation';
// The triangular indicator below the menu toggle button that visually links it /**
// to the menu content. * The triangular indicator below the menu toggle button that visually links it
const menuArrow = className => ( * to the menu content.
*
* @param {object} props
* @param {string} [props.className]
*/
function MenuArrow({ className }) {
return (
<svg className={classnames('Menu__arrow', className)} width={15} height={8}> <svg className={classnames('Menu__arrow', className)} width={15} height={8}>
<path d="M0 8 L7 0 L15 8" stroke="currentColor" strokeWidth="2" /> <path d="M0 8 L7 0 L15 8" stroke="currentColor" strokeWidth="2" />
</svg> </svg>
); );
}
/** /**
* Flag indicating whether the next click event on the menu's toggle button * Flag indicating whether the next click event on the menu's toggle button
...@@ -198,7 +205,7 @@ export default function Menu({ ...@@ -198,7 +205,7 @@ export default function Menu({
</button> </button>
{isOpen && ( {isOpen && (
<> <>
{menuArrow(arrowClass)} <MenuArrow className={arrowClass} />
<div <div
className={classnames( className={classnames(
'Menu__content', 'Menu__content',
......
/* global process */
import { parseJsonConfig } from '../boot/parse-json-config'; import { parseJsonConfig } from '../boot/parse-json-config';
import * as rendererOptions from '../shared/renderer-options'; import * as rendererOptions from '../shared/renderer-options';
...@@ -29,9 +27,7 @@ disableOpenerForExternalLinks(document.body); ...@@ -29,9 +27,7 @@ disableOpenerForExternalLinks(document.body);
import 'focus-visible'; import 'focus-visible';
// Enable debugging checks for Preact. // Enable debugging checks for Preact.
if (process.env.NODE_ENV !== 'production') { import 'preact/debug';
require('preact/debug');
}
// Install Preact renderer options to work around browser quirks // Install Preact renderer options to work around browser quirks
rendererOptions.setupBrowserFixes(); rendererOptions.setupBrowserFixes();
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment