diff --git a/package.json b/package.json index 7ef843259..dabb4c9b6 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/autocannon": "^7.9.0", "@types/cross-spawn": "^6.0.2", "@types/mocha": "^9.1.1", - "@types/node": "^18.8.4", + "@types/node": "^18.11.0", "@types/semver": "^7.3.12", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.40.0", @@ -56,7 +56,7 @@ "fs-extra": "^10.1.0", "globby": "11.1.0", "latest-version": "7.0.0", - "mocha": "^10.0.0", + "mocha": "^10.1.0", "mongodb": "^3.7.3", "mongodb-memory-server": "^8.9.3", "nyc": "^15.1.0", diff --git a/packages/hydrooj/src/model/schedule.ts b/packages/hydrooj/src/model/schedule.ts index 39534aae9..41611f956 100644 --- a/packages/hydrooj/src/model/schedule.ts +++ b/packages/hydrooj/src/model/schedule.ts @@ -73,7 +73,7 @@ class WorkerService extends Service { sleep(300000), ]); const spent = Date.now() - start; - if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc); + if (spent > 500) logger.warn('Slow worker task (%d ms): %o', spent, doc); } catch (e) { logger.error('Worker task fail: ', e); logger.error('%o', doc); diff --git a/packages/ui-default/build/config/webpack.ts b/packages/ui-default/build/config/webpack.ts index d2215ec33..8d3bea96a 100644 --- a/packages/ui-default/build/config/webpack.ts +++ b/packages/ui-default/build/config/webpack.ts @@ -9,6 +9,7 @@ import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; import { dirname } from 'path'; import webpack from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; import WebpackBar from 'webpackbar'; import root from '../utils/root'; @@ -18,7 +19,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: loader: 'esbuild-loader', options: { loader: 'tsx', - target: 'es2015', + target: 'chrome80', sourcemap: true, }, }; @@ -220,7 +221,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: minifyIdentifiers: true, treeShaking: true, target: [ - 'chrome60', + 'chrome80', ], exclude: [/mathmaps/, /\.min\.js$/], })], @@ -240,8 +241,9 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: new ExtractCssPlugin({ filename: '[name].css?[fullhash:6]', }), + new WebpackManifestPlugin({}), new FriendlyErrorsPlugin(), - new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs)/ }), + new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }), new CopyWebpackPlugin({ patterns: [ { from: root('static') }, diff --git a/packages/ui-default/components/zipDownloader/index.js b/packages/ui-default/components/zipDownloader/index.js index c7bdc391a..e75ab9d31 100644 --- a/packages/ui-default/components/zipDownloader/index.js +++ b/packages/ui-default/components/zipDownloader/index.js @@ -49,7 +49,7 @@ export default async function download(filename, targets) { } catch (e) { window.captureException?.(e); stopDownload(); - Notification.error(i18n('Download Error', [target.filename, e.toString()])); + Notification.error(i18n('Download Error: {0} {1}', [target.filename, e.toString()])); } return {}; } diff --git a/packages/ui-default/index.ts b/packages/ui-default/index.ts index b3be7557d..00d3504a2 100644 --- a/packages/ui-default/index.ts +++ b/packages/ui-default/index.ts @@ -161,6 +161,14 @@ class LanguageHandler extends ResourceHandler { } } +class SWConfigHandler extends ResourceHandler { + async get() { + this.response.body = { + preload: SystemModel.get('ui-default.preload'), + }; + } +} + class SystemConfigSchemaHandler extends Handler { async get() { const schema = convert(Schema.intersect(SystemSettings) as any, true); @@ -227,6 +235,7 @@ export function apply(ctx: Context) { ctx.Route('markdown', '/markdown', MarkdownHandler); ctx.Route('config_schema', '/manage/config/schema.json', SystemConfigSchemaHandler, PRIV.PRIV_EDIT_SYSTEM); ctx.Route('lang', '/l/:lang', LanguageHandler); + ctx.Route('sw_config', '/sw-config', SWConfigHandler); ctx.Route('media', '/media', RichMediaHandler); ctx.on('app/started', buildUI); ctx.on('app/started', updateLogo); diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 4394d997b..8a840d26f 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.41.18", + "version": "4.42.0", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", @@ -14,14 +14,14 @@ "@blueprintjs/select": "4.8.3", "@fontsource/dm-mono": "^4.5.10", "@fontsource/fira-code": "^4.5.12", - "@fontsource/inconsolata": "^4.5.8", + "@fontsource/inconsolata": "^4.5.9", "@fontsource/jetbrains-mono": "^4.5.11", "@fontsource/pt-mono": "^4.5.11", "@fontsource/roboto-mono": "^4.5.8", "@fontsource/source-code-pro": "^4.5.12", "@fontsource/ubuntu-mono": "^4.5.11", "@hydrooj/utils": "workspace:*", - "@svgr/webpack": "^6.4.0", + "@svgr/webpack": "^6.5.0", "@types/gulp-if": "^0.0.34", "@types/jquery": "^3.5.14", "@types/json-schema": "^7.0.11", @@ -37,7 +37,7 @@ "@vscode/codicons": "^0.0.32", "autoprefixer": "^10.4.12", "browser-update": "^3.3.40", - "chalk": "^5.1.0", + "chalk": "^5.1.2", "classnames": "^2.3.2", "clean-webpack-plugin": "^4.0.0", "clipboard": "^2.0.11", @@ -65,10 +65,10 @@ "matchmedia-polyfill": "^0.3.2", "mini-css-extract-plugin": "^2.6.1", "moment": "^2.29.4", - "monaco-editor": "^0.34.0", + "monaco-editor": "^0.34.1", "monaco-editor-nls": "^2.0.0", "monaco-editor-webpack-plugin": "^7.0.1", - "monaco-graphql": "^1.1.2", + "monaco-graphql": "^1.1.3", "monaco-themes": "^0.4.2", "monaco-yaml": "^4.0.2", "nanoid": "^4.0.0", @@ -112,6 +112,7 @@ "webpack": "^5.74.0", "webpack-bundle-analyzer": "^4.6.1", "webpack-dev-server": "^4.11.1", + "webpack-manifest-plugin": "^5.0.0", "webpackbar": "^5.0.2" }, "dependencies": { diff --git a/packages/ui-default/service-worker.ts b/packages/ui-default/service-worker.ts index 4f3783bbb..5b471bf1d 100644 --- a/packages/ui-default/service-worker.ts +++ b/packages/ui-default/service-worker.ts @@ -17,3 +17,87 @@ self.addEventListener('notificationclick', (event) => { return null; })); }); + +const PRECACHE = `precache-${process.env.VERSION}`; +const DO_NOT_CACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco']; + +function shouldCache(name: string) { + if (!name.split('/').pop()) return false; + if (!name.split('/').pop().includes('.')) return false; + return true; +} +function shouldPreCache(name: string) { + if (!shouldCache(name)) return false; + if (DO_NOT_CACHE.filter((i) => name.includes(i)).length) return false; + return true; +} + +interface ServiceWorkerConfig { + base?: string; + targets?: string[]; + preload?: string; +} +let config: ServiceWorkerConfig = null; + +async function get(url: string) { + const pending = [url, ...(config?.targets || []).map((i) => url.replace(config.base, i))]; + let response: Promise; + for (const source of pending) { + response = fetch(source); + try { + console.log('From ', source); + const r = await response; + if (r.ok) return r; + } catch (error) { + console.warn(source, ' Load fail ', error); + } + } + return response; +} + +self.addEventListener('install', (event) => event.waitUntil((async () => { + const [cache, manifest, cfg] = await Promise.all([ + caches.open(PRECACHE), + fetch('/manifest.json').then((res) => res.json()), + fetch('/sw-config').then((res) => res.json()), + ]); + config = cfg; + if (process.env.NODE_ENV === 'production' && config?.preload) { + const files = Object.values(manifest).filter(shouldPreCache) + .map((i: string) => new URL(i, config.preload).toString()); + await cache.addAll(files); // NOTE: CORS header + } + self.skipWaiting(); +})())); + +self.addEventListener('activate', (event) => { + const valid = [PRECACHE]; + event.waitUntil( + caches.keys() + .then((names) => Promise.all( + names.filter((name) => !valid.includes(name)).map((p) => caches.delete(p)), + )).then(() => self.clients.claim()), + ); +}); + +self.addEventListener('fetch', (event: FetchEvent) => { + if (!['get', 'head'].includes(event.request.method.toLowerCase())) return; + if (!event.request.url.startsWith('http')) return; + if (!shouldCache(event.request.url) && config?.base && config?.targets?.length) { + if (new URL(event.request.url).origin !== location.origin) return; + event.respondWith(get(event.request.url)); + return; + } + if (process.env.NODE_ENV !== 'production' || !shouldCache(event.request.url)) return; + event.respondWith((async () => { + const cachedResponse = await caches.match(event.request); + if (cachedResponse) return cachedResponse; + console.log(`Caching ${event.request.url}`); + const [cache, response] = await Promise.all([ + caches.open(PRECACHE), + fetch(event.request), + ]); + if (response.ok) await cache.put(event.request, response.clone()); + return response; + })()); +}); diff --git a/packages/ui-default/setting.yaml b/packages/ui-default/setting.yaml index bb43f8fcd..a02145a66 100644 --- a/packages/ui-default/setting.yaml +++ b/packages/ui-default/setting.yaml @@ -80,6 +80,9 @@ formatCode: desc: Auto format code category: preference value: true +preload: + type: text + default: '' about: type: markdown name: about diff --git a/packages/ui-default/static/manifest.json b/packages/ui-default/static/manifest.json deleted file mode 100644 index d0692819c..000000000 --- a/packages/ui-default/static/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Hydro", - "icons": [ - { - "src": "\/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": 4 - } - ] -} diff --git a/packages/ui-default/sw.ts b/packages/ui-default/sw.ts deleted file mode 100644 index 40b3ab7d9..000000000 --- a/packages/ui-default/sw.ts +++ /dev/null @@ -1,30 +0,0 @@ -const base = 'https://hydro.ac'; -const target = [ - 'https://hydro.ac', - 'https://us.hydro.ac', -]; - -(this as any).addEventListener('install', () => { - console.log('Service Worker installing'); -}); -async function get(url) { - let current = 0; - let response; - while (target[current]) { - response = fetch(url.replace(base, target[current])); - try { - await response; - if (current) console.log('From ', target[current]); - return response; - } catch (error) { - console.warn(target[current], ' Load fail ', error); - current++; - } - } - return response; -} -(this as any).addEventListener('fetch', (event) => { - // eslint-disable-next-line no-restricted-globals - if (new URL(event.request.url).origin !== location.origin) return; - event.respondWith(get(event.request.url)); -});