@thestarweb/star-framework-app-web 0.1.0

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.
Files changed (147) hide show
  1. package/dist/browser/index.js +8 -0
  2. package/dist/browser/index.js.map +1 -0
  3. package/dist/cli/dev-server.js +64 -0
  4. package/dist/cli/dev-server.js.map +1 -0
  5. package/dist/cli/index.js +117 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/types.js +2 -0
  8. package/dist/cli/types.js.map +1 -0
  9. package/dist/cli-helper.js +2 -0
  10. package/dist/cli-helper.js.map +1 -0
  11. package/dist/index.js +14 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/interceptor/index.js +26 -0
  14. package/dist/interceptor/index.js.map +1 -0
  15. package/dist/interceptor/middleware-to-interceptor/index.js +32 -0
  16. package/dist/interceptor/middleware-to-interceptor/index.js.map +1 -0
  17. package/dist/interceptor/middleware-to-interceptor/mock-res.js +241 -0
  18. package/dist/interceptor/middleware-to-interceptor/mock-res.js.map +1 -0
  19. package/dist/interceptor/middleware-to-interceptor/mook-req.js +180 -0
  20. package/dist/interceptor/middleware-to-interceptor/mook-req.js.map +1 -0
  21. package/dist/interceptor/types.js +6 -0
  22. package/dist/interceptor/types.js.map +1 -0
  23. package/dist/params/index.js +10 -0
  24. package/dist/params/index.js.map +1 -0
  25. package/dist/request/index.js +46 -0
  26. package/dist/request/index.js.map +1 -0
  27. package/dist/return-types/index.js +2 -0
  28. package/dist/return-types/index.js.map +1 -0
  29. package/dist/return-types/response-with-meta.js +25 -0
  30. package/dist/return-types/response-with-meta.js.map +1 -0
  31. package/dist/route/index.js +20 -0
  32. package/dist/route/index.js.map +1 -0
  33. package/dist/route/types.js +17 -0
  34. package/dist/route/types.js.map +1 -0
  35. package/dist/runtime/index.js +84 -0
  36. package/dist/runtime/index.js.map +1 -0
  37. package/dist/runtime/type.js +11 -0
  38. package/dist/runtime/type.js.map +1 -0
  39. package/dist/server/body/helper.js +118 -0
  40. package/dist/server/body/helper.js.map +1 -0
  41. package/dist/server/body/json.js +211 -0
  42. package/dist/server/body/json.js.map +1 -0
  43. package/dist/server/body/types.js +6 -0
  44. package/dist/server/body/types.js.map +1 -0
  45. package/dist/server/dump-api-type.js +59 -0
  46. package/dist/server/dump-api-type.js.map +1 -0
  47. package/dist/server/index.js +103 -0
  48. package/dist/server/index.js.map +1 -0
  49. package/dist/server/types.js +8 -0
  50. package/dist/server/types.js.map +1 -0
  51. package/dist/server/utils/pass-through-readable.js +13 -0
  52. package/dist/server/utils/pass-through-readable.js.map +1 -0
  53. package/dist/view/const.js +2 -0
  54. package/dist/view/const.js.map +1 -0
  55. package/dist/view/data-context/conetext.js +70 -0
  56. package/dist/view/data-context/conetext.js.map +1 -0
  57. package/dist/view/data-context/index.js +13 -0
  58. package/dist/view/data-context/index.js.map +1 -0
  59. package/dist/view/html/create-element.js +20 -0
  60. package/dist/view/html/create-element.js.map +1 -0
  61. package/dist/view/html/index.js +4 -0
  62. package/dist/view/html/index.js.map +1 -0
  63. package/dist/view/html/parse-html-to-v-dom.js +317 -0
  64. package/dist/view/html/parse-html-to-v-dom.js.map +1 -0
  65. package/dist/view/html/render-v-dom-to-string.js +46 -0
  66. package/dist/view/html/render-v-dom-to-string.js.map +1 -0
  67. package/dist/view/html/types.js +21 -0
  68. package/dist/view/html/types.js.map +1 -0
  69. package/dist/view/index.js +7 -0
  70. package/dist/view/index.js.map +1 -0
  71. package/dist/view/layout.js +24 -0
  72. package/dist/view/layout.js.map +1 -0
  73. package/dist/view/render/html-render.js +21 -0
  74. package/dist/view/render/html-render.js.map +1 -0
  75. package/dist/view/render/index.js +3 -0
  76. package/dist/view/render/index.js.map +1 -0
  77. package/dist/view/render/render.js +183 -0
  78. package/dist/view/render/render.js.map +1 -0
  79. package/dist/view/render/sf-v-dom-render.js +40 -0
  80. package/dist/view/render/sf-v-dom-render.js.map +1 -0
  81. package/dist/view/types.js +10 -0
  82. package/dist/view/types.js.map +1 -0
  83. package/dist/view/utils/find-route-dom.js +22 -0
  84. package/dist/view/utils/find-route-dom.js.map +1 -0
  85. package/dist/view/utils/get-render/client.js +77 -0
  86. package/dist/view/utils/get-render/client.js.map +1 -0
  87. package/dist/view/utils/get-render/common.js +87 -0
  88. package/dist/view/utils/get-render/common.js.map +1 -0
  89. package/dist/view/utils/get-render/index.js +4 -0
  90. package/dist/view/utils/get-render/index.js.map +1 -0
  91. package/dist/view/utils/get-render/server.js +60 -0
  92. package/dist/view/utils/get-render/server.js.map +1 -0
  93. package/dist/view/utils/index.js +3 -0
  94. package/dist/view/utils/index.js.map +1 -0
  95. package/dist/view/view.js +10 -0
  96. package/dist/view/view.js.map +1 -0
  97. package/package.json +41 -0
  98. package/resource/default-index.html +11 -0
  99. package/src/browser/index.ts +7 -0
  100. package/src/cli/dev-server.ts +66 -0
  101. package/src/cli/index.ts +122 -0
  102. package/src/cli/types.ts +7 -0
  103. package/src/cli-helper.ts +5 -0
  104. package/src/index.ts +24 -0
  105. package/src/interceptor/index.ts +26 -0
  106. package/src/interceptor/middleware-to-interceptor/index.ts +29 -0
  107. package/src/interceptor/middleware-to-interceptor/mock-res.ts +255 -0
  108. package/src/interceptor/middleware-to-interceptor/mook-req.ts +218 -0
  109. package/src/interceptor/types.ts +7 -0
  110. package/src/params/index.ts +12 -0
  111. package/src/request/index.ts +65 -0
  112. package/src/return-types/index.ts +1 -0
  113. package/src/return-types/response-with-meta.ts +17 -0
  114. package/src/route/index.ts +43 -0
  115. package/src/route/types.ts +11 -0
  116. package/src/server/body/helper.ts +110 -0
  117. package/src/server/body/json.ts +217 -0
  118. package/src/server/body/types.ts +6 -0
  119. package/src/server/dump-api-type.ts +64 -0
  120. package/src/server/index.ts +113 -0
  121. package/src/server/types.ts +11 -0
  122. package/src/server/utils/pass-through-readable.ts +10 -0
  123. package/src/view/const.ts +1 -0
  124. package/src/view/data-context/conetext.ts +59 -0
  125. package/src/view/data-context/index.ts +18 -0
  126. package/src/view/html/create-element.ts +20 -0
  127. package/src/view/html/index.ts +3 -0
  128. package/src/view/html/parse-html-to-v-dom.ts +348 -0
  129. package/src/view/html/render-v-dom-to-string.ts +79 -0
  130. package/src/view/html/types.ts +32 -0
  131. package/src/view/index.ts +6 -0
  132. package/src/view/layout.ts +20 -0
  133. package/src/view/render/html-render.ts +21 -0
  134. package/src/view/render/index.ts +2 -0
  135. package/src/view/render/render.ts +225 -0
  136. package/src/view/render/sf-v-dom-render.ts +34 -0
  137. package/src/view/types.ts +18 -0
  138. package/src/view/utils/find-route-dom.ts +19 -0
  139. package/src/view/utils/get-render/client.ts +76 -0
  140. package/src/view/utils/get-render/common.ts +113 -0
  141. package/src/view/utils/get-render/index.ts +3 -0
  142. package/src/view/utils/get-render/server.ts +71 -0
  143. package/src/view/utils/index.ts +2 -0
  144. package/src/view/view.ts +7 -0
  145. package/test/body.test.ts +29 -0
  146. package/tsconfig.build.json +9 -0
  147. package/tsconfig.json +8 -0
