diff --git a/.github/workflows/build-binary-for-release.yml b/.github/workflows/build-binary-for-release.yml index 2bce67142..76f1c9530 100644 --- a/.github/workflows/build-binary-for-release.yml +++ b/.github/workflows/build-binary-for-release.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: 20.18.1 + node-version: 20.19.0 - name: Node Build run: make install-ui-packages ui diff --git a/.gitignore b/.gitignore index ba66f51a0..d5aed0da5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,8 @@ /go.work* /logs /ui/node_modules -/ui/build/*/*/* -/ui/build/*.json -/ui/build/*.html -/ui/build/*.txt +/ui/build/* +!/ui/build/favicon.ico /vendor Thumbs*.db tmp diff --git a/ui/.env.development b/ui/.env.development index a634cee2e..3e28eec99 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1,2 +1 @@ -PUBLIC_URL REACT_APP_API_URL = http://10.0.20.84:8080/ diff --git a/ui/.env.production b/ui/.env.production index f86b9ccdd..192714cf7 100644 --- a/ui/.env.production +++ b/ui/.env.production @@ -1,6 +1,4 @@ -TSC_COMPILE_ON_ERROR=true -ESLINT_NO_DEV_ERRORS=true -PUBLIC_URL=/ +REACT_APP_PUBLIC_URL=/ REACT_APP_API_URL=/ REACT_APP_BASE_URL= REACT_APP_API_BASE_URL= diff --git a/ui/.eslintignore b/ui/.eslintignore index 1e6d1c5cd..0e9877734 100644 --- a/ui/.eslintignore +++ b/ui/.eslintignore @@ -1,5 +1,4 @@ public -config-overrides.js commitlint.config.js build .eslintrc.js diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 1d9052600..bd81c2d6b 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -24,7 +24,6 @@ module.exports = { es2021: true, }, extends: [ - 'react-app/jest', 'plugin:react/recommended', 'airbnb', 'airbnb-typescript', diff --git a/ui/.gitignore b/ui/.gitignore index 3b1e96bd4..653e95a4e 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -10,10 +10,8 @@ node_modules # production -/build/*/*/* -/build/*.json -/build/*.html -/build/*.txt +/build/* +!/build/favicon.ico # misc .DS_Store diff --git a/ui/config-overrides.js b/ui/config-overrides.js deleted file mode 100644 index 7d62b1d8e..000000000 --- a/ui/config-overrides.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { - addWebpackModuleRule, - addWebpackAlias, - setWebpackOptimizationSplitChunks, - addWebpackPlugin, -} = require("customize-cra"); -const webpack = require('webpack'); - -const path = require("path"); -const i18nPath = path.resolve(__dirname, "../i18n"); - -module.exports = { - webpack: function(config, env) { - addWebpackAlias({ - "@": path.resolve(__dirname, "src"), - "@i18n": i18nPath, - buffer: 'buffer', - })(config); - - addWebpackModuleRule({ - test: /\.ya?ml$/, - use: "yaml-loader" - })(config); - - addWebpackPlugin( - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }) - )(config); - - setWebpackOptimizationSplitChunks({ - maxInitialRequests: 20, - minSize: 20 * 1024, - minChunks: 2, - cacheGroups: { - automaticNamePrefix: 'chunk', - mix1: { - test: (module, chunks) => { - return ( - module.resource && - (module.resource.includes('components') || - /\/node_modules\/react-bootstrap\//.test(module.resource)) - ); - }, - name: 'chunk-mix1', - filename: 'static/js/[name].[contenthash:8].chunk.js', - priority: 14, - reuseExistingChunk: true, - minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, - chunks: 'initial', - }, - mix2: { - name: 'chunk-mix2', - test: /[\/]node_modules[\/](i18next|lodash|marked|next-share)[\/]/, - filename: 'static/js/[name].[contenthash:8].chunk.js', - priority: 13, - reuseExistingChunk: true, - minChunks: 1, - chunks: 'initial', - }, - mix3: { - name: 'chunk-mix3', - test: /[\/]node_modules[\/](@remix-run|@restart|axios|diff)[\/]/, - filename: 'static/js/[name].[contenthash:8].chunk.js', - priority: 12, - reuseExistingChunk: true, - minChunks: 1, - chunks: 'initial', - }, - codemirror: { - name: 'codemirror', - test: /[\/]node_modules[\/](\@codemirror)[\/]/, - priority: 10, - reuseExistingChunk: true, - minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, - chunks: 'initial', - enforce: true, - }, - lezer: { - name: 'lezer', - test: /[\/]node_modules[\/](\@lezer)[\/]/, - priority: 9, - reuseExistingChunk: true, - minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, - chunks: 'initial', - enforce: true, - }, - reactDom: { - name: 'react-dom', - test: /[\/]node_modules[\/](react-dom)[\/]/, - filename: 'static/js/[name].[contenthash:8].chunk.js', - priority: 8, - reuseExistingChunk: true, - chunks: 'all', - enforce: true, - }, - nodesInitial: { - name: 'chunk-nodesInitial', - filename: 'static/js/[name].[contenthash:8].chunk.js', - test: /[\/]node_modules[\/]/, - priority: 1, - minChunks: 1, - chunks: 'initial', - reuseExistingChunk: true, - }, - }, - })(config); - - // add i18n dir to ModuleScopePlugin allowedPaths - const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); - if (moduleScopePlugin) { - moduleScopePlugin.allowedPaths.push(i18nPath); - } - - return config; - }, - devServer: function(configFunction) { - return function(proxy, allowedHost) { - const config = configFunction(proxy, allowedHost); - config.proxy = [ - { - context: ['/answer', '/installation'], - target: process.env.REACT_APP_API_URL, - changeOrigin: true, - secure: false, - }, - { - context: ['/custom.css'], - target: process.env.REACT_APP_API_URL, - } - ]; - return config; - }; - } -}; diff --git a/ui/public/index.html b/ui/index.html similarity index 97% rename from ui/public/index.html rename to ui/index.html index 5bca47e40..af48ece0b 100644 --- a/ui/public/index.html +++ b/ui/index.html @@ -25,7 +25,7 @@ - +
@@ -86,6 +86,7 @@ + + {{end}} {{if $.siteinfo.JsonLD }}{{ .siteinfo.JsonLD | templateHTML}}{{end}} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 648dd0253..02d23f751 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -33,7 +33,6 @@ }, "include": [ "src", - "node_modules/@testing-library/jest-dom", "scripts" ], "exclude": [ diff --git a/ui/vite.config.mts b/ui/vite.config.mts new file mode 100644 index 000000000..90f47e78d --- /dev/null +++ b/ui/vite.config.mts @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import path from 'path'; +import { fileURLToPath } from 'url'; + +import react from '@vitejs/plugin-react'; +import yaml from '@modyfi/vite-plugin-yaml'; +import { CORE_SCHEMA } from 'js-yaml'; +import { defineConfig, loadEnv } from 'vite'; + +// This file is loaded as a real ES module, where __dirname does not exist. +const rootDir = path.dirname(fileURLToPath(import.meta.url)); +const i18nDir = path.resolve(rootDir, '../i18n'); + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, rootDir, 'REACT_APP_'); + + // configs/config.yaml ui.public_url, as written by scripts/env.js, may or + // may not already carry a trailing slash (the root value is exactly "/"). + // Vite requires base to end with one, so add it only when missing rather + // than concatenating blindly and risking "//". + // + // Vite keeps an absolute external base (e.g. a CDN URL) exactly as given + // only for `vite build`. `vite dev` and `vite preview` reduce the same + // base to its bare pathname, dropping the scheme and host. That split is + // intentional here, not a bug to unify: those two commands only serve + // this app locally, and the Go server only ever embeds `vite build`'s + // output, so the reduction never reaches anything a real deployment + // serves. + const publicUrl = env.REACT_APP_PUBLIC_URL || '/'; + const base = publicUrl.endsWith('/') ? publicUrl : `${publicUrl}/`; + + return { + // The previous yaml-loader (yaml@2.6.1 core schema) kept bare dates as + // strings and left merge keys unresolved. @modyfi/vite-plugin-yaml + // defaults to js-yaml's DEFAULT_SCHEMA, which resolves bare YYYY-MM-DD + // scalars to JS Date objects and enables merge keys. Pin CORE_SCHEMA so + // yaml imports keep parsing the way they did before the migration. + plugins: [react(), yaml({ schema: CORE_SCHEMA })], + + css: { + preprocessorOptions: { + // bootstrap 5.3.3's own scss internals emit dozens of deprecation + // warnings (color functions, mixed-decls) on every build. They are + // unactionable here and bury warnings that point at our own code. + scss: { quietDeps: true }, + }, + }, + + // scripts/env.js generates .env.production from the server's own + // configs/config.yaml using REACT_APP_ names. Reading that prefix keeps the + // generator as the single source of truth for both sides. + envPrefix: 'REACT_APP_', + + base, + + resolve: { + alias: { + '@': path.resolve(rootDir, 'src'), + '@i18n': i18nDir, + }, + }, + + build: { + // ui/static.go embeds this directory, and internal/router/ui.go serves + // /static from it. Neither path is configurable from here. + outDir: 'build', + assetsDir: 'static', + // Matches the previous build so before/after size comparisons measure the + // bundler rather than a change of sourcemap setting. + sourcemap: true, + rollupOptions: { + output: { + // Keep emitted files grouped under static/js, static/css and + // static/media. The analyze script globs that layout, and a flat + // static/ directory silently matches nothing. + entryFileNames: 'static/js/[name].[hash].js', + chunkFileNames: 'static/js/[name].[hash].chunk.js', + assetFileNames: (assetInfo) => { + const name = assetInfo.names?.[0] ?? ''; + if (name.endsWith('.css')) { + return 'static/css/[name].[hash][extname]'; + } + return 'static/media/[name].[hash][extname]'; + }, + }, + }, + }, + + server: { + port: 3000, + proxy: { + '/answer': { + target: env.REACT_APP_API_URL, + changeOrigin: true, + secure: false, + }, + '/installation': { + target: env.REACT_APP_API_URL, + changeOrigin: true, + secure: false, + }, + '/custom.css': { + target: env.REACT_APP_API_URL, + }, + }, + fs: { + // Languages live outside this root and are loaded through @i18n. + allow: [rootDir, i18nDir], + }, + }, + }; +});