mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
ui-next: refactor plugin loader (#1190)
This commit is contained in:
1 parent
0e688f77b7
commit
f9cd5bcb40
12 files changed
+181
-17
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
import { Context, Handler } from 'hydrooj';
|
||||||
|
|
||||||
|
// Server side of the example plugin. It registers a couple of routes whose
|
||||||
|
// handlers set a `template`; with @hydrooj/ui-next installed the `next` renderer
|
||||||
|
// (priority 100) wins for every template, ignores the template file, and injects
|
||||||
|
// the route/template name into the SPA shell. The client then renders the page
|
||||||
|
// registered under `page:<template without .html>` (falling back to `page:<route name>`).
|
||||||
|
|
||||||
|
class ExampleHomeHandler extends Handler {
|
||||||
|
async get() {
|
||||||
|
// Stem (`example_home`) must match the slot registered in ui/index.tsx.
|
||||||
|
this.response.template = 'example_home.html';
|
||||||
|
// response.body is forwarded to the page as `args` (see usePageData).
|
||||||
|
this.response.body = { message: 'Hello from the ui-next example plugin server side!' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExampleCssHandler extends Handler {
|
||||||
|
async get() {
|
||||||
|
this.response.template = 'example_css.html';
|
||||||
|
this.response.body = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apply(ctx: Context) {
|
||||||
|
// A couple of test routes to host the example pages.
|
||||||
|
ctx.Route('example_home', '/example', ExampleHomeHandler);
|
||||||
|
ctx.Route('example_css', '/example/css', ExampleCssHandler);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"cli": false
|
"cli": false
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hydrooj/ui-next": "workspace:*"
|
"@hydrooj/ui-next": "workspace:*",
|
||||||
|
"hydrooj": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { PluginAPI } from '@hydrooj/ui-next';
|
import type { PluginAPI } from '@hydrooj/ui-next';
|
||||||
|
|
||||||
export function setup(api: PluginAPI) {
|
export function setup(api: PluginAPI) {
|
||||||
api.before('page:app', () => {
|
// (2) Page registration — slot names match the server route names / template stems
|
||||||
console.log('before app');
|
// registered in ../index.ts. Pages are lazy-loaded on demand.
|
||||||
// throw new Error('test error boundary in before interceptor');
|
api.registerPage('example_home', () => import('./pages/example_home'));
|
||||||
return <div>before app via @hydrooj/ui-next-plugin-sample</div>;
|
api.registerPage('example_css', () => import('./pages/example_css'));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from '@hydrooj/ui-next';
|
||||||
|
import styles from '../styles.module.css'; // (1) CSS module — scoped class names
|
||||||
|
|
||||||
|
export default function ExampleCss() {
|
||||||
|
return (
|
||||||
|
<div className={styles.card}>
|
||||||
|
<span className={styles.badge}>CSS module</span>
|
||||||
|
<h2 className={styles.title}>Scoped class names</h2>
|
||||||
|
<p><Link to="example_home">← Back</Link></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import '../styles.css'; // (3) plain CSS import — applied globally when this page loads
|
||||||
|
|
||||||
|
import { Link, usePageData } from '@hydrooj/ui-next';
|
||||||
|
|
||||||
|
export default function ExampleHome() {
|
||||||
|
const { args } = usePageData(); // args carries the handler's response.body
|
||||||
|
return (
|
||||||
|
<div className="example-page">
|
||||||
|
<h1>ui-next example plugin</h1>
|
||||||
|
<p>{args.message as string}</p>
|
||||||
|
<Link to="example_css">See the CSS-module demo →</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* Imported via `import './styles.css'` — bundled and injected globally as a <style>. */
|
||||||
|
.example-page {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.example-page h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.example-page a {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* Imported via `import styles from './styles.module.css'`; class names are hashed/scoped,
|
||||||
|
accessed as styles.card / styles.badge / styles.title. */
|
||||||
|
.card {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
/// <reference path="./css.d.ts" />
|
||||||
|
|
||||||
export * from './src/api';
|
export * from './src/api';
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
declare module '*.css';
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: Record<string, string>;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
+75
-11
@@ -1,4 +1,3 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
@@ -7,7 +6,7 @@ import { createServer, type Plugin } from 'vite';
|
|||||||
import { serializer } from '@hydrooj/framework';
|
import { serializer } from '@hydrooj/framework';
|
||||||
import {
|
import {
|
||||||
Context, Handler, Logger,
|
Context, Handler, Logger,
|
||||||
NotFoundError, param, size, Types,
|
NotFoundError, param, sha1, size, Types,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
|
|
||||||
const logger = new Logger('ui-next');
|
const logger = new Logger('ui-next');
|
||||||
@@ -100,12 +99,25 @@ const federationPlugin: esbuild.Plugin = {
|
|||||||
const vfs: Record<string, string> = {};
|
const vfs: Record<string, string> = {};
|
||||||
const hashes: Record<string, string> = {};
|
const hashes: Record<string, string> = {};
|
||||||
|
|
||||||
|
const applyCss = (css: string) => `
|
||||||
|
(() => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = ${JSON.stringify(css)};
|
||||||
|
document.head.appendChild(style);
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
function addFile(name: string, content: string) {
|
||||||
|
vfs[name] = content;
|
||||||
|
hashes[name] = sha1(content).substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
class UiNextConstantHandler extends Handler {
|
class UiNextConstantHandler extends Handler {
|
||||||
noCheckPermView = true;
|
noCheckPermView = true;
|
||||||
|
|
||||||
@param('name', Types.Filename)
|
@param('name', Types.Filename)
|
||||||
async all(domainId: string, name: string) {
|
async all(domainId: string, name: string) {
|
||||||
if (!vfs[name]) throw new NotFoundError(name);
|
if (!(name in vfs)) throw new NotFoundError(name);
|
||||||
this.response.type = 'application/javascript';
|
this.response.type = 'application/javascript';
|
||||||
this.response.body = vfs[name];
|
this.response.body = vfs[name];
|
||||||
this.response.addHeader('ETag', hashes[name]);
|
this.response.addHeader('ETag', hashes[name]);
|
||||||
@@ -118,9 +130,23 @@ export async function buildPlugins() {
|
|||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
const entries = getAddonEntries();
|
const entries = getAddonEntries();
|
||||||
|
|
||||||
|
const newPluginFiles = new Set<string>();
|
||||||
|
const emit = (name: string, content: string) => {
|
||||||
|
addFile(name, content);
|
||||||
|
newPluginFiles.add(name);
|
||||||
|
};
|
||||||
|
const purge = () => {
|
||||||
|
for (const key of Object.keys(vfs)) {
|
||||||
|
if (!newPluginFiles.has(key)) {
|
||||||
|
delete vfs[key];
|
||||||
|
delete hashes[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!Object.keys(entries).length) {
|
if (!Object.keys(entries).length) {
|
||||||
vfs['plugins.js'] = 'window.__hydroPlugins = [];';
|
emit('plugins.js', 'window.__hydroPlugins = [];');
|
||||||
hashes['plugins.js'] = '00000000';
|
purge();
|
||||||
logger.info('No plugins to build');
|
logger.info('No plugins to build');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -132,11 +158,18 @@ export async function buildPlugins() {
|
|||||||
...Object.entries(entries).map(([_, e], i) => `import * as plugin${i} from '${e}';`),
|
...Object.entries(entries).map(([_, e], i) => `import * as plugin${i} from '${e}';`),
|
||||||
`window.__hydroPlugins = [${Object.entries(entries).map(([n], i) => `{ name: '${n}', ...plugin${i} }`).join(', ')}];`,
|
`window.__hydroPlugins = [${Object.entries(entries).map(([n], i) => `{ name: '${n}', ...plugin${i} }`).join(', ')}];`,
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
|
sourcefile: 'plugins.ts',
|
||||||
resolveDir: process.cwd(),
|
resolveDir: process.cwd(),
|
||||||
loader: 'ts',
|
loader: 'ts',
|
||||||
},
|
},
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'iife',
|
format: 'esm',
|
||||||
|
splitting: true,
|
||||||
|
outdir: 'plugins-dist',
|
||||||
|
entryNames: 'plugins',
|
||||||
|
chunkNames: 'chunk-[hash]',
|
||||||
|
assetNames: 'asset-[hash]',
|
||||||
|
metafile: true,
|
||||||
write: false,
|
write: false,
|
||||||
target: ['chrome90'],
|
target: ['chrome90'],
|
||||||
plugins: [federationPlugin],
|
plugins: [federationPlugin],
|
||||||
@@ -145,11 +178,42 @@ export async function buildPlugins() {
|
|||||||
jsxImportSource: 'react',
|
jsxImportSource: 'react',
|
||||||
});
|
});
|
||||||
if (result.errors.length) logger.error('Plugin build errors: %o', result.errors);
|
if (result.errors.length) logger.error('Plugin build errors: %o', result.errors);
|
||||||
const content = result.outputFiles?.[0]?.text || 'window.__hydroPlugins = [];';
|
|
||||||
vfs['plugins.js'] = content;
|
const cssText = new Map<string, string>();
|
||||||
hashes['plugins.js'] = crypto.createHash('sha1').update(content).digest('hex').substring(0, 8);
|
for (const f of result.outputFiles) {
|
||||||
totalSize += content.length;
|
if (f.path.endsWith('.css')) cssText.set(f.path, f.text);
|
||||||
logger.success('Plugins built in %dms (%d entries, %s)', Date.now() - start, entries.length, size(totalSize));
|
}
|
||||||
|
|
||||||
|
const cssForJs = new Map<string, string>();
|
||||||
|
const claimed = new Set<string>();
|
||||||
|
for (const [rel, meta] of Object.entries(result.metafile.outputs)) {
|
||||||
|
if (!meta.cssBundle) continue;
|
||||||
|
const css = path.resolve(meta.cssBundle);
|
||||||
|
cssForJs.set(path.resolve(rel), css);
|
||||||
|
claimed.add(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unclaimedCss = '';
|
||||||
|
for (const [abs, text] of cssText) {
|
||||||
|
if (!claimed.has(abs)) unclaimedCss += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of result.outputFiles) {
|
||||||
|
if (f.path.endsWith('.css')) continue;
|
||||||
|
|
||||||
|
const name = path.basename(f.path);
|
||||||
|
let content = f.text;
|
||||||
|
|
||||||
|
const css = cssText.get(cssForJs.get(f.path) ?? '');
|
||||||
|
if (css) content = applyCss(css) + content;
|
||||||
|
if (name === 'plugins.js' && unclaimedCss) content = applyCss(unclaimedCss) + content;
|
||||||
|
|
||||||
|
totalSize += content.length;
|
||||||
|
emit(name, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
purge();
|
||||||
|
logger.success('Plugins built in %dms (%d entries, %s)', Date.now() - start, Object.keys(entries).length, size(totalSize));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Plugin build failed: %o', e);
|
logger.error('Plugin build failed: %o', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"index.ts",
|
"index.ts",
|
||||||
"api.ts",
|
"api.ts",
|
||||||
|
"css.d.ts",
|
||||||
"src/",
|
"src/",
|
||||||
"index.html",
|
"index.html",
|
||||||
"public/",
|
"public/",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
'@hydrooj/ui-next': path.resolve(__dirname, 'src/api.ts'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
publicDir: 'pub',
|
publicDir: 'pub',
|
||||||
|
|||||||
Reference in new issue
Block a user