diff --git a/.env.sample b/.env.sample index e9e3b31a..1f10957c 100644 --- a/.env.sample +++ b/.env.sample @@ -3,7 +3,7 @@ REACT_APP_WORKSPACES_ENABLED=false # Enable when working with Dug REACT_APP_SEMANTIC_SEARCH_ENABLED='true' # Points the app to the Dug API -REACT_APP_HELX_SEARCH_URL='https:\/\/heal-dev.apps.renci.org' +REACT_APP_HELX_SEARCH_URL='https:\/\/heal-dev.apps.renci.org\/search-api' # Only necessary if working with analytics specifically. # Should be a project token/GA property. Make sure to configure details # in env.json @@ -19,4 +19,4 @@ REACT_APP_HIDDEN_RESULT_TABS='' # Some static assets such as app logo are loaded externally from the dockstore repository (helx-apps). REACT_APP_DOCKSTORE_BRANCH=master # Some static assets such as brand logo are loaded externally from the Appstore repository. -REACT_APP_APPSTORE_ASSET_BRANCH=master \ No newline at end of file +REACT_APP_APPSTORE_ASSET_BRANCH=master diff --git a/Dockerfile b/Dockerfile index 8117c2f0..f7acc43d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN npm run build FROM nginx:latest COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /usr/src/app/build/ /usr/share/nginx/static/ -RUN mv /usr/share/nginx/static/frontend/index.html /usr/share/nginx/html/ +# RUN mv /usr/share/nginx/static/frontend/index.html /usr/share/nginx/html/ WORKDIR /usr/src/app COPY bin /usr/src/app/bin diff --git a/bin/create_index b/bin/create_index new file mode 100755 index 00000000..9030b542 --- /dev/null +++ b/bin/create_index @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +if [ -z "$1" ] + then + echo "Usage: create_index /path/to/build" + exit 1 +fi + +title="${META_TITLE:-HeLx UI}" +description="${META_DESCRIPTION:-HeLx UI}" + +cat "$1/index_template.html" | sed \ + -e "s/%META_TITLE%/$title/" \ + -e "s/%META_DESCRIPTION%/$description/" \ + > $1/index.html \ No newline at end of file diff --git a/bin/populate_env b/bin/populate_env index e88ed36e..d99de9b6 100755 --- a/bin/populate_env +++ b/bin/populate_env @@ -16,6 +16,8 @@ hidden_support_sections="${REACT_APP_HIDDEN_SUPPORT_SECTIONS}" hidden_result_tabs="${REACT_APP_HIDDEN_RESULT_TABS}" deployment_namespace="${REACT_APP_DEPLOYMENT_NAMESPACE}" appstore_asset_branch="${REACT_APP_APPSTORE_ASSET_BRANCH}" +meta_title="${META_TITLE:-HeLx UI}" +meta_description="${META_DESCRIPTION:-HeLx UI}" template='{ @@ -36,7 +38,11 @@ template='{ "hidden_support_sections": "%HIDDEN_SUPPORT_SECTIONS%", "hidden_result_tabs": "%HIDDEN_RESULT_TABS%", "deployment_namespace": "%DEPLOYMENT_NAMESPACE%", - "appstore_asset_branch": "%APPSTORE_ASSET_BRANCH%" + "appstore_asset_branch": "%APPSTORE_ASSET_BRANCH%", + "meta": { + "title": "%META_TITLE%", + "description": "%META_DESCRIPTION%" + } }' echo "$template" | sed \ @@ -50,4 +56,6 @@ echo "$template" | sed \ -e "s/%TRANQL_URL%/$tranql_url/" \ -e "s/%DEPLOYMENT_NAMESPACE%/$deployment_namespace/" \ -e "s/%APPSTORE_ASSET_BRANCH%/$appstore_asset_branch/" \ + -e "s/%META_TITLE%/$meta_title/" \ + -e "s/%META_DESCRIPTION%/$meta_description/" \ > $1 \ No newline at end of file diff --git a/bin/start_server b/bin/start_server index 010b8136..b9863566 100755 --- a/bin/start_server +++ b/bin/start_server @@ -6,5 +6,12 @@ if [ -z "$1" ] echo "Usage: start_server /path/to/output/env.json" exit 1 fi + +# Create env.json file populate_env $1 +# Generate the index.html file +create_index /usr/share/nginx/static/frontend +# Move into nginx html folder +mv /usr/share/nginx/static/frontend/index.html /usr/share/nginx/html/ + nginx -g "daemon off;" \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico index a11777cc..491d22e5 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.ejs b/public/index.ejs index f8a0b7dd..d0c576f1 100644 --- a/public/index.ejs +++ b/public/index.ejs @@ -2,19 +2,21 @@ - + " /> - + - + - HeLx UI + <%= isDevelopment ? "HeLx UI" : "%META_TITLE%" %> diff --git a/src/components/bouncing-dots.js b/src/components/bouncing-dots.js new file mode 100644 index 00000000..6f8f28e2 --- /dev/null +++ b/src/components/bouncing-dots.js @@ -0,0 +1,27 @@ +export const BouncingDots = ({ height=8, style={}, ...props }) => { + return ( + // Functionality pulled from antd mobile library + + + + + + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index 3cf44cf8..13cb1998 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -2,5 +2,6 @@ export * from './workspaces/navigation-tab-group' export * from './link' export * from './notifications' export * from './debounced-input' +export * from './bouncing-dots' export * from './side-collapse' -export * from './info-helpers' \ No newline at end of file +export * from './info-helpers' diff --git a/src/components/search/concept-card/concept-card.js b/src/components/search/concept-card/concept-card.js index b6080137..3eb34fad 100644 --- a/src/components/search/concept-card/concept-card.js +++ b/src/components/search/concept-card/concept-card.js @@ -1,11 +1,12 @@ import { Fragment, useState, useEffect, useMemo, forwardRef } from 'react' import PropTypes from 'prop-types' import { Badge, Card, Space, Typography } from 'antd' -import { ExpandOutlined as ViewIcon } from '@ant-design/icons' +import { ExpandOutlined as ViewIcon, LoadingOutlined } from '@ant-design/icons' import { useHelxSearch } from '../' import { OverviewTab } from './overview-tab' import { StudiesTab } from './studies-tab' // import { CdesTab } from './cdes-tab' +import { BouncingDots } from '../../' import { useAnalytics, useEnvironment } from '../../../contexts' import classNames from 'classnames' import './concept-card.css' @@ -23,11 +24,30 @@ export const ConceptCard = forwardRef(({ index, result, openModalHandler, icon=V const { context } = useEnvironment() const { query, fetchVariablesForConceptId, fetchCDEs } = useHelxSearch() + const studyTitle = useMemo(() => ( +
+ { !studies && ( + + ) } + Studies + { studies && ` (${ Object.keys(studies).length })` } +
+ ), [studies]) + const cdeTitle = useMemo(() => ( +
+ { !cdeStudies && ( + + ) } + CDEs + { cdeStudies && ` (${ Object.keys(cdeStudies).length })` } +
+ ), [cdeStudies]) + const tabs = useMemo(() => ({ - 'overview': { title: 'Overview', content: }, - 'studies': { title: `Studies`, content: }, - 'cdes': { title: `CDEs`, content: }, - }), [result, studies, cdeStudies]) + 'overview': { title: 'Overview', content: }, + 'studies': { title: studyTitle, content: }, + 'cdes': { title: cdeTitle, content: }, + }), [result, studies, cdeStudies, studyTitle, cdeTitle]) context.hidden_result_tabs.forEach((tab) => { delete tabs[tab] diff --git a/src/components/search/concept-modal/concept-modal.js b/src/components/search/concept-modal/concept-modal.js index 76e18f47..fd3ea6e5 100644 --- a/src/components/search/concept-modal/concept-modal.js +++ b/src/components/search/concept-modal/concept-modal.js @@ -13,6 +13,7 @@ import CustomIcon, { } from '@ant-design/icons' import { CdesTab, OverviewTab, StudiesTab, KnowledgeGraphsTab, TranQLTab } from './tabs' import { useHelxSearch } from '../' +import { BouncingDots } from '../../' import { useAnalytics, useEnvironment } from '../../../contexts' import { kgLink } from '../../../utils' import './concept-modal.css' @@ -84,13 +85,23 @@ export const ConceptModalBody = ({ result }) => { const fetchCdesTranqlController = useRef([]) const fetchKgsController = useRef() + const studyTitle = ( +
+ Studies { studies ? `(${ Object.keys(studies).length })` : } +
+ ) + const cdeTitle = ( +
+ CDEs { cdes ? `(${ Object.keys(cdes).length })` : } +
+ ) + const tabs = { - 'overview': { title: 'Overview', icon: , content: , }, - 'studies': { title: 'Studies', icon: , content: , }, - 'cdes': { title: `CDEs`, icon: , content: }, - 'kgs': { title: 'Knowledge Graphs', icon: , content: , }, - 'tranql': { title: 'TranQL', icon: , content: }, - // 'robokop': { title: 'Robokop', icon: , content: } + 'overview': { title: 'Overview', icon: , content: , }, + 'studies': { title: studyTitle, icon: , content: , }, + 'cdes': { title: cdeTitle, icon: , content: }, + 'kgs': { title: 'Knowledge Graphs', icon: , content: , }, + 'tranql': { title: 'TranQL', icon: , content: } } const links = { 'robokop' : { title: 'ROBOKOP', icon: , url: "https://robokop.renci.org/" } diff --git a/src/components/search/context.js b/src/components/search/context.js index e782c539..4c97a8d3 100644 --- a/src/components/search/context.js +++ b/src/components/search/context.js @@ -228,6 +228,7 @@ export const HelxSearch = ({ children }) => { useEffect(() => { setConceptPages({}) + setError({}) setTypeFilter(null) setSelectedResult(null) setVariableStudyResults([]) @@ -279,6 +280,7 @@ export const HelxSearch = ({ children }) => { } catch (error) { console.log(error) setError({ message: 'An error occurred!' }) + setTotalConcepts(0) setIsLoadingConcepts(false) analyticsEvents.searchExecuted(query, Date.now() - startTime, 0, error) } diff --git a/src/components/search/results/concepts-grid-layout/concept-search-results.js b/src/components/search/results/concepts-grid-layout/concept-search-results.js index a3a42d77..2c5aa48b 100644 --- a/src/components/search/results/concepts-grid-layout/concept-search-results.js +++ b/src/components/search/results/concepts-grid-layout/concept-search-results.js @@ -1,5 +1,5 @@ import React, { Fragment, useMemo, useCallback } from 'react' -import { Spin, Grid as AntGrid, Typography, Radio, Tooltip } from 'antd' +import { Spin, Grid as AntGrid, Typography, Radio, Tooltip, Empty } from 'antd' import { ConceptCard, useHelxSearch, SearchLayout, ExpandedResultsLayout } from '../../' import InfiniteScroll from 'react-infinite-scroll-component' import { BackTop } from '../../../layout' @@ -9,8 +9,8 @@ const { Text } = Typography const { useBreakpoint } = AntGrid export const ConceptSearchResults = () => { - const { query, conceptPages, perPage, currentPage, pageCount, typeFilter, isLoadingVariableResults, - isLoadingConcepts, error, layout, setCurrentPage, setSelectedResult } = useHelxSearch() + const { query, conceptPages, perPage, currentPage, pageCount, typeFilter, + isLoadingConcepts, error, setCurrentPage, setSelectedResult } = useHelxSearch() const { md } = useBreakpoint(); const concepts = useMemo(() => Object.values(conceptPages).flat(), [conceptPages]) const hasMore = useMemo(() => ( @@ -25,9 +25,17 @@ export const ConceptSearchResults = () => { return ( { - query && !error.message && ( + query && (
+ { error.message ? ( + { error.message } + ) : concepts.length === 0 && !isLoadingConcepts ? ( + No results were found for "{ query }" + } /> + // No results found + ) : null } {concepts.length > 0 && } { - const { error } = useHelxSearch() return ( - { error && { error.message } } ) diff --git a/src/components/search/results/expanded-results-layout/expanded-results-layout.js b/src/components/search/results/expanded-results-layout/expanded-results-layout.js index f349ab16..185856a9 100644 --- a/src/components/search/results/expanded-results-layout/expanded-results-layout.js +++ b/src/components/search/results/expanded-results-layout/expanded-results-layout.js @@ -1,5 +1,5 @@ import { Fragment, useEffect, useState } from 'react' -import { Divider, Grid, Spin } from 'antd' +import { Divider, Grid, Spin, Typography, Empty } from 'antd' import { SearchForm, useHelxSearch } from '../..' import { ExpandedResultsSidebar } from './expanded-results-sidebar' import { ExpandedResultsContent } from './expanded-results-content' @@ -7,10 +7,12 @@ import { ResultsHeader } from '..' import classNames from 'classnames' import './expanded-results-layout.css' + +const { Text } = Typography const { useBreakpoint } = Grid export const ExpandedResultsLayout = () => { - const { selectedResult, setSelectedResult, concepts, totalConcepts, isLoadingConcepts, query } = useHelxSearch() + const { selectedResult, setSelectedResult, concepts, totalConcepts, isLoadingConcepts, query, error } = useHelxSearch() const { md } = useBreakpoint() const [expanded, setExpanded] = useState(true) @@ -52,6 +54,14 @@ export const ExpandedResultsLayout = () => { {isLoadingConcepts && totalConcepts === 0 && ( )} + { error.message ? ( + { error.message } + ) : concepts.length === 0 && !isLoadingConcepts ? ( + No results were found for "{ query }" + } /> + // No results found + ) : null } {totalConcepts > 0 && ( {/* { md && } */} diff --git a/src/components/search/results/variable-view-layout/variable-results.css b/src/components/search/results/variable-view-layout/variable-results.css index eb230735..bb530f69 100644 --- a/src/components/search/results/variable-view-layout/variable-results.css +++ b/src/components/search/results/variable-view-layout/variable-results.css @@ -1,3 +1,12 @@ +.variable-histogram-collapse-panel > .ant-collapse-header { + padding: 0 !important; + align-items: center !important; + margin-bottom: 16px; +} +.variable-histogram-collapse-panel > .ant-collapse-content > .ant-collapse-content-box { + padding: 0 !important; +} + .histogram-startover-btn { margin-bottom: 10px; } diff --git a/src/components/search/results/variable-view-layout/variable-results.js b/src/components/search/results/variable-view-layout/variable-results.js index 2393587e..43075624 100644 --- a/src/components/search/results/variable-view-layout/variable-results.js +++ b/src/components/search/results/variable-view-layout/variable-results.js @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react' -import { Typography, Button, Space, Divider, Slider, Tooltip } from 'antd' +import React, { useEffect, useState, useRef, useMemo, useCallback, Fragment } from 'react' +import { Typography, Button, Space, Divider, Slider, Tooltip, Collapse } from 'antd' import { ArrowLeftOutlined, ArrowRightOutlined, InfoCircleFilled } from '@ant-design/icons' import { generate, presetPalettes, red, volcano, orange, gold, yellow, green, cyan, blue, geekblue, purple, magenta } from '@ant-design/colors' import { Column } from '@ant-design/plots'; @@ -14,6 +14,7 @@ import { useDebounce, useDebouncedCallback } from 'use-debounce' import './variable-results.css'; const { Text, Title } = Typography +const { Panel } = Collapse // Between 0-9 const COLOR_INTENSITY = 4 @@ -132,6 +133,7 @@ const HistogramLegend = ({ title: _title, items, style, ...props }) => { export const VariableSearchResults = () => { const { variableResults, variableStudyResults, totalVariableResults } = useHelxSearch() + const [collapseHistogram, setCollapseHistogram] = useState(false) const [page, setPage] = useState(0) /** filteredVariables holds the variables displayed in the histogram */ const [_filteredVariables, _setFilteredVariables] = useState([variableResults]) @@ -361,122 +363,137 @@ export const VariableSearchResults = () => { return (
- {/* The results header has a bottom margin of 16, so the divider shouldn't have a top margin. */} - -
- Variables according to Dug score - -
- Dug score -
-
    -
  • - This is the metric used by Dug to describe how relevant a result is -
  • -
  • - The score is calculated from how closely the search query matches information known about a result -
-
- } iconProps={{ style: { marginLeft: 8 } }} /> -
-
- { filteredVariables.length < totalVariableResults && ( -
- - Viewing {filteredVariables.length} variables within the {Math.floor(filteredPercentileLower)}-{Math.floor(filteredPercentileUpper)} percentiles - -
- ) } - -
-
- - result.score)) } - max={ Math.max(...variableResults.map((result) => result.score)) } - step={ null } - marks={ variableResults.reduce((acc, cur) => { - acc[cur.score] = { - label: cur.score, - style: { - display: "none" - } + setCollapseHistogram(!collapseHistogram) } + > + + {/* The results header has a bottom margin of 16, so the divider shouldn't have a top margin. */} + +
+ Variables according to Dug score + +
+ Dug score +
+
    +
  • + This is the metric used by Dug to describe how relevant a result is +
  • +
  • + The score is calculated from how closely the search query matches information known about a result +
+
} - return acc - }, {}) } - // Margin to align with the histogram - style={{ marginRight: 0, marginBottom: 4, marginTop: 16, flexGrow: 1 }} - className="histogram-slider" - /> + iconProps={{ style: { marginLeft: 8 } }} + trigger="hover" + /> +
+ + + }> + { filteredVariables.length < totalVariableResults && ( +
+ + Viewing {filteredVariables.length} variables within the {Math.floor(filteredPercentileLower)}-{Math.floor(filteredPercentileUpper)} percentiles +
- { - const [minScore, maxScore] = absScoreRange - const startRatio = Math.round(i / GRADIENT_CONSTITUENTS.length * 100) / 100 - const endRatio = Math.round((i + 1) / GRADIENT_CONSTITUENTS.length * 100) / 100 - const startScore = (startRatio * maxScore) - (startRatio * minScore) + minScore - const endScore = (endRatio * maxScore) - (endRatio * minScore) + minScore - const count = variableResults.filter((result) => result.score >= startScore && result.score <= endScore).length - const filteredCount = filteredVariables.filter((result) => result.score >= startScore && result.score <= endScore).length - return { - id: color, - name: { - name: `${ startRatio === 0 ? Math.floor(startScore) : Math.ceil(startScore) } - ${ endRatio === 1 ? Math.ceil(endScore) : Math.floor(endScore) }`, - style: filteredCount === 0 ? { color: "rgba(0, 0, 0, 0.25)" } : undefined - }, - description: { - description: filteredCount < count ? `(${ filteredCount } / ${ count } variables)` : `(${ count } variables)`, - style: filteredCount === 0 ? { color: "rgba(0, 0, 0, 0.25)" } : undefined - }, - marker: { - path: (w, h) => { - const x = 0 - const y = 0 - return `M ${ x },${ y } L ${ x + w }, ${ y } L ${ x + w }, ${ y + h } L ${ x }, ${ y + h } L ${ x },${ y } Z` + ) } + +
+
+ + result.score)) } + max={ Math.max(...variableResults.map((result) => result.score)) } + step={ null } + marks={ variableResults.reduce((acc, cur) => { + acc[cur.score] = { + label: cur.score, + style: { + display: "none" + } + } + return acc + }, {}) } + // Margin to align with the histogram + style={{ marginRight: 0, marginBottom: 4, marginTop: 16, flexGrow: 1 }} + className="histogram-slider" + /> +
+ { + const [minScore, maxScore] = absScoreRange + const startRatio = Math.round(i / GRADIENT_CONSTITUENTS.length * 100) / 100 + const endRatio = Math.round((i + 1) / GRADIENT_CONSTITUENTS.length * 100) / 100 + const startScore = (startRatio * maxScore) - (startRatio * minScore) + minScore + const endScore = (endRatio * maxScore) - (endRatio * minScore) + minScore + const count = variableResults.filter((result) => result.score >= startScore && result.score <= endScore).length + const filteredCount = filteredVariables.filter((result) => result.score >= startScore && result.score <= endScore).length + return { + id: color, + name: { + name: `${ startRatio === 0 ? Math.floor(startScore) : Math.ceil(startScore) } - ${ endRatio === 1 ? Math.ceil(endScore) : Math.floor(endScore) }`, + style: filteredCount === 0 ? { color: "rgba(0, 0, 0, 0.25)" } : undefined + }, + description: { + description: filteredCount < count ? `(${ filteredCount } / ${ count } variables)` : `(${ count } variables)`, + style: filteredCount === 0 ? { color: "rgba(0, 0, 0, 0.25)" } : undefined }, - // path: [["M", x - x_r, y - y_r], ["L", x + x_r, y - y_r], ["L", x + x_r, y + y_r], ["L", x - x_r, y + y_r], ["Z"]], - style: { - fill: filteredCount > 0 ? color : "rgba(0, 0, 0, 0.15)" + marker: { + path: (w, h) => { + const x = 0 + const y = 0 + return `M ${ x },${ y } L ${ x + w }, ${ y } L ${ x + w }, ${ y + h } L ${ x }, ${ y + h } L ${ x },${ y } Z` + }, + // path: [["M", x - x_r, y - y_r], ["L", x + x_r, y - y_r], ["L", x + x_r, y + y_r], ["L", x - x_r, y + y_r], ["Z"]], + style: { + fill: filteredCount > 0 ? color : "rgba(0, 0, 0, 0.15)" + } } } - } - }) } - style={{ marginLeft: 24, marginRight: 8, flexShrink: 0 }} - /> -
-
- - - - - - - -
+
+ + + + + - -
-
+ + + + +
+
+ + Studies { studyResultsForDisplay.length < variableStudyResults.length && (
diff --git a/src/components/search/results/variable-view-layout/variable-view-layout.js b/src/components/search/results/variable-view-layout/variable-view-layout.js index 953f5fe4..44e4c759 100644 --- a/src/components/search/results/variable-view-layout/variable-view-layout.js +++ b/src/components/search/results/variable-view-layout/variable-view-layout.js @@ -1,20 +1,29 @@ import { Fragment } from 'react' -import { Spin } from 'antd' +import { Spin, Typography, Empty } from 'antd' import { VariableSearchResults } from './' import { ResultsHeader } from '../' import { useHelxSearch, SearchForm } from '../../' +const { Text } = Typography + export const VariableViewLayout = () => { - const { error, totalVariableResults, isLoadingVariableResults } = useHelxSearch() + const { query, error, totalVariableResults, isLoadingVariableResults } = useHelxSearch() return ( - { error && { error.message } } { isLoadingVariableResults ? ( ) : ( + { error.message ? ( + { error.message } + ) : query && totalVariableResults === 0 ? ( + No results were found for "{ query }" + } /> + // No results found + ) : null } {totalVariableResults > 0 && } diff --git a/src/views/view.tsx b/src/views/view.tsx index 053197ac..bfc9d4a3 100644 --- a/src/views/view.tsx +++ b/src/views/view.tsx @@ -1,4 +1,5 @@ -import { ComponentType, useEffect } from 'react' +import { useEffect, useMemo } from 'react' +import { useEnvironment } from '../contexts' interface ViewProps { // Note: a null title is equivalent to an empty string or empty array, it will just display "HeLx UI" as the title. @@ -12,11 +13,15 @@ interface ViewProps { * The "HeLx UI" component is appended to the end of all titles, does not need to be included. */ export const useTitle = (title: string | string[] | null) => { + const { context } = useEnvironment() as any if (title === "") title = [] const titleSegments = Array.isArray(title) ? title : [title] + const websiteTitle = useMemo(() => { + return context?.meta.title ?? "HeLx UI" + }, [context]) useEffect(() => { - if (title !== null) document.title = [...titleSegments, "HeLx UI"].join(" · ") - }, [titleSegments]) + if (title !== null) document.title = [...titleSegments, websiteTitle].join(" · ") + }, [titleSegments, websiteTitle]) } // export function withView( diff --git a/webpack.base.config.js b/webpack.base.config.js index 879f64ca..1d7ec7a1 100644 --- a/webpack.base.config.js +++ b/webpack.base.config.js @@ -44,6 +44,52 @@ const createCopyPlugin = ({ ] }) ) +const createHtmlWebpackPlugin = ({ + isDevelopment +}) => { + /** + * In production, we have an extra step where we generate an index template + * which is then turned into our final index file using sed substitutions from + * environment variables. + * + * In development, we just want to directly generate an index.html. + */ + return new HtmlWebpackPlugin({ + template: path.resolve(paths.public, 'index.ejs'), + filename: isDevelopment ? "index.html" : "index_template.html", + templateParameters: { + isDevelopment, + publicUrl: '/static/frontend/' + }, + minify: { + html5: true, + collapseWhitespace: true, + minifyCSS: true, + minifyJS: true, + minifyURLs: true, + removeAttributeQuotes: false, + removeComments: true, + removeEmptyAttributes: true, + removeOptionalTags: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + useShortDoctype: true + } + }) +} +const createMiniCssExtractPlugin = ({ + isDevelopment +}) => ( + // CSS HMR doesn't work with contenthash + isDevelopment ? new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[id].css' + }) : new MiniCssExtractPlugin({ + filename: '[name].[contenthash].css', + chunkFilename: '[id].[contenthash].css' + }) +) const baseConfig = { entry: paths.entryPoint, output: { @@ -90,36 +136,10 @@ const baseConfig = { { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', - } + }, ] }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.resolve(paths.public, 'index.ejs'), - templateParameters: { - publicUrl: '/static/frontend/' - }, - minify: { - html5: true, - collapseWhitespace: true, - minifyCSS: true, - minifyJS: true, - minifyURLs: true, - removeAttributeQuotes: true, - removeComments: true, - removeEmptyAttributes: true, - removeOptionalTags: true, - removeRedundantAttributes: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributese: true, - useShortDoctype: true - } - }), - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - chunkFilename: '[id].[contenthash].css' - }) - ], + plugins: [], resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, @@ -134,5 +154,7 @@ module.exports = { paths, baseConfig, createBabelLoader, - createCopyPlugin + createCopyPlugin, + createHtmlWebpackPlugin, + createMiniCssExtractPlugin } \ No newline at end of file diff --git a/webpack.dev.config.js b/webpack.dev.config.js index d6f5059c..0f91ef45 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -1,11 +1,15 @@ -const express = require('express') const path = require('path') const webpack = require('webpack') const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin') const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin') const ESLintPlugin = require('eslint-webpack-plugin') const { merge } = require('webpack-merge') -const { baseConfig, paths, createBabelLoader, createCopyPlugin } = require('./webpack.base.config.js') +const { + baseConfig, paths, + createBabelLoader, createCopyPlugin, createHtmlWebpackPlugin, + createMiniCssExtractPlugin +} = require('./webpack.base.config.js') +const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = merge(baseConfig, { mode: 'development', @@ -34,6 +38,8 @@ module.exports = merge(baseConfig, { rules: [ createBabelLoader({ isDevelopment: true }) ] }, plugins: [ + createMiniCssExtractPlugin({ isDevelopment: true }), + createHtmlWebpackPlugin({ isDevelopment: true }), new ReactRefreshPlugin(), new ForkTsCheckerPlugin({ async: false diff --git a/webpack.prod.config.js b/webpack.prod.config.js index d1a5add7..5a8fab32 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -1,8 +1,13 @@ const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin') -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const { merge } = require('webpack-merge') -const { baseConfig, paths, createBabelLoader, createCopyPlugin } = require('./webpack.base.config.js'); +const { + baseConfig, paths, + createBabelLoader, createCopyPlugin, createHtmlWebpackPlugin, + createMiniCssExtractPlugin +} = require('./webpack.base.config.js') console.log("Creating optimized production build. This may take a while...") module.exports = merge(baseConfig, { @@ -19,6 +24,8 @@ module.exports = merge(baseConfig, { rules: [ createBabelLoader({ isDevelopment: false }) ] }, plugins: [ + createMiniCssExtractPlugin({ isDevelopment: false }), + createHtmlWebpackPlugin({ isDevelopment: false }), new webpack.EnvironmentPlugin({ 'NODE_ENV': 'production' }),