@@ -0,0 +1,225 @@
1
+ import {
2
+ forkDataContextRecord,
3
+ RenderDataContext,
4
+ SFDataContext,
5
+ SystemRenderDataContext,
6
+ UserRenderDataContext,
7
+ } from "../data-context/index.js";
8
+ import { parseHtmlToVDom } from "../html/parse-html-to-v-dom.js";
9
+ import { findRouteDom } from "../utils/index.js";
10
+
11
+ export interface RenderInstance {
12
+ unMount?(): void;
13
+ updateProp?(newProp: any): void;
14
+ updateChild?(newContext: RenderContext): void;
15
+ }
16
+ export class WarpRenderInstance {
17
+ constructor(
18
+ public readonly conetxt: RenderContext,
19
+ public readonly dom: HTMLElement,
20
+ public readonly rawInstance: RenderInstance,
21
+ public readonly dataContext: RenderDataContext,
22
+ ) {
23
+ this.conetxt._instances.push(this);
24
+ }
25
+ unMount() {
26
+ if (this.rawInstance.unMount) {
27
+ this.rawInstance.unMount();
28
+ } else {
29
+ this.dom.innerHTML = "";
30
+ }
31
+ Object.values(this.dataContext).forEach((o) => {
32
+ Object.values<any>(o).forEach(
33
+ (c) => c instanceof SFDataContext && c.distory(),
34
+ );
35
+ });
36
+ this.conetxt._instances = this.conetxt._instances.filter((i) => i !== this);
37
+ }
38
+ }
39
+
40
+ export interface IRender {
41
+ fetchResource?(): Promise<void> | void;
42
+ renderToString?(
43
+ dataContext: RenderDataContext,
44
+ prop: any,
45
+ chrild: RenderContext,
46
+ ): string | Promise<string>;
47
+ mount?(
48
+ dom: HTMLElement,
49
+ dataContext: RenderDataContext,
50
+ prop: any,
51
+ chrild: RenderContext,
52
+ ): RenderInstance;
53
+ hydrate?(
54
+ dom: HTMLElement,
55
+ dataContext: RenderDataContext,
56
+ prop: any,
57
+ chrild: RenderContext,
58
+ ): RenderInstance;
59
+ }
60
+ export class RenderContext {
61
+ private constructor(
62
+ private _systemDataContext: SystemRenderDataContext,
63
+ private _render: IRender,
64
+ private _prop: any,
65
+ // 结尾始终要跟一个空的RenderContext
66
+ // 原因:比如一个layout命中了 但是下面的页面没有命中,但是layout切换页面时,layout本身不应该重新渲染,所以需要一开始给它一个占位的空姐点,然后更新这个空节点
67
+ private _child?: RenderContext,
68
+ ) {}
69
+ public static createNoop(systemDataContext: SystemRenderDataContext) {
70
+ return new RenderContext(systemDataContext, {}, {});
71
+ }
72
+ public static create(
73
+ systemDataContext: SystemRenderDataContext,
74
+ render: IRender,
75
+ prop: any,
76
+ child: RenderContext = this.createNoop(systemDataContext),
77
+ ) {
78
+ return new RenderContext(systemDataContext, render, prop, child);
79
+ }
80
+ get render() {
81
+ return this._render;
82
+ }
83
+ get prop() {
84
+ return this._prop;
85
+ }
86
+ get child() {
87
+ return this._child;
88
+ }
89
+ async warpRenderToString(dataContext: UserRenderDataContext) {
90
+ if (this._child) {
91
+ return (
92
+ (await this.render.renderToString?.(
93
+ {
94
+ system: forkDataContextRecord(this._systemDataContext),
95
+ user: forkDataContextRecord(dataContext),
96
+ },
97
+ this.prop,
98
+ this._child,
99
+ )) || ""
100
+ );
101
+ }
102
+ return "";
103
+ }
104
+ /**
105
+ * @internal
106
+ */
107
+ _instances: WarpRenderInstance[] = [];
108
+ mount(dom: HTMLElement, dataContext: UserRenderDataContext) {
109
+ let instance: RenderInstance = {};
110
+ const fullDataContext = {
111
+ system: forkDataContextRecord(this._systemDataContext),
112
+ user: forkDataContextRecord(dataContext),
113
+ };
114
+ if (this._child) {
115
+ if (this.render.mount) {
116
+ instance = this.render.mount(
117
+ dom,
118
+ fullDataContext,
119
+ this.prop,
120
+ this._child,
121
+ );
122
+ } else if (this.render.renderToString) {
123
+ Promise.resolve(
124
+ this.render.renderToString(fullDataContext, this.prop, this._child),
125
+ ).then((html) => (dom.innerHTML = html));
126
+ }
127
+ }
128
+ return new WarpRenderInstance(this, dom, instance, fullDataContext);
129
+ }
130
+ hydrate(dom: HTMLElement, dataContext: UserRenderDataContext) {
131
+ let instance: RenderInstance = {};
132
+ const fullDataContext = {
133
+ system: forkDataContextRecord(this._systemDataContext),
134
+ user: forkDataContextRecord(dataContext),
135
+ };
136
+ if (this._child) {
137
+ if (this.render.hydrate) {
138
+ instance = this.render.hydrate(
139
+ dom,
140
+ fullDataContext,
141
+ this.prop,
142
+ this._child,
143
+ );
144
+ } else if (this.render.renderToString) {
145
+ // const vDom = parseHtmlToVDom(
146
+ // this.render.renderToString(this.prop, this._child),
147
+ // );
148
+ const routeDoms = findRouteDom(dom);
149
+ routeDoms.forEach((r) => this.child!.hydrate(r, dataContext));
150
+ } else if (this.render.mount) {
151
+ instance = this.render.mount(
152
+ dom,
153
+ fullDataContext,
154
+ this.prop,
155
+ this._child,
156
+ );
157
+ }
158
+ }
159
+ return new WarpRenderInstance(this, dom, instance, fullDataContext);
160
+ }
161
+ unmountAll() {
162
+ this._instances.forEach((i) => i.unMount());
163
+ }
164
+ async updateTree(node: RenderContext, allowRender = true) {
165
+ let shoudldRerender = false;
166
+ const unRerenderTask: (() => any)[] = [];
167
+ if (this._render !== node._render) {
168
+ this._render = node._render;
169
+ if (allowRender) {
170
+ shoudldRerender = true;
171
+ }
172
+ }
173
+ if (this._prop !== node._prop) {
174
+ this._prop = node._prop;
175
+ if (allowRender) {
176
+ if (this._instances.every((i) => i.rawInstance.updateProp)) {
177
+ unRerenderTask.push(() => {
178
+ this._instances.forEach((i) => {
179
+ i.rawInstance.updateProp!(node._prop);
180
+ });
181
+ });
182
+ } else {
183
+ shoudldRerender = true;
184
+ }
185
+ }
186
+ }
187
+ let childUpdated = false;
188
+ let shoudUpdateSubtree = false;
189
+ if (this._child !== node._child) {
190
+ if (!this._child || !node._child) {
191
+ this._child = node._child;
192
+ } else {
193
+ shoudUpdateSubtree = true;
194
+ }
195
+ if (this._instances.every((i) => i.rawInstance.updateChild)) {
196
+ childUpdated = true;
197
+ unRerenderTask.push(() => {
198
+ this._instances.forEach((i) => {
199
+ if (i.rawInstance.updateChild) {
200
+ i.rawInstance.updateChild(node._child!);
201
+ }
202
+ });
203
+ });
204
+ }
205
+ }
206
+ let isRerender = allowRender && shoudldRerender;
207
+
208
+ if (shoudUpdateSubtree) {
209
+ await this._child?.updateTree(
210
+ node._child!,
211
+ allowRender && !childUpdated && !isRerender,
212
+ );
213
+ }
214
+ if (isRerender) {
215
+ await Promise.all(
216
+ this._instances.map(async (i) => {
217
+ i.unMount();
218
+ await this.mount(i.dom, i.dataContext.user);
219
+ }),
220
+ );
221
+ } else {
222
+ await Promise.all(unRerenderTask.map((i) => i()));
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,34 @@
1
+ import { RenderDataContext } from "../data-context/index.js";
2
+ import { renderVDomToString } from "../html/render-v-dom-to-string.js";
3
+ import { HTMLVNode } from "../html/types.js";
4
+ import { IRender, RenderContext } from "./render.js";
5
+
6
+ export class SFVDomRender implements IRender {
7
+ constructor(
8
+ private readonly domFn:
9
+ | HTMLVNode
10
+ | HTMLVNode[]
11
+ | (() => Promise<
12
+ HTMLVNode | HTMLVNode[] | { default: HTMLVNode | HTMLVNode[] }
13
+ >),
14
+ ) {}
15
+ private dom: HTMLVNode | HTMLVNode[] = [];
16
+ async fetchResource() {
17
+ if (typeof this.domFn === "function") {
18
+ const res = await this.domFn();
19
+ this.dom =
20
+ typeof res === "object" && "default" in res ? res.default : res;
21
+ } else {
22
+ this.dom = this.domFn;
23
+ }
24
+ }
25
+ renderToString(context: RenderDataContext, prop: any, chrild: RenderContext) {
26
+ return renderVDomToString(this.dom, {
27
+ customizeChildrenRender: (el) => {
28
+ if (el.tagName === "sf-route") {
29
+ return () => chrild.warpRenderToString(context.user);
30
+ }
31
+ },
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ import { IRender } from "./render/render.js";
2
+
3
+ export interface ViewMeta {
4
+ title: string;
5
+ /**
6
+ * html的meta标签,通常用于seo,当前仅再ssr时生效,浏览器页面切换时不修改
7
+ */
8
+ htmlMeta?: Record<string, string>;
9
+ }
10
+
11
+ export interface ViewInfo<T = any> {
12
+ render: IRender;
13
+ meta?: ViewMeta | ((data: T) => ViewMeta);
14
+ }
15
+
16
+ export interface RenderConfig {
17
+ baseUrl: string;
18
+ }
@@ -0,0 +1,19 @@
1
+ import { routeTagName } from "../const.js";
2
+
3
+ export function findRouteDom(root: HTMLElement = document.body) {
4
+ const result: HTMLElement[] = [];
5
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
6
+ acceptNode(node) {
7
+ if (
8
+ node instanceof HTMLElement &&
9
+ node.tagName.toLocaleLowerCase() === routeTagName.toLocaleLowerCase()
10
+ ) {
11
+ result.push(node);
12
+ return NodeFilter.FILTER_REJECT; // 跳过子节点,因为它们是嵌套的
13
+ }
14
+ return NodeFilter.FILTER_ACCEPT;
15
+ },
16
+ });
17
+ while (walker.nextNode()) {}
18
+ return result;
19
+ }
@@ -0,0 +1,76 @@
1
+ import { parseRoute } from "../../../route/index.js";
2
+ import { RenderContext } from "../../render/index.js";
3
+ import { RenderConfig, ViewMeta } from "../../types.js";
4
+ import { findRouteDom } from "../find-route-dom.js";
5
+ import {
6
+ createRootRenderContext,
7
+ ssrToBrowserDataKey,
8
+ updateRootRenderContext,
9
+ } from "./common.js";
10
+
11
+ function listenerRoute(callback: () => void) {
12
+ const rawPush = history.pushState;
13
+ history.pushState = (data, un, url) => {
14
+ rawPush.call(history, data, un, url);
15
+ if (url) setTimeout(callback);
16
+ };
17
+ window.addEventListener("popstate", () => {
18
+ setTimeout(callback);
19
+ });
20
+ }
21
+
22
+ function handleMeta(meta: ViewMeta) {
23
+ document.title = meta.title;
24
+ }
25
+
26
+ export const clientRender = async (routes: ReturnType<typeof parseRoute>) => {
27
+ const { config, pageData } = (window as any)[ssrToBrowserDataKey];
28
+ delete (window as any)[ssrToBrowserDataKey];
29
+ const route = routes("GET", location.pathname);
30
+ const getData = async () => {
31
+ const res = await fetch(location.href);
32
+ if (res.headers.get("content-type")?.includes("json")) {
33
+ return await res.json();
34
+ }
35
+ return undefined;
36
+ };
37
+ const context = await createRootRenderContext(config, route, () => pageData);
38
+ handleMeta(context.meta);
39
+ listenerRoute(async () => {
40
+ const route = routes("GET", location.pathname);
41
+ const c = await updateRootRenderContext(context, route, getData);
42
+ handleMeta(c.meta);
43
+ });
44
+ const addedEl = new WeakSet<HTMLAnchorElement>();
45
+ function handleAClick(ev: MouseEvent) {
46
+ const target = ev.target as HTMLAnchorElement;
47
+ if (target.download || target.target || ev.defaultPrevented) return;
48
+ const rawHref = target.getAttribute("href");
49
+ if (!rawHref || rawHref.startsWith("#")) return;
50
+ const route = routes("GET", target.pathname);
51
+ if (route.finalRoute) {
52
+ history.pushState(null, "", target.href.substring(target.origin.length));
53
+ ev.preventDefault();
54
+ }
55
+ }
56
+ window.addEventListener(
57
+ "click",
58
+ (ev) => {
59
+ if (ev.target instanceof HTMLElement) {
60
+ let el: HTMLElement | null = ev.target as HTMLElement;
61
+ while (el && el !== document.body) {
62
+ if (el instanceof HTMLAnchorElement && !addedEl.has(el)) {
63
+ addedEl.add(el);
64
+ el.addEventListener("click", handleAClick, true);
65
+ }
66
+ el = el.parentElement;
67
+ }
68
+ }
69
+ },
70
+ true,
71
+ );
72
+ const routesElements = findRouteDom();
73
+ routesElements.forEach((el) => {
74
+ context.render.hydrate(el as HTMLElement, {});
75
+ });
76
+ };
@@ -0,0 +1,113 @@
1
+ import { title } from "node:process";
2
+ import { RouteType } from "../../../route/index.js";
3
+ import {
4
+ SFRawDataContext,
5
+ SystemRenderDataContext,
6
+ } from "../../data-context/index.js";
7
+ import { getLayots } from "../../layout.js";
8
+ import { RenderContext } from "../../render/index.js";
9
+ import { RenderConfig, ViewMeta } from "../../types.js";
10
+ import { SFWebRequest } from "../../../request/index.js";
11
+
12
+ function createRawDataContext(config: RenderConfig, route: RouteType<"GET">) {
13
+ const routeRawcontext = new SFRawDataContext(route);
14
+ const { baseUrl } = config;
15
+ return {
16
+ update(route: RouteType<"GET">) {
17
+ routeRawcontext.update(route);
18
+ },
19
+ createChild(): SystemRenderDataContext {
20
+ return {
21
+ route: routeRawcontext.createChild(),
22
+ request: new SFWebRequest(baseUrl),
23
+ baseUrl,
24
+ };
25
+ },
26
+ };
27
+ }
28
+
29
+ export interface RootRenderContext {
30
+ render: RenderContext;
31
+ rawSystemDataContext: ReturnType<typeof createRawDataContext>;
32
+ meta: ViewMeta;
33
+ }
34
+
35
+ async function createRenderContext(
36
+ route: RouteType<"GET">,
37
+ dataFn: () => any,
38
+ dataContext: ReturnType<typeof createRawDataContext>,
39
+ ): Promise<Pick<RootRenderContext, "render" | "meta">> {
40
+ if (route.finalRoute?.context?.view) {
41
+ const { obj, propertyKey } = route.finalRoute;
42
+ let view = route.finalRoute.context.view;
43
+ const layouts = route.allRoute.flatMap((r) => getLayots(r.obj));
44
+ const [data] = await Promise.all([
45
+ typeof (obj as any)[propertyKey] === "function"
46
+ ? dataFn()
47
+ : (obj as any)[propertyKey],
48
+ view.render.fetchResource?.(),
49
+ ...layouts.map((l) => l.render.fetchResource?.()),
50
+ ]);
51
+ let render = RenderContext.create(
52
+ dataContext.createChild(),
53
+ view.render,
54
+ data,
55
+ );
56
+ let meta =
57
+ typeof view.meta === "function"
58
+ ? view.meta(data)
59
+ : view.meta || { title: "" };
60
+ return layouts.reduceRight(
61
+ ({ render, meta }, currentValue) => {
62
+ return {
63
+ render: RenderContext.create(
64
+ dataContext.createChild(),
65
+ currentValue.render,
66
+ undefined,
67
+ render,
68
+ ),
69
+ meta:
70
+ typeof currentValue.meta === "function"
71
+ ? currentValue.meta(meta)
72
+ : currentValue.meta || meta,
73
+ };
74
+ },
75
+ { render, meta },
76
+ );
77
+ } else {
78
+ return {
79
+ render: RenderContext.createNoop(dataContext.createChild()),
80
+ meta: { title: "" },
81
+ };
82
+ }
83
+ }
84
+
85
+ export async function createRootRenderContext(
86
+ config: RenderConfig,
87
+ route: RouteType<"GET">,
88
+ dataFn: () => any,
89
+ ): Promise<RootRenderContext> {
90
+ const rawSystemDataContext = createRawDataContext(config, route);
91
+ return {
92
+ ...(await createRenderContext(route, dataFn, rawSystemDataContext)),
93
+ rawSystemDataContext,
94
+ };
95
+ }
96
+ export async function updateRootRenderContext(
97
+ rootContext: RootRenderContext,
98
+ route: RouteType<"GET">,
99
+ dataFn: () => any,
100
+ ) {
101
+ const newRenderContext = await createRenderContext(
102
+ route,
103
+ dataFn,
104
+ rootContext.rawSystemDataContext,
105
+ );
106
+ rootContext.meta = newRenderContext.meta;
107
+ rootContext.render.updateTree(newRenderContext.render);
108
+ rootContext.rawSystemDataContext.update(route);
109
+ return rootContext;
110
+ }
111
+
112
+ export const routeElementSelector = "sf-route";
113
+ export const ssrToBrowserDataKey = "__SFWebAppInitData";
@@ -0,0 +1,3 @@
1
+ // @side-only server
2
+ export { createServerRender } from "./server.js";
3
+ export { clientRender } from "./client.js";
@@ -0,0 +1,71 @@
1
+ import { ServerRuntimeContext } from "@thestarweb/star-framework-web-runtime-node";
2
+ import { Interceptor } from "../../../interceptor/index.js";
3
+ import { ResponseWithMeta } from "../../../return-types/index.js";
4
+ import { parseHtmlToVDom, renderVDomToString } from "../../html/index.js";
5
+ import {
6
+ createRootRenderContext,
7
+ routeElementSelector,
8
+ ssrToBrowserDataKey,
9
+ } from "./common.js";
10
+ import { RenderConfig } from "../../types.js";
11
+
12
+ export const createServerRender: (
13
+ context: ServerRuntimeContext,
14
+ ) => Interceptor = (context) => {
15
+ const htmlVDdom = parseHtmlToVDom(context.indexHtml);
16
+ return async (prop, next) => {
17
+ const config: RenderConfig = {
18
+ baseUrl: "/",
19
+ };
20
+ const nextPromise = next(prop);
21
+ const { accept, "sec-fetch-dest": fetchDest } = prop.header;
22
+ let pageData: any = undefined;
23
+ const rootContext =
24
+ prop.method === "GET" &&
25
+ (prop.route.finalRoute?.context as any)?.view &&
26
+ (accept?.includes("text/html") || ["document"].includes(fetchDest))
27
+ ? await createRootRenderContext(config, prop.route, async () => {
28
+ const ret = await nextPromise;
29
+ pageData = ret.data;
30
+ return ret.data;
31
+ })
32
+ : undefined;
33
+ if (rootContext) {
34
+ const { render, meta } = rootContext;
35
+ const nextRet = await nextPromise;
36
+ const res = await render.warpRenderToString({});
37
+ return new ResponseWithMeta(
38
+ renderVDomToString(htmlVDdom, {
39
+ customizeChildrenRender: (dom) => {
40
+ if (dom.tagName === routeElementSelector) {
41
+ return () => res;
42
+ }
43
+ if (dom.tagName === "head") {
44
+ return (dom, raw) => {
45
+ let res = [
46
+ `<script>window.${ssrToBrowserDataKey}=${JSON.stringify({ config, pageData })}</script>`,
47
+ raw(),
48
+ ];
49
+ res.push(`<title>${meta.title}</title>`);
50
+ if (meta.htmlMeta) {
51
+ Object.entries(meta.htmlMeta).forEach(([k, v]) =>
52
+ res.push(`<meta name="${k}" content="${v}">`),
53
+ );
54
+ }
55
+ return res.join("");
56
+ };
57
+ }
58
+ },
59
+ }),
60
+ {
61
+ ...nextRet,
62
+ header: {
63
+ "content-type": "text/html; charset=utf-8",
64
+ ...nextRet.header,
65
+ },
66
+ },
67
+ );
68
+ }
69
+ return nextPromise;
70
+ };
71
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./get-render/index.js";
2
+ export * from "./find-route-dom.js";
@@ -0,0 +1,7 @@
1
+ import { Get } from "../route/index.js";
2
+ import { IRender } from "./render/render.js";
3
+ import { ViewInfo } from "./types.js";
4
+
5
+ export function View(path: string, render: IRender, meta?: ViewInfo["meta"]) {
6
+ return Get(path, { view: { render, meta } });
7
+ }
@@ -0,0 +1,29 @@
1
+ import { expect, test } from "vitest";
2
+ import { JSONBody } from "../src/server/body/json.js";
3
+ import { Readable } from "stream";
4
+
5
+ test("JSONBody", async () => {
6
+ const p = JSONBody({ "content-type": "application/json" }, { type: "any" })!;
7
+ async function check(name: string, data: any, str?: string) {
8
+ const stream = new ReadableStream({
9
+ start(controller) {
10
+ controller.enqueue(
11
+ new TextEncoder().encode(str || JSON.stringify(data)),
12
+ );
13
+ controller.close();
14
+ },
15
+ });
16
+ expect(await p(stream), name).toEqual(data);
17
+ }
18
+ await check("string 最基础的字符串", "awa");
19
+ await check("string 带有常规转义字符的字符串", "1\r\n3dw\t");
20
+ await check("null", null);
21
+ await check("true", true);
22
+ await check("false", false);
23
+ await check("arrar 空数组", []);
24
+ await check("arrar 有内容数组", ["ewf"]);
25
+ await check("object 空对象", {});
26
+ await check("object 有内容的对象", { a: "123456" });
27
+ await check("整数", 123);
28
+ await check("嵌套", [{ a: 1, b: "666", c: [true, null] }]);
29
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./dist"
5
+ },
6
+ "extends": "../../tsconfig.json",
7
+ "reflection": true,
8
+ "include": ["src/**/*"]
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "rootDir": "./"
5
+ },
6
+ "extends":"./tsconfig.build.json",
7
+ "include": ["src/**/*", "tests/**/*"]
8
+ }