Skip to content

Commit c1dfea1

Browse files
committedMar 9, 2025·
[ssr] Add gem-ssr package for server-side rendering support
1 parent b375228 commit c1dfea1

File tree

23 files changed

+218
-90
lines changed

23 files changed

+218
-90
lines changed
 

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"devDependencies": {
1414
"@biomejs/biome": "^1.9.4",
15-
"@types/node": "^20.10.0",
15+
"@types/node": "^22.13.10",
1616
"@typescript-eslint/eslint-plugin": "^6.13.1",
1717
"@typescript-eslint/parser": "^6.13.1",
1818
"concurrently": "^8.2.2",

‎packages/duoyun-ui/src/elements/carousel.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,13 @@ export class DuoyunCarouselElement extends GemElement {
198198
this.#reset();
199199
};
200200

201-
#timer = 0;
201+
#timer: ReturnType<typeof setTimeout> | number = 0;
202202
#isFirstRender = true;
203203
#waitLeave = Promise.resolve();
204204

205205
#reset = () => {
206206
this.#clearTimer();
207-
this.#timer = window.setTimeout(async () => {
207+
this.#timer = setTimeout(async () => {
208208
await this.#waitLeave;
209209
this.#add(Direction.Right);
210210
}, this.#interval);

‎packages/duoyun-ui/src/elements/page-loadbar.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,15 @@ const style = css`
3838
@shadow()
3939
export class DuoyunPageLoadbarElement extends GemElement {
4040
static instance?: DuoyunPageLoadbarElement;
41-
static timer = 0;
42-
41+
static timer: ReturnType<typeof setTimeout> | number = 0;
4342
/**在延时时间内结束将不会显示加载条 */
4443
static async start({ delay = 100 }: { delay?: number } = {}) {
4544
clearInterval(Loadbar.timer);
46-
Loadbar.timer = window.setTimeout(() => {
45+
Loadbar.timer = setTimeout(() => {
4746
const instance = Loadbar.instance || new Loadbar();
4847
instance.#state({ progress: 0 });
4948
if (!instance.isConnected) document.body.append(instance);
50-
Loadbar.timer = window.setInterval(() => {
49+
Loadbar.timer = setInterval(() => {
5150
instance.#state({ progress: instance.#state.progress + (95 - instance.#state.progress) * 0.1 });
5251
}, 100);
5352
}, delay);

‎packages/duoyun-ui/src/elements/toast.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ type ToastOptions = Partial<ToastItem> & {
105105
debug?: boolean;
106106
};
107107

108-
const itemTimerMap = new WeakMap<ToastItem, number>();
108+
const itemTimerMap = new WeakMap<ToastItem, ReturnType<typeof setTimeout>>();
109109
const removedSet = new WeakSet<ToastItem>();
110110

111111
@customElement('dy-toast')
@@ -136,7 +136,7 @@ export class DuoyunToastElement extends GemElement {
136136
// 取消正在执行移除动画的删除定时器
137137
removedSet.delete(item);
138138
clearTimeout(itemTimerMap.get(item));
139-
const removeTimer = window.setTimeout(() => toast.#removeItem(item), debug ? 1000000 : duration);
139+
const removeTimer = setTimeout(() => toast.#removeItem(item), debug ? 1000000 : duration);
140140
itemTimerMap.set(item, removeTimer);
141141
}
142142

@@ -159,7 +159,7 @@ export class DuoyunToastElement extends GemElement {
159159
await this.#over;
160160
removedSet.add(item);
161161
this.update();
162-
window.setTimeout(() => {
162+
setTimeout(() => {
163163
if (!this.items || !removedSet.has(item)) return;
164164
this.items = this.items.filter((e) => e !== item);
165165
if (this.items.length === 0) this.remove();

‎packages/gem-book/src/common/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export function debounce<T extends (...args: any) => any>(func: T, wait = 100) {
6363
}
6464

6565
export function throttle<T extends (...args: any) => any>(fn: T, wait = 1000) {
66-
let timer = 0;
66+
let timer: ReturnType<typeof setTimeout> | number = 0;
6767
return (...rest: Parameters<T>) => {
6868
clearTimeout(timer);
69-
timer = window.setTimeout(() => {
69+
timer = setTimeout(() => {
7070
fn(...(rest as any));
7171
timer = 0;
7272
}, wait);

‎packages/gem-book/src/element/elements/loadbar.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ const styles = css`
3535
@shadow()
3636
export class GemBookLoadbarElement extends GemElement {
3737
static instance?: GemBookLoadbarElement;
38-
static timer = 0;
38+
static timer: ReturnType<typeof setTimeout> | number = 0;
3939

4040
static async start({ delay = 100 }: { delay?: number } = {}) {
4141
clearInterval(Loadbar.timer);
42-
Loadbar.timer = window.setTimeout(() => {
42+
Loadbar.timer = setTimeout(() => {
4343
const instance = Loadbar.instance || new Loadbar();
4444
instance.#state({ progress: 0 });
4545
if (!instance.isConnected) document.body.append(instance);
46-
Loadbar.timer = window.setInterval(() => {
46+
Loadbar.timer = setInterval(() => {
4747
instance.#state({ progress: instance.#state.progress + (95 - instance.#state.progress) * 0.1 });
4848
}, 100);
4949
}, delay);

‎packages/gem-book/src/element/lib/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function checkBuiltInPlugin(container: HTMLElement) {
109109
tagSet.forEach((tag) => {
110110
const [namespace, ...rest] = tag.split('-');
111111
if (namespace === 'gbp' && rest.length) {
112-
window.dispatchEvent(new CustomEvent('plugin', { detail: rest.join('-') }));
112+
dispatchEvent(new CustomEvent('plugin', { detail: rest.join('-') }));
113113
}
114114
});
115115
}

‎packages/gem-book/src/plugins/docsearch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class _GbpDocsearchElement extends GemBookPluginElement {
213213
const record = Object.fromEntries(documents.map((document) => [document.id, document.title]));
214214
documents.forEach(async (document) => {
215215
if (!document.text) return;
216-
await new Promise((resolve) => (window.requestIdleCallback || setTimeout)(resolve));
216+
await new Promise((resolve) => (globalThis.requestIdleCallback || setTimeout)(resolve));
217217

218218
const df = new DocumentFragment();
219219
df.append(...Utils.parseMarkdown(document.text));

‎packages/gem-book/src/website/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ if (gaId) {
3131
gtag('config', gaId, { send_page_view: false });
3232
send();
3333
// https://book.gemjs.org/en/api/event
34-
window.addEventListener('routechange', send);
34+
addEventListener('routechange', send);
3535
};
3636
document.body.append(script);
3737
script.remove();
@@ -101,7 +101,7 @@ style.textContent = /*css*/ `
101101
document.head.append(style);
102102

103103
if (!dev) {
104-
window.addEventListener('load', () => {
104+
addEventListener('load', () => {
105105
navigator.serviceWorker?.register('/service-worker.js');
106106
});
107107
}

‎packages/gem-ssr/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Gem SSR
2+
3+
- app render to string

‎packages/gem-ssr/package.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "gem-ssr",
3+
"version": "0.0.1",
4+
"description": "Render to string",
5+
"keywords": ["gem", "react", "generator"],
6+
"main": "src/index.ts",
7+
"type": "module",
8+
"scripts": {
9+
"test": "tsx --test",
10+
"prepublishOnly": "pnpm build"
11+
},
12+
"dependencies": {
13+
"@mantou/gem": "^2.2.0",
14+
"@lit-labs/ssr-dom-shim": "^1.3.0",
15+
"parse5": "^7.1.1"
16+
},
17+
"devDependencies": {
18+
"@gemjs/config": "^2.1.0",
19+
"tsx": "^4.19.3"
20+
},
21+
"author": "mantou132",
22+
"license": "MIT",
23+
"repository": {
24+
"type": "git",
25+
"url": "git+https://github.com/mantou132/gem.git",
26+
"directory": "packages/gem-ssr"
27+
},
28+
"bugs": {
29+
"url": "https://github.com/mantou132/gem/issues"
30+
},
31+
"homepage": "https://github.com/mantou132/gem#readme"
32+
}

‎packages/gem-ssr/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './lib/shim';
2+
3+
export function render() {
4+
return '';
5+
}

‎packages/gem-ssr/src/lib/shim.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CustomElementRegistry, CustomEvent, Element, Event, EventTarget, HTMLElement } from '@lit-labs/ssr-dom-shim';
2+
3+
class CSSStyleSheet {}
4+
5+
Object.assign(globalThis, {
6+
customElements: new CustomElementRegistry(),
7+
CSSStyleSheet,
8+
HTMLElement,
9+
Element,
10+
Event,
11+
CustomEvent,
12+
CustomElementRegistry,
13+
EventTarget,
14+
});
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import '../lib/shim';
2+
3+
import assert from 'node:assert';
4+
import { describe, it } from 'node:test';
5+
import { customElement } from '@mantou/gem/lib/decorators';
6+
import { GemElement } from '@mantou/gem/lib/reactive';
7+
8+
@customElement('app-demo')
9+
class DemoElement extends GemElement {}
10+
11+
describe('Basic element render', async () => {
12+
it('The easiest', async () => {
13+
assert.strictEqual(1, 1);
14+
});
15+
});

‎packages/gem-ssr/tsconfig.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "@gemjs/config/tsconfig",
3+
"compilerOptions": {
4+
"outDir": "./",
5+
"verbatimModuleSyntax": false,
6+
"module": "commonjs"
7+
},
8+
"include": ["src"],
9+
"exclude": []
10+
}

‎packages/gem/src/elements/base/gesture.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class GemGestureElement extends GemElement {
7373
@state grabbing: boolean;
7474

7575
#pressed = false; // 触发 press 之后不触发其他事件
76-
#pressTimer = 0;
76+
#pressTimer: ReturnType<typeof setTimeout> | number = 0;
7777

7878
#startEventMap: Map<number, PointerEvent> = new Map();
7979

@@ -134,7 +134,7 @@ export class GemGestureElement extends GemElement {
134134
this.#startEventMap.set(evt.pointerId, evt);
135135
if (evt.isPrimary) {
136136
this.#pressed = false;
137-
this.#pressTimer = window.setTimeout(() => {
137+
this.#pressTimer = setTimeout(() => {
138138
this.press(evt);
139139
this.#pressed = true;
140140
}, 251);
@@ -187,7 +187,7 @@ export class GemGestureElement extends GemElement {
187187
Math.abs(move.clientX - startEvent.clientX) > accuracy ||
188188
Math.abs(move.clientY - startEvent.clientY) > accuracy
189189
) {
190-
window.clearTimeout(this.#pressTimer);
190+
clearTimeout(this.#pressTimer);
191191
}
192192
}
193193
};
@@ -205,7 +205,7 @@ export class GemGestureElement extends GemElement {
205205
#onEnd = async (evt: PointerEvent) => {
206206
evt.stopPropagation();
207207
const { pointerId } = evt;
208-
window.clearTimeout(this.#pressTimer);
208+
clearTimeout(this.#pressTimer);
209209

210210
if (this.movesMap.size === 1) {
211211
this.grabbing = false;

‎packages/gem/src/elements/base/route.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ class ParamsRegExp extends RegExp {
3030

3131
type Params = Record<string, string>;
3232
declare global {
33-
interface Window {
34-
// https://bugzilla.mozilla.org/show_bug.cgi?id=1731418
35-
// https://github.com/WebKit/standards-positions/issues/61
36-
URLPattern: any;
37-
}
33+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1731418
34+
// https://github.com/WebKit/standards-positions/issues/61
35+
// eslint-disable-next-line no-var
36+
var URLPattern: any;
3837
}
3938

4039
/**
@@ -51,8 +50,8 @@ declare global {
5150
* ```
5251
*/
5352
export function matchPath(pattern: string, path: string) {
54-
if (window.URLPattern) {
55-
const urLPattern = new window.URLPattern({ pathname: pattern });
53+
if (globalThis.URLPattern) {
54+
const urLPattern = new URLPattern({ pathname: pattern });
5655
const matchResult = urLPattern.exec({ pathname: path }) || urLPattern.exec({ pathname: `${path}/` });
5756
if (!matchResult) return null;
5857
return matchResult.pathname.groups as Params;

‎packages/gem/src/elements/base/title.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import { connect } from '../../lib/store';
1818
import { titleStore } from '../../lib/history';
1919

2020
// 避免重定向时的中间状态标题
21-
let timer = 0;
21+
let timer: ReturnType<typeof setTimeout> | number = 0;
2222
const setTitle = (documentTitle: string) => {
2323
clearTimeout(timer);
24-
timer = window.setTimeout(() => (document.title = documentTitle));
24+
timer = setTimeout(() => (document.title = documentTitle));
2525
};
2626

2727
function setDocumentTitle(defaultTitle?: string | null, prefix = '', suffix = '') {

‎packages/gem/src/helper/mediaquery.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// https://gist.github.com/gokulkrishh/242e68d1ee94ad05f488
22
// https://mediag.com/news/popular-screen-resolutions-designing-for-all/
33

4-
const match = (query: string) => window.matchMedia(query).matches;
4+
const match = (query: string) => matchMedia(query).matches;
55

66
export const mediaQuery = {
77
PRINT: 'print',

‎packages/gem/src/helper/ssr-shim.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/// 简单的垫片,让 GemElement 服务端渲染不报错
2+
/// 渲染时将忽略 GemElement 内容
3+
14
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
// @ts-nocheck
36
if (typeof window === 'undefined') {

‎packages/gem/src/lib/element.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ export { repeat } from './repeat';
1515
export { svg, mathml, TemplateResult, createRef } from './lit-html';
1616

1717
declare global {
18-
interface Window {
19-
__GEM_DEVTOOLS__HOOK__?:
20-
| (typeof reactiveExports & typeof decoratorsExports & typeof storeExports & typeof versionExports)
21-
| Record<string, never>;
22-
}
18+
// eslint-disable-next-line no-var
19+
var __GEM_DEVTOOLS__HOOK__:
20+
| (typeof reactiveExports & typeof decoratorsExports & typeof storeExports & typeof versionExports)
21+
| Record<string, never>
22+
| undefined;
2323
}
2424

2525
// 只记录第一次定义,往往是最外层 App
26-
if (window.__GEM_DEVTOOLS__HOOK__ && !window.__GEM_DEVTOOLS__HOOK__.GemElement) {
27-
Object.assign(window.__GEM_DEVTOOLS__HOOK__, {
26+
if (globalThis.__GEM_DEVTOOLS__HOOK__ && !globalThis.__GEM_DEVTOOLS__HOOK__.GemElement) {
27+
Object.assign(globalThis.__GEM_DEVTOOLS__HOOK__, {
2828
...reactiveExports,
2929
...decoratorsExports,
3030
...storeExports,

‎packages/gem/src/lib/history.ts

+11-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { connect, createStore } from './store';
44
import { QueryString, cleanObject, GemError, absoluteLocation } from './utils';
55

6-
const nativeHistory = window.history;
6+
const nativeHistory = globalThis.history;
77
const nativePushState: typeof nativeHistory.pushState = nativeHistory.pushState.bind(nativeHistory);
88
const nativeReplaceState: typeof nativeHistory.replaceState = nativeHistory.replaceState.bind(nativeHistory);
99

@@ -107,7 +107,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
107107
const url = getUrlBarPath(path) + new QueryString(query) + hash;
108108
const prevHash = decodeURIComponent(location.hash);
109109
native(state, title, url);
110-
if (prevHash !== hash) window.dispatchEvent(new CustomEvent('hashchange'));
110+
if (prevHash !== hash) dispatchEvent(new CustomEvent('hashchange'));
111111
}
112112

113113
// 跨框架时,调用者对 basePath 无感知
@@ -128,7 +128,7 @@ function updateHistoryByNative(data: any, title: string, originUrl: string, nati
128128
const prevHash = location.hash;
129129
native(state, title, url);
130130
// `location.hash` 和 `hash` 都已经进行了 url 编码,可以直接进行相等判断
131-
if (prevHash !== hash) window.dispatchEvent(new CustomEvent('hashchange'));
131+
if (prevHash !== hash) dispatchEvent(new CustomEvent('hashchange'));
132132
}
133133

134134
const gemBasePathStore = createStore({
@@ -185,16 +185,15 @@ const gemHistory = new GemHistory();
185185

186186
const gemTitleStore = createStore({ defaultTitle: document.title, url: '', title: '' });
187187

188-
const _GEMHISTORY = { history: gemHistory, titleStore: gemTitleStore, basePathStore: gemBasePathStore };
188+
const HISTORY = { history: gemHistory, titleStore: gemTitleStore, basePathStore: gemBasePathStore };
189189

190190
declare global {
191-
interface Window {
192-
_GEMHISTORY?: typeof _GEMHISTORY;
193-
}
191+
// eslint-disable-next-line no-var
192+
var _GEMHISTORY: typeof HISTORY | undefined;
194193
}
195194

196-
if (!window._GEMHISTORY) {
197-
window._GEMHISTORY = _GEMHISTORY;
195+
if (!globalThis._GEMHISTORY) {
196+
globalThis._GEMHISTORY = HISTORY;
198197

199198
nativeHistory.pushState = function (state: any, title: string, path: string) {
200199
updateHistoryByNative(state, title, path, nativePushState);
@@ -205,7 +204,7 @@ if (!window._GEMHISTORY) {
205204
};
206205

207206
// 点击 `<a>`
208-
window.addEventListener('hashchange', ({ isTrusted }) => {
207+
addEventListener('hashchange', ({ isTrusted }) => {
209208
if (isTrusted) {
210209
gemHistory.replace({ hash: location.hash });
211210
}
@@ -250,7 +249,7 @@ if (!window._GEMHISTORY) {
250249
* 表示 popstate handler 中正在进行导航
251250
*/
252251
let navigating = false;
253-
window.addEventListener('popstate', (event) => {
252+
addEventListener('popstate', (event) => {
254253
const newState = event.state as HistoryState | null;
255254
if (!newState?.$key) {
256255
// 比如作为其他框架 app 的宿主 app
@@ -316,5 +315,5 @@ if (!window._GEMHISTORY) {
316315
});
317316
}
318317

319-
const { history, titleStore, basePathStore } = window._GEMHISTORY;
318+
const { history, titleStore, basePathStore } = globalThis._GEMHISTORY;
320319
export { history, titleStore, basePathStore };

‎pnpm-lock.yaml

+87-38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.