@ubean/pages 0.1.13 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +215 -3
- package/dist/index.js +440 -26
- package/package.json +7 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ref, ShallowRef } from "vue";
|
|
2
|
+
import { PageHead, PageHead as PageHead$1, PageHead as PageHeadMeta } from "@ubean/shared";
|
|
2
3
|
//#region src/protocol.d.ts
|
|
3
4
|
interface PageObject<Props = Record<string, unknown>> {
|
|
4
5
|
component: string;
|
|
@@ -45,8 +46,19 @@ type PageRenderResult = string | {
|
|
|
45
46
|
state?: Record<string, unknown>;
|
|
46
47
|
};
|
|
47
48
|
type PageRenderFn = (pageObj: PageObject, shellHtml: string, assetTags: PageAssetTags, renderContext?: PageRenderContext) => PageRenderResult | Promise<PageRenderResult>;
|
|
49
|
+
/**
|
|
50
|
+
* 流式渲染函数:返回一个 `ReadableStream<Uint8Array>`,将完整 HTML 文档
|
|
51
|
+
* 分块流式输出(头部先发送,app HTML 边渲染边输出,state 在尾部注入)。
|
|
52
|
+
*
|
|
53
|
+
* 由 `@ubean/ssr` 的 `createVueRenderer` 实现。当 renderer 提供此方法且
|
|
54
|
+
* 应用配置启用了 `streaming` 时,页面处理器会优先使用流式渲染,显著改善
|
|
55
|
+
* TTFB/LCP(浏览器可在 app 渲染期间提前加载 CSS/JS)。
|
|
56
|
+
*/
|
|
57
|
+
type PageStreamRenderFn = (pageObj: PageObject, shellHtml: string, assetTags: PageAssetTags, renderContext?: PageRenderContext) => ReadableStream<Uint8Array>;
|
|
48
58
|
interface PageRenderer {
|
|
49
59
|
render: PageRenderFn;
|
|
60
|
+
/** 流式渲染(可选)。提供时优先于 `render` 用于流式 SSR 响应。 */
|
|
61
|
+
renderToStream?: PageStreamRenderFn;
|
|
50
62
|
preambleScript?: string;
|
|
51
63
|
}
|
|
52
64
|
declare const PAGE_DATA_ID = "__UBEAN_PAGE_DATA__";
|
|
@@ -64,6 +76,11 @@ declare function isPagesRequest(c: {
|
|
|
64
76
|
declare function safeJsonStringify(value: unknown): string;
|
|
65
77
|
declare function serializePageData(pageObj: PageObject): string;
|
|
66
78
|
declare function pageJsonResponse(pageObj: PageObject, headers?: Record<string, string>): Response;
|
|
79
|
+
/**
|
|
80
|
+
* 生成 favicon `<link>` 标签字符串。
|
|
81
|
+
* 当 `favicon` 为空/未设置时返回空字符串。
|
|
82
|
+
*/
|
|
83
|
+
declare function renderFaviconLink(favicon?: string): string;
|
|
67
84
|
declare function buildPageShell(pageObj: PageObject, assetTags: PageAssetTags, preambleScript?: string, appId?: string, renderContext?: PageRenderContext): string;
|
|
68
85
|
declare function insertSsrContent(shell: string, appHtml: string): string;
|
|
69
86
|
/**
|
|
@@ -78,6 +95,17 @@ declare function insertSsrContent(shell: string, appHtml: string): string;
|
|
|
78
95
|
declare function insertStateContent(shell: string, stateJson: string | null): string;
|
|
79
96
|
declare function buildClientOnlyShell(pageObj: PageObject, assetTags: PageAssetTags, preambleScript?: string, appId?: string, renderContext?: PageRenderContext): string;
|
|
80
97
|
declare function renderPage(pageObj: PageObject, assetTags: PageAssetTags, renderer: PageRenderer | null, appId?: string, renderContext?: PageRenderContext): Promise<string>;
|
|
98
|
+
/**
|
|
99
|
+
* 流式渲染页面:当 renderer 提供 `renderToStream` 时,返回一个
|
|
100
|
+
* `ReadableStream<Uint8Array>`,将完整 HTML 文档分块流式输出。
|
|
101
|
+
*
|
|
102
|
+
* 当 renderer 不支持流式(无 `renderToStream`)时,回退到缓冲式 `renderPage`,
|
|
103
|
+
* 返回一个包含完整 HTML 的单块流(保持调用方接口一致)。
|
|
104
|
+
*
|
|
105
|
+
* 调用方应使用 `c.body(stream, { headers: { 'Content-Type': 'text/html; charset=utf-8' } })`
|
|
106
|
+
* 将流作为 HTTP 响应返回。
|
|
107
|
+
*/
|
|
108
|
+
declare function renderPageToStream(pageObj: PageObject, assetTags: PageAssetTags, renderer: PageRenderer | null, appId?: string, renderContext?: PageRenderContext): ReadableStream<Uint8Array>;
|
|
81
109
|
//#endregion
|
|
82
110
|
//#region src/data.d.ts
|
|
83
111
|
type DataKey = string | symbol;
|
|
@@ -90,24 +118,110 @@ interface DataCacheEntry<T = unknown> {
|
|
|
90
118
|
}
|
|
91
119
|
type DataFetcher<T> = () => T | Promise<T>;
|
|
92
120
|
declare function defineDataKey(key: string): symbol;
|
|
121
|
+
/**
|
|
122
|
+
* `__UBEAN_DATA__` script 标签 ID — SSR 渲染时,useData 解析的数据以 JSON 形式
|
|
123
|
+
* 注入到此标签中,客户端水合时读取,避免二次请求。
|
|
124
|
+
*
|
|
125
|
+
* 与 `__UBEAN_DEFERRED__`(非关键延迟数据)和 `__UBEAN_STATE__`(用户状态如 Pinia)
|
|
126
|
+
* 分离,职责单一,便于 SSG payload 提取(roadmap Task 3)。
|
|
127
|
+
*/
|
|
128
|
+
declare const DATA_PAYLOAD_ID = "__UBEAN_DATA__";
|
|
129
|
+
/**
|
|
130
|
+
* SSR payload 条目:记录 useData 解析结果,序列化到 `__UBEAN_DATA__`。
|
|
131
|
+
* `error` 以字符串形式存储(可序列化),客户端水合时重建为 Error。
|
|
132
|
+
*/
|
|
133
|
+
interface DataPayloadEntry {
|
|
134
|
+
data: unknown;
|
|
135
|
+
error: string | null;
|
|
136
|
+
timestamp: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* SSR 内部:注册一个 useData 解析结果到 payload。
|
|
140
|
+
*
|
|
141
|
+
* 在 `useData` 的 SSR 分支中调用。每次渲染前应调用 `__clearDataPayload()`。
|
|
142
|
+
* 仅 string key 可注册(symbol 无法序列化)。
|
|
143
|
+
*/
|
|
144
|
+
declare function __registerDataPayload(key: string, entry: DataPayloadEntry): void;
|
|
145
|
+
/**
|
|
146
|
+
* SSR 内部:返回所有已注册的 payload 条目,用于序列化到 `__UBEAN_DATA__`。
|
|
147
|
+
*/
|
|
148
|
+
declare function __resolveDataPayload(): Record<string, DataPayloadEntry>;
|
|
149
|
+
/**
|
|
150
|
+
* SSR 内部:清空 payload 注册表 + 全局 inflight。每次渲染前调用,
|
|
151
|
+
* 避免上一个请求的残留。
|
|
152
|
+
*/
|
|
153
|
+
declare function __clearDataPayload(): void;
|
|
154
|
+
/**
|
|
155
|
+
* SSR 内部:将 data payload 序列化为 `<script>` 标签字符串。
|
|
156
|
+
* 无数据时返回空字符串(不注入标签)。
|
|
157
|
+
*/
|
|
158
|
+
declare function __serializeDataPayload(data: Record<string, DataPayloadEntry>): string;
|
|
159
|
+
/**
|
|
160
|
+
* 重置客户端 payload 缓存(测试用)。
|
|
161
|
+
*/
|
|
162
|
+
declare function __resetDataPayloadCache(): void;
|
|
93
163
|
interface UseDataOptions<T> {
|
|
94
164
|
key?: DataKey;
|
|
95
165
|
tags?: string[];
|
|
96
166
|
ttl?: number;
|
|
97
167
|
staleWhileRevalidate?: boolean;
|
|
98
168
|
fetcher: DataFetcher<T>;
|
|
169
|
+
/**
|
|
170
|
+
* Dedupe in-flight requests with the same key (default: `true`).
|
|
171
|
+
* When enabled, concurrent calls within the same request cycle share a
|
|
172
|
+
* single Promise, preventing duplicate fetcher invocations.
|
|
173
|
+
*/
|
|
174
|
+
dedupe?: boolean;
|
|
99
175
|
}
|
|
176
|
+
type DataStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
100
177
|
interface DataResult<T> {
|
|
101
178
|
data: T | undefined;
|
|
102
179
|
error: Error | null;
|
|
103
180
|
loading: boolean;
|
|
181
|
+
/** Alias for `!loading` (Nuxt `useAsyncData` compat). */
|
|
182
|
+
pending: boolean;
|
|
183
|
+
/** Current fetch status. */
|
|
184
|
+
status: DataStatus;
|
|
104
185
|
timestamp: number | undefined;
|
|
105
186
|
invalidate: () => void;
|
|
187
|
+
/**
|
|
188
|
+
* Force re-fetch, bypassing cache and dedupe.
|
|
189
|
+
* Mutates this result object in place and resolves when done.
|
|
190
|
+
*/
|
|
191
|
+
refresh: () => Promise<void>;
|
|
106
192
|
}
|
|
107
193
|
declare function useData<T>(options: UseDataOptions<T>, context?: object): Promise<DataResult<T>>;
|
|
194
|
+
/**
|
|
195
|
+
* `useAsyncData` 选项:Nuxt 风格的 `useAsyncData(key, fn, options)` 第三参数。
|
|
196
|
+
*
|
|
197
|
+
* 是 `UseDataOptions` 的子集(去掉 `key`/`fetcher`,改用位置参数)。
|
|
198
|
+
*/
|
|
199
|
+
interface UseAsyncDataOptions {
|
|
200
|
+
tags?: string[];
|
|
201
|
+
ttl?: number;
|
|
202
|
+
staleWhileRevalidate?: boolean;
|
|
203
|
+
/** Dedupe in-flight requests (default: `true`). */
|
|
204
|
+
dedupe?: boolean;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Nuxt 风格的 `useAsyncData(key, fn, options)`,作为 `useData` 的超集/别名。
|
|
208
|
+
*
|
|
209
|
+
* 与 `useData` 的区别仅在于调用签名:位置参数 `(key, fn, options)` vs
|
|
210
|
+
* 选项对象 `{ key, fetcher, ... }`。返回值与 `useData` 完全一致。
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const { data, pending, error, refresh } = await useAsyncData(
|
|
215
|
+
* 'posts',
|
|
216
|
+
* () => fetch('/api/posts').then(r => r.json()),
|
|
217
|
+
* { ttl: 60_000 }
|
|
218
|
+
* );
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
declare function useAsyncData<T>(key: string, fn: DataFetcher<T>, options?: UseAsyncDataOptions, context?: object): Promise<DataResult<T>>;
|
|
108
222
|
declare function invalidateData(keyOrTag: DataKey | string, context?: object): number;
|
|
109
223
|
declare function invalidateAll(context?: object): void;
|
|
110
|
-
declare function
|
|
224
|
+
declare function clearPageData(context?: object): void;
|
|
111
225
|
declare function hasData(key: DataKey, context?: object): boolean;
|
|
112
226
|
interface DependencyDeclaration {
|
|
113
227
|
keys: DataKey[];
|
|
@@ -138,4 +252,102 @@ interface StreamHelper {
|
|
|
138
252
|
declare function createStreamResponse(init?: ResponseInit, onStart?: (stream: StreamHelper) => void | Promise<void>): Response;
|
|
139
253
|
declare function createSseStream(onStart?: (stream: StreamHelper) => void | Promise<void>): Response;
|
|
140
254
|
//#endregion
|
|
141
|
-
|
|
255
|
+
//#region src/defer.d.ts
|
|
256
|
+
/**
|
|
257
|
+
* `__UBEAN_DEFERRED__` script 标签 ID — SSR 流式渲染完成后,延迟数据
|
|
258
|
+
* 以 JSON 形式注入到此标签中,客户端水合时读取。
|
|
259
|
+
*/
|
|
260
|
+
declare const DEFERRED_DATA_ID = "__UBEAN_DEFERRED__";
|
|
261
|
+
/**
|
|
262
|
+
* 延迟值:包装一个 factory 函数,标记为"非关键数据"。
|
|
263
|
+
*
|
|
264
|
+
* SSR 时不阻塞初始渲染,数据在主内容之后流式注入。
|
|
265
|
+
*/
|
|
266
|
+
interface DeferredValue<T> {
|
|
267
|
+
readonly __isDeferred: true;
|
|
268
|
+
readonly factory: () => Promise<T>;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 标记一个 Promise 为可延迟的(非关键数据)。
|
|
272
|
+
*
|
|
273
|
+
* 在 SSR 流式渲染中,`defer()` 包装的 Promise 不会阻塞初始 HTML 输出。
|
|
274
|
+
* 主内容渲染完成后,延迟数据解析结果作为 `<script>` 标签流式注入,
|
|
275
|
+
* 客户端水合后立即可用。
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* // 阻塞初始渲染(关键数据)
|
|
280
|
+
* const critical = await fetchCritical();
|
|
281
|
+
* // 不阻塞,流式输出(非关键数据)
|
|
282
|
+
* const nonCritical = defer(() => fetchNonCritical());
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
declare function defer<T>(factory: (() => Promise<T>) | Promise<T>): DeferredValue<T>;
|
|
286
|
+
/**
|
|
287
|
+
* 判断值是否为 DeferredValue(类型守卫)。
|
|
288
|
+
*/
|
|
289
|
+
declare function isDeferredValue<T>(value: unknown): value is DeferredValue<T>;
|
|
290
|
+
/**
|
|
291
|
+
* SSR 内部:注册一个 deferred promise,在流式渲染后统一解析。
|
|
292
|
+
*
|
|
293
|
+
* 在 `useDeferredData` 的 SSR 分支中调用。每次渲染前应调用 `__clearDeferred()`。
|
|
294
|
+
*/
|
|
295
|
+
declare function __registerDeferred(key: string, promise: Promise<unknown>): void;
|
|
296
|
+
/**
|
|
297
|
+
* SSR 内部:解析所有已注册的 deferred promise,返回 key → data 映射。
|
|
298
|
+
*
|
|
299
|
+
* 失败的 promise 以 `{ __deferredError: message }` 形式包含在结果中,
|
|
300
|
+
* 不影响其他 promise 的解析。
|
|
301
|
+
*/
|
|
302
|
+
declare function __resolveDeferred(): Promise<Record<string, unknown>>;
|
|
303
|
+
/**
|
|
304
|
+
* SSR 内部:清空注册表。每次渲染前调用,避免上一个请求的残留。
|
|
305
|
+
*/
|
|
306
|
+
declare function __clearDeferred(): void;
|
|
307
|
+
/**
|
|
308
|
+
* SSR 内部:将 deferred 数据序列化为 `<script>` 标签字符串。
|
|
309
|
+
*
|
|
310
|
+
* 无数据时返回空字符串(不注入标签)。
|
|
311
|
+
*/
|
|
312
|
+
declare function __serializeDeferred(data: Record<string, unknown>): string;
|
|
313
|
+
/**
|
|
314
|
+
* 重置客户端缓存(测试用)。
|
|
315
|
+
*/
|
|
316
|
+
declare function __resetDeferredCache(): void;
|
|
317
|
+
interface UseDeferredDataResult<T> {
|
|
318
|
+
/** 延迟数据(初始为 undefined,解析后更新) */
|
|
319
|
+
data: ShallowRef<T | undefined>;
|
|
320
|
+
/** 是否正在获取 */
|
|
321
|
+
pending: Ref<boolean>;
|
|
322
|
+
/** 错误信息(获取失败时设置) */
|
|
323
|
+
error: ShallowRef<Error | null>;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 在组件中使用延迟数据。
|
|
327
|
+
*
|
|
328
|
+
* - **SSR**: 注册 promise 但不阻塞渲染,组件渲染 fallback;渲染完成后数据流式注入
|
|
329
|
+
* - **客户端水合**: 从 `__UBEAN_DEFERRED__` 读取已解析数据,立即显示(无闪烁)
|
|
330
|
+
* - **客户端导航**: 调用 factory 获取数据,显示 pending → resolved
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```vue
|
|
334
|
+
* <script setup>
|
|
335
|
+
* import { defer, useDeferredData } from 'ubean';
|
|
336
|
+
*
|
|
337
|
+
* const { data: comments, pending } = useDeferredData(
|
|
338
|
+
* 'comments',
|
|
339
|
+
* defer(() => fetch('/api/comments').then(r => r.json()))
|
|
340
|
+
* );
|
|
341
|
+
* </script>
|
|
342
|
+
*
|
|
343
|
+
* <template>
|
|
344
|
+
* <div v-if="pending">Loading comments...</div>
|
|
345
|
+
* <ul v-else>
|
|
346
|
+
* <li v-for="c in comments" :key="c.id">{{ c.text }}</li>
|
|
347
|
+
* </ul>
|
|
348
|
+
* </template>
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
declare function useDeferredData<T>(key: string, deferred: DeferredValue<T>): UseDeferredDataResult<T>;
|
|
352
|
+
//#endregion
|
|
353
|
+
export { DATA_PAYLOAD_ID, DEFERRED_DATA_ID, type DataCacheEntry, type DataKey, type DataPayloadEntry, type DataResult, type DataStatus, type DeferredValue, type DependencyDeclaration, type InternalFetchOptions, LOCALE_DATA_ID, type LocaleMetaInfo, PAGE_DATA_ID, PAGE_REQUEST_HEADER, type PageAssetTags, type PageHead, type PageHeadMeta, type PageObject, type PageRenderContext, type PageRenderFn, type PageRenderResult, type PageRenderer, type PageStreamRenderFn, SSR_CONTENT_MARKER, STATE_DATA_ID, STATE_MARKER, type StreamHelper, type UseAsyncDataOptions, type UseDataOptions, type UseDeferredDataResult, __clearDataPayload, __clearDeferred, __registerDataPayload, __registerDeferred, __resetDataPayloadCache, __resetDeferredCache, __resolveDataPayload, __resolveDeferred, __serializeDataPayload, __serializeDeferred, buildClientOnlyShell, buildPageShell, clearPageData, createInternalFetch, createSseStream, createStreamResponse, declareDependencies, defer, defineDataKey, getInvalidatedKeysForAction, hasData, insertSsrContent, insertStateContent, invalidateAll, invalidateData, isDeferredValue, isPagesRequest, pageJsonResponse, renderFaviconLink, renderPage, renderPageToStream, safeJsonStringify, serializePageData, useAsyncData, useData, useDeferredData, withDependencies };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ref, shallowRef } from "vue";
|
|
1
2
|
//#region src/protocol.ts
|
|
2
3
|
const PAGE_DATA_ID = "__UBEAN_PAGE_DATA__";
|
|
3
4
|
const LOCALE_DATA_ID = "__UBEAN_LOCALE__";
|
|
@@ -189,12 +190,35 @@ async function renderPage(pageObj, assetTags, renderer, appId = "app", renderCon
|
|
|
189
190
|
else finalHtml = insertStateContent(finalHtml, "");
|
|
190
191
|
return finalHtml;
|
|
191
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* 流式渲染页面:当 renderer 提供 `renderToStream` 时,返回一个
|
|
195
|
+
* `ReadableStream<Uint8Array>`,将完整 HTML 文档分块流式输出。
|
|
196
|
+
*
|
|
197
|
+
* 当 renderer 不支持流式(无 `renderToStream`)时,回退到缓冲式 `renderPage`,
|
|
198
|
+
* 返回一个包含完整 HTML 的单块流(保持调用方接口一致)。
|
|
199
|
+
*
|
|
200
|
+
* 调用方应使用 `c.body(stream, { headers: { 'Content-Type': 'text/html; charset=utf-8' } })`
|
|
201
|
+
* 将流作为 HTTP 响应返回。
|
|
202
|
+
*/
|
|
203
|
+
function renderPageToStream(pageObj, assetTags, renderer, appId = "app", renderContext) {
|
|
204
|
+
if (!renderer || !renderer.renderToStream) {
|
|
205
|
+
const encoder = new TextEncoder();
|
|
206
|
+
return new ReadableStream({ async start(controller) {
|
|
207
|
+
const html = await renderPage(pageObj, assetTags, renderer, appId, renderContext);
|
|
208
|
+
controller.enqueue(encoder.encode(html));
|
|
209
|
+
controller.close();
|
|
210
|
+
} });
|
|
211
|
+
}
|
|
212
|
+
const shell = buildPageShell(pageObj, assetTags, renderer.preambleScript ?? "", appId, renderContext);
|
|
213
|
+
return renderer.renderToStream(pageObj, shell, assetTags, renderContext);
|
|
214
|
+
}
|
|
192
215
|
//#endregion
|
|
193
216
|
//#region src/data.ts
|
|
194
217
|
function createRegistry() {
|
|
195
218
|
return {
|
|
196
219
|
entries: /* @__PURE__ */ new Map(),
|
|
197
|
-
tagIndex: /* @__PURE__ */ new Map()
|
|
220
|
+
tagIndex: /* @__PURE__ */ new Map(),
|
|
221
|
+
inflight: /* @__PURE__ */ new Map()
|
|
198
222
|
};
|
|
199
223
|
}
|
|
200
224
|
const globalRegistry = createRegistry();
|
|
@@ -213,24 +237,171 @@ function getRegistry(context) {
|
|
|
213
237
|
function defineDataKey(key) {
|
|
214
238
|
return Symbol.for(`ubean:data:${key}`);
|
|
215
239
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
240
|
+
/**
|
|
241
|
+
* `__UBEAN_DATA__` script 标签 ID — SSR 渲染时,useData 解析的数据以 JSON 形式
|
|
242
|
+
* 注入到此标签中,客户端水合时读取,避免二次请求。
|
|
243
|
+
*
|
|
244
|
+
* 与 `__UBEAN_DEFERRED__`(非关键延迟数据)和 `__UBEAN_STATE__`(用户状态如 Pinia)
|
|
245
|
+
* 分离,职责单一,便于 SSG payload 提取(roadmap Task 3)。
|
|
246
|
+
*/
|
|
247
|
+
const DATA_PAYLOAD_ID = "__UBEAN_DATA__";
|
|
248
|
+
const ssrPayload = /* @__PURE__ */ new Map();
|
|
249
|
+
/**
|
|
250
|
+
* SSR 内部:注册一个 useData 解析结果到 payload。
|
|
251
|
+
*
|
|
252
|
+
* 在 `useData` 的 SSR 分支中调用。每次渲染前应调用 `__clearDataPayload()`。
|
|
253
|
+
* 仅 string key 可注册(symbol 无法序列化)。
|
|
254
|
+
*/
|
|
255
|
+
function __registerDataPayload(key, entry) {
|
|
256
|
+
ssrPayload.set(key, entry);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* SSR 内部:返回所有已注册的 payload 条目,用于序列化到 `__UBEAN_DATA__`。
|
|
260
|
+
*/
|
|
261
|
+
function __resolveDataPayload() {
|
|
262
|
+
return Object.fromEntries(ssrPayload);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* SSR 内部:清空 payload 注册表 + 全局 inflight。每次渲染前调用,
|
|
266
|
+
* 避免上一个请求的残留。
|
|
267
|
+
*/
|
|
268
|
+
function __clearDataPayload() {
|
|
269
|
+
ssrPayload.clear();
|
|
270
|
+
globalRegistry.inflight.clear();
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* SSR 内部:将 data payload 序列化为 `<script>` 标签字符串。
|
|
274
|
+
* 无数据时返回空字符串(不注入标签)。
|
|
275
|
+
*/
|
|
276
|
+
function __serializeDataPayload(data) {
|
|
277
|
+
if (Object.keys(data).length === 0) return "";
|
|
278
|
+
return `<script id="${DATA_PAYLOAD_ID}" type="application/json">${safeJsonStringify(data)}<\/script>`;
|
|
279
|
+
}
|
|
280
|
+
let clientPayloadCache;
|
|
281
|
+
/**
|
|
282
|
+
* 客户端:实际执行 payload 解析。
|
|
283
|
+
*
|
|
284
|
+
* 优先级:
|
|
285
|
+
* 1. `globalThis.__UBEAN_DATA_PAYLOAD__`(SSG 模式注入,可能是 Promise / 对象 / null)
|
|
286
|
+
* - Promise → await,解析失败返回 null
|
|
287
|
+
* - 对象 → 直接使用
|
|
288
|
+
* - null/undefined → 降级到 DOM 路径
|
|
289
|
+
* 2. DOM 读取 `__UBEAN_DATA__` script 标签(SSR/无 SSG 提取场景)
|
|
290
|
+
*
|
|
291
|
+
* SSR 环境(无 document)返回 `null`。
|
|
292
|
+
*/
|
|
293
|
+
async function computeClientPayload() {
|
|
294
|
+
const injected = globalThis.__UBEAN_DATA_PAYLOAD__;
|
|
295
|
+
if (injected !== void 0) try {
|
|
296
|
+
const value = injected instanceof Promise ? await injected : injected;
|
|
297
|
+
if (value && typeof value === "object") return value;
|
|
298
|
+
return null;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (typeof document === "undefined") return null;
|
|
303
|
+
const el = document.getElementById(DATA_PAYLOAD_ID);
|
|
304
|
+
if (!el?.textContent) return null;
|
|
222
305
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
306
|
+
return JSON.parse(el.textContent);
|
|
307
|
+
} catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 客户端:读取 payload,带 in-flight 去重缓存。
|
|
313
|
+
*
|
|
314
|
+
* 首次调用启动计算并存 Promise 到缓存(去重并发调用);解析后用结果替换 Promise,
|
|
315
|
+
* 后续调用直接命中同步缓存。
|
|
316
|
+
* SSR 环境返回 `null`。
|
|
317
|
+
*/
|
|
318
|
+
function readClientPayload() {
|
|
319
|
+
const cached = clientPayloadCache;
|
|
320
|
+
if (cached !== void 0) return Promise.resolve(cached);
|
|
321
|
+
const promise = computeClientPayload();
|
|
322
|
+
clientPayloadCache = promise;
|
|
323
|
+
return promise.then((value) => {
|
|
324
|
+
clientPayloadCache = value;
|
|
325
|
+
return value;
|
|
326
|
+
}, () => {
|
|
327
|
+
clientPayloadCache = null;
|
|
328
|
+
return null;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 重置客户端 payload 缓存(测试用)。
|
|
333
|
+
*/
|
|
334
|
+
function __resetDataPayloadCache() {
|
|
335
|
+
clientPayloadCache = void 0;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 客户端水合:从 payload 缓存中读取指定 key 的初始数据,注入到 data registry。
|
|
339
|
+
*
|
|
340
|
+
* 在 `useData` 的客户端分支中调用:若 payload 命中,将数据写入 registry,
|
|
341
|
+
* 后续逻辑直接走缓存命中路径,避免二次请求。
|
|
342
|
+
* 仅处理 string key,且仅当 registry 中尚无该 key 时注入(不覆盖已有缓存)。
|
|
343
|
+
*
|
|
344
|
+
* @returns true 表示 payload 命中并已注入
|
|
345
|
+
*/
|
|
346
|
+
async function hydrateFromPayload(key, registry) {
|
|
347
|
+
if (typeof key !== "string") return false;
|
|
348
|
+
if (registry.entries.has(key)) return false;
|
|
349
|
+
const payload = await readClientPayload();
|
|
350
|
+
if (!payload) return false;
|
|
351
|
+
const entry = payload[key];
|
|
352
|
+
if (!entry) return false;
|
|
353
|
+
if (entry.error !== null) return false;
|
|
354
|
+
registry.entries.set(key, {
|
|
355
|
+
key,
|
|
356
|
+
data: entry.data,
|
|
357
|
+
timestamp: entry.timestamp,
|
|
358
|
+
tags: void 0
|
|
359
|
+
});
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 判断当前是否为 SSR 环境(无 `window`)。
|
|
364
|
+
* 与 `defer.ts` 保持一致的环境检测方式。
|
|
365
|
+
*/
|
|
366
|
+
function isSSR() {
|
|
367
|
+
return typeof window === "undefined";
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 将解析结果注册到 SSR payload(仅 string key + SSR 环境)。
|
|
371
|
+
*/
|
|
372
|
+
function registerSSRPayload(key, data, error) {
|
|
373
|
+
if (!isSSR()) return;
|
|
374
|
+
if (typeof key !== "string") return;
|
|
375
|
+
__registerDataPayload(key, {
|
|
376
|
+
data: error ? void 0 : data,
|
|
377
|
+
error: error ? error instanceof Error ? error.message : String(error) : null,
|
|
378
|
+
timestamp: Date.now()
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 执行 fetcher,管理 inflight 注册 + cache 写入 + tag 索引 + SSR payload 注册。
|
|
383
|
+
* dedupe 决定是否复用同 key 的 in-flight Promise。
|
|
384
|
+
*/
|
|
385
|
+
async function executeFetch(options, key, registry, _context) {
|
|
386
|
+
const dedupe = options.dedupe !== false;
|
|
387
|
+
if (dedupe) {
|
|
388
|
+
const existing = registry.inflight.get(key);
|
|
389
|
+
if (existing) try {
|
|
390
|
+
return {
|
|
391
|
+
data: await existing,
|
|
392
|
+
error: null
|
|
393
|
+
};
|
|
394
|
+
} catch (err) {
|
|
395
|
+
return {
|
|
396
|
+
data: void 0,
|
|
397
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
231
398
|
};
|
|
232
399
|
}
|
|
233
|
-
|
|
400
|
+
}
|
|
401
|
+
const promise = Promise.resolve(options.fetcher());
|
|
402
|
+
if (dedupe) registry.inflight.set(key, promise);
|
|
403
|
+
try {
|
|
404
|
+
const data = await promise;
|
|
234
405
|
const entry = {
|
|
235
406
|
key,
|
|
236
407
|
data,
|
|
@@ -247,22 +418,99 @@ async function useData(options, context) {
|
|
|
247
418
|
}
|
|
248
419
|
tagSet.add(key);
|
|
249
420
|
}
|
|
421
|
+
registerSSRPayload(key, data, null);
|
|
250
422
|
return {
|
|
251
423
|
data,
|
|
252
|
-
error: null
|
|
253
|
-
loading: false,
|
|
254
|
-
timestamp: entry.timestamp,
|
|
255
|
-
invalidate
|
|
424
|
+
error: null
|
|
256
425
|
};
|
|
257
426
|
} catch (err) {
|
|
427
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
428
|
+
registerSSRPayload(key, void 0, error);
|
|
258
429
|
return {
|
|
259
430
|
data: void 0,
|
|
260
|
-
error
|
|
261
|
-
loading: false,
|
|
262
|
-
timestamp: void 0,
|
|
263
|
-
invalidate
|
|
431
|
+
error
|
|
264
432
|
};
|
|
433
|
+
} finally {
|
|
434
|
+
if (dedupe) registry.inflight.delete(key);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function useData(options, context) {
|
|
438
|
+
const registry = getRegistry(context);
|
|
439
|
+
const key = options.key || Symbol();
|
|
440
|
+
const invalidate = () => {
|
|
441
|
+
invalidateData(key, context);
|
|
442
|
+
};
|
|
443
|
+
if (!isSSR()) await hydrateFromPayload(key, registry);
|
|
444
|
+
const result = {
|
|
445
|
+
data: void 0,
|
|
446
|
+
error: null,
|
|
447
|
+
loading: false,
|
|
448
|
+
pending: false,
|
|
449
|
+
status: "idle",
|
|
450
|
+
timestamp: void 0,
|
|
451
|
+
invalidate,
|
|
452
|
+
refresh: async () => {
|
|
453
|
+
invalidateData(key, context);
|
|
454
|
+
result.loading = true;
|
|
455
|
+
result.pending = true;
|
|
456
|
+
result.status = "pending";
|
|
457
|
+
const { data, error } = await executeFetch(options, key, registry, context);
|
|
458
|
+
result.data = data;
|
|
459
|
+
result.error = error;
|
|
460
|
+
result.loading = false;
|
|
461
|
+
result.pending = false;
|
|
462
|
+
result.status = error ? "error" : "success";
|
|
463
|
+
result.timestamp = registry.entries.get(key)?.timestamp;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const cached = registry.entries.get(key);
|
|
467
|
+
if (cached) {
|
|
468
|
+
if (!(cached.ttl !== void 0 && Date.now() - cached.timestamp > cached.ttl)) {
|
|
469
|
+
result.data = cached.data;
|
|
470
|
+
result.error = null;
|
|
471
|
+
result.loading = false;
|
|
472
|
+
result.pending = false;
|
|
473
|
+
result.status = "success";
|
|
474
|
+
result.timestamp = cached.timestamp;
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
265
477
|
}
|
|
478
|
+
result.loading = true;
|
|
479
|
+
result.pending = true;
|
|
480
|
+
result.status = "pending";
|
|
481
|
+
const { data, error } = await executeFetch(options, key, registry, context);
|
|
482
|
+
result.data = data;
|
|
483
|
+
result.error = error;
|
|
484
|
+
result.loading = false;
|
|
485
|
+
result.pending = false;
|
|
486
|
+
result.status = error ? "error" : "success";
|
|
487
|
+
result.timestamp = registry.entries.get(key)?.timestamp;
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Nuxt 风格的 `useAsyncData(key, fn, options)`,作为 `useData` 的超集/别名。
|
|
492
|
+
*
|
|
493
|
+
* 与 `useData` 的区别仅在于调用签名:位置参数 `(key, fn, options)` vs
|
|
494
|
+
* 选项对象 `{ key, fetcher, ... }`。返回值与 `useData` 完全一致。
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* ```ts
|
|
498
|
+
* const { data, pending, error, refresh } = await useAsyncData(
|
|
499
|
+
* 'posts',
|
|
500
|
+
* () => fetch('/api/posts').then(r => r.json()),
|
|
501
|
+
* { ttl: 60_000 }
|
|
502
|
+
* );
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
async function useAsyncData(key, fn, options, context) {
|
|
506
|
+
return useData({
|
|
507
|
+
key,
|
|
508
|
+
fetcher: fn,
|
|
509
|
+
tags: options?.tags,
|
|
510
|
+
ttl: options?.ttl,
|
|
511
|
+
staleWhileRevalidate: options?.staleWhileRevalidate,
|
|
512
|
+
dedupe: options?.dedupe
|
|
513
|
+
}, context);
|
|
266
514
|
}
|
|
267
515
|
function invalidateData(keyOrTag, context) {
|
|
268
516
|
const registry = getRegistry(context);
|
|
@@ -300,8 +548,9 @@ function invalidateAll(context) {
|
|
|
300
548
|
const registry = getRegistry(context);
|
|
301
549
|
registry.entries.clear();
|
|
302
550
|
registry.tagIndex.clear();
|
|
551
|
+
registry.inflight.clear();
|
|
303
552
|
}
|
|
304
|
-
function
|
|
553
|
+
function clearPageData(context) {
|
|
305
554
|
invalidateAll(context);
|
|
306
555
|
}
|
|
307
556
|
function hasData(key, context) {
|
|
@@ -426,4 +675,169 @@ function createSseStream(onStart) {
|
|
|
426
675
|
} });
|
|
427
676
|
}
|
|
428
677
|
//#endregion
|
|
429
|
-
|
|
678
|
+
//#region src/defer.ts
|
|
679
|
+
/**
|
|
680
|
+
* `__UBEAN_DEFERRED__` script 标签 ID — SSR 流式渲染完成后,延迟数据
|
|
681
|
+
* 以 JSON 形式注入到此标签中,客户端水合时读取。
|
|
682
|
+
*/
|
|
683
|
+
const DEFERRED_DATA_ID = "__UBEAN_DEFERRED__";
|
|
684
|
+
/**
|
|
685
|
+
* 标记一个 Promise 为可延迟的(非关键数据)。
|
|
686
|
+
*
|
|
687
|
+
* 在 SSR 流式渲染中,`defer()` 包装的 Promise 不会阻塞初始 HTML 输出。
|
|
688
|
+
* 主内容渲染完成后,延迟数据解析结果作为 `<script>` 标签流式注入,
|
|
689
|
+
* 客户端水合后立即可用。
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* // 阻塞初始渲染(关键数据)
|
|
694
|
+
* const critical = await fetchCritical();
|
|
695
|
+
* // 不阻塞,流式输出(非关键数据)
|
|
696
|
+
* const nonCritical = defer(() => fetchNonCritical());
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
function defer(factory) {
|
|
700
|
+
return {
|
|
701
|
+
__isDeferred: true,
|
|
702
|
+
factory: typeof factory === "function" ? factory : () => factory
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* 判断值是否为 DeferredValue(类型守卫)。
|
|
707
|
+
*/
|
|
708
|
+
function isDeferredValue(value) {
|
|
709
|
+
return value !== null && typeof value === "object" && value.__isDeferred === true;
|
|
710
|
+
}
|
|
711
|
+
const entries = [];
|
|
712
|
+
/**
|
|
713
|
+
* SSR 内部:注册一个 deferred promise,在流式渲染后统一解析。
|
|
714
|
+
*
|
|
715
|
+
* 在 `useDeferredData` 的 SSR 分支中调用。每次渲染前应调用 `__clearDeferred()`。
|
|
716
|
+
*/
|
|
717
|
+
function __registerDeferred(key, promise) {
|
|
718
|
+
entries.push({
|
|
719
|
+
key,
|
|
720
|
+
promise
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* SSR 内部:解析所有已注册的 deferred promise,返回 key → data 映射。
|
|
725
|
+
*
|
|
726
|
+
* 失败的 promise 以 `{ __deferredError: message }` 形式包含在结果中,
|
|
727
|
+
* 不影响其他 promise 的解析。
|
|
728
|
+
*/
|
|
729
|
+
async function __resolveDeferred() {
|
|
730
|
+
if (entries.length === 0) return {};
|
|
731
|
+
const results = {};
|
|
732
|
+
await Promise.allSettled(entries.map(async (entry) => {
|
|
733
|
+
try {
|
|
734
|
+
results[entry.key] = await entry.promise;
|
|
735
|
+
} catch (err) {
|
|
736
|
+
results[entry.key] = { __deferredError: err instanceof Error ? err.message : String(err) };
|
|
737
|
+
}
|
|
738
|
+
}));
|
|
739
|
+
return results;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* SSR 内部:清空注册表。每次渲染前调用,避免上一个请求的残留。
|
|
743
|
+
*/
|
|
744
|
+
function __clearDeferred() {
|
|
745
|
+
entries.length = 0;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* SSR 内部:将 deferred 数据序列化为 `<script>` 标签字符串。
|
|
749
|
+
*
|
|
750
|
+
* 无数据时返回空字符串(不注入标签)。
|
|
751
|
+
*/
|
|
752
|
+
function __serializeDeferred(data) {
|
|
753
|
+
if (Object.keys(data).length === 0) return "";
|
|
754
|
+
return `<script id="${DEFERRED_DATA_ID}" type="application/json">${safeJsonStringify(data)}<\/script>`;
|
|
755
|
+
}
|
|
756
|
+
let clientCache;
|
|
757
|
+
/**
|
|
758
|
+
* 客户端:从 DOM 读取 `__UBEAN_DEFERRED__` script 标签内容。
|
|
759
|
+
*
|
|
760
|
+
* 首次调用后缓存结果,后续调用直接返回缓存。
|
|
761
|
+
* SSR 环境返回 `null`。
|
|
762
|
+
*/
|
|
763
|
+
function readClientCache() {
|
|
764
|
+
const cached = clientCache;
|
|
765
|
+
if (cached !== void 0) return cached;
|
|
766
|
+
let result;
|
|
767
|
+
if (typeof document === "undefined") result = null;
|
|
768
|
+
else {
|
|
769
|
+
const el = document.getElementById(DEFERRED_DATA_ID);
|
|
770
|
+
if (!el?.textContent) result = {};
|
|
771
|
+
else try {
|
|
772
|
+
result = JSON.parse(el.textContent);
|
|
773
|
+
} catch {
|
|
774
|
+
result = {};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
clientCache = result;
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* 重置客户端缓存(测试用)。
|
|
782
|
+
*/
|
|
783
|
+
function __resetDeferredCache() {
|
|
784
|
+
clientCache = void 0;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* 在组件中使用延迟数据。
|
|
788
|
+
*
|
|
789
|
+
* - **SSR**: 注册 promise 但不阻塞渲染,组件渲染 fallback;渲染完成后数据流式注入
|
|
790
|
+
* - **客户端水合**: 从 `__UBEAN_DEFERRED__` 读取已解析数据,立即显示(无闪烁)
|
|
791
|
+
* - **客户端导航**: 调用 factory 获取数据,显示 pending → resolved
|
|
792
|
+
*
|
|
793
|
+
* @example
|
|
794
|
+
* ```vue
|
|
795
|
+
* <script setup>
|
|
796
|
+
* import { defer, useDeferredData } from 'ubean';
|
|
797
|
+
*
|
|
798
|
+
* const { data: comments, pending } = useDeferredData(
|
|
799
|
+
* 'comments',
|
|
800
|
+
* defer(() => fetch('/api/comments').then(r => r.json()))
|
|
801
|
+
* );
|
|
802
|
+
* <\/script>
|
|
803
|
+
*
|
|
804
|
+
* <template>
|
|
805
|
+
* <div v-if="pending">Loading comments...</div>
|
|
806
|
+
* <ul v-else>
|
|
807
|
+
* <li v-for="c in comments" :key="c.id">{{ c.text }}</li>
|
|
808
|
+
* </ul>
|
|
809
|
+
* </template>
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
function useDeferredData(key, deferred) {
|
|
813
|
+
const data = shallowRef(void 0);
|
|
814
|
+
const pending = ref(true);
|
|
815
|
+
const error = shallowRef(null);
|
|
816
|
+
if (typeof window === "undefined") {
|
|
817
|
+
__registerDeferred(key, deferred.factory());
|
|
818
|
+
pending.value = true;
|
|
819
|
+
} else {
|
|
820
|
+
const cached = readClientCache()?.[key];
|
|
821
|
+
if (cached !== void 0) {
|
|
822
|
+
if (cached !== null && typeof cached === "object" && "__deferredError" in cached) error.value = new Error(String(cached.__deferredError));
|
|
823
|
+
else data.value = cached;
|
|
824
|
+
pending.value = false;
|
|
825
|
+
} else {
|
|
826
|
+
pending.value = true;
|
|
827
|
+
deferred.factory().then((result) => {
|
|
828
|
+
data.value = result;
|
|
829
|
+
pending.value = false;
|
|
830
|
+
}).catch((err) => {
|
|
831
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
832
|
+
pending.value = false;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
data,
|
|
838
|
+
pending,
|
|
839
|
+
error
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
843
|
+
export { DATA_PAYLOAD_ID, DEFERRED_DATA_ID, LOCALE_DATA_ID, PAGE_DATA_ID, PAGE_REQUEST_HEADER, SSR_CONTENT_MARKER, STATE_DATA_ID, STATE_MARKER, __clearDataPayload, __clearDeferred, __registerDataPayload, __registerDeferred, __resetDataPayloadCache, __resetDeferredCache, __resolveDataPayload, __resolveDeferred, __serializeDataPayload, __serializeDeferred, buildClientOnlyShell, buildPageShell, clearPageData, createInternalFetch, createSseStream, createStreamResponse, declareDependencies, defer, defineDataKey, getInvalidatedKeysForAction, hasData, insertSsrContent, insertStateContent, invalidateAll, invalidateData, isDeferredValue, isPagesRequest, pageJsonResponse, renderFaviconLink, renderPage, renderPageToStream, safeJsonStringify, serializePageData, useAsyncData, useData, useDeferredData, withDependencies };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/pages",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Page data protocol and isomorphic data layer for ubean (PageObject, useData, invalidateData)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@ubean/
|
|
19
|
+
"@ubean/shared": "0.2.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^26.
|
|
22
|
+
"@types/node": "^26.2.0",
|
|
23
23
|
"typescript": "7.0.2",
|
|
24
|
-
"vite-plus": "0.2.
|
|
24
|
+
"vite-plus": "0.2.9"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"vue": ">=3.4.0"
|
|
25
28
|
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build": "vp pack",
|