diff --git a/.gitignore b/.gitignore index a17e03c23e..6d1b11f7a5 100644 --- a/.gitignore +++ b/.gitignore @@ -84,4 +84,5 @@ certs /.phpunit.cache /CallGraph /PHP-SQL-Parser -/storage \ No newline at end of file +/storage +/public/build/ diff --git a/.htaccess b/.htaccess index 749a526e45..e541ce788e 100644 --- a/.htaccess +++ b/.htaccess @@ -9,9 +9,8 @@ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 #RewriteRule ^offline\.html$ /lib/View/offline.html [L] #RewriteCond %{REQUEST_URI} !/offline.html [NC] -#RewriteCond %{REQUEST_URI} !/public/build/runtime\.(.+)\.js [NC] -#RewriteCond %{REQUEST_URI} !/public/build/commonCss\.(.+)\.(css|js) [NC] -#RewriteCond %{REQUEST_URI} !/public/build/images/(.+)\.svg [NC] +#RewriteCond %{REQUEST_URI} !/public/build/(.+)\.js [NC] +#RewriteCond %{REQUEST_URI} !/public/build/assets/(.+)\.css [NC] #RewriteCond %{REQUEST_URI} !/img/meta/favicon(.+)\.svg [NC] #RewriteRule $ /offline.html [R=307,L] RewriteCond %{REQUEST_URI} /offline.html diff --git a/README.md b/README.md index 4af0700e47..2570faa725 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The following yarn commands are available for development and building: ### `yarn watch` -Watches for changes in the source files and automatically rebuilds the project in development mode. Useful for local development as it provides live updates. +Starts the Vite development server with Hot Module Replacement (HMR). Useful for local development as it provides instant updates without full page reloads. ### `yarn build:dev` diff --git a/babel.config.js b/babel.config.js index 74c94fb3ae..1f0772d89d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,8 +4,6 @@ module.exports = (api) => { switch (api.env()) { case 'test': return getBabelPresets('node') - case 'development': - return getBabelPresets('browser') default: throw new Error('babel.config.js :: environment not supported, yet.') diff --git a/check-circular-deps.js b/check-circular-deps.js index f340787974..afab4f88d3 100644 --- a/check-circular-deps.js +++ b/check-circular-deps.js @@ -1,30 +1,30 @@ #!/usr/bin/env node -const {execSync} = require('child_process') +const madge = require('madge') -let cycles -try { - const output = execSync( - 'npx madge --circular --extensions js --json public/js/', - {encoding: 'utf8', maxBuffer: 10 * 1024 * 1024}, - ) - cycles = JSON.parse(output) -} catch (e) { - if (e.stdout) { - cycles = JSON.parse(e.stdout) - } else { - console.error('Failed to run madge:', e.message) - process.exit(1) - } -} +madge('public/js/', { + fileExtensions: ['js'], + detectiveOptions: { + es6: { + skipAsyncImports: true, + }, + }, +}) + .then((res) => { + const cycles = res.circular() -if (cycles.length > 0) { - console.error( - `\x1b[31m✖ Found ${cycles.length} circular dependencies:\x1b[0m\n`, - ) - cycles.forEach((cycle, i) => { - console.error(` ${i + 1}) ${cycle.join(' → ')}`) + if (cycles.length > 0) { + console.error( + `\x1b[31m✖ Found ${cycles.length} circular dependencies:\x1b[0m\n`, + ) + cycles.forEach((cycle, i) => { + console.error(` ${i + 1}) ${cycle.join(' → ')}`) + }) + process.exit(1) + } else { + console.log('\x1b[32m✔ No circular dependencies\x1b[0m') + } + }) + .catch((err) => { + console.error('Failed to run madge:', err.message) + process.exit(1) }) - process.exit(1) -} else { - console.log('\x1b[32m✔ No circular dependencies\x1b[0m') -} diff --git a/docker b/docker index b0553a128b..1be999a369 160000 --- a/docker +++ b/docker @@ -1 +1 @@ -Subproject commit b0553a128b136e615216d555e49ba0d7882f5b8b +Subproject commit 1be999a369607ce44b33dfe760f56980fc69f93e diff --git a/lib/Controller/Abstracts/BaseKleinViewController.php b/lib/Controller/Abstracts/BaseKleinViewController.php index 474eb7c5ac..6aaf4d7996 100644 --- a/lib/Controller/Abstracts/BaseKleinViewController.php +++ b/lib/Controller/Abstracts/BaseKleinViewController.php @@ -20,6 +20,7 @@ use Utils\Templating\PHPTalMap; use Utils\Templating\PHPTALWithAppend; use Utils\Tools\Utils; +use Utils\Vite\ViteAssets; /** * Created by PhpStorm. @@ -67,9 +68,21 @@ public function __construct(Request $request, Response $response, ?ServiceProvid */ public function setView(string $template_name, array $params = [], int $code = 200): void { - $this->view = new PHPTALWithAppend(AppConfig::$TEMPLATE_ROOT . "/$template_name"); + $viteDevMode = ViteAssets::isDevMode(); + + if ( $viteDevMode ) { + $templatePath = AppConfig::$TEMPLATE_ROOT . "/templates/_$template_name"; + } else { + $templatePath = AppConfig::$TEMPLATE_ROOT . "/$template_name"; + } + + $this->view = new PHPTALWithAppend($templatePath); $this->httpCode = $code; + if ( $viteDevMode ) { + $this->view->setTemplateRepository( AppConfig::$TEMPLATE_ROOT ); + } + $this->view->{'basepath'} = AppConfig::$BASEURL; $this->view->{'hostpath'} = AppConfig::$HTTPHOST; $this->view->{'build_number'} = AppConfig::$BUILD_NUMBER; @@ -92,7 +105,18 @@ public function setView(string $template_name, array $params = [], int $code = 2 $this->view->{'footer_js'} = []; $this->view->{'config_js'} = []; - $this->view->{'css_resources'} = []; + + /** + * This is a unique ID generated at runtime. + * It is injected into the nonce attribute of `< script >` tags to allow browsers to safely execute the contained CSS and JavaScript. + */ + $nonce = Utils::uuid4(); + $this->view->{'x_nonce_unique_id'} = $nonce; + + $this->view->{'vite_html'} = ''; + if ( $viteDevMode ) { + $this->view->{'vite_html'} = ViteAssets::getHtml( $template_name, $nonce ?? '' ); + } // init oauth clients $this->view->{'googleAuthURL'} = (AppConfig::$GOOGLE_OAUTH_CLIENT_ID) ? OauthClient::getInstance(GoogleProvider::PROVIDER_NAME)->getAuthorizationUrl($_SESSION) : ""; @@ -107,11 +131,6 @@ public function setView(string $template_name, array $params = [], int $code = 2 AppConfig::$HTTPHOST . "/gdrive/oauth/response" )->getAuthorizationUrl($_SESSION, 'drive') : ""; - /** - * This is a unique ID generated at runtime. - * It is injected into the nonce attribute of `< script >` tags to allow browsers to safely execute the contained CSS and JavaScript. - */ - $this->view->{'x_nonce_unique_id'} = Utils::uuid4(); $this->view->{'x_self_ajax_location_hosts'} = AppConfig::$ENABLE_MULTI_DOMAIN_API ? " *.ajax." . parse_url(AppConfig::$HTTPHOST)['host'] : null; $this->addParamsToView($params); diff --git a/lib/Utils/Vite/ViteAssets.php b/lib/Utils/Vite/ViteAssets.php new file mode 100644 index 0000000000..88361263aa --- /dev/null +++ b/lib/Utils/Vite/ViteAssets.php @@ -0,0 +1,89 @@ +>|null */ + private static ?array $groups = null; + + public static function isDevMode(): bool { + if ( self::$devMode !== null ) { + return self::$devMode; + } + self::$devMode = !empty( $_ENV[ 'VITE_DEV' ] ) || !empty( $_SERVER[ 'VITE_DEV' ] ); + + return self::$devMode; + } + + /** + * @return array> + */ + private static function loadGroups(): array { + if ( self::$groups !== null ) { + return self::$groups; + } + + $path = AppConfig::$ROOT . '/' . self::GROUPS_PATH; + if ( !file_exists( $path ) ) { + self::$groups = []; + + return self::$groups; + } + + $raw = file_get_contents( $path ); + if ( $raw === false ) { + self::$groups = []; + + return self::$groups; + } + + self::$groups = json_decode( $raw, true ) ?: []; + + return self::$groups; + } + + public static function getHtml( string $templateName, string $nonce = '' ): string { + if ( !self::isDevMode() ) { + return ''; + } + + return self::buildDevHtml( $templateName, $nonce ); + } + + private static function buildDevHtml( string $templateName, string $nonce ): string { + $groups = self::loadGroups(); + $entries = $groups[ $templateName ] ?? []; + + if ( empty( $entries ) ) { + return ''; + } + + $host = AppConfig::$HTTPHOST; + $n = $nonce ? " nonce=\"{$nonce}\"" : ''; + $lines = []; + + $lines[] = ''; + + $lines[] = ''; + + foreach ( $entries as $entry ) { + $lines[] = ''; + } + + return implode( "\n", $lines ); + } + +} diff --git a/lib/View/templates/_manage.html b/lib/View/templates/_manage.html index e72fcd4698..55927e2ec3 100755 --- a/lib/View/templates/_manage.html +++ b/lib/View/templates/_manage.html @@ -39,9 +39,7 @@
-
-
Loading Projects
-
+
diff --git a/lib/View/templates/common.html b/lib/View/templates/common.html index ef63df133d..c583444778 100644 --- a/lib/View/templates/common.html +++ b/lib/View/templates/common.html @@ -46,8 +46,6 @@ - - - + ${structure vite_html} diff --git a/package.json b/package.json index 90458f9da0..ae8e53e9d0 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "lint": "eslint --ignore-path .gitignore .", "test": "jest --watchAll", "coverage": "jest --silent --coverage", - "watch": "sed -i \"s/version .*/version = \\\"v${npm_package_version}\\\"/g\" ./nodejs/config.ini && webpack --mode development --watch", - "build:dev": "sed -i \"s/version .*/version = \\\"v${npm_package_version}\\\"/g\" ./nodejs/config.ini && webpack --mode development", - "build:production": "webpack --mode production", + "watch": "vite", + "build:dev": "vite build --mode development", + "build:production": "vite build", "check:circular-deps": "node check-circular-deps.js", "prepare": "husky" }, @@ -26,6 +26,7 @@ "crypto-js": "^4.1.1", "diff-match-patch": "^1.0.5", "draft-js": "^0.11.4", + "events": "^3.3.0", "file-saver": "^2.0.5", "flux": "^4.0.4", "format-message": "^6.2.4", @@ -60,18 +61,16 @@ "devDependencies": { "@babel/core": "^7.28.5", "@babel/eslint-parser": "^7.28.5", - "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", - "@sentry/webpack-plugin": "4.9.1", + "@sentry/vite-plugin": "^5.2.0", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", - "babel-loader": "10.1.1", + "@vitejs/plugin-react": "^6.0.1", "baseline-browser-mapping": "^2.9.15", "broadcast-channel": "^7.1.0", - "css-loader": "7.1.4", "eslint": "^8.0.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^29.0.0", @@ -81,7 +80,6 @@ "eslint-plugin-testing-library": "^7.0.0", "fs-extra": "^11.1.1", "glob": "^11.1.0", - "html-webpack-plugin": "5.6.6", "husky": ">=6", "ini": "^5.0.0", "jest": "^30.0.0", @@ -89,20 +87,12 @@ "jest-transform-css": "^6.0.1", "lint-staged": ">=10", "madge": "^8.0.0", - "mini-css-extract-plugin": "^2.7.6", "msw": "^2.7.3", "prettier": "^3.0.0", "sass": "^1.66.0", - "sass-loader": "16.0.7", - "style-loader": "4.0.0", - "terser-webpack-plugin": "^5.3.11", - "thread-loader": "^4.0.4", "undici": "^5.28.2", + "vite": "^8.0.9", "web-streams-polyfill": "^4.1.0", - "webpack": "5.104.1", - "webpack-bundle-analyzer": "^4.10.2", - "webpack-cli": "6.0.1", - "webpack-concat-files-plugin": "^0.5.2", "whatwg-fetch": "^3.6.20" }, "resolutions": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 96a5223869..4b43154aa6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6673,7 +6673,7 @@ parameters: path: lib/Controller/Abstracts/BaseKleinViewController.php - - message: '#^Access to an undefined property Utils\\Templating\\PHPTALWithAppend\:\:\$css_resources\.$#' + message: '#^Access to an undefined property Utils\\Templating\\PHPTALWithAppend\:\:\$vite_html\.$#' identifier: property.notFound count: 1 path: lib/Controller/Abstracts/BaseKleinViewController.php diff --git a/plugins/translated b/plugins/translated index c957a29a76..ff0e0f8055 160000 --- a/plugins/translated +++ b/plugins/translated @@ -1 +1 @@ -Subproject commit c957a29a768561735bdec0276d7fce45aeedcecd +Subproject commit ff0e0f8055c2d0002a6282e6d31262c1f3d9f1bf diff --git a/public/css/sass/components/common/Button.scss b/public/css/sass/components/common/Button.scss index 431fdb6e71..589abda59a 100644 --- a/public/css/sass/components/common/Button.scss +++ b/public/css/sass/components/common/Button.scss @@ -56,7 +56,7 @@ a.button-component-container { box-shadow: inset 0 0 0 1px var(--btnBorderColor); color: var(--btnAltTextColor); - &:not(:disabled):global(.button--active) { + &:not(:disabled).button--active { box-shadow: inset 0 0 0 1px var(--btnBorderColorActive); background-color: var(--btnBgColorSemitransAlt); } @@ -80,8 +80,8 @@ a.button-component-container { &.ghost { color: var(--btnAltTextColor); - &:not(:disabled):global(.button--active), - &:not(:disabled):global(.button--active):hover { + &:not(:disabled).button--active, + &:not(:disabled).button--active:hover { background-color: var(--btnBgColorSemitransAlt); color: var(--btnAltTextColor); } @@ -99,7 +99,7 @@ a.button-component-container { &.link { color: var(--btnAltTextColor); - &:not(:disabled):global(.button--active) { + &:not(:disabled).button--active { color: colors.$grey8; } diff --git a/public/css/sass/mbc-style.scss b/public/css/sass/mbc-style.scss index e4d7340f14..1ff224b620 100644 --- a/public/css/sass/mbc-style.scss +++ b/public/css/sass/mbc-style.scss @@ -620,9 +620,6 @@ Close icon ballon - Right panel Clearfix helpers ****** */ -.mbc-clearfix { - *zoom: 1; -} .mbc-clearfix:before, .mbc-clearfix:after { display: table; diff --git a/public/js/actions/SegmentActions.js b/public/js/actions/SegmentActions.js index fdb48821b9..64c49e8c79 100644 --- a/public/js/actions/SegmentActions.js +++ b/public/js/actions/SegmentActions.js @@ -62,19 +62,25 @@ import {getTranslationMismatches as getTranslationMismatchesApi} from '../api/ge import TextUtils from '../utils/textUtils' import {TAB} from '../constants/SegmentTabConstants' -// Lazy-loaded to break circular dependencies -// Using require() instead of import so madge's ES6 detective doesn't -// register these as static edges — webpack still resolves them correctly -// at call time. +// Async-loaded to break circular dependency for static analysis. let _SegmentsFilterUtil let _SetTranslationUtil -const getSegmentsFilterUtil = () => - _SegmentsFilterUtil || - (_SegmentsFilterUtil = - require('../components/header/cattol/segment_filter/segment_filter').default) -const getSetTranslationUtil = () => - _SetTranslationUtil || - (_SetTranslationUtil = require('../setTranslationUtil')) +import( + '../components/header/cattol/segment_filter/segment_filter' +).then((m) => { + _SegmentsFilterUtil = m.default +}) +import('../setTranslationUtil').then((m) => { + _SetTranslationUtil = m +}) +const getSegmentsFilterUtil = () => { + if (!_SegmentsFilterUtil) throw new Error('[SegmentActions] SegmentsFilterUtil not loaded yet') + return _SegmentsFilterUtil +} +const getSetTranslationUtil = () => { + if (!_SetTranslationUtil) throw new Error('[SegmentActions] SetTranslationUtil not loaded yet') + return _SetTranslationUtil +} const SegmentActions = { localStorageCommentsClosed: diff --git a/public/js/components/segments/SegmentButtons.js b/public/js/components/segments/SegmentButtons.js index 4ed397b1af..524c6f8530 100644 --- a/public/js/components/segments/SegmentButtons.js +++ b/public/js/components/segments/SegmentButtons.js @@ -189,7 +189,7 @@ export const SegmentButton = ({segment, disabled, isReview}) => { title="Revise and go to next translated" > {' '} - A+>> + {'A+>>'}

{isMac ? 'CMD' : 'CTRL'} @@ -299,7 +299,7 @@ export const SegmentButton = ({segment, disabled, isReview}) => { title="Translate and go to next untranslated" > {' '} - T+>> + {'T+>>'}

{isMac ? 'CMD' : 'CTRL'} @@ -325,7 +325,7 @@ export const SegmentButton = ({segment, disabled, isReview}) => { data-segmentid={'segment-' + segment.sid} title="Translate and go to next repetition" > - REP > + {'REP >'}

  • @@ -336,7 +336,7 @@ export const SegmentButton = ({segment, disabled, isReview}) => { data-segmentid={'segment-' + segment.sid} title="Translate and go to next repetition group" > - REP >> + {'REP >>'}
  • diff --git a/public/js/components/segments/SegmentTargetToolbar.js b/public/js/components/segments/SegmentTargetToolbar.js index 13f996a93b..92116afa00 100644 --- a/public/js/components/segments/SegmentTargetToolbar.js +++ b/public/js/components/segments/SegmentTargetToolbar.js @@ -35,10 +35,11 @@ export const SegmentTargetToolbar = ({ const [isIconsBundled, setIsIconsBundled] = useState(false) const getIconButton = (props) => { - const {children, ...rest} = props + const {children, key, ...rest} = props return (