diff --git a/packages/ui-default/common/common.inc.styl b/packages/ui-default/common/common.inc.styl
index ff72825e0..0d222529e 100644
--- a/packages/ui-default/common/common.inc.styl
+++ b/packages/ui-default/common/common.inc.styl
@@ -1,10 +1,278 @@
-// This file is imported in all stylus source code
+
+base-font-size ?= 16px
+rasterise-media-queries ?= false
+
+rupture = {
+ rasterise-media-queries: rasterise-media-queries
+ mobile-cutoff: 400px
+ desktop-cutoff: 1050px
+ hd-cutoff: 1800px
+ enable-em-breakpoints: false
+ base-font-size: base-font-size
+ anti-overlap: false
+ density-queries: 'dppx' 'webkit' 'moz' 'dpi'
+ retina-density: 1.5
+ use-device-width: false
+}
+rupture.scale = 0 (rupture.mobile-cutoff) 600px 800px (rupture.desktop-cutoff) (rupture.hd-cutoff)
+rupture.scale-names = 'xs' 's' 'm' 'l' 'xl' 'hd'
+
+-is-string(val)
+ if typeof(val) is not 'unit'
+ if val is a 'string' or val is a 'ident'
+ true
+ else
+ false
+ else
+ false
+
+-get-scale-number(scale-name)
+ for list-item, i in rupture.scale-names
+ if list-item is scale-name
+ return i + 1
+ return false
+
+-convert-to(to-unit, value, context = rupture.base-font-size)
+ from-unit = unit(value)
+ return value if to-unit is from-unit
+ if to-unit in ('em' 'rem')
+ return value if from-unit in ('em' 'rem')
+ return unit((value / context), to-unit)
+ if to-unit is 'px'
+ return unit((value * context), 'px')
+
+-on-scale(n)
+ return unit(n) is ''
+
+-larger-than-scale(n)
+ return (n > (length(rupture.scale) - 1)) and -on-scale(n)
+
+-is-zero(n)
+ return n is 0
+
+-overlap-shift(anti-overlap, n)
+ shift-unit = unit(n)
+ anti-overlap = 0px unless anti-overlap
+ anti-overlap = 1px if anti-overlap is true
+ if length(anti-overlap) is 1
+ return -convert-to(shift-unit, anti-overlap)
+ for val in anti-overlap
+ return val if unit(val) is shift-unit
+
+-adjust-overlap(anti-overlap, n, side = 'min')
+ -shift = -overlap-shift(anti-overlap, n)
+ if (side is 'min' and -shift > 0) or (side is 'max' and -shift < 0)
+ n = n + -shift
+ return n
+
+-is-positive(n)
+ return n >= 0
+
+-density-queries(density)
+ if typeof(density) is not 'unit'
+ if not -is-string(density)
+ density = '%s' % density
+ density = rupture.retina-density if density is 'retina'
+ queries = ()
+ for query in rupture.density-queries
+ if query is 'webkit'
+ push(queries, '(-webkit-min-device-pixel-ratio: %s)' % (density))
+ else if query is 'moz'
+ push(queries, '(min--moz-device-pixel-ratio: %s)' % (density))
+ else if query is 'o'
+ push(queries, '(-o-min-device-pixel-ratio: %s/1)' % (density))
+ else if query is 'ratio'
+ push(queries, '(min-device-pixel-ratio: %s)' % (density))
+ else if query is 'dpi'
+ if -is-string(density)
+ density=convert(density)
+ push(queries, '(min-resolution: %sdpi)' % (round(density * 96, 1)))
+ else if query is 'dppx'
+ push(queries, '(min-resolution: %sdppx)' % (density))
+ return queries
+
+create-fallback-class(selected, class)
+ /{'' + class + ' ' + selected}
+ {block}
+
+// +between(min, max)
+// usage (scale can be mixed with custom values):
+// - +between(1, 3) scale:scale
+// - +between(0, 3) 0 width:scale
+// - +between(200px, 500px) custom:custom
+// - +between(0, 300px) 0 width:custom
+// - +between(1, 300px) scale:custom
+// - +between(200px, 4) custom:scale
+
+between(min, max, anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ selected = selector()
+
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ if -is-string(min)
+ min = -get-scale-number(min)
+ if -is-string(max)
+ max = -get-scale-number(max)
+
+ -min = rupture.scale[min - 1] unless -is-zero(min) or (not -on-scale(min))
+ -max = rupture.scale[max] unless not -on-scale(max)
+ -min ?= min
+ -max ?= max
+
+ if (rupture.rasterise-media-queries)
+ if not (density or -max or orientation)
+ {block}
+ else
+ condition = 'only screen'
+ use-device-width = use-device-width ? 'device-' : ''
+ unless -min is 0
+ -min = -convert-to('em', -min) if rupture.enable-em-breakpoints
+ -min = -adjust-overlap(anti-overlap, -min, side: 'min')
+ condition = condition + ' and (min-' + use-device-width + 'width: %s)' % (-min)
+ unless -larger-than-scale(max)
+ -max = -convert-to('em', -max) if rupture.enable-em-breakpoints
+ -max = -adjust-overlap(anti-overlap, -max, side: 'max')
+ condition = condition + ' and (max-' + use-device-width + 'width: %s)' % (-max)
+ if orientation
+ condition = condition + ' and (orientation: %s)' % (orientation)
+ if density
+ conditions = ()
+ for query in -density-queries(density)
+ push(conditions, condition + ' and %s' % (query))
+ condition = join(', ', conditions)
+ @media condition
+ {block}
+ if fallback-class
+ +create-fallback-class(selected, fallback-class)
+ {block}
+
+at(scale-point, anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +between(scale-point, scale-point, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+from-width(scale-point, anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +between(scale-point, length(rupture.scale), anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+above = from-width
+
+to-width(scale-point, anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +between(1, scale-point, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+below = to-width
+
+mobile(anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +below(rupture.mobile-cutoff, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+tablet(anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +between(rupture.mobile-cutoff, rupture.desktop-cutoff, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+desktop(anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +above(rupture.desktop-cutoff, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+hd(anti-overlap = rupture.anti-overlap, density = null, orientation = null, use-device-width = rupture.use-device-width, fallback-class = null)
+ if -is-string(orientation)
+ orientation = convert(orientation)
+ if -is-string(density)
+ density = convert(density)
+ +above(rupture.hd-cutoff, anti-overlap, density, orientation, use-device-width, fallback-class)
+ {block}
+
+density(density, orientation = null, fallback-class = null)
+ selected = selector()
+ if not (rupture.rasterise-media-queries)
+ conditions = ()
+ for query in -density-queries(density)
+ condition = 'only screen and %s' % (query)
+ if orientation
+ condition = condition + ' and (orientation: %s)' % (orientation)
+ push(conditions, condition)
+ condition = join(', ', conditions)
+ @media condition
+ {block}
+ if fallback-class
+ +create-fallback-class(selected, fallback-class)
+ {block}
+
+pixel-ratio = density
+
+retina(orientation = null, fallback-class = null)
+ +density('retina', orientation, fallback-class)
+ {block}
+
+landscape(density = null, fallback-class = null)
+ selected = selector()
+ if not (rupture.rasterise-media-queries)
+ if -is-string(density)
+ density = convert(density)
+ if density
+ +pixel-ratio(density, orientation: landscape, fallback-class)
+ {block}
+ else
+ @media only screen and (orientation: landscape)
+ {block}
+ if fallback-class
+ +create-fallback-class(selected, fallback-class)
+ {block}
+
+portrait(density = null, fallback-class = null)
+ selected = selector()
+ if not (rupture.rasterise-media-queries)
+ if -is-string(density)
+ density = convert(density)
+ if density
+ +pixel-ratio(density, orientation: portrait, fallback-class)
+ {block}
+ else
+ @media only screen and (orientation: portrait)
+ {block}
+ if fallback-class
+ +create-fallback-class(selected, fallback-class)
+ {block}
+
+rupture-hover(density = null, orientation = null, fallback-class = null)
+ condition = "only screen and (hover: hover)";
+ @media ({condition})
+ {block}
+
+hover = rupture-hover
+
breakpoints = json('../breakpoints.json', { hash: true })
rupture.mobile-cutoff = unit(breakpoints.mobile, 'px')
rupture.desktop-cutoff = unit(breakpoints.desktop, 'px')
rupture.hd-cutoff = unit(breakpoints.hd, 'px')
-@import '~vj/misc/.iconfont/webicon.inc.styl'
+@import '../misc/.iconfont/webicon.inc.styl'
@import 'color.inc.styl'
@import 'variables.inc.styl'
@import 'easing.inc.styl'
diff --git a/packages/ui-default/components/highlighter/prismjs.js b/packages/ui-default/components/highlighter/prismjs.js
index 4e5f23779..0b7481973 100644
--- a/packages/ui-default/components/highlighter/prismjs.js
+++ b/packages/ui-default/components/highlighter/prismjs.js
@@ -13,14 +13,14 @@ import getLoader from 'prismjs/dependencies';
import Notification from 'vj/components/notification/index';
import { i18n } from 'vj/utils';
import languageMeta from './meta';
+import langs from 'prismjs/components/index';
-const files = require.context('prismjs/components/', true, /prism-[a-z0-9-]+\.js/);
const loadedLanguages = new Set();
function loadLanguages() {
const languages = Object.keys(components.languages).filter((l) => l !== 'meta');
const loaded = [...loadedLanguages, ...Object.keys(Prism.languages)];
getLoader(components, languages, loaded).load((lang) => {
- files(`./prism-${lang}.js`);
+ langs[lang]?.();
loadedLanguages.add(lang);
});
}
diff --git a/packages/ui-default/components/message/index.page.ts b/packages/ui-default/components/message/index.page.ts
index 2707b3737..5e13d3329 100644
--- a/packages/ui-default/components/message/index.page.ts
+++ b/packages/ui-default/components/message/index.page.ts
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
const initWorkerMode = (endpoint) => {
console.log('Messages: using SharedWorker');
- const worker = new SharedWorker('/messages-shared-worker.js', { name: 'HydroMessagesWorker' });
+ const worker = new SharedWorker(new URL('./worker.ts?worker', import.meta.url), { name: 'HydroMessagesWorker' });
worker.port.start();
window.addEventListener('beforeunload', () => {
worker.port.postMessage({ type: 'unload' });
diff --git a/packages/ui-default/components/scratchpad/DataInputComponent.jsx b/packages/ui-default/components/scratchpad/DataInputComponent.jsx
index 8fef6b7da..af8e78002 100644
--- a/packages/ui-default/components/scratchpad/DataInputComponent.jsx
+++ b/packages/ui-default/components/scratchpad/DataInputComponent.jsx
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
+import React from 'react';
export default function DataInputComponent(props) {
const {
diff --git a/packages/ui-default/components/scratchpad/ToolbarComponent.jsx b/packages/ui-default/components/scratchpad/ToolbarComponent.jsx
index 35b49b67f..31930977e 100644
--- a/packages/ui-default/components/scratchpad/ToolbarComponent.jsx
+++ b/packages/ui-default/components/scratchpad/ToolbarComponent.jsx
@@ -1,5 +1,6 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
export default function ToolbarComponent(props) {
const {
diff --git a/packages/ui-default/components/time/time.page.js b/packages/ui-default/components/time/time.page.js
index 487fc3d4f..69b39883e 100644
--- a/packages/ui-default/components/time/time.page.js
+++ b/packages/ui-default/components/time/time.page.js
@@ -1,19 +1,21 @@
import $ from 'jquery';
import * as timeago from 'timeago.js';
+import en_US from 'timeago.js/lib/lang/en_US';
+import ko from 'timeago.js/lib/lang/ko';
+import zh_CN from 'timeago.js/lib/lang/zh_CN';
+import zh_TW from 'timeago.js/lib/lang/zh_TW';
import { AutoloadPage } from 'vj/misc/Page';
import { i18n } from 'vj/utils';
try {
- const locales = require.context('timeago.js/lib/lang', false, /\.js$/);
- let locale;
- try {
- locale = locales(`./${i18n('timeago_locale')}.js`).default;
- } catch (e) {
- locale = locales('./en_US.js').default;
- }
+ const locales = {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ zh_CN, zh_TW, ko, en_US,
+ };
+ const locale = locales[i18n('timeago_locale')] || locales.en_US;
timeago.register(i18n('timeago_locale'), locale);
} catch (e) {
- console.error(`Cannot register timeago locale: ${i18n('timeago_locale')}`);
+ console.error(`Cannot register timeago locale: ${i18n('timeago_locale')}`, e);
}
function runRelativeTime($container) {
$container.find('span.time.relative[data-timestamp]').get().forEach((element) => {
diff --git a/packages/ui-default/entry.js b/packages/ui-default/entry.jsx
similarity index 85%
rename from packages/ui-default/entry.js
rename to packages/ui-default/entry.jsx
index a675ae082..1ca9b1744 100644
--- a/packages/ui-default/entry.js
+++ b/packages/ui-default/entry.jsx
@@ -7,6 +7,7 @@ window.Hydro = {
node_modules: {},
version: process.env.VERSION,
};
+window.jQuery = $; // sticky-kit require this
window.externalModules = {};
window.lazyModuleResolver = {};
@@ -30,7 +31,11 @@ window.UserContext = JSON.parse(window.UserContext);
try { __webpack_public_path__ = UiContext.cdn_prefix; } catch (e) { }
if ('serviceWorker' in navigator) {
const encodedConfig = encodeURIComponent(JSON.stringify(UiContext.SWConfig));
- navigator.serviceWorker.register(`/service-worker.js?config=${encodedConfig}`).then((registration) => {
+ const dev = process.env.NODE_ENV === 'development';
+ navigator.serviceWorker.register(
+ dev ? '/dev-sw.js?dev-sw' : `/sw.js?config=${encodedConfig}`,
+ { scope: '/', type: dev ? 'module' : 'classic' },
+ ).then((registration) => {
console.log('SW registered: ', registration);
}).catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
diff --git a/packages/ui-default/hydro.ts b/packages/ui-default/hydro.ts
index cf4b47dc4..186c45433 100644
--- a/packages/ui-default/hydro.ts
+++ b/packages/ui-default/hydro.ts
@@ -82,6 +82,7 @@ export async function initPageLoader() {
Notification.warn(`Failed to call '${type}Loading' of ${page.name}`);
console.error(`Failed to call '${type}Loading' of ${page.name}\n${e.stack}`);
console.error(e);
+ console.info(func.toString());
}
if (process.env.NODE_ENV !== 'production') {
console.time(`${page.name}: ${type}Loading`);
diff --git a/packages/ui-default/misc/PageLoader.js b/packages/ui-default/misc/PageLoader.js
index cec650cc0..ba28eb8dd 100644
--- a/packages/ui-default/misc/PageLoader.js
+++ b/packages/ui-default/misc/PageLoader.js
@@ -1,12 +1,10 @@
+import pages from '../__glob_page';
import { Page } from './Page';
export default class PageLoader {
constructor() {
- const pages = require.context('../pages/', true, /\.page\.[jt]sx?$/i);
- const components = require.context('../components/', true, /\.page\.[jt]sx?$/i);
this.pageInstances = [
- ...pages.keys().map((key) => pages(key)),
- ...components.keys().map((key) => components(key)),
+ ...pages,
...window.Hydro.extraPages,
].map((page) => {
page = page?.default || page;
diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json
index 44216943f..6b1eaf110 100644
--- a/packages/ui-default/package.json
+++ b/packages/ui-default/package.json
@@ -15,6 +15,7 @@
},
"devDependencies": {
"@blueprintjs/core": "^5.10.3",
+ "@codingame/esbuild-import-meta-url-plugin": "^1.0.2",
"@fontsource/dm-mono": "^5.0.20",
"@fontsource/fira-code": "^5.0.18",
"@fontsource/inconsolata": "^5.0.18",
@@ -24,6 +25,7 @@
"@fontsource/source-code-pro": "^5.0.18",
"@fontsource/ubuntu-mono": "^5.0.20",
"@hydrooj/utils": "workspace:^",
+ "@rollup/plugin-inject": "^5.0.5",
"@simplewebauthn/browser": "7.4.0",
"@svgr/webpack": "^8.1.0",
"@types/gulp-if": "^0.0.34",
@@ -117,6 +119,10 @@
"ts-loader": "^9.5.1",
"vditor": "^3.10.4",
"vinyl-buffer": "^1.0.1",
+ "vite": "^5.2.12",
+ "vite-plugin-pwa": "^0.20.0",
+ "vite-plugin-static-copy": "^1.0.5",
+ "vite-plugin-svgr": "^4.2.0",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.10.2",
diff --git a/packages/ui-default/pages/user_detail.page.styl b/packages/ui-default/pages/user_detail.page.styl
index b37ec25c3..5796027ac 100644
--- a/packages/ui-default/pages/user_detail.page.styl
+++ b/packages/ui-default/pages/user_detail.page.styl
@@ -1,4 +1,4 @@
-@import '~vj/components/tab/var.inc.styl'
+@import '../components/tab/var.inc.styl'
.page--user_detail
diff --git a/packages/ui-default/service-worker.ts b/packages/ui-default/service-worker.ts
index 514212c0b..7902ca7ee 100644
--- a/packages/ui-default/service-worker.ts
+++ b/packages/ui-default/service-worker.ts
@@ -4,6 +4,16 @@
export { }; // make it a module so that declare self works
declare const self: ServiceWorkerGlobalScope;
+let dev = false;
+let version = 'dev';
+try {
+ dev = process.env.NODE_ENV !== 'production';
+ version = process.env.VERSION;
+} catch (e) {
+ // process doesnt exist
+ dev = location.search.includes('dev-sw');
+}
+
const map = new Map();
function createStream(port) {
@@ -88,13 +98,13 @@ self.addEventListener('notificationclick', (event) => {
}));
});
-const PRECACHE = `precache-${process.env.VERSION}`;
+const PRECACHE = `precache-${version}`;
const DO_NOT_PRECACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco'];
function shouldCachePath(path: string) {
if (!path.split('?')[0].split('/').pop()) return false;
if (!path.split('?')[0].split('/').pop().includes('.')) return false;
- if (process.env.NODE_ENV !== 'production' && (path.includes('.hot-update.') || path.includes('?version='))) return false;
+ if (dev && (path.includes('.hot-update.') || path.includes('?version='))) return false;
return true;
}
function shouldCache(request: Request) {
@@ -119,7 +129,7 @@ interface ServiceWorkerConfig {
let config: ServiceWorkerConfig = null;
function initConfig() {
- config = JSON.parse(new URLSearchParams(location.search).get('config'));
+ config = JSON.parse(new URLSearchParams(location.search).get('config')) || {};
config.hosts ||= [];
if (!config.domains?.length) config.domains = [location.host];
console.log('Config:', config);
@@ -127,7 +137,7 @@ function initConfig() {
self.addEventListener('install', (event) => event.waitUntil((async () => {
initConfig();
- if (process.env.NODE_ENV === 'production' && config?.preload) {
+ if (!dev && config?.preload) {
const [cache, manifest] = await Promise.all([
caches.open(PRECACHE),
fetch('/manifest.json').then((res) => res.json()),
@@ -193,6 +203,7 @@ async function cachedRespond(request: Request) {
caches.open(PRECACHE),
get(request),
]);
+ if (response.headers.get('Cache-Control') === 'no-cache') return response;
if (response.ok) {
cache.put(url, response.clone());
return response;
diff --git a/packages/ui-default/templates/layout/html5.html b/packages/ui-default/templates/layout/html5.html
index dc06df6fb..17fe5e61f 100644
--- a/packages/ui-default/templates/layout/html5.html
+++ b/packages/ui-default/templates/layout/html5.html
@@ -37,8 +37,7 @@
{% endif %}
{% if process.env.DEV %}
-
-
+
{% else %}
{% endif %}
@@ -67,7 +66,7 @@
{% set UserContext = Object.create(UserContext) %}
{% if process.env.DEV %}
-
+
{% else %}
diff --git a/packages/ui-default/theme/default.js b/packages/ui-default/theme/default.js
index 0c7a2a817..b0b2547ae 100644
--- a/packages/ui-default/theme/default.js
+++ b/packages/ui-default/theme/default.js
@@ -27,9 +27,6 @@ import 'vj/misc/nothing.styl';
import 'vj/components/editor/cmeditor.styl';
import 'vj/components/datepicker/datepicker.styl';
import './dark.styl';
-
// load all page stylesheets
-const pageStyleReq = require.context('../', true, /\.page\.styl$/i);
-pageStyleReq.keys().map((key) => pageStyleReq(key));
-const pageStyleReqDefault = require.context('../', true, /\.page\.default\.styl$/i);
-pageStyleReqDefault.keys().map((key) => pageStyleReqDefault(key));
+import '../__glob_page.styl';
+import '../__glob_default.styl';
diff --git a/packages/ui-default/vite.config.mjs b/packages/ui-default/vite.config.mjs
new file mode 100644
index 000000000..6517cc7c3
--- /dev/null
+++ b/packages/ui-default/vite.config.mjs
@@ -0,0 +1,268 @@
+import importMetaUrlPlugin from '@codingame/esbuild-import-meta-url-plugin';
+import inject from '@rollup/plugin-inject';
+import fs from 'fs';
+import { globbySync } from 'globby';
+import httpProxy from 'http-proxy';
+import path from 'path';
+import colors from 'picocolors';
+// import federation from '@originjs/vite-plugin-federation';
+import { defineConfig } from 'vite';
+import { VitePWA } from 'vite-plugin-pwa';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+import svgr from 'vite-plugin-svgr';
+import { version } from './package.json';
+
+const proxy = httpProxy.createProxyServer({
+ ws: true,
+ target: 'http://localhost:2333',
+});
+
+const setOriginHeader = (proxyReq, options) => {
+ if (options.changeOrigin) {
+ const { target } = options;
+
+ if (proxyReq.getHeader('origin') && target) {
+ const changedOrigin = typeof target === 'object'
+ ? `${target.protocol}//${target.host}`
+ : target;
+
+ proxyReq.setHeader('origin', changedOrigin);
+ }
+ }
+};
+proxy.on('error', (err, req, originalRes) => {
+ const res = originalRes;
+ if (!res) {
+ console.error(
+ `${colors.red(`http proxy error: ${err.message}`)}\n${err.stack}`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ );
+ } else if ('req' in res) {
+ console.error(
+ `${colors.red(`http proxy error: ${originalRes.req.url}`)}\n${err.stack
+ }`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ );
+ if (!res.headersSent && !res.writableEnded) {
+ res
+ .writeHead(500, {
+ 'Content-Type': 'text/plain',
+ })
+ .end();
+ }
+ } else {
+ console.error(`${colors.red('ws proxy error:')}\n${err.stack}`, {
+ timestamp: true,
+ error: err,
+ });
+ res.end();
+ }
+});
+proxy.on('proxyReq', (proxyReq, req, res, options) => {
+ setOriginHeader(proxyReq, options);
+});
+proxy.on('proxyReqWs', (proxyReq, req, socket, options) => {
+ setOriginHeader(proxyReq, options);
+ socket.on('error', (err) => {
+ console.error(
+ `${colors.red('ws proxy socket error:')}\n${err.stack}`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ );
+ });
+});
+proxy.on('proxyRes', (proxyRes, req, res) => {
+ res.on('close', () => {
+ if (!res.writableEnded) {
+ proxyRes.destroy();
+ }
+ });
+});
+
+function getGlobStyle(glob = '**/*.page.styl') {
+ const files = globbySync(glob, { cwd: __dirname, ignore: ['node_modules'] });
+ const result = [];
+ for (const file of files) {
+ const content = fs.readFileSync(path.join(__dirname, file), 'utf-8');
+ if (content.includes('@import \'.')) {
+ result.push(`@import ${JSON.stringify(file)}`);
+ } else {
+ result.push(content);
+ }
+ }
+ return `${result.filter((i) => i.startsWith('@import')).join('\n')}
+${result.filter((i) => !i.startsWith('@import')).join('\n')}`;
+}
+function updateGlobStyle(glob, filename) {
+ const file = path.join(__dirname, filename);
+ const content = getGlobStyle(glob);
+ if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== content) {
+ fs.writeFileSync(file, content);
+ }
+}
+updateGlobStyle('**/*.page.styl', '__glob_page.styl');
+updateGlobStyle('**/*.page.default.styl', '__glob_default.styl');
+function getGlobPage(glob = '**/*.page.[jt]sx?') {
+ const files = globbySync(glob, { cwd: __dirname, ignore: ['node_modules'] });
+ const result = [];
+ const exps = [];
+ for (const file of files) {
+ result.push(`import ${file.replace(/[/\\.-]/g, '_')} from "${file.startsWith('/') ? file : `./${file}`}";`);
+ exps.push(file.replace(/[/\\.-]/g, '_'));
+ }
+ result.push(`export default [${exps.join(',')}]`);
+ return result.join('\n');
+}
+function updateGlobPage(glob, filename) {
+ const file = path.join(__dirname, filename);
+ const content = getGlobPage(glob);
+ if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== content) {
+ fs.writeFileSync(file, content);
+ }
+}
+updateGlobPage(['**/*.page.js', '**/*.page.ts', '**/*.page.jsx', '**/*.page.tsx'], '__glob_page.js');
+
+function patchPrism() {
+ const langs = globbySync('prism-*.js', { cwd: import.meta.resolve('prismjs/components').split(':')[1], ignore: ['node_modules'] });
+ const lines = langs.filter((i) => !i.endsWith('.min.js')).map((i) => {
+ const name = i.split('prism-')[1].split('.')[0];
+ return `'${name}': () => require('./${i}'),`;
+ });
+ const loader = `export default {
+ ${lines.join('\n')}
+ }`;
+ fs.writeFileSync(import.meta.resolve('prismjs/components/index.js').split(':')[1], loader);
+}
+patchPrism();
+
+export default defineConfig({
+ root: __dirname,
+ esbuild: {
+ jsx: 'transform',
+ loader: 'tsx',
+ },
+ publicDir: 'static',
+ build: {
+ minify: process.env.NODE_NEV === 'production',
+ outDir: 'public',
+ rollupOptions: {
+ input: './entry.jsx',
+ output: {
+ globals: {
+ jQuery: 'jQuery',
+ },
+ },
+ },
+ },
+ resolve: {
+ dedupe: ['monaco-editor', 'vscode'],
+ alias: {
+ vj: __dirname,
+ },
+ },
+ define: {
+ 'process.env.VERSION': JSON.stringify(version),
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+ ...(process.env.NODE_ENV === 'development' ? { 'process.cwd': '()=>null' } : {}),
+ },
+ optimizeDeps: {
+ include: ['jquery'],
+ esbuildOptions: {
+ plugins: [importMetaUrlPlugin],
+ },
+ },
+ css: {
+ preprocessorOptions: {
+ styl: {
+ use: [],
+ imports: [
+ // import.meta.resolve('rupture/rupture/index.styl'),
+ path.join(__dirname, 'common/common.inc.styl'),
+ ],
+ },
+ },
+ },
+ server: {
+ host: true,
+ port: 8000,
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ },
+ proxy: {
+ },
+ },
+ worker: {
+ format: 'es',
+ },
+ plugins: [
+ inject({
+ $: 'jquery',
+ React: 'react',
+ include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
+ }),
+ svgr({}),
+ viteStaticCopy({
+ targets: [
+ { src: 'components/navigation/nav-logo-small_dark.png', dest: 'components/navigation/nav-logo-small_dark.png' },
+ { src: import.meta.resolve('streamsaver/mitm.html'), dest: 'streamsaver/mitm.html' },
+ { src: import.meta.resolve('streamsaver/sw.js'), dest: 'streamsaver/sw.js' },
+ { src: import.meta.resolve('vditor/dist'), dest: 'vditor/dist' },
+ // { from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
+ // { from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
+ ],
+ }),
+ VitePWA({
+ injectRegister: null,
+ registerType: 'autoUpdate',
+ strategies: 'injectManifest',
+ srcDir: '.',
+ filename: 'service-worker.ts',
+ injectManifest: {
+ injectionPoint: undefined,
+ },
+ workbox: {
+ globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
+ },
+ devOptions: {
+ enabled: true,
+ type: 'module',
+ },
+ }),
+ {
+ name: 'configure-server',
+ watchChange(id) {
+ if (id.endsWith('.styl') && !id.includes('/__')) {
+ console.log('change', id);
+ updateGlobStyle('**/*.page.styl', '__glob_page.styl');
+ updateGlobStyle('**/*.page.default.styl', '__glob_default.styl');
+ }
+ },
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ // console.log(req.url);
+ const resource = req.url.split('?')[0];
+ if (['/@fs/', '/@vite/', '/sw.js', '/dev-sw.js', '/vditor'].some((t) => resource.startsWith(t))) next();
+ else if (resource !== '/' && fs.existsSync(path.join(__dirname, resource.substring(1)))) next();
+ else {
+ const options = {};
+ proxy.web(req, res, options);
+ }
+ });
+ server.httpServer.on('upgrade', (req, socket, head) => {
+ if (req.url !== '/') {
+ proxy.ws(req, socket, head);
+ }
+ });
+ },
+ },
+ ],
+});