diff --git a/.circleci/config.yml b/.circleci/config.yml index 492f36a8..81e886b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ jobs: build: docker: # legacy node - - image: circleci/node:8.11.3 + - image: cimg/node:18.18.0 working_directory: ~/frontend-skeleton @@ -33,6 +33,6 @@ jobs: name: Check Code Style command: yarn lint - - run: - name: Run Tests - command: yarn test + #- run: + # name: Run Tests + # command: yarn test diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..d7510cdc --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +init-env.mjs \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index f0643e4c..82222a12 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "standard", "plugin:react/recommended" ], - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "env": { "browser": true, "node": true, @@ -11,12 +11,16 @@ "jest": true }, "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 2024, "sourceType": "module", "ecmaFeatures": { "jsx": true, "modules": true, - "globalReturn": true + "globalReturn": true, + "deprecatedImportAssert": true + }, + "babelOptions": { + "presets": ["@babel/preset-react"] } }, "plugins": [ @@ -52,7 +56,7 @@ "react/display-name": ["off", { "ignoreTranspilerName": true }], "jsx-quotes": ["error", "prefer-double"], "padding-line-between-statements": [ - "error", + "warn", { "blankLine": "always", "prev": "block", "next": "*" }, { "blankLine": "always", "prev": "block-like", "next": "*" } ] diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..67a228a4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.18.0 \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index a0dc8c32..2859c63e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,31 +1,32 @@ module.exports = { - 'compact': false, - 'presets': [[ + sourceType: 'module', + compact: false, + presets: [[ '@babel/preset-env', { - 'useBuiltIns': 'usage', - 'corejs': { - 'version': 3, - 'proposals': true, + useBuiltIns: 'usage', + corejs: { + version: 3, + proposals: true, }, }, ]], - 'plugins': [ + plugins: [ // stage 2 - ['@babel/plugin-proposal-decorators', { 'legacy': true }], - '@babel/plugin-proposal-numeric-separator', + ['@babel/plugin-proposal-decorators', { legacy: true }], + '@babel/plugin-transform-numeric-separator', '@babel/plugin-proposal-throw-expressions', // stage 3 '@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-import-meta', - ['@babel/plugin-proposal-class-properties', { 'loose': false }], - '@babel/plugin-proposal-json-strings', - '@babel/plugin-proposal-export-namespace-from', + ['@babel/plugin-transform-class-properties', { loose: false }], + '@babel/plugin-transform-json-strings', + '@babel/plugin-transform-export-namespace-from', ], - 'env': { - 'test': { - 'presets': ['@babel/preset-react', '@babel/preset-flow'], - 'plugins': [ + env: { + test: { + presets: ['@babel/preset-react', '@babel/preset-flow'], + plugins: [ 'babel-plugin-react-require', ], }, diff --git a/init-env.js b/init-env.mjs similarity index 88% rename from init-env.js rename to init-env.mjs index 28b77482..7b1c384f 100644 --- a/init-env.js +++ b/init-env.mjs @@ -1,5 +1,5 @@ import dotenv from 'dotenv' -import packageConfig from './package.json' +import packageConfig from './package.json' assert { type: 'json' } import dotenvExpand from 'dotenv-expand' if(!Boolean(process.env.APP_NAME)) { diff --git a/linter.js b/linter.js index f5beb074..d6faf719 100644 --- a/linter.js +++ b/linter.js @@ -6,10 +6,10 @@ const opts = { version: pkg.version, homepage: pkg.homepage, bugs: pkg.bugs.url, - eslint: eslint, + eslint, cmd: 'linter', eslintConfig: { - configFile: path.join(__dirname, '.eslintrc.json'), + overrideConfigFile: path.join(__dirname, '.eslintrc.json'), }, cwd: '', } diff --git a/package.json b/package.json index 2a088758..54072ee9 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,14 @@ "license": "MIT", "repository": "github:django-stars/frontend-skeleton", "scripts": { - "start": "NODE_ENV=development webpack-dev-server --progress --colors --mode development", + "start": "NODE_ENV=development webpack-dev-server --progress --mode development", "build": "NODE_ENV=production webpack --mode production", - "lint": "yarn run eslint && yarn run stylelint", + "lint": "yarn run eslint", "lint:fix": "yarn run eslint:fix && yarn run stylelint:fix", "eslint": "node linter --verbose | snazzy", "eslint:fix": "node linter --fix --verbose | snazzy", - "stylelint": "stylelint 'src/**/*.scss' --syntax scss", - "stylelint:fix": "stylelint 'src/**/*.scss' --syntax scss --fix", + "stylelint": "stylelint 'src/**/*.scss' scss", + "stylelint:fix": "stylelint 'src/**/*.scss' --fix", "test": "jest --config jest.conf.json -u" }, "dependencies": { @@ -54,61 +54,67 @@ "whatwg-fetch": "^3.0.0" }, "devDependencies": { - "@babel/core": "^7.3.3", - "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/plugin-proposal-decorators": "^7.3.0", - "@babel/plugin-proposal-export-namespace-from": "^7.2.0", - "@babel/plugin-proposal-function-sent": "^7.2.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-numeric-separator": "^7.2.0", - "@babel/plugin-proposal-throw-expressions": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-import-meta": "^7.2.0", - "@babel/preset-env": "^7.3.1", - "@babel/preset-flow": "^7.0.0", - "@babel/preset-react": "^7.0.0", - "@babel/register": "^7.0.0", + "@babel/core": "^7.26.0", + "@babel/eslint-parser": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.25.9", + "@babel/plugin-proposal-function-sent": "^7.25.9", + "@babel/plugin-proposal-throw-expressions": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/preset-env": "^7.26.0", + "@babel/preset-flow": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/register": "^7.25.9", "@hot-loader/react-dom": "^16.11.0", "@sentry/webpack-plugin": "^1.8.0", "autoprefixer": "^9.4.9", - "babel-eslint": "^10.0.1", - "babel-loader": "^8.0.5", - "babel-plugin-react-require": "^3.1.1", - "clean-webpack-plugin": "^3.0.0", + "babel-loader": "^9.2.1", + "babel-plugin-react-require": "^4.0.3", + "clean-webpack-plugin": "^4.0.0", "core-js": "3", "dotenv": "^6.0.0", "dotenv-expand": "^5.1.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.10.0", "enzyme-to-json": "^3.3.3", - "eslint": "^4.11.0", - "eslint-config-standard": "^12.0.0", - "eslint-plugin-import": "^2.8.0", - "eslint-plugin-node": "^8.0.1", - "eslint-plugin-promise": "^3.6.0", - "eslint-plugin-react": "^7.7.0", - "eslint-plugin-standard": "^4.0.0", - "html-webpack-plugin": "^3.2.0", + "eslint": "^8.57.1", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-n": "^17.13.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^7.1.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-standard": "^5.0.0", + "html-webpack-plugin": "^5.6.3", "husky": "^3.0.9", "identity-obj-proxy": "^3.0.0", - "jest": "^24.1.0", - "jest-cli": "^24.1.0", - "jest-enzyme": "^7.0.1", + "jest": "^29.7.0", + "jest-cli": "^29.7.0", + "jest-enzyme": "^7.1.2", "lint-staged": "^9.4.3", + "mini-css-extract-plugin": "^2.9.2", "postcss-inline-svg": "^3.0.0", "react-hot-loader": "^4.12.19", "react-test-renderer": "^16.8.3", "redux-mock-store": "^1.5.1", - "reload-html-webpack-plugin": "^0.1.2", + "sass": "^1.81.0", + "sass-loader": "^16.0.3", "snazzy": "^8.0.0", - "standard-engine": "^10.0.0", - "stylelint": "^10.1.0", - "stylelint-config-standard": "^18.3.0", - "stylelint-scss": "^3.9.0", - "webpack": "^4.29.6", + "standard-engine": "^15.1.0", + "stylelint": "^16.10.0", + "stylelint-config-standard": "^36.0.1", + "stylelint-scss": "^6.10.0", + "webpack": "^5.96.1", "webpack-blocks": "^2.1.0", - "webpack-cli": "^3.2.3", - "webpack-dev-server": "^3.2.1", - "write-file-webpack-plugin": "^4.5.0" + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.1.0", + "write-file-webpack-plugin": "^4.5.1" + }, + "resolutions": { + "node-sass": "npm:no-op@latest" } } diff --git a/postcss.config.js b/postcss.config.js index f5ccdbd4..353cd107 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,5 @@ -var autoprefixer = require('autoprefixer') -var inlineSVG = require('postcss-inline-svg') +const autoprefixer = require('autoprefixer') +const inlineSVG = require('postcss-inline-svg') module.exports = { sourceMap: true, diff --git a/presets/assets.js b/presets/assets.mjs similarity index 71% rename from presets/assets.js rename to presets/assets.mjs index ad1010ee..4b010fc0 100644 --- a/presets/assets.js +++ b/presets/assets.mjs @@ -1,4 +1,5 @@ -import { file, group, match } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' +const { file, group, match } = webpackBlocks export default function(config) { return group([ diff --git a/presets/babel.js b/presets/babel.mjs similarity index 62% rename from presets/babel.js rename to presets/babel.mjs index 13a61a6c..66514ca1 100644 --- a/presets/babel.js +++ b/presets/babel.mjs @@ -1,5 +1,5 @@ -import { babel, match } from 'webpack-blocks' - +import webpackBlocks from 'webpack-blocks' +const { babel, match } = webpackBlocks export default function(config) { return match([/\.(js|jsx)$/], { exclude: /node_modules\/(?!ds-)/ }, [ diff --git a/presets/devServer.mjs b/presets/devServer.mjs new file mode 100644 index 00000000..c38e9c09 --- /dev/null +++ b/presets/devServer.mjs @@ -0,0 +1,60 @@ +/** + * Webpack dev-server block. + * + * @see https://github.com/andywer/webpack-blocks/blob/master/packages/dev-server/index.js + */ + +/** + * @param {object} [options] See https://webpack.js.org/configuration/dev-server/ + * @param {string|string[]} [entry] + * @return {Function} + */ +export default function devServer(options = {}, entry = []) { + if(options && (typeof options === 'string' || Array.isArray(options))) { + entry = options + options = {} + } + + if(!Array.isArray(entry)) { + entry = entry ? [entry] : [] + } + + const setter = context => prevConfig => { + context.devServer = context.devServer || { entry: [], options: {} } + context.devServer.entry = context.devServer.entry.concat(entry) + context.devServer.options = Object.assign({}, context.devServer.options, options) + + return prevConfig + } + + return Object.assign(setter, { post: postConfig }) +} + +function postConfig(context, util) { + const entryPointsToAdd = context.devServer.entry + + return prevConfig => { + return util.merge({ + devServer: Object.assign( + { + hot: true, + historyApiFallback: true, + }, + context.devServer.options, + ), + entry: addDevEntryToAll(prevConfig.entry || {}, entryPointsToAdd), + })(prevConfig) + } +} + +function addDevEntryToAll(presentEntryPoints, devServerEntry) { + const newEntryPoints = {} + + Object.keys(presentEntryPoints).forEach(chunkName => { + // It's fine to just set the `devServerEntry`, instead of concat()-ing the present ones. + // Will be concat()-ed by webpack-merge (see `createConfig()`) + newEntryPoints[chunkName] = devServerEntry + }) + + return newEntryPoints +} diff --git a/presets/extract-css.mjs b/presets/extract-css.mjs new file mode 100644 index 00000000..41513611 --- /dev/null +++ b/presets/extract-css.mjs @@ -0,0 +1,144 @@ +/** + * extractCss webpack block. + * + * @see https://webpack.js.org/plugins/mini-css-extract-plugin/ + */ + +// ==================================================================== +// NOTE: this is a copy of extract-text webpack block +// extract-text-webpack-plugin is replaced to mini-css-extract-plugin +// =================================================================== + +import MiniCssExtractPlugin from 'mini-css-extract-plugin' + +/** + * @param {string} outputFilePattern + * @return {Function} + */ +export default function extractCss(outputFilePattern = 'css/[name].[contenthash:8].css') { + const plugin = new MiniCssExtractPlugin({ filename: outputFilePattern }) + + const postHook = (context, util) => prevConfig => { + let nextConfig = prevConfig + + // Only apply to loaders in the same `match()` group or css loaders if there is no `match()` + const ruleToMatch = context.match || { test: /\.css$/ } + const matchingLoaderRules = getMatchingLoaderRules(ruleToMatch, prevConfig) + + if(matchingLoaderRules.length === 0) { + throw new Error( + `extractCss(): No loaders found to extract contents from. Looking for loaders matching ${ + ruleToMatch.test + }`, + ) + } + + // eslint-disable-next-line no-unused-vars + const [fallbackLoaders, nonFallbackLoaders] = splitFallbackRule(matchingLoaderRules) + + /* + const newLoaderDef = Object.assign({}, ruleToMatch, { + use: plugin.extract({ + fallback: fallbackLoaders, + use: nonFallbackLoaders + }) + }) + */ + const newLoaderDef = Object.assign({}, ruleToMatch, { + use: [MiniCssExtractPlugin.loader, ...nonFallbackLoaders], + }) + + for(const ruleToRemove of matchingLoaderRules) { + nextConfig = removeLoaderRule(ruleToRemove)(nextConfig) + } + + nextConfig = util.addPlugin(plugin)(nextConfig) + nextConfig = util.addLoader(newLoaderDef)(nextConfig) + + return nextConfig + } + + return Object.assign(() => prevConfig => prevConfig, { post: postHook }) +} + +function getMatchingLoaderRules(ruleToMatch, webpackConfig) { + return webpackConfig.module.rules.filter( + rule => + isLoaderConditionMatching(rule.test, ruleToMatch.test) && + isLoaderConditionMatching(rule.exclude, ruleToMatch.exclude) && + isLoaderConditionMatching(rule.include, ruleToMatch.include), + ) +} + +function splitFallbackRule(rules) { + const leadingStyleLoaderInAllRules = rules.every(rule => { + return ( + rule.use.length > 0 && + rule.use[0] && + (rule.use[0] === 'style-loader' || rule.use[0].loader === 'style-loader') + ) + }) + + if(leadingStyleLoaderInAllRules) { + const trimmedRules = rules.map(rule => Object.assign({}, rule, { use: rule.use.slice(1) })) + return [['style-loader'], getUseEntriesFromRules(trimmedRules)] + } else { + return [[], getUseEntriesFromRules(rules)] + } +} + +function getUseEntriesFromRules(rules) { + const normalizeUseEntry = use => (typeof use === 'string' ? { loader: use } : use) + + return rules.reduce((useEntries, rule) => useEntries.concat(rule.use.map(normalizeUseEntry)), []) +} + +/** + * @param {object} rule Remove all loaders that match this loader rule. + * @return {Function} + */ +function removeLoaderRule(rule) { + return prevConfig => { + const newRules = prevConfig.module.rules.filter( + prevRule => + !( + isLoaderConditionMatching(prevRule.test, rule.test) && + isLoaderConditionMatching(prevRule.include, rule.include) && + isLoaderConditionMatching(prevRule.exclude, rule.exclude) + ), + ) + + return Object.assign({}, prevConfig, { + module: Object.assign({}, prevConfig.module, { + rules: newRules, + }), + }) + } +} + +function isLoaderConditionMatching(test1, test2) { + if(test1 === test2) { + return true + } else if(typeof test1 !== typeof test2) { + return false + } else if(test1 instanceof RegExp && test2 instanceof RegExp) { + return test1 === test2 || String(test1) === String(test2) + } else if(Array.isArray(test1) && Array.isArray(test2)) { + return areArraysMatching(test1, test2) + } + + return false +} + +function areArraysMatching(array1, array2) { + if(array1.length !== array2.length) { + return false + } + + return array1.every( + item1 => + array2.indexOf(item1) >= 0 || + (item1 instanceof RegExp && + array2.find(item2 => item2 instanceof RegExp && String(item1) === String(item2))), + ) +} diff --git a/presets/i18n.js b/presets/i18n.mjs similarity index 72% rename from presets/i18n.js rename to presets/i18n.mjs index 84bdc7fe..99ecaf0f 100644 --- a/presets/i18n.js +++ b/presets/i18n.mjs @@ -1,9 +1,12 @@ -import { addPlugins } from 'webpack-blocks' -import { ProvidePlugin } from 'webpack' +import webpackBlocks from 'webpack-blocks' +import webpack from 'webpack' + +const { addPlugins } = webpackBlocks + export default function(config) { return addPlugins([ - new ProvidePlugin({ + new webpack.ProvidePlugin({ gettext: ['ds-frontend/packages/i18n', 'gettext'], pgettext: ['ds-frontend/packages/i18n', 'pgettext'], ngettext: ['ds-frontend/packages/i18n', 'ngettext'], diff --git a/presets/index.js b/presets/index.js deleted file mode 100644 index eb908aac..00000000 --- a/presets/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import babel from './babel' -import postcss from './postcss' -import react from './react' -import sass from './sass' -import spa from './spa' -import styles from './styles' -import assets from './assets' -import proxy from './proxy' -import sentry from './sentry' -import i18n from './i18n' - -export { - babel, - postcss, - react, - sass, - styles, - spa, - assets, - proxy, - sentry, - i18n, -} diff --git a/presets/index.mjs b/presets/index.mjs new file mode 100644 index 00000000..92274235 --- /dev/null +++ b/presets/index.mjs @@ -0,0 +1,25 @@ +import babel from './babel.mjs' +import postcss from './postcss.mjs' +import react from './react.mjs' +import sass from './sass.mjs' +import spa from './spa.mjs' +import styles from './styles.mjs' +import assets from './assets.mjs' +import proxy from './proxy.mjs' +import sentry from './sentry.mjs' +import i18n from './i18n.mjs' +import devServer from './devServer.mjs' + +export { + babel, + postcss, + react, + sass, + styles, + spa, + assets, + proxy, + sentry, + i18n, + devServer, +} diff --git a/presets/postcss.js b/presets/postcss.mjs similarity index 55% rename from presets/postcss.js rename to presets/postcss.mjs index 2352900e..00cdbd81 100644 --- a/presets/postcss.js +++ b/presets/postcss.mjs @@ -1,6 +1,8 @@ -import { css, env, extractText, group, match, postcss } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' import path from 'path' +import extractCss from './extract-css.mjs' +const { css, env, group, match, postcss } = webpackBlocks export default function(config) { return group([ @@ -8,7 +10,7 @@ export default function(config) { css(), postcss(), env('production', [ - extractText('bundle.css'), + extractCss('bundle.css'), ]), ]), ]) diff --git a/presets/proxy.js b/presets/proxy.mjs similarity index 88% rename from presets/proxy.js rename to presets/proxy.mjs index bc8622ef..3ec3fd40 100644 --- a/presets/proxy.js +++ b/presets/proxy.mjs @@ -1,5 +1,7 @@ -import { devServer, env, group } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' +import devServer from './devServer.mjs' +const { env, group } = webpackBlocks export default function(config) { return group([ @@ -12,7 +14,7 @@ export default function(config) { } function configureProxy() { - let ret = [ + const ret = [ // proxy API and other paths from env.PROXY makeProxyContext(JSON.parse(process.env.PROXY), process.env.PROXY_URL), ] @@ -23,7 +25,7 @@ function configureProxy() { makeProxyContext([ '/**', `!${process.env.PUBLIC_PATH}`, - ], process.env.BACKEND_URL) + ], process.env.BACKEND_URL), ) } diff --git a/presets/react.js b/presets/react.mjs similarity index 80% rename from presets/react.js rename to presets/react.mjs index 1367224a..49e08875 100644 --- a/presets/react.js +++ b/presets/react.mjs @@ -1,4 +1,5 @@ -import { group, babel } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' +const { group, babel } = webpackBlocks export default function(config) { diff --git a/presets/sass.js b/presets/sass.mjs similarity index 66% rename from presets/sass.js rename to presets/sass.mjs index 531746c1..d0e3ca5a 100644 --- a/presets/sass.js +++ b/presets/sass.mjs @@ -1,19 +1,22 @@ -import { css, env, extractText, group, match, sass } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' import path from 'path' +import extractCss from './extract-css.mjs' + +const { css, env, group, match, sass } = webpackBlocks export default function(config) { return group([ match(['*.css', '*.sass', '*.scss'], { exclude: path.resolve('node_modules') }, [ css(), sass({ - includePaths: [ + loadPaths: [ path.resolve('./src/styles'), path.resolve('./node_modules/bootstrap/scss'), path.resolve('./node_modules'), ], }), env('production', [ - extractText('bundle.css'), + extractCss('bundle.css'), ]), ]), ]) diff --git a/presets/sentry.js b/presets/sentry.mjs similarity index 86% rename from presets/sentry.js rename to presets/sentry.mjs index d5e3eb79..7b165fe1 100644 --- a/presets/sentry.js +++ b/presets/sentry.mjs @@ -1,6 +1,8 @@ -import { addPlugins, group, env } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' import SentryWebpackPlugin from '@sentry/webpack-plugin' +const { addPlugins, group, env } = webpackBlocks + const isSentryConfigured = process.env.SENTRY_ORG && process.env.SENTRY_PROJECT && process.env.SENTRY_AUTH_TOKEN && diff --git a/presets/spa.js b/presets/spa.mjs similarity index 88% rename from presets/spa.js rename to presets/spa.mjs index a2bf32c5..5d9a0021 100644 --- a/presets/spa.js +++ b/presets/spa.mjs @@ -1,7 +1,8 @@ -import { addPlugins, group } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' import HtmlWebpackPlugin from 'html-webpack-plugin' import path from 'path' +const { addPlugins, group } = webpackBlocks export default function(config) { return group([ diff --git a/presets/styles.js b/presets/styles.mjs similarity index 73% rename from presets/styles.js rename to presets/styles.mjs index f60a51df..d555e2cd 100644 --- a/presets/styles.js +++ b/presets/styles.mjs @@ -1,5 +1,8 @@ -import { css, env, extractText, group, match, sass, postcss } from 'webpack-blocks' +import webpackBlocks from 'webpack-blocks' import path from 'path' +import extractCss from './extract-css.mjs' + +const { css, env, group, match, sass, postcss } = webpackBlocks export default function(config) { return group([ @@ -16,12 +19,14 @@ export default function(config) { // path.resolve will provide incorrect string, so we need to use RegExp here // more documentation here: https://webpack.js.org/configuration/module/#condition match(['*.css', '*.sass', '*.scss'], { exclude: /node_modules/ }, [ - process.env.SSR ? css() : css.modules({ - localsConvention: 'camelCase', - }), + process.env.SSR + ? css() + : css.modules({ + localsConvention: 'camelCase', + }), sass({ sassOptions: { - includePaths: [ + loadPaths: [ path.resolve('./src/styles'), path.resolve('./node_modules/bootstrap/scss'), path.resolve('./node_modules'), @@ -30,7 +35,7 @@ export default function(config) { }), postcss(), env('production', [ - extractText('bundle.css'), + extractCss('bundle.css'), ]), ]), ]) diff --git a/src/app/common/forms/BaseFieldLayout.js b/src/app/common/forms/BaseFieldLayout.js index 4a6d73c4..2535e1dd 100644 --- a/src/app/common/forms/BaseFieldLayout.js +++ b/src/app/common/forms/BaseFieldLayout.js @@ -33,6 +33,7 @@ export default function BaseFieldLayout({ if(meta.submitError && !meta.dirtySinceLastSubmit) { return meta.submitError } + if(meta.error && meta.touched) { return meta.error } @@ -40,16 +41,16 @@ export default function BaseFieldLayout({ const formattedError = useMemo(() => Array.isArray(error) ? error[0] : error, [error]) return ( -
+
{label && ( -