Migrate from webpack to vite (#37002)
Replace webpack with Vite 8 as the frontend bundler. Frontend build is around 3-4 times faster than before. Will work on all platforms including riscv64 (via wasm). `iife.js` is a classic render-blocking script in `<head>` (handles web components/early DOM setup). `index.js` is loaded as a `type="module"` script in the footer. All other JS chunks are also module scripts (supported in all browsers since 2018). Entry filenames are content-hashed (e.g. `index.C6Z2MRVQ.js`) and resolved at runtime via the Vite manifest, eliminating the `?v=` cache busting (which was unreliable in some scenarios like vscode dev build). Replaces: https://github.com/go-gitea/gitea/pull/36896 Fixes: https://github.com/go-gitea/gitea/issues/17793 Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import {showGlobalErrorMessage, shouldIgnoreError} from './errors.ts';
|
||||
|
||||
test('showGlobalErrorMessage', () => {
|
||||
document.body.innerHTML = '<div class="page-content"></div>';
|
||||
showGlobalErrorMessage('test msg 1');
|
||||
showGlobalErrorMessage('test msg 2');
|
||||
showGlobalErrorMessage('test msg 1'); // duplicated
|
||||
|
||||
expect(document.body.innerHTML).toContain('>test msg 1 (2)<');
|
||||
expect(document.body.innerHTML).toContain('>test msg 2<');
|
||||
expect(document.querySelectorAll('.js-global-error').length).toEqual(2);
|
||||
});
|
||||
|
||||
test('shouldIgnoreError', () => {
|
||||
for (const url of [
|
||||
'https://gitea.test/assets/js/monaco.D14TzjS9.js',
|
||||
'https://gitea.test/assets/js/editor.api2.BdhK7zNg.js',
|
||||
'https://gitea.test/assets/js/editor.worker.BYgvyFya.js',
|
||||
]) {
|
||||
const err = new Error('test');
|
||||
err.stack = `Error: test\n at ${url}:1:1`;
|
||||
expect(shouldIgnoreError(err)).toEqual(true);
|
||||
}
|
||||
|
||||
const otherError = new Error('test');
|
||||
otherError.stack = 'Error: test\n at https://gitea.test/assets/js/index.js:1:1';
|
||||
expect(shouldIgnoreError(otherError)).toEqual(false);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
// keep this file lightweight, it's imported into IIFE chunk in bootstrap
|
||||
import {html} from '../utils/html.ts';
|
||||
import type {Intent} from '../types.ts';
|
||||
|
||||
export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
|
||||
const msgContainer = document.querySelector('.page-content') ?? document.body;
|
||||
if (!msgContainer) {
|
||||
alert(`${msgType}: ${msg}`);
|
||||
return;
|
||||
}
|
||||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
|
||||
let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
|
||||
if (!msgDiv) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`;
|
||||
msgDiv = el.childNodes[0] as HTMLDivElement;
|
||||
}
|
||||
// merge duplicated messages into "the message (count)" format
|
||||
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
|
||||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
|
||||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
|
||||
msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
|
||||
msgContainer.prepend(msgDiv);
|
||||
}
|
||||
|
||||
export function shouldIgnoreError(err: Error) {
|
||||
const ignorePatterns: Array<RegExp> = [
|
||||
// https://github.com/go-gitea/gitea/issues/30861
|
||||
// https://github.com/microsoft/monaco-editor/issues/4496
|
||||
// https://github.com/microsoft/monaco-editor/issues/4679
|
||||
/\/assets\/js\/.*(monaco|editor\.(api|worker))/,
|
||||
];
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (pattern.test(err.stack ?? '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
|
||||
const err = error ?? reason;
|
||||
const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin));
|
||||
const {runModeIsProd} = window.config ?? {};
|
||||
|
||||
// `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a
|
||||
// non-critical event from the browser. We log them but don't show them to users. Examples:
|
||||
// - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
|
||||
// - https://github.com/mozilla-mobile/firefox-ios/issues/10817
|
||||
// - https://github.com/go-gitea/gitea/issues/20240
|
||||
if (!err) {
|
||||
if (message) console.error(new Error(message));
|
||||
if (runModeIsProd) return;
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
// If the error stack trace does not include the base URL of our script assets, it likely came
|
||||
// from a browser extension or inline script. Do not show such errors in production.
|
||||
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
|
||||
// Ignore some known errors that are unable to fix
|
||||
if (shouldIgnoreError(err)) return;
|
||||
}
|
||||
|
||||
let msg = err?.message ?? message;
|
||||
if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
|
||||
const dot = msg.endsWith('.') ? '' : '.';
|
||||
const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
|
||||
showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {initAriaCheckboxPatch} from './fomantic/checkbox.ts';
|
||||
import {initAriaFormFieldPatch} from './fomantic/form.ts';
|
||||
import {initAriaDropdownPatch} from './fomantic/dropdown.ts';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {generateElemId} from '../../utils/dom.ts';
|
||||
|
||||
export function linkLabelAndInput(label: Element, input: Element) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {queryElemChildren} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticDimmer() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
import {generateElemId, queryElems} from '../../utils/dom.ts';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
import {queryElems} from '../../utils/dom.ts';
|
||||
import {hideToastsFrom} from '../toast.ts';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {queryElemSiblings} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticTab() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
export function initFomanticTransition() {
|
||||
const transitionNopBehaviors = new Set([
|
||||
'clear queue', 'stop', 'stop all', 'destroy',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
|
||||
window.MonacoEnvironment = {
|
||||
getWorker(_: string, label: string) {
|
||||
if (label === 'json') return new jsonWorker();
|
||||
if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker();
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker();
|
||||
if (label === 'typescript' || label === 'javascript') return new tsWorker();
|
||||
return new editorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
export * from 'monaco-editor';
|
||||
@@ -3,7 +3,7 @@ import type SortableType from 'sortablejs';
|
||||
|
||||
export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
|
||||
// type reassigned because typescript derives the wrong type from this import
|
||||
const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType});
|
||||
const {Sortable} = (await import('sortablejs') as unknown as {Sortable: typeof SortableType});
|
||||
|
||||
return new Sortable(el, {
|
||||
animation: 150,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const {appSubUrl, assetVersionEncoded} = window.config;
|
||||
const {appSubUrl, sharedWorkerUri} = window.config;
|
||||
|
||||
export class UserEventsSharedWorker {
|
||||
sharedWorker: SharedWorker;
|
||||
|
||||
// options can be either a string (the debug name of the worker) or an object of type WorkerOptions
|
||||
constructor(options?: string | WorkerOptions) {
|
||||
const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options);
|
||||
const worker = new SharedWorker(sharedWorkerUri, options);
|
||||
this.sharedWorker = worker;
|
||||
worker.addEventListener('error', (event) => {
|
||||
console.error('worker error', event);
|
||||
|
||||
Reference in New Issue
Block a user