UNPKG

@dr.pogodin/react-utils

Version:

Collection of generic ReactJS components and utils

353 lines (352 loc) 18.1 kB
"use strict"; /* 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; }