@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
353 lines (352 loc) • 18.1 kB
JavaScript
;
/* eslint-disable import/no-extraneous-dependencies */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = configFactory;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const sitemap_1 = require("sitemap");
const clone_js_1 = __importDefault(require("lodash/clone.js"));
const defaults_js_1 = __importDefault(require("lodash/defaults.js"));
const isFunction_js_1 = __importDefault(require("lodash/isFunction.js"));
const isObject_js_1 = __importDefault(require("lodash/isObject.js"));
const autoprefixer_1 = __importDefault(require("autoprefixer"));
const mini_css_extract_plugin_1 = __importDefault(require("mini-css-extract-plugin"));
const node_forge_1 = __importDefault(require("node-forge"));
const webpack_1 = require("webpack");
const workbox_webpack_plugin_1 = __importDefault(require("workbox-webpack-plugin"));
const utils_1 = require("@dr.pogodin/babel-plugin-react-css-modules/utils");
/**
* Creates a new Webpack config object, and performs some auxiliary operations
* on the way.
* @param {object} ops Configuration params. This allows to modify some
* frequently changed options in a convenient way, without a need to manipulate
* directly with the created config object.
* @param {string} ops.babelEnv Babel environment to use for the Babel
* compilation step.
* @param [ops.babelLoaderExclude] Overrides the default value of `exclude`
* option of babel-loader, which is [/node_modules/].
* @param {object} [ops.babelLoaderOptions] Overrides for default Babel options
* of JSX and SVG files loader.
* @param ops.context Base URL for resolution of relative config paths.
* @param {string} [ops.cssLocalIdent=hash:base64:6] The template for
* CSS classnames generation by the Webpack's `css-loader`; it is passed into
* the `localIdentName` param of the loader. It should match the corresponding
* setting in the Babel config.
* @param {boolean} [ops.dontEmitBuildInfo] If set the `.build-info` file won't
* be created at the disk during the compilation.
* @param [ops.dontUseProgressPlugin] Set to not include progress
* plugin.
* @param {string|string[]} ops.entry Entry points for "main" chunk. The config
* will prepend them by some necessary polyfills, e.g.:
* ([babel-polyfill](https://babeljs.io/docs/usage/polyfill/),
* [nodelist-foreach-polyfill](https://www.npmjs.com/package/nodelist-foreach-polyfill)).
* @param {boolean|object} ops.workbox If evaluates to a truthy value,
* [Workbox's InjectManifest plugin](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#injectmanifest_plugin)
* is added to the array of Webpack plugins, to generate service worker for
* browser. If the value is an object, it is merged into the options passed
* into the plugin, otherwise default options are used:
* ```json
* {
* "importWorkboxFrom": "local",
* "swSrc": "@dr.pogodin/react-utils/config/workbox/default.js",
* "swDest": "__service-worker.js"
* }
* ```
* If service worker is generated by this option, it will be automatically
* initiated at the client side by the standard
* [client-side initialization script](docs/client.md)
* provided by **@dr.pogodin/react-utils**. Note that `swDest`'s value cannot be
* overriden by config options provided via `workbox` object.
* @param {object} [ops.fs] Filesystem to use instead of the Node's FS.
* @param {boolean|object} [ops.keepBuildInfo] If `true` and a `.build-info`
* file from a previous build exists in the context directory, it will be
* loaded and used, rather than re-generated by the config factory. It allows
* to re-create the Webpack config during a server launch without re-generation
* of the build info file created during a previous build (and thus bundled
* into the frontend bundle). If an object is provided, it will be used as
* the build info, instead of trying to load it from the filesystem. This
* feature is intended for testing context.
* @param {string} ops.mode
* [Webpack mode](https://webpack.js.org/concepts/mode/).
* @param {string} [ops.outputPath=build] Optional. Output path for the build.
* @param {string} ops.publicPath Base URL for the output of the build assets.
* @param {string} [ops.sitemap] The path to JS or JSON config for sitemap.
* It can be relative to the context, and can be a factory, which returns
* the config. The config should be compatible with
* [`sitemap`](https://www.npmjs.com/package/sitemap) library, and if
* provided the Webpack config factory will use it to gererate `sitemap.xml`
* file in the output folder, and then serve it from the app root.
* @return The generated config will opt to:
* - Bundle the font assets (EOF, OTF, TTF, WOFF, WOFF2 files from
* the `src/assets/fonts` folder of your source code will be bundled
* and output into the `[PUBLIC_PATH]/fonts` folder);
* - Bundle image assets (GIF, JPEG, JPG, PNG files from any folder of
* your source code will be bundled and output into the
* `[PUBLIC_PATH]/images` folder);
* - Bundle SCSS files from any folder of your source code, beside
* `node_modules` and its subfolders. The files will be compiled,
* bundled and extracted into the `[PUBLIC_PATH]/[CHUNK_NAME].css`
* bundles;
* - Bundle CSS files from any folder of your code. The files will be
* bundled and extracted into the `[PUBLIC_PATH]/[CHUNK_NAME].css`
* bundles;
* - Bundle JS, JSX, and SVG files; they will be compiled into the
* `[PUBLIC_PATH]/[CHUNK_NAME].js` bundles, using the Babel environment
* specified in the factory options, and
* [`config/babel/webpack`](./babel-config.js#webpack) config.
*
* - The following path aliases will be automatically set:
* - **`assets`** for `[CONTEXT]/src/assets`;
* - **`components`** for `[CONTEXT]/src/shared/components`;
* - **`fonts`** for `[CONTEXT]/src/assets/fonts`;
* - **`styles`** for `[CONTEXT]/src/styles`.
*
* Also `resolve.symlinks` Webpack option is set to *false* to avoid problems
* with resolution of assets from packages linked with `npm link`.
*
* - The following global variables will be emulated inside the output
* JS bundle:
* - **`BUILD_RNDKEY`** — A random 32 bit key that can be used
* for encryption, it is set just as a global variable accessible in
* the code;
* - **`BUILD_TIMESTAMP`** — UTC timestamp of the beginning of
* the build;
* - **`FRONT_END`** — It will be set *true* inside the bundle,
* so that shared code can use it to determine that it is executed
* at the client side.
*
* - It also opts to polyfill the `__dirname` global variable,
* and to ignore imports of the `fs` Node package;
*
* - Also, it will store to the disk (re-writes if exists) the file
* `[CONTEXT]/.build-info` which will contain a stringified JSON
* object with the following fields:
* - **`rndkey`** — The value set for `BUILD_RNDKEY`;
* - **`timestamp`** — The value set for `BUILD_TIMESTAMP`.
*/
function configFactory(ops) {
var _a, _b;
const o = (0, defaults_js_1.default)((0, clone_js_1.default)(ops), {
babelLoaderOptions: {},
cssLocalIdent: '[hash:base64:6]',
outputPath: 'build/web-public',
publicPath: '',
});
const fs = (_a = ops.fs) !== null && _a !== void 0 ? _a : node_fs_1.default;
// TODO: Should it be improved and re-validated? Are we using it in any project
// as is?
/* TODO: This works in practice, but being async and not awaited it is a bad
* pattern. */
if (o.sitemap) {
const sitemapUrl = node_path_1.default.resolve(o.context, o.sitemap);
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-require-imports
let source = require(sitemapUrl);
if ((0, isFunction_js_1.default)(source))
source = source();
const sm = new sitemap_1.SitemapStream();
source.forEach((item) => sm.write(item));
sm.end();
void (0, sitemap_1.streamToPromise)(sm).then((sitemap) => {
const outUrl = node_path_1.default.resolve(o.context, o.outputPath);
if (!fs.existsSync(outUrl))
fs.mkdirSync(outUrl);
fs.writeFileSync(node_path_1.default.resolve(o.context, o.outputPath, 'sitemap.xml'), new DataView(sitemap.buffer));
});
}
// TODO: Once all assets are named by hashes, we probably don't need build
// info anymore beside the key, which can be merged into stats object?
// On the other hand, it is still handy to have to pass around the build
// timestamp, and any other similar information to the actual app, so it
// can be used in some scenarious.
let buildInfo;
const buildInfoUrl = node_path_1.default.resolve(o.context, '.build-info');
if (o.keepBuildInfo) {
// If "true" - attempt to load from the filesystem.
if (o.keepBuildInfo === true) {
if (fs.existsSync(buildInfoUrl)) {
buildInfo = JSON.parse(fs.readFileSync(buildInfoUrl, 'utf8'));
}
// Otherwise we assume .keepBuildInfo value itself is the build info object
// to use in the build.
}
else
buildInfo = o.keepBuildInfo;
}
// Even if "keepBuildInfo" option was provided, we still generate a new
// build info object in case nothing could be loaded.
buildInfo !== null && buildInfo !== void 0 ? buildInfo : (buildInfo = Object.freeze({
/* A random 32-bit key, that can be used for encryption. */
key: node_forge_1.default.random.getBytesSync(32),
/* Public path used during build. */
publicPath: o.publicPath,
/* `true` if client-side code should setup a service worker. */
useServiceWorker: Boolean(o.workbox),
// Build timestamp.
timestamp: new Date().toISOString(),
}));
// If not opted-out, we write the build info to the filesystem. We also attach
// it to the factory function itself, so it can be easily accessed right after
// the factory call in testing scenarios.
if (!o.dontEmitBuildInfo) {
// Note: this is needed if "fs" option is provided, to ensure the factory
// does not crash if the folder is not created in that filesystem.
fs.mkdirSync(o.context, { recursive: true });
fs.writeFileSync(buildInfoUrl, JSON.stringify(buildInfo));
}
/* Entry points normalization. */
const entry = [
'core-js/stable',
'regenerator-runtime/runtime',
'nodelist-foreach-polyfill',
...Array.isArray(o.entry) ? o.entry : [o.entry],
];
const plugins = [
new webpack_1.DefinePlugin({ BUILD_INFO: JSON.stringify(buildInfo) }),
];
if (!ops.dontUseProgressPlugin)
plugins.push(new webpack_1.ProgressPlugin());
/* Adds InjectManifest plugin from WorkBox, if opted to. */
if (o.workbox) {
if (!(0, isObject_js_1.default)(o.workbox))
o.workbox = {};
plugins.push(new workbox_webpack_plugin_1.default.InjectManifest(Object.assign(Object.assign({ swSrc: node_path_1.default.resolve(__dirname, '../workbox/default.js') }, o.workbox), { swDest: '__service-worker.js' })));
}
const res = {
context: o.context,
entry,
mode: o.mode,
module: {
rules: [{
/* Loads font resources from "src/assets/fonts" folder. */
test: /\.(eot|otf|ttf|woff2?)$/,
generator: {
filename: 'fonts/[contenthash][ext][query]',
},
type: 'asset/resource',
}, {
// Aggregates source maps from dependencies.
test: /\.js$/,
enforce: 'pre',
use: ['source-map-loader'],
}, {
// Loads JS modules (.cjs, .js, .jsx, .mjs); TS modules (.ts, .tsx);
// and SVG assets (.svg).
test: /\.(cjs|js|jsx|mjs|svg|ts|tsx)$/,
exclude: ops.babelLoaderExclude,
loader: 'babel-loader',
options: Object.assign({ babelrc: false, configFile: false, envName: o.babelEnv, presets: [['@dr.pogodin/react-utils/config/babel/webpack', {
typescript: ops.typescript,
}]], sourceType: 'unambiguous' }, o.babelLoaderOptions),
}, {
/* Loads image assets. */
test: /\.(gif|jpe?g|png)$/,
generator: {
filename: 'images/[contenthash][ext][query]',
},
type: 'asset/resource',
}, {
/* Loads SCSS stylesheets. */
test: /\.scss$/,
use: [
mini_css_extract_plugin_1.default.loader, {
loader: 'css-loader',
options: {
modules: {
getLocalIdent: utils_1.getLocalIdent,
localIdentName: o.cssLocalIdent,
// This flag defaults `true` for ES module builds since css-loader@7.0.0:
// https://github.com/webpack-contrib/css-loader/releases/tag/v7.0.0
// We'll keep it `false` to avoid a breaking change for dependant
// projects, and I am also not sure what are the benefits of
// named CSS exports anyway.
namedExport: false,
},
},
}, {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [autoprefixer_1.default],
},
},
}, 'resolve-url-loader', {
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
}, {
/* Loads CSS stylesheets. It is assumed that CSS stylesheets come only
* from dependencies, as we use SCSS inside our own code. */
test: /\.css$/,
use: [
mini_css_extract_plugin_1.default.loader,
'css-loader',
],
}],
},
node: {
__dirname: true,
},
output: {
chunkFilename: '[contenthash].js',
filename: '[contenthash].js',
path: node_path_1.default.resolve(__dirname, o.context, o.outputPath),
publicPath: `${o.publicPath}/`,
},
plugins,
resolve: {
alias: {
// Aliases to JS an JSX files are handled by Babel.
assets: node_path_1.default.resolve(o.context, 'src/assets'),
components: node_path_1.default.resolve(o.context, 'src/shared/components'),
fonts: node_path_1.default.resolve(o.context, 'src/assets/fonts'),
styles: node_path_1.default.resolve(o.context, 'src/styles'),
},
// NOTE: This is primarily motivated by the issue #413
// https://github.com/birdofpreyru/react-utils/issues/413
// caused by react-router exporting different package builds
// for "import" and "default" conditions, resulting in Webpack
// picking up different module versions for import() and require()
// imports. Adding "import" with the highest priorty below forces
// the "import" to be used, working around the problem.
conditionNames: ['import', '...'],
extensions: [
'.ts',
'.tsx',
'.js',
'.jsx',
'.json',
'.scss',
],
fallback: { module: false },
symlinks: false,
},
};
// TODO: Can we do anything better about it? Otherwise, we potentially
// have to alias all Babel's runtime helpers, otherwise if it tries to
// use a new helper it breaks (probably only production) build in a very
// confusing, hard to debug way. Not sure what to do, as the problem with
// RR exports is still there, and we still can't fully move to ES modules
// because Jest and other tools. Also such aliases easily break E2E tests
// with Jest (with our setup), that's why they are avoided when NODE_ENV
// is test (but it might be not the solution that always helps).
//
// NOTE: The "conditionNames" workaround below messes up the loading of
// Babel's runtime helper for require of CJS and ES styles of modules
// (without this alias it is resolved to
// @babel/runtime/helpers/esm/interopRequireDefault, which has
// the hepler function attached to "default" export).
if (process.env.NODE_ENV !== 'test') {
const aliases = (_b = res.resolve) === null || _b === void 0 ? void 0 : _b.alias;
aliases['@babel/runtime/helpers/defineProperty']
= node_path_1.default.resolve(o.context, 'node_modules/@babel/runtime/helpers/defineProperty');
aliases['@babel/runtime/helpers/interopRequireDefault']
= node_path_1.default.resolve(o.context, 'node_modules/@babel/runtime/helpers/interopRequireDefault');
}
return res;
}