diff --git a/.commitlintrc.js b/.commitlintrc.js index 3347cb96..5073c20d 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -1 +1 @@ -module.exports = {extends: ['@commitlint/config-conventional']}; +module.exports = { extends: ["@commitlint/config-conventional"] }; diff --git a/.prettierrc.json b/.prettierrc similarity index 89% rename from .prettierrc.json rename to .prettierrc index e1627383..6c1d94bd 100644 --- a/.prettierrc.json +++ b/.prettierrc @@ -3,7 +3,7 @@ "singleQuote": false, "bracketSpacing": true, "jsxBracketSameLine": false, - "tabWidth": 1, + "tabWidth": 2, "semi": true, "trailingComma": "none" } diff --git a/.vscode/settings.json b/.vscode/settings.json index c02ad941..8ec9748c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "comments": false, "strings": true }, - "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "eslint.alwaysShowStatus": true, "editor.codeActionsOnSave": { @@ -13,14 +13,17 @@ }, "eslint.validate": ["javascript"], "[json]": { - "editor.defaultFormatter": "svipas.prettier-plus", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[jsonc]": { - "editor.defaultFormatter": "svipas.prettier-plus" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { - "editor.defaultFormatter": "svipas.prettier-plus" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "emmet.triggerExpansionOnTab": true + "emmet.triggerExpansionOnTab": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/README.md b/README.md index 214208fd..9f70e66c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,19 @@ +## Refs + - preview demo: https://privoce.rustchat.com/ - design: https://www.figma.com/file/EHnNr53kNmDWgUT86It6CH/UI - backend APIs: https://dev.rustchat.com/api/swagger - text editor: https://plate.udecode.io/docs/installation - markdown editor: https://nhn.github.io/tui.editor/latest/ -- redux: [@reduxjs/toolkit](https://redux-toolkit.js.org/introduction/getting-started) -- indexDB wrapper: https://github.com/localForage/localForage \ No newline at end of file +- redux: [@reduxjs/toolkit](https://redux-toolkit.js.org/introduction/getting-started) +- indexDB wrapper: https://github.com/localForage/localForage + +## Local Development + +- [VS Code](https://code.visualstudio.com/) Editor Recommended +- VS Code plugins: + - dbaeumer.vscode-eslint: ESLint + - esbenp.prettier-vscode: Prettier + - components.vscode-styled-components: Syntax highlighting for styled-components + - jonkwheeler.styled-components-snippets: Styled-Components Snippets for VSCode + - dsznajder.es7-react-js-snippets:Extensions for React, React-Native and Redux in JS/TS with ES7+ syntax diff --git a/config/env.js b/config/env.js index 89ca60d8..3e6d56e4 100644 --- a/config/env.js +++ b/config/env.js @@ -9,9 +9,7 @@ delete require.cache[require.resolve("./paths")]; const NODE_ENV = process.env.NODE_ENV; if (!NODE_ENV) { - throw new Error( - "The NODE_ENV environment variable is required but was not specified." - ); + throw new Error("The NODE_ENV environment variable is required but was not specified."); } // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use @@ -22,7 +20,7 @@ const dotenvFiles = [ // results for everyone NODE_ENV !== "test" && `${paths.dotenv}.local`, `${paths.dotenv}.${NODE_ENV}`, - paths.dotenv, + paths.dotenv ].filter(Boolean); // Load environment variables from .env* files. Suppress warnings using silent @@ -34,7 +32,7 @@ dotenvFiles.forEach((dotenvFile) => { if (fs.existsSync(dotenvFile)) { require("dotenv-expand")( require("dotenv").config({ - path: dotenvFile, + path: dotenvFile }) ); } @@ -88,7 +86,7 @@ function getClientEnvironment(publicUrl) { WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, // Whether or not react-refresh is enabled. // It is defined here so it is available in the webpackHotDevClient. - FAST_REFRESH: process.env.FAST_REFRESH !== "false", + FAST_REFRESH: process.env.FAST_REFRESH !== "false" } ); // Stringify all values so we can feed into webpack DefinePlugin @@ -96,7 +94,7 @@ function getClientEnvironment(publicUrl) { "process.env": Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); return env; - }, {}), + }, {}) }; return { raw, stringified }; diff --git a/config/getHttpsConfig.js b/config/getHttpsConfig.js index 013d493c..828a7828 100644 --- a/config/getHttpsConfig.js +++ b/config/getHttpsConfig.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const chalk = require('react-dev-utils/chalk'); -const paths = require('./paths'); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const chalk = require("react-dev-utils/chalk"); +const paths = require("./paths"); // Ensure the certificate and key provided are valid and if not // throw an easy to debug error @@ -12,22 +12,16 @@ function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { let encrypted; try { // publicEncrypt will throw an error with an invalid cert - encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); + encrypted = crypto.publicEncrypt(cert, Buffer.from("test")); } catch (err) { - throw new Error( - `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` - ); + throw new Error(`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`); } try { // privateDecrypt will throw an error with an invalid key crypto.privateDecrypt(key, encrypted); } catch (err) { - throw new Error( - `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ - err.message - }` - ); + throw new Error(`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${err.message}`); } } @@ -35,9 +29,9 @@ function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { function readEnvFile(file, type) { if (!fs.existsSync(file)) { throw new Error( - `You specified ${chalk.cyan( - type - )} in your env, but the file "${chalk.yellow(file)}" can't be found.` + `You specified ${chalk.cyan(type)} in your env, but the file "${chalk.yellow( + file + )}" can't be found.` ); } return fs.readFileSync(file); @@ -47,14 +41,14 @@ function readEnvFile(file, type) { // Return cert files if provided in env, otherwise just true or false function getHttpsConfig() { const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; - const isHttps = HTTPS === 'true'; + const isHttps = HTTPS === "true"; if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); const config = { - cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), - key: readEnvFile(keyFile, 'SSL_KEY_FILE'), + cert: readEnvFile(crtFile, "SSL_CRT_FILE"), + key: readEnvFile(keyFile, "SSL_KEY_FILE") }; validateKeyAndCerts({ ...config, keyFile, crtFile }); diff --git a/config/modules.js b/config/modules.js index e58bce0a..c1709df5 100644 --- a/config/modules.js +++ b/config/modules.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const fs = require('fs'); -const path = require('path'); -const paths = require('./paths'); -const chalk = require('react-dev-utils/chalk'); -const resolve = require('resolve'); +const fs = require("fs"); +const path = require("path"); +const paths = require("./paths"); +const chalk = require("react-dev-utils/chalk"); +const resolve = require("resolve"); /** * Get additional module paths based on the baseUrl of a compilerOptions object. @@ -15,19 +15,19 @@ function getAdditionalModulePaths(options = {}) { const baseUrl = options.baseUrl; if (!baseUrl) { - return ''; + return ""; } const baseUrlResolved = path.resolve(paths.appPath, baseUrl); // We don't need to do anything if `baseUrl` is set to `node_modules`. This is // the default behavior. - if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + if (path.relative(paths.appNodeModules, baseUrlResolved) === "") { return null; } // Allow the user set the `baseUrl` to `appSrc`. - if (path.relative(paths.appSrc, baseUrlResolved) === '') { + if (path.relative(paths.appSrc, baseUrlResolved) === "") { return [paths.appSrc]; } @@ -36,7 +36,7 @@ function getAdditionalModulePaths(options = {}) { // not transpiled outside of `src`. We do allow importing them with the // absolute path (e.g. `src/Components/Button.js`) but we set that up with // an alias. - if (path.relative(paths.appPath, baseUrlResolved) === '') { + if (path.relative(paths.appPath, baseUrlResolved) === "") { return null; } @@ -44,7 +44,7 @@ function getAdditionalModulePaths(options = {}) { throw new Error( chalk.red.bold( "Your project's `baseUrl` can only be set to `src` or `node_modules`." + - ' Create React App does not support other values at this time.' + " Create React App does not support other values at this time." ) ); } @@ -63,14 +63,13 @@ function getWebpackAliases(options = {}) { const baseUrlResolved = path.resolve(paths.appPath, baseUrl); - if (path.relative(paths.appPath, baseUrlResolved) === '') { + if (path.relative(paths.appPath, baseUrlResolved) === "") { return { - src: paths.appSrc, + src: paths.appSrc }; } } - function getModules() { // Check if TypeScript is setup const hasTsConfig = fs.existsSync(paths.appTsConfig); @@ -78,7 +77,7 @@ function getModules() { if (hasTsConfig && hasJsConfig) { throw new Error( - 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + "You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file." ); } @@ -88,8 +87,8 @@ function getModules() { // TypeScript project and set up the config // based on tsconfig.json if (hasTsConfig) { - const ts = require(resolve.sync('typescript', { - basedir: paths.appNodeModules, + const ts = require(resolve.sync("typescript", { + basedir: paths.appNodeModules })); config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; // Otherwise we'll check if there is jsconfig.json @@ -106,7 +105,7 @@ function getModules() { return { additionalModulePaths: additionalModulePaths, webpackAliases: getWebpackAliases(options), - hasTsConfig, + hasTsConfig }; } diff --git a/config/paths.js b/config/paths.js index f0a6cd9c..3d008e9f 100644 --- a/config/paths.js +++ b/config/paths.js @@ -1,13 +1,13 @@ -'use strict'; +"use strict"; -const path = require('path'); -const fs = require('fs'); -const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); +const path = require("path"); +const fs = require("fs"); +const getPublicUrlOrPath = require("react-dev-utils/getPublicUrlOrPath"); // Make sure any symlinks in the project folder are resolved: // https://github.com/facebook/create-react-app/issues/637 const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = relativePath => path.resolve(appDirectory, relativePath); +const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. @@ -16,30 +16,30 @@ const resolveApp = relativePath => path.resolve(appDirectory, relativePath); // We can't use a relative path in HTML because we don't want to load something // like /todos/42/static/js/bundle.7289d.js. We have to know the root. const publicUrlOrPath = getPublicUrlOrPath( - process.env.NODE_ENV === 'development', - require(resolveApp('package.json')).homepage, + process.env.NODE_ENV === "development", + require(resolveApp("package.json")).homepage, process.env.PUBLIC_URL ); -const buildPath = process.env.BUILD_PATH || 'build'; +const buildPath = process.env.BUILD_PATH || "build"; const moduleFileExtensions = [ - 'web.mjs', - 'mjs', - 'web.js', - 'js', - 'web.ts', - 'ts', - 'web.tsx', - 'tsx', - 'json', - 'web.jsx', - 'jsx', + "web.mjs", + "mjs", + "web.js", + "js", + "web.ts", + "ts", + "web.tsx", + "tsx", + "json", + "web.jsx", + "jsx" ]; // Resolve file paths in the same order as webpack const resolveModule = (resolveFn, filePath) => { - const extension = moduleFileExtensions.find(extension => + const extension = moduleFileExtensions.find((extension) => fs.existsSync(resolveFn(`${filePath}.${extension}`)) ); @@ -52,26 +52,24 @@ const resolveModule = (resolveFn, filePath) => { // config after eject: we're in ./config/ module.exports = { - dotenv: resolveApp('.env'), - appPath: resolveApp('.'), + dotenv: resolveApp(".env"), + appPath: resolveApp("."), appBuild: resolveApp(buildPath), - appPublic: resolveApp('public'), - appHtml: resolveApp('public/index.html'), - appIndexJs: resolveModule(resolveApp, 'src/index'), - appPackageJson: resolveApp('package.json'), - appSrc: resolveApp('src'), - appTsConfig: resolveApp('tsconfig.json'), - appJsConfig: resolveApp('jsconfig.json'), - yarnLockFile: resolveApp('yarn.lock'), - testsSetup: resolveModule(resolveApp, 'src/setupTests'), - proxySetup: resolveApp('src/setupProxy.js'), - appNodeModules: resolveApp('node_modules'), - appWebpackCache: resolveApp('node_modules/.cache'), - appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'), - swSrc: resolveModule(resolveApp, 'src/service-worker'), - publicUrlOrPath, + appPublic: resolveApp("public"), + appHtml: resolveApp("public/index.html"), + appIndexJs: resolveModule(resolveApp, "src/index"), + appPackageJson: resolveApp("package.json"), + appSrc: resolveApp("src"), + appTsConfig: resolveApp("tsconfig.json"), + appJsConfig: resolveApp("jsconfig.json"), + yarnLockFile: resolveApp("yarn.lock"), + testsSetup: resolveModule(resolveApp, "src/setupTests"), + proxySetup: resolveApp("src/setupProxy.js"), + appNodeModules: resolveApp("node_modules"), + appWebpackCache: resolveApp("node_modules/.cache"), + appTsBuildInfoFile: resolveApp("node_modules/.cache/tsconfig.tsbuildinfo"), + swSrc: resolveModule(resolveApp, "src/service-worker"), + publicUrlOrPath }; - - module.exports.moduleFileExtensions = moduleFileExtensions; diff --git a/config/webpack.config.js b/config/webpack.config.js index 94331ff3..4af9f123 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -33,16 +33,14 @@ const babelRuntimeEntryHelpers = require.resolve( { paths: [babelRuntimeEntry] } ); const babelRuntimeRegenerator = require.resolve("@babel/runtime/regenerator", { - paths: [babelRuntimeEntry], + paths: [babelRuntimeEntry] }); // Some apps do not need the benefits of saving a web request, so not inlining the chunk // makes for a smoother build process. const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== "false"; -const imageInlineSizeLimit = parseInt( - process.env.IMAGE_INLINE_SIZE_LIMIT || "10000" -); +const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || "10000"); // Check if TypeScript is setup const useTypeScript = fs.existsSync(paths.appTsConfig); @@ -75,8 +73,7 @@ module.exports = function (webpackEnv) { // Variable used for enabling profiling in Production // passed into alias object. Uses a flag if passed into the build command - const isEnvProductionProfile = - isEnvProduction && process.argv.includes("--profile"); + const isEnvProductionProfile = isEnvProduction && process.argv.includes("--profile"); // We will provide `paths.publicUrlOrPath` to our app // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. @@ -94,14 +91,12 @@ module.exports = function (webpackEnv) { loader: MiniCssExtractPlugin.loader, // css is located in `static/css`, use '../../' to locate index.html folder // in production `paths.publicUrlOrPath` can be a relative path - options: paths.publicUrlOrPath.startsWith(".") - ? { publicPath: "../../" } - : {}, + options: paths.publicUrlOrPath.startsWith(".") ? { publicPath: "../../" } : {} }, { loader: require.resolve("css-loader"), - options: cssOptions, - }, + options: cssOptions + } ].filter(Boolean); return loaders; }; @@ -140,13 +135,9 @@ module.exports = function (webpackEnv) { publicPath: paths.publicUrlOrPath, // Point sourcemap entries to original disk location (format as URL on Windows) devtoolModuleFilenameTemplate: isEnvProduction - ? (info) => - path - .relative(paths.appSrc, info.absoluteResourcePath) - .replace(/\\/g, "/") + ? (info) => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, "/") : isEnvDevelopment && - ((info) => - path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")), + ((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")) }, cache: { type: "filesystem", @@ -156,13 +147,11 @@ module.exports = function (webpackEnv) { buildDependencies: { defaultWebpack: ["webpack/lib/"], config: [__filename], - tsconfig: [paths.appTsConfig, paths.appJsConfig].filter((f) => - fs.existsSync(f) - ), - }, + tsconfig: [paths.appTsConfig, paths.appJsConfig].filter((f) => fs.existsSync(f)) + } }, infrastructureLogging: { - level: "none", + level: "none" }, optimization: { minimize: isEnvProduction, @@ -176,7 +165,7 @@ module.exports = function (webpackEnv) { // into invalid ecma 5 code. This is why the 'compress' and 'output' // sections only apply transformations that are ecma 5 safe // https://github.com/facebook/create-react-app/pull/4234 - ecma: 8, + ecma: 8 }, compress: { drop_console: true, @@ -191,10 +180,10 @@ module.exports = function (webpackEnv) { // https://github.com/facebook/create-react-app/issues/5250 // Pending further investigation: // https://github.com/terser-js/terser/issues/120 - inline: 2, + inline: 2 }, mangle: { - safari10: true, + safari10: true }, // Added for profiling in devtools keep_classnames: isEnvProductionProfile, @@ -204,22 +193,20 @@ module.exports = function (webpackEnv) { comments: false, // Turned on because emoji and regex is not minified properly using default // https://github.com/facebook/create-react-app/issues/2488 - ascii_only: true, - }, - }, + ascii_only: true + } + } }), // This is only used in production mode - new CssMinimizerPlugin(), - ], + new CssMinimizerPlugin() + ] }, resolve: { // This allows you to set a fallback for where webpack should look for modules. // We placed these paths second because we want `node_modules` to "win" // if there are any conflicts. This matches Node resolution mechanism. // https://github.com/facebook/create-react-app/issues/253 - modules: ["node_modules", paths.appNodeModules].concat( - modules.additionalModulePaths || [] - ), + modules: ["node_modules", paths.appNodeModules].concat(modules.additionalModulePaths || []), // These are the reasonable defaults supported by the Node ecosystem. // We also include JSX as a common component filename extension to support // some tools, although we do not recommend using it, see: @@ -233,9 +220,9 @@ module.exports = function (webpackEnv) { // Allows for better profiling with ReactDevTools ...(isEnvProductionProfile && { "react-dom$": "react-dom/profiling", - "scheduler/tracing": "scheduler/tracing-profiling", + "scheduler/tracing": "scheduler/tracing-profiling" }), - ...(modules.webpackAliases || {}), + ...(modules.webpackAliases || {}) }, plugins: [ // Prevents users from importing files from outside of src/ (or node_modules/). @@ -249,9 +236,9 @@ module.exports = function (webpackEnv) { reactRefreshWebpackPluginRuntimeEntry, babelRuntimeEntry, babelRuntimeEntryHelpers, - babelRuntimeRegenerator, - ]), - ], + babelRuntimeRegenerator + ]) + ] }, module: { strictExportPresence: true, @@ -261,7 +248,7 @@ module.exports = function (webpackEnv) { enforce: "pre", exclude: /@babel(?:\/|\\{1,2})runtime/, test: /\.(js|mjs|jsx|ts|tsx|css)$/, - loader: require.resolve("source-map-loader"), + loader: require.resolve("source-map-loader") }, { // "oneOf" will traverse all following loaders until one will @@ -276,9 +263,9 @@ module.exports = function (webpackEnv) { mimetype: "image/avif", parser: { dataUrlCondition: { - maxSize: imageInlineSizeLimit, - }, - }, + maxSize: imageInlineSizeLimit + } + } }, // "url" loader works like "file" loader except that it embeds assets // smaller than specified limit in bytes as data URLs to avoid requests. @@ -288,9 +275,9 @@ module.exports = function (webpackEnv) { type: "asset", parser: { dataUrlCondition: { - maxSize: imageInlineSizeLimit, - }, - }, + maxSize: imageInlineSizeLimit + } + } }, { test: /\.svg$/, @@ -301,17 +288,17 @@ module.exports = function (webpackEnv) { prettier: false, svgo: false, svgoConfig: { - plugins: [{ removeViewBox: false }], + plugins: [{ removeViewBox: false }] }, titleProp: true, - ref: true, - }, - }, + ref: true + } + } ], issuer: { - and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + and: [/\.(ts|tsx|js|jsx|md|mdx)$/] }, - resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url + resourceQuery: { not: [/url/] } // exclude react component if *.svg?url }, // Process application JS with Babel. // The preset includes JSX, Flow, TypeScript, and some ESnext features. @@ -320,22 +307,20 @@ module.exports = function (webpackEnv) { include: paths.appSrc, loader: require.resolve("babel-loader"), options: { - customize: require.resolve( - "babel-preset-react-app/webpack-overrides" - ), + customize: require.resolve("babel-preset-react-app/webpack-overrides"), presets: [ [ require.resolve("babel-preset-react-app"), { - runtime: hasJsxRuntime ? "automatic" : "classic", - }, - ], + runtime: hasJsxRuntime ? "automatic" : "classic" + } + ] ], plugins: [ isEnvDevelopment && shouldUseReactRefresh && - require.resolve("react-refresh/babel"), + require.resolve("react-refresh/babel") ].filter(Boolean), // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ @@ -343,8 +328,8 @@ module.exports = function (webpackEnv) { cacheDirectory: true, // See #6846 for context on why cacheCompression is disabled cacheCompression: false, - compact: isEnvProduction, - }, + compact: isEnvProduction + } }, // Process any JS outside of the app with Babel. // Unlike the application JS, we only compile the standard ES features. @@ -357,10 +342,7 @@ module.exports = function (webpackEnv) { configFile: false, compact: false, presets: [ - [ - require.resolve("babel-preset-react-app/dependencies"), - { helpers: true }, - ], + [require.resolve("babel-preset-react-app/dependencies"), { helpers: true }] ], cacheDirectory: true, // See #6846 for context on why cacheCompression is disabled @@ -370,8 +352,8 @@ module.exports = function (webpackEnv) { // code. Without the options below, debuggers like VSCode // show incorrect code and set breakpoints on the wrong lines. sourceMaps: shouldUseSourceMap, - inputSourceMap: shouldUseSourceMap, - }, + inputSourceMap: shouldUseSourceMap + } }, // "css" loader resolves paths in CSS and adds assets as dependencies. // "style" loader turns CSS into JS modules that inject