├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── bin
└── index.js
├── compiler
├── generate.mjs
└── parse.mjs
├── demo
├── action
│ └── count.js
├── app.jsx
├── public
│ ├── client.mjs
│ ├── data.json
│ └── style.css
└── server.mjs
├── index.html
├── package.json
├── src
├── $import.mjs
├── action
│ └── count.js
├── app.js
├── app.mjs
├── build.mjs
├── cli.mjs
├── count.js
├── h.mjs
├── public
│ ├── client.mjs
│ ├── data.json
│ └── style.css
├── resume.mjs
└── s.mjs
└── yarn.lock
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - run: npm install
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | *.log
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 |
7 | # Runtime data
8 | *.pid
9 | *.seed
10 | *.pid.lock
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # nyc test coverage
19 | .nyc_output
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # Bower dependency directory (https://bower.io/)
25 | bower_components
26 |
27 | # node-waf configuration
28 | .lock-wscript
29 |
30 | # Compiled binary addons (https://nodejs.org/api/addons.html)
31 | build/Release
32 |
33 | # Dependency directories
34 | node_modules/
35 | jspm_packages/
36 |
37 | # Optional npm cache directory
38 | .npm
39 |
40 | # Optional eslint cache
41 | .eslintcache
42 |
43 | # Optional REPL history
44 | .node_repl_history
45 |
46 | # Output of 'npm pack'
47 | *.tgz
48 |
49 | # Yarn Integrity file
50 | .yarn-integrity
51 |
52 | # dotenv environment variables file
53 | .env
54 |
55 | # next.js build output
56 | .next
57 |
58 | # OS X temporary files
59 | .DS_Store
60 | # Heft
61 | .heft
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {"semi": false,"singleQuote": true,"printWidth": 140,"trailingComma": "es5","useTabs": true,"tabWidth": 2}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 伊撒尔
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 | # Asta [](https://npmjs.com/package/asta) [](https://npmjs.com/package/asta)
5 |
6 | :dart: Asta is a highly specialized full stack framework for SSR. It has no vdom on the server side and 0 js on the client side. Finally, it gets best QPS and Google scores.
7 |
8 | > Note this is early Development! It is not recommended to use this for anything serious yet.
9 |
10 | - no VDOM on server, 0 javascript on client.
11 | - write JSX and react-like syntax.
12 |
13 |
14 |
15 | ### Run demo
16 |
17 | ```shell
18 | yarn start
19 | ```
20 |
21 |
22 | ### Syntax
23 |
24 | input:
25 |
26 | ```jsx
27 | const addCount = $import('./action.js#addCount')
28 |
29 | // state: will run in server and inject to client
30 | export const loader = async (req) => {
31 | return {
32 | count: req.query.count,
33 | }
34 | }
35 |
36 | // view: will run in both client and server, but s() in server h() in client
37 | export default ({ count }) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 | ```
45 |
46 | output:
47 |
48 | ```html
49 |
50 | ```
51 |
52 | ### Compiler
53 |
54 | Jointing on server, Resumable on client
55 |
56 | ```js
57 | // jsx input
58 | const view = ({list}) => {list.map(i=>{i})}
59 | // server output
60 | const view = ({list}) => s.openTag('div')+s.expression(list.map(i=>s.openTag('i')+s.text(i)+s.closeTag('i')))+s.closeTag('div')
61 | // client output
62 | const view = ({list}) => h('div',{children:[list.map(i=>h('i',{children:[i]}))]})
63 | ```
64 |
65 | # How and why
66 |
67 | ### How is This Different from Next.js, Remix.js, Fresh.js or Other SSR Solutions?
68 |
69 | There are two biggest differences.
70 |
71 | First, the server side. Asta does not run any VDOM-based framework runtime. It generates the `s function` through the compiler, which is only used for string splicing. At this point, it is a little like Marko.js.
72 |
73 | Second, on the client side, Asta is 0 javascript, and it does not require any hydration. This is a new concept, called Resumable, a little like qwik.js.
74 |
75 | So, `Asta ≈ Marko + Qwik`.
76 |
77 | Because there is no Vdom overhead on the server side, Asta can get super high QPS and throughput.
78 |
79 | Then because the client side is 0 js, it can continuously get a high Google score, and the score will not decrease with the increase of components.
80 |
81 | ### How is This Different from Qwik.js or Marko.js?
82 |
83 | In principle, asta is the sum of them, Asta is a double optimization, but the implementation details are quite different.
84 |
85 | At the same time, Asta attempts to migrate Elm's mental model to SSR.
86 |
87 | There is only a single state tree, and components are pure functions without states or any overhead.
88 |
89 | These helps to completely solve performance problems.
90 |
91 | ### Why not Fre SSR or and other Vdom-based frameworks?
92 |
93 | Although JSX of fre can also be optimized at compile time, and the client side can also be selective hydrated, it is important that Fre or other Vdom-based framework components are not completely cost free.
94 |
95 | ### 说人话?
96 |
97 | Asta 的核心是根治性能问题,已知的 SSR 框架有几个性能瓶颈:
98 |
99 | 1. server 端的 vdom 开销,组件开销
100 |
101 | - server 端生成和遍历 vdom 成本是巨大的,Asta 在 server 端没有 vdom,它通过一个特殊的编译器将 jsx 编译成 s 函数,只用来拼接字符串
102 |
103 | - server 端组件的初始化,状态更新,生命周期的开销,也是巨大的,Asta 也有组件,但它的组件是纯函数,也只用来拼接字符串,没有任何私有状态和生命周期,这得益于 Elm 的心智模型,单 state tree,组件是纯函数
104 |
105 | 2. client 0 js
106 |
107 | - 一个新兴的概念,叫做 Resumable,client 不再水合,而是将必要的信息序列化到 html 里,然后直接从 html 进行恢复,所有的 js 都根据交互懒加载,这样就可以做到 0 js,0 水合,而且这是 O(1) 的,不会因为业务增长而性能下降
108 |
109 | Asta 双重优化,彻底根除 SSR 的性能瓶颈
110 |
111 |
112 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import('../src/cli.mjs');
--------------------------------------------------------------------------------
/compiler/generate.mjs:
--------------------------------------------------------------------------------
1 | import { parse } from './parse.mjs'
2 | const whitespaceRE = /^\s+$/;
3 |
4 | const textSpecialRE = /(^|[^\\])("|\n)/g;
5 |
6 | export const isComponent = type => type[0] === type[0].toUpperCase() && type[0] !== type[0].toLowerCase()
7 |
8 | function generateName(nameTree, close) {
9 | const name = generate(nameTree);
10 | const isComp = isComponent(name)
11 | let tag = ''
12 | if (isComp) {
13 | tag = close ? 's.empty' : 's.component'
14 | } else {
15 | tag = close ? 's.closeTag' : 's.openTag'
16 | }
17 | return isComp ? `${tag}(${name}` : `${tag}('${name}'`
18 | }
19 |
20 | function generate(tree) {
21 | const type = tree.type;
22 | if (typeof tree === "string") {
23 | return tree;
24 | } else if (Array.isArray(tree)) {
25 | let output = "";
26 | for (let i = 0; i < tree.length; i++) {
27 | output += generate(tree[i]);
28 | }
29 | return output;
30 | } else if (type === "comment") {
31 | return `/*${generate(tree.value[1])}*/`;
32 | } else if (type === "attributes") {
33 | const value = tree.value;
34 | let output = "";
35 | let separator = "";
36 |
37 | for (let i = 0; i < value.length; i++) {
38 | const pair = value[i];
39 | output += `${separator}"${generate(pair[0])}":${generate(pair[2])}${generate(pair[3])}`;
40 | separator = ",";
41 | }
42 |
43 | if (output.length > 0) {
44 | output += ","
45 | }
46 | return {
47 | output,
48 | separator
49 | };
50 | } else if (type === "text") {
51 | const textGenerated = generate(tree.value);
52 | const textGeneratedIsWhitespace = whitespaceRE.test(textGenerated) && textGenerated.indexOf("\n") !== -1;
53 | return {
54 | output: textGeneratedIsWhitespace ?
55 | textGenerated :
56 | `"${textGenerated.replace(textSpecialRE, (match, character, characterSpecial) =>
57 | character + (characterSpecial === "\"" ? "\\\"" : "\\n\\\n"))
58 | }"+`,
59 | isWhitespace: textGeneratedIsWhitespace
60 | };
61 | } else if (type === "interpolation") {
62 | return `s.expression(${generate(tree.value[1])})`;
63 | } else if (type === "node") {
64 | const value = tree.value;
65 | return generate(value[1]) + generateName(value[2]) + generate(value[3]);
66 | } else if (type === "nodeData") {
67 | const value = tree.value;
68 | const data = value[4];
69 | const dataGenerated = generate(data);
70 | return `${generate(value[1])}${generateName(value[2])}${generate(value[3])},${data.type === "attributes" ? `{${dataGenerated.output}}` : dataGenerated
71 | })`;
72 | } else if (type === "nodeDataChildren") {
73 | const value = tree.value;
74 | const data = generate(value[4]);
75 | const children = value[6];
76 | const childrenLength = children.length;
77 | let childrenGenerated;
78 |
79 | if (childrenLength === 0) {
80 | childrenGenerated = "";
81 | } else {
82 | childrenGenerated = "";
83 |
84 | for (let i = 0; i < childrenLength; i++) {
85 | const child = children[i];
86 | const childGenerated = generate(child);
87 |
88 | if (child.type === "text") {
89 | if (childGenerated.isWhitespace) {
90 | childrenGenerated += childGenerated.output;
91 | } else {
92 | childrenGenerated += childGenerated.output;
93 | }
94 | } else {
95 | childrenGenerated += childGenerated + '+';
96 | }
97 | }
98 |
99 | childrenGenerated;
100 | }
101 |
102 | let sdom = `${generate(value[1])}${generateName(value[2], false)}${generate(value[3])},{${data.output}})+${childrenGenerated}${generateName(value[2], true)})`
103 | return sdom;
104 | }
105 | }
106 |
107 | function compile(input) {
108 | const { ast } = parse(input);
109 | if (process.env.MOON_ENV === "development" && ast.constructor.name === "ParseError") {
110 | error(`Invalid input to parser.
111 | Attempted to parse input.
112 | Expected ${ast.expected}.
113 | Received:
114 | ${format(input, ast.index)}`);
115 | }
116 | return generate(ast[0][0]);
117 | }
118 |
119 | export { compile }
--------------------------------------------------------------------------------
/compiler/parse.mjs:
--------------------------------------------------------------------------------
1 | const identifierRE = /[$\w.]/;
2 |
3 | function ParseError(expected, index) {
4 | this.expected = expected;
5 | this.index = index;
6 | }
7 |
8 | const parser = {
9 | type: (type, parse) => (input, index) => {
10 | const output = parse(input, index);
11 | return output instanceof ParseError ?
12 | output :
13 | [{ type, value: output[0] }, output[1]];
14 | },
15 | EOF: (input, index) => {
16 | return index === input.length ?
17 | ["EOF", index] :
18 | new ParseError("EOF", index);
19 | },
20 | any: (input, index) => {
21 | return index < input.length ?
22 | [input[index], index + 1] :
23 | new ParseError("any", index);
24 | },
25 | character: character => (input, index) => {
26 | const head = input[index];
27 |
28 | return head === character ?
29 | [head, index + 1] :
30 | new ParseError(`"${character}"`, index);
31 | },
32 | regex: regex => (input, index) => {
33 | const head = input[index];
34 |
35 | return head !== undefined && regex.test(head) ?
36 | [head, index + 1] :
37 | new ParseError(regex.toString(), index);
38 | },
39 | string: string => (input, index) => {
40 | const indexNew = index + string.length;
41 |
42 | return input.slice(index, indexNew) === string ?
43 | [string, indexNew] :
44 | new ParseError(`"${string}"`, index);
45 | },
46 | not: strings => (input, index) => {
47 | if (index < input.length) {
48 | for (let i = 0; i < strings.length; i++) {
49 | const string = strings[i];
50 |
51 | if (input.slice(index, index + string.length) === string) {
52 | return new ParseError(`not "${string}"`, index);
53 | }
54 | }
55 |
56 | return [input[index], index + 1];
57 | } else {
58 | return new ParseError(`not ${strings.map(JSON.stringify).join(", ")}`, index);
59 | }
60 | },
61 | or: (parse1, parse2) => (input, index) => {
62 | const output1 = parse1(input, index);
63 |
64 | if (output1 instanceof ParseError && output1.index === index) {
65 | // If the first parser has an error and consumes no input, then try
66 | // the second parser.
67 | return parse2(input, index);
68 | } else {
69 | return output1;
70 | }
71 | },
72 | and: (parse1, parse2) => (input, index) => {
73 | const output1 = parse1(input, index);
74 |
75 | if (output1 instanceof ParseError) {
76 | return output1;
77 | } else {
78 | const output2 = parse2(input, output1[1]);
79 |
80 | return output2 instanceof ParseError ?
81 | output2 :
82 | [[output1[0], output2[0]], output2[1]];
83 | }
84 | },
85 | sequence: parses => (input, index) => {
86 | const values = [];
87 |
88 | for (let i = 0; i < parses.length; i++) {
89 | const output = parses[i](input, index);
90 |
91 | if (output instanceof ParseError) {
92 | return output;
93 | } else {
94 | values.push(output[0]);
95 | index = output[1];
96 | }
97 | }
98 |
99 | return [values, index];
100 | },
101 | alternates: parses => (input, index) => {
102 | let alternatesError = new ParseError("alternates", -1);
103 |
104 | for (let i = 0; i < parses.length; i++) {
105 | const output = parses[i](input, index);
106 |
107 | if (output instanceof ParseError && output.index === index) {
108 | if (output.index > alternatesError.index) {
109 | alternatesError = output;
110 | }
111 | } else {
112 | return output;
113 | }
114 | }
115 |
116 | return alternatesError;
117 | },
118 | many: parse => (input, index) => {
119 | const values = [];
120 | let output;
121 |
122 | while (!((output = parse(input, index)) instanceof ParseError)) {
123 | values.push(output[0]);
124 | index = output[1];
125 | }
126 |
127 | if (output.index === index) {
128 | return [values, index];
129 | } else {
130 | return output;
131 | }
132 | },
133 | many1: parse => (input, index) => {
134 | const values = [];
135 | let output = parse(input, index);
136 |
137 | if (output instanceof ParseError) {
138 | return output;
139 | }
140 |
141 | values.push(output[0]);
142 | index = output[1];
143 |
144 | while (!((output = parse(input, index)) instanceof ParseError)) {
145 | values.push(output[0]);
146 | index = output[1];
147 | }
148 |
149 | if (output.index === index) {
150 | return [values, index];
151 | } else {
152 | return output;
153 | }
154 | },
155 | try: parse => (input, index) => {
156 | const output = parse(input, index);
157 |
158 | if (output instanceof ParseError) {
159 | output.index = index;
160 | }
161 |
162 | return output;
163 | }
164 | };
165 |
166 |
167 | const grammar = {
168 | comment: parser.type("comment", parser.sequence([
169 | parser.character("#"),
170 | parser.many(parser.or(parser.and(parser.character("\\"), parser.any), parser.not(["#"]))),
171 | parser.character("#")
172 | ])),
173 | separator: (input, index) => parser.many(parser.or(
174 | parser.alternates([
175 | parser.character(" "),
176 | parser.character("\t"),
177 | parser.character("\n")
178 | ]),
179 | grammar.comment
180 | ))(input, index),
181 | value: (input, index) => parser.alternates([
182 | parser.many1(parser.regex(identifierRE)),
183 | parser.sequence([
184 | parser.character("\""),
185 | parser.many(parser.or(parser.and(parser.character("\\"), parser.any), parser.not(["\""]))),
186 | parser.character("\"")
187 | ]),
188 | parser.sequence([
189 | parser.character("'"),
190 | parser.many(parser.or(parser.and(parser.character("\\"), parser.any), parser.not(["'"]))),
191 | parser.character("'")
192 | ]),
193 | parser.sequence([
194 | parser.character("`"),
195 | parser.many(parser.or(parser.and(parser.character("\\"), parser.any), parser.not(["`"]))),
196 | parser.character("`")
197 | ]),
198 | parser.sequence([
199 | parser.character("("),
200 | grammar.expression,
201 | parser.character(")")
202 | ]),
203 | parser.sequence([
204 | parser.character("["),
205 | grammar.expression,
206 | parser.character("]")
207 | ]),
208 | parser.sequence([
209 | parser.character("{"),
210 | grammar.expression,
211 | parser.character("}")
212 | ])
213 | ])(input, index),
214 | attributes: (input, index) => parser.type("attributes", parser.many(parser.sequence([
215 | grammar.value,
216 | parser.character("="),
217 | grammar.value,
218 | grammar.separator
219 | ])))(input, index),
220 | text: parser.type("text", parser.many1(parser.or(
221 | parser.and(parser.character("\\"), parser.any),
222 | parser.not(["{", "<"])
223 | ))),
224 | interpolation: (input, index) => parser.type("interpolation", parser.sequence([
225 | parser.character("{"),
226 | grammar.expression,
227 | parser.character("}")
228 | ]))(input, index),
229 | node: (input, index) => parser.type("node", parser.sequence([
230 | parser.character("<"),
231 | grammar.separator,
232 | grammar.value,
233 | grammar.separator,
234 | parser.string("*>")
235 | ]))(input, index),
236 | nodeData: (input, index) => parser.type("nodeData", parser.sequence([
237 | parser.character("<"),
238 | grammar.separator,
239 | grammar.value,
240 | grammar.separator,
241 | parser.or(parser.try(grammar.attributes), grammar.value),
242 | parser.string("/>")
243 | ]))(input, index),
244 | nodeDataChildren: (input, index) => parser.type("nodeDataChildren", parser.sequence([
245 | parser.character("<"),
246 | grammar.separator,
247 | grammar.value,
248 | grammar.separator,
249 | grammar.attributes,
250 | parser.character(">"),
251 | parser.many(parser.alternates([
252 | parser.try(grammar.node),
253 | parser.try(grammar.nodeData),
254 | parser.try(grammar.nodeDataChildren),
255 | grammar.text,
256 | grammar.interpolation
257 | ])),
258 | parser.string(""),
259 | parser.many(parser.not([">"])),
260 | parser.character(">")
261 | ]))(input, index),
262 | expression: (input, index) => parser.many(parser.alternates([
263 | // Single line comment
264 | parser.sequence([
265 | parser.string("//"),
266 | parser.many(parser.not(["\n"]))
267 | ]),
268 |
269 | // Multi-line comment
270 | parser.sequence([
271 | parser.string("/*"),
272 | parser.many(parser.not(["*/"])),
273 | parser.string("*/")
274 | ]),
275 |
276 | // Regular expression
277 | parser.try(parser.sequence([
278 | parser.character("/"),
279 | parser.many1(parser.or(
280 | parser.and(parser.character("\\"), parser.not(["\n"])),
281 | parser.not(["/", "\n"])
282 | )),
283 | parser.character("/")
284 | ])),
285 | grammar.comment,
286 | grammar.value,
287 | parser.try(grammar.node),
288 | parser.try(grammar.nodeData),
289 | parser.try(grammar.nodeDataChildren),
290 |
291 | parser.character("/"),
292 | parser.character("<"),
293 | parser.many1(parser.not(["/", "#", "\"", "'", "`", "(", ")", "[", "]", "{", "}", "<"]))
294 | ]))(input, index),
295 | main: (input, index) => parser.and(grammar.expression, parser.EOF)(input, index)
296 | };
297 |
298 | function parse(input) {
299 | let ast = grammar.main(input, 0);
300 | return {ast}
301 | }
302 |
303 | export { parse };
304 |
--------------------------------------------------------------------------------
/demo/action/count.js:
--------------------------------------------------------------------------------
1 | export const addCount = async (state, event) => {
2 | await new Promise(r => setTimeout(() => r(), 1000))
3 | return {
4 | ...state,
5 | count: state.count + 1,
6 | }
7 | }
--------------------------------------------------------------------------------
/demo/app.jsx:
--------------------------------------------------------------------------------
1 | import { $import } from '../src/$import.mjs'
2 |
3 | export const loader = async (req) => {
4 | // await new Promise(r=> setTimeout(()=>r(null), 100))
5 | const data = await fetch('http://localhost:1234/data')
6 | .then((res) => res.json())
7 | .then((data) => data)
8 | return {
9 | ...data,
10 | count: 0,
11 | }
12 | }
13 |
14 | const addCount = $import('./action/count.js#addCount')
15 |
16 | const Header = ({ cover, title, rate }) => (
17 |
18 |
19 | {title}
20 | {rate}
21 |
22 | )
23 |
24 | export default ({ title, comments, rate, imgs, info, cover, count }) => {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
截图
33 |
34 | {imgs.map((i) => (
35 | -
36 |
37 |
38 | ))}
39 |
40 |
41 |
42 |
46 |
47 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/demo/public/client.mjs:
--------------------------------------------------------------------------------
1 | import { resume } from '../resume.mjs'
2 |
3 | resume(document.body)
--------------------------------------------------------------------------------
/demo/public/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Can You Escape VintageBungalow 封测国际服",
3 | "rate": "8.4",
4 | "comments": [
5 | {
6 | "name": "阿呆",
7 | "avatar": "https://img3.tapimg.com/default_avatars/755e9ca449be08245191a743a128a8df.jpg?imageMogr2/auto-orient/strip/thumbnail/!300x300r/gravity/Center/crop/300x300/format/jpg/interlace/1/quality/80",
8 | "content": "bdbnxjcjcjj"
9 | },
10 | {
11 | "name": "迪卢克",
12 | "avatar": "https://img3.tapimg.com/default_avatars/7d713c00e515de52a63c0f51c8697c84.jpg?imageMogr2/auto-orient/strip/thumbnail/!300x300r/gravity/Center/crop/300x300/format/jpg/interlace/1/quality/80",
13 | "content": "Vbjjnnn😂"
14 | }
15 | ],
16 | "imgs": [
17 | "https://img.tapimg.com/market/images/de62537f7b8aad4f6b8b53cb968901f0.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
18 | "https://img.tapimg.com/market/images/123ec01bb9b5c42de4fa214303cf1383.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
19 | "https://img.tapimg.com/market/images/286c9889acad05a6e3ae2f07b5035760.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
20 | "https://img.tapimg.com/market/images/ea16c10e162a5b9b2e2fe6746a1de6f3.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1"
21 | ],
22 | "info": "Can You Escape VintageBungalow is new android escape game developed by KnfGame.In this game your locked inside a Vintage Bungalow House, the only way to escape from bungalow is to find the hidden key. For that you have click on the useful objects around the house and solve some interesting puzzles to find the hidden key. Good Luck and have fun playing Knf escape games and free online point and click escape games.",
23 | "cover": "https://img.tapimg.com/market/icons/9e99c190fdb4f28136921fcc74a7467f_360.png?imageMogr2/auto-orient/strip"
24 | }
--------------------------------------------------------------------------------
/demo/public/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | :root {
7 | --theme-color: #07b9c9;
8 | }
9 |
10 | li {
11 | list-style: none;
12 | }
13 |
14 | button {
15 | background-color: var(--theme-color);
16 | color: aliceblue;
17 | padding: 20px;
18 | margin: 20px;
19 | border: 0;
20 | border-radius: 10px;
21 | font-size: 16px;
22 | }
23 |
24 | header {
25 | display: flex;
26 | padding: 10px;
27 | align-items: center;
28 | }
29 |
30 | header img {
31 | height: 100px;
32 | width: 100px;
33 | }
34 |
35 | header h1 {
36 | width: 220px;
37 | font-size: 18px;
38 | padding: 10px;
39 | }
40 |
41 | header .rate {
42 | width: 50px;
43 | height: 50px;
44 | background: url(https://assets.tapimg.com/img/background/rating_score@2x-99a4cf2f6b.png);
45 | color: #fff;
46 | background-size: cover;
47 | text-align: center;
48 | font-size: 24px;
49 | }
50 |
51 | main {
52 | display: flex;
53 | justify-content: center;
54 | }
55 |
56 | .screenshot {
57 | width: 100%;
58 | overflow: scroll;
59 | border-top: 1px solid #eee;
60 | padding: 15px;
61 | box-sizing: border-box;
62 | }
63 |
64 | .comments {
65 | width: 100%;
66 | overflow: scroll;
67 | border-top: 10px solid #eee;
68 | }
69 |
70 | .screenshot ul li {
71 | display: inline-block;
72 | }
73 |
74 | .screenshot ul {
75 | display: flex;
76 | }
77 |
78 | .screenshot img {
79 | height: 170px;
80 | padding: 5px;
81 | }
82 | .screenshot h3 {
83 | padding: 5px;
84 | }
85 |
86 | .screenshot p {
87 | color: #666;
88 | padding: 10px 5px;
89 | height: 75px;
90 | font-size: 14px;
91 | overflow: hidden;
92 |
93 | }
94 |
95 | .comments li .bio {
96 | display: flex;
97 | align-items: center;
98 | }
99 |
100 | .comments li {
101 | padding: 5px 15px;
102 | border-bottom: 10px solid #eee;
103 | }
104 |
105 | .comments h3 {
106 | padding: 15px;
107 | }
108 |
109 | .bio .avatar {
110 | height: 40px;
111 | width: 40px;
112 | border-radius: 20px;
113 | }
114 | .bio .avatar{
115 | margin: 10px 10px 0 0;
116 | }
117 | .comments li p{
118 | padding: 15px 0;
119 | }
--------------------------------------------------------------------------------
/demo/server.mjs:
--------------------------------------------------------------------------------
1 | import polka from 'polka'
2 | import chalk from 'chalk'
3 | import sirv from 'sirv'
4 | import fs from 'fs/promises'
5 | import path from 'path'
6 |
7 | export default function serve(options) {
8 | console.log(options)
9 | const app = polka()
10 | .use(sirv(options.serverOutputDir))
11 | .get("/", async (req, res) => {
12 | const module = await import(`file://${options.serverOutput}`)
13 | const state = await module.loader(req)
14 | const html = module.default(state)
15 | const str = `
16 |
17 |
18 |
19 |
20 |
21 | Asta
22 |
23 |
24 |
25 |
26 |
27 |
30 | ${html}
31 | `
32 | res.end(str)
33 | }).get('/data', async (req, res) => {
34 | const json = await fs.readFile(path.join(options.serverOutputDir, 'public/data.json'))
35 | res.end(json)
36 | })
37 | .listen(1234, (err) => {
38 | if (err) throw err
39 | console.log(chalk.green(`serve on localhost:1234`))
40 | })
41 | return app.server
42 | }
43 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "asta",
3 | "version": "0.0.0",
4 | "description": "SSR resumable framework",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node src/cli.mjs -e ./demo -o ./src"
8 | },
9 | "bin": {
10 | "asta": "bin/index.js"
11 | },
12 | "engines": {
13 | "node": ">=17.5.0"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/yisar/asta.git"
18 | },
19 | "keywords": [],
20 | "author": {
21 | "email": "1533540012@qq.com",
22 | "name": "yisar",
23 | "url": "https://www.zhihu.com/people/132yse"
24 | },
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/yisar/asta/issues"
28 | },
29 | "homepage": "https://github.com/yisar/asta#readme",
30 | "dependencies": {
31 | "acorn": "^8.8.0",
32 | "chalk": "^5.1.2",
33 | "es-module-lexer": "^1.0.3",
34 | "esbuild": "^0.15.10",
35 | "fs-extra": "^10.1.0",
36 | "polka": "^0.5.2",
37 | "sirv": "^2.0.2"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/$import.mjs:
--------------------------------------------------------------------------------
1 | export function $import(url, e) {
2 | if (typeof window === 'undefined') {
3 | return url
4 | } else {
5 | console.log(url)
6 | const [path, mod] = url.split('#')
7 | console.log(path)
8 | import(path).then(async (mods) => {
9 | const newState = await mods[mod](window.__state, e)
10 | window.dispatch(newState)
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/action/count.js:
--------------------------------------------------------------------------------
1 | export const addCount = async (state, event) => {
2 | await new Promise(r => setTimeout(() => r(), 1000))
3 | return {
4 | ...state,
5 | count: state.count + 1,
6 | }
7 | }
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | // src/h.mjs
2 | var TEXT_NODE = 3;
3 | var EMPTY_OBJ = {};
4 | var EMPTY_ARR = [];
5 | var isArray = Array.isArray;
6 | var simpleNode = "";
7 | var h = function(tag, props, ...args) {
8 | let children = [];
9 | props = props || EMPTY_OBJ;
10 | let key = props.key || null;
11 | for (let i = 0; i < args.length; i++) {
12 | let vnode = args[i];
13 | const isEnd = i === args.length - 1;
14 | if (isArray(vnode)) {
15 | children.push(...vnode);
16 | } else if (vnode === false || vnode === true || vnode == null) {
17 | vnode = "";
18 | } else {
19 | const isStrNode = isStr(vnode);
20 | if (isStrNode) {
21 | simpleNode += String(vnode);
22 | }
23 | if (simpleNode && (!isStrNode || isEnd)) {
24 | children.push(createText(simpleNode));
25 | simpleNode = "";
26 | }
27 | if (!isStrNode) {
28 | children.push(vnode);
29 | }
30 | }
31 | }
32 | props.key = void 0;
33 | return typeof tag === "function" ? tag(props, children) : createVNode(tag, props, children, key);
34 | };
35 | var createText = function(value) {
36 | return createVNode(value, EMPTY_OBJ, EMPTY_ARR, null, TEXT_NODE);
37 | };
38 | var isStr = (x) => typeof x === "string" || typeof x === "number";
39 | var createVNode = function(tag, props, children, key, type) {
40 | return {
41 | tag,
42 | props,
43 | children,
44 | type,
45 | key
46 | };
47 | };
48 |
49 | // src/$import.mjs
50 | function $import(url, e) {
51 | if (typeof window === "undefined") {
52 | return url;
53 | } else {
54 | console.log(url);
55 | const [path, mod] = url.split("#");
56 | console.log(path);
57 | import(path).then(async (mods) => {
58 | const newState = await mods[mod](window.__state, e);
59 | window.dispatch(newState);
60 | });
61 | }
62 | }
63 |
64 | // demo/app.jsx
65 | var loader = async (req) => {
66 | const data = await fetch("http://localhost:1234/data").then((res) => res.json()).then((data2) => data2);
67 | return {
68 | ...data,
69 | count: 0
70 | };
71 | };
72 | var addCount = $import("./action/count.js#addCount");
73 | var Header = ({ cover, title, rate }) => /* @__PURE__ */ h("header", null, /* @__PURE__ */ h("img", {
74 | src: cover,
75 | alt: ""
76 | }), /* @__PURE__ */ h("h1", null, title), /* @__PURE__ */ h("div", {
77 | class: "rate"
78 | }, rate));
79 | var app_default = ({ title, comments, rate, imgs, info, cover, count }) => {
80 | return /* @__PURE__ */ h("div", null, /* @__PURE__ */ h(Header, {
81 | cover,
82 | title,
83 | rate
84 | }), /* @__PURE__ */ h("main", null, /* @__PURE__ */ h("button", {
85 | $onclick: addCount
86 | }, "Count: ", count)), /* @__PURE__ */ h("div", {
87 | class: "screenshot"
88 | }, /* @__PURE__ */ h("h3", null, "\u622A\u56FE"), /* @__PURE__ */ h("ul", null, imgs.map((i) => /* @__PURE__ */ h("li", {
89 | key: i
90 | }, /* @__PURE__ */ h("img", {
91 | src: i
92 | }))))), /* @__PURE__ */ h("div", {
93 | class: "screenshot"
94 | }, /* @__PURE__ */ h("h3", null, "\u7B80\u4ECB"), /* @__PURE__ */ h("p", null, info)), /* @__PURE__ */ h("div", {
95 | class: "comments"
96 | }, /* @__PURE__ */ h("h3", null, "\u8BC4\u4EF7"), /* @__PURE__ */ h("ul", null, comments.map(({ avatar, name, content }) => /* @__PURE__ */ h("li", {
97 | key: name
98 | }, /* @__PURE__ */ h("div", {
99 | class: "bio"
100 | }, /* @__PURE__ */ h("img", {
101 | class: "avatar",
102 | src: avatar
103 | }), /* @__PURE__ */ h("b", {
104 | class: "name"
105 | }, name)), /* @__PURE__ */ h("p", null, content))))));
106 | };
107 | export {
108 | app_default as default,
109 | loader
110 | };
111 |
--------------------------------------------------------------------------------
/src/app.mjs:
--------------------------------------------------------------------------------
1 | // src/s.mjs
2 | var s = {
3 | openTag(tag, attrs) {
4 | let code = "";
5 | code += `<${tag}`;
6 | for (const name in attrs) {
7 | let value = attrs[name];
8 | if (typeof attrs[name] === "object") {
9 | value = Object.values(attrs[name])[0].toString().replace(/[\s]/g, "");
10 | }
11 | code += ` ${name}="${value || ""}"`;
12 | }
13 | code += ">";
14 | return code;
15 | },
16 | closeTag(tag) {
17 | return `${tag}>`;
18 | },
19 | expression(content) {
20 | if (typeof content === "string" || typeof content === "number") {
21 | return content.toString();
22 | } else if (Array.isArray(content)) {
23 | return content.join("");
24 | } else {
25 | return "";
26 | }
27 | },
28 | component(comp, attrs) {
29 | let props = {};
30 | for (const name in attrs) {
31 | let value = attrs[name];
32 | if (typeof attrs[name] === "object") {
33 | value = Object.values(attrs[name])[0];
34 | }
35 | props[name] = value;
36 | }
37 | return comp(props);
38 | },
39 | empty() {
40 | return "";
41 | }
42 | };
43 |
44 | // src/$import.mjs
45 | function $import(url, e) {
46 | if (typeof window === "undefined") {
47 | return url;
48 | } else {
49 | console.log(url);
50 | const [path, mod] = url.split("#");
51 | console.log(path);
52 | import(path).then(async (mods) => {
53 | const newState = await mods[mod](window.__state, e);
54 | window.dispatch(newState);
55 | });
56 | }
57 | }
58 |
59 | // demo/app.jsx
60 | var loader = async (req) => {
61 | const data = await fetch("http://localhost:1234/data").then((res) => res.json()).then((data2) => data2);
62 | return {
63 | ...data,
64 | count: 0
65 | };
66 | };
67 | var addCount = $import("./action/count.js#addCount");
68 | var Header = ({ cover, title, rate }) => s.openTag("header", {}) + s.openTag("img", { "src": { cover }, "alt": "" }) + s.openTag("h1", {}) + s.expression(title) + s.closeTag("h1") + s.openTag("div", { "class": "rate" }) + s.expression(rate) + s.closeTag("div") + s.closeTag("header");
69 | var app_default = ({ title, comments, rate, imgs, info, cover, count }) => {
70 | return s.openTag("div", {}) + s.component(Header, { "cover": { cover }, "title": { title }, "rate": { rate } }) + s.openTag("main", {}) + s.openTag("button", { "$onclick": { addCount } }) + "Count: " + s.expression(count) + s.closeTag("button") + s.closeTag("main") + s.openTag("div", { "class": "screenshot" }) + s.openTag("h3", {}) + "\u622A\u56FE" + s.closeTag("h3") + s.openTag("ul", {}) + s.expression(imgs.map((i) => s.openTag("li", { "key": { i } }) + s.openTag("img", { "src": { i } }) + s.closeTag("li"))) + s.closeTag("ul") + s.closeTag("div") + s.openTag("div", { "class": "screenshot" }) + s.openTag("h3", {}) + "\u7B80\u4ECB" + s.closeTag("h3") + s.openTag("p", {}) + s.expression(info) + s.closeTag("p") + s.closeTag("div") + s.openTag("div", { "class": "comments" }) + s.openTag("h3", {}) + "\u8BC4\u4EF7" + s.closeTag("h3") + s.openTag("ul", {}) + s.expression(comments.map(({ avatar, name, content }) => s.openTag("li", { "key": { name } }) + s.openTag("div", { "class": "bio" }) + s.openTag("img", { "class": "avatar", "src": { avatar } }) + s.openTag("b", { "class": "name" }) + s.expression(name) + s.closeTag("b") + s.closeTag("div") + s.openTag("p", {}) + s.expression(content) + s.closeTag("p") + s.closeTag("li"))) + s.closeTag("ul") + s.closeTag("div") + s.closeTag("div");
71 | };
72 | export {
73 | app_default as default,
74 | loader
75 | };
76 |
--------------------------------------------------------------------------------
/src/build.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild'
2 | import fs from 'fs/promises'
3 | import path from 'node:path'
4 | import url from 'node:url'
5 | import { compile } from '../compiler/generate.mjs'
6 | import fse from 'fs-extra'
7 |
8 | const __filename = url.fileURLToPath(import.meta.url)
9 | const __dirname = path.dirname(__filename)
10 |
11 | let actionMap = {}
12 |
13 | function astaPlugin(type) {
14 | return {
15 | name: 'plugin',
16 | setup: (build) => {
17 | build.onLoad({ filter: /.*/ }, async (args) => {
18 | const content = await fs.readFile(args.path)
19 | const code = content.toString()
20 | let res = ''
21 |
22 | if (type === 'server') {
23 | res += compile(code)
24 | } else {
25 | res += code
26 | }
27 |
28 | if (/\.(?:j|t)sx\b/.test(args.path)) {
29 | if (type === 'server') {
30 | res = `import {s} from '../src/s.mjs';` + res
31 | } else {
32 | res = `import {h} from '../src/h.mjs';` + res
33 | }
34 | }
35 |
36 | return {
37 | contents: res,
38 | loader: 'jsx',
39 | }
40 | })
41 | },
42 | }
43 | }
44 |
45 | export function pathPlugin(type) {
46 | return {
47 | name: 'path-plugin',
48 | setup(build) {
49 | build.onResolve({ filter: /^~action/ }, async (args) => ({
50 | path: args.path,
51 | namespace: 'asta-path',
52 | }))
53 |
54 | build.onLoad({ filter: /.*/, namespace: 'asta-path' }, async (args) => {
55 | let code = ''
56 |
57 | if (type === 'server') {
58 | const map = actionMap[args.path]
59 | code += `export const ${map.name} = '${map.value}';`
60 | } else {
61 | const p = args.path.replace(/~action/g, './action')
62 | const file = await fs.readFile(path.join(__dirname, '../demo', p))
63 | code = file.toString()
64 | }
65 |
66 | return {
67 | contents: code,
68 | loader: 'js',
69 | }
70 | })
71 | },
72 | }
73 | }
74 |
75 | export async function build() {
76 | await esbuild.build({
77 | entryPoints: [path.join(__dirname, '../demo/app.jsx')],
78 | bundle: true,
79 | platform: 'node',
80 | format: 'esm',
81 | treeShaking: false,
82 | outfile: 'src/app.mjs',
83 | allowOverwrite: true,
84 | plugins: [pathPlugin('server'), astaPlugin('server')],
85 | watch: process.env.WATCH === 'true',
86 | })
87 |
88 | await esbuild.build({
89 | entryPoints: [path.join(__dirname, '../demo/app.jsx')],
90 | bundle: true,
91 | platform: 'browser',
92 | format: 'esm',
93 | treeShaking: false,
94 | outfile: 'src/app.js',
95 | jsxFactory: 'h',
96 | allowOverwrite: true,
97 | plugins: [pathPlugin('client'), astaPlugin('client')],
98 | watch: process.env.WATCH === 'true',
99 | })
100 |
101 | await fse.copy(path.join(process.cwd(), 'demo/public'), path.join(__dirname, 'public'))
102 | await fse.copy(path.join(process.cwd(), 'demo/action'), path.join(__dirname, 'action'))
103 | }
104 |
--------------------------------------------------------------------------------
/src/cli.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import url from 'node:url'
3 | import {build} from './build.mjs'
4 |
5 |
6 | const __filename = url.fileURLToPath(import.meta.url)
7 | const __dirname = path.dirname(__filename)
8 |
9 | const getOptions = (argv) => {
10 | let out = {
11 | e: './',
12 | o: './dist/',
13 | }
14 | for (let i = 0; i < argv.length; i++) {
15 | const name = argv[i]
16 | const value = argv[i + 1]
17 | if (name === '-w' || name === '--watch') {
18 | out['watch'] = true
19 | }
20 | if (name[0] !== '-' || !value) {
21 | continue
22 | }
23 | if (name === '-e' || name === '--entry') {
24 | out['entryDir'] = value
25 | }
26 | if (name === '-o' || name === '--output') {
27 | out['outputDir'] = value
28 | }
29 | }
30 | return out
31 | }
32 |
33 | async function run(argv) {
34 | if (argv[0] === '-v' || argv[0] === '--version') {
35 | console.log('v0.0.1')
36 | } else {
37 | const options = getOptions(argv)
38 | start(options)
39 | }
40 | }
41 |
42 | async function start(options) {
43 | const clientOutput = path.join(process.cwd(),options.outputDir, 'app.js')
44 | const serverOutput = path.join(process.cwd(),options.outputDir, 'app.mjs')
45 |
46 | const serverEntry = path.join(process.cwd(), options.entryDir, 'server.mjs')
47 | const serverOutputDir = __dirname
48 |
49 | console.log(serverEntry)
50 |
51 | await build(options)
52 |
53 | const mod = await import(`file://${serverEntry}`)
54 | mod.default({ serverOutput, serverOutputDir })
55 | }
56 |
57 | run(process.argv.slice(2))
--------------------------------------------------------------------------------
/src/count.js:
--------------------------------------------------------------------------------
1 | export const addCount = (state, event) => {
2 | return {
3 | ...state,
4 | count: state.count + 1,
5 | }
6 | }
--------------------------------------------------------------------------------
/src/h.mjs:
--------------------------------------------------------------------------------
1 | const TEXT_NODE = 3
2 | const EMPTY_OBJ = {}
3 | const EMPTY_ARR = []
4 | const isArray = Array.isArray
5 |
6 | let simpleNode = '';
7 |
8 | export const h = function (tag, props, ...args) {
9 | let children = []
10 | props = props || EMPTY_OBJ
11 |
12 |
13 | let key = props.key || null
14 |
15 | for (let i = 0; i < args.length; i++) {
16 | let vnode = args[i]
17 | const isEnd = i === args.length - 1;
18 |
19 | if (isArray(vnode)) {
20 | children.push(...vnode)
21 | } else if (vnode === false || vnode === true || vnode == null) {
22 | vnode = ''
23 | } else {
24 | const isStrNode = isStr(vnode);
25 | // merge simple nodes
26 | if (isStrNode) {
27 | simpleNode += String(vnode);
28 | }
29 |
30 | if (simpleNode && (!isStrNode || isEnd)) {
31 | children.push(createText(simpleNode));
32 | simpleNode = '';
33 | }
34 |
35 | if (!isStrNode) {
36 | children.push(vnode)
37 | }
38 |
39 | }
40 | }
41 |
42 | props.key = undefined;
43 | return typeof tag === "function"
44 | ? tag(props, children)
45 | : createVNode(tag, props, children, key)
46 | }
47 |
48 | const createText = function (value) {
49 | return createVNode(value, EMPTY_OBJ, EMPTY_ARR, null, TEXT_NODE)
50 | }
51 |
52 | const isStr = x => typeof x === 'string' || typeof x === 'number'
53 |
54 | const createVNode = function (tag, props, children, key, type) {
55 | return {
56 | tag,
57 | props,
58 | children,
59 | type,
60 | key
61 | }
62 | }
--------------------------------------------------------------------------------
/src/public/client.mjs:
--------------------------------------------------------------------------------
1 | import { resume } from '../resume.mjs'
2 |
3 | resume(document.body)
--------------------------------------------------------------------------------
/src/public/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Can You Escape VintageBungalow 封测国际服",
3 | "rate": "8.4",
4 | "comments": [
5 | {
6 | "name": "阿呆",
7 | "avatar": "https://img3.tapimg.com/default_avatars/755e9ca449be08245191a743a128a8df.jpg?imageMogr2/auto-orient/strip/thumbnail/!300x300r/gravity/Center/crop/300x300/format/jpg/interlace/1/quality/80",
8 | "content": "bdbnxjcjcjj"
9 | },
10 | {
11 | "name": "迪卢克",
12 | "avatar": "https://img3.tapimg.com/default_avatars/7d713c00e515de52a63c0f51c8697c84.jpg?imageMogr2/auto-orient/strip/thumbnail/!300x300r/gravity/Center/crop/300x300/format/jpg/interlace/1/quality/80",
13 | "content": "Vbjjnnn😂"
14 | }
15 | ],
16 | "imgs": [
17 | "https://img.tapimg.com/market/images/de62537f7b8aad4f6b8b53cb968901f0.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
18 | "https://img.tapimg.com/market/images/123ec01bb9b5c42de4fa214303cf1383.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
19 | "https://img.tapimg.com/market/images/286c9889acad05a6e3ae2f07b5035760.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1",
20 | "https://img.tapimg.com/market/images/ea16c10e162a5b9b2e2fe6746a1de6f3.png?imageView2/2/h/560/w/9999/q/80/format/jpg/interlace/1/ignore-error/1"
21 | ],
22 | "info": "Can You Escape VintageBungalow is new android escape game developed by KnfGame.In this game your locked inside a Vintage Bungalow House, the only way to escape from bungalow is to find the hidden key. For that you have click on the useful objects around the house and solve some interesting puzzles to find the hidden key. Good Luck and have fun playing Knf escape games and free online point and click escape games.",
23 | "cover": "https://img.tapimg.com/market/icons/9e99c190fdb4f28136921fcc74a7467f_360.png?imageMogr2/auto-orient/strip"
24 | }
--------------------------------------------------------------------------------
/src/public/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | :root {
7 | --theme-color: #07b9c9;
8 | }
9 |
10 | li {
11 | list-style: none;
12 | }
13 |
14 | button {
15 | background-color: var(--theme-color);
16 | color: aliceblue;
17 | padding: 20px;
18 | margin: 20px;
19 | border: 0;
20 | border-radius: 10px;
21 | font-size: 16px;
22 | }
23 |
24 | header {
25 | display: flex;
26 | padding: 10px;
27 | align-items: center;
28 | }
29 |
30 | header img {
31 | height: 100px;
32 | width: 100px;
33 | }
34 |
35 | header h1 {
36 | width: 220px;
37 | font-size: 18px;
38 | padding: 10px;
39 | }
40 |
41 | header .rate {
42 | width: 50px;
43 | height: 50px;
44 | background: url(https://assets.tapimg.com/img/background/rating_score@2x-99a4cf2f6b.png);
45 | color: #fff;
46 | background-size: cover;
47 | text-align: center;
48 | font-size: 24px;
49 | }
50 |
51 | main {
52 | display: flex;
53 | justify-content: center;
54 | }
55 |
56 | .screenshot {
57 | width: 100%;
58 | overflow: scroll;
59 | border-top: 1px solid #eee;
60 | padding: 15px;
61 | box-sizing: border-box;
62 | }
63 |
64 | .comments {
65 | width: 100%;
66 | overflow: scroll;
67 | border-top: 10px solid #eee;
68 | }
69 |
70 | .screenshot ul li {
71 | display: inline-block;
72 | }
73 |
74 | .screenshot ul {
75 | display: flex;
76 | }
77 |
78 | .screenshot img {
79 | height: 170px;
80 | padding: 5px;
81 | }
82 | .screenshot h3 {
83 | padding: 5px;
84 | }
85 |
86 | .screenshot p {
87 | color: #666;
88 | padding: 10px 5px;
89 | height: 75px;
90 | font-size: 14px;
91 | overflow: hidden;
92 |
93 | }
94 |
95 | .comments li .bio {
96 | display: flex;
97 | align-items: center;
98 | }
99 |
100 | .comments li {
101 | padding: 5px 15px;
102 | border-bottom: 10px solid #eee;
103 | }
104 |
105 | .comments h3 {
106 | padding: 15px;
107 | }
108 |
109 | .bio .avatar {
110 | height: 40px;
111 | width: 40px;
112 | border-radius: 20px;
113 | }
114 | .bio .avatar{
115 | margin: 10px 10px 0 0;
116 | }
117 | .comments li p{
118 | padding: 15px 0;
119 | }
--------------------------------------------------------------------------------
/src/resume.mjs:
--------------------------------------------------------------------------------
1 | import { $import } from './$import.mjs'
2 |
3 | const events = ['click', 'keydown', 'input', 'mousedown', 'mouseover', 'mouseenter', 'blur', 'focus', 'dbclick', 'wheel', 'change', 'touchstart', 'touchmove', 'touchend', 'drag']
4 |
5 | for (const event of events) {
6 | document.addEventListener(event, (e) => {
7 | const target = e.target
8 | if (target) {
9 | $import(target.getAttribute('$on' + event), e)
10 | }
11 | })
12 | }
13 |
14 | export function resume(root) {
15 | window.dispatch = (newState) => {
16 | window.__state = { ...window.__state, ...newState }
17 | import('./app.js').then((mod) => {
18 | const vdom = mod.default(window.__state)
19 | patch(root, root.firstChild, vdom)
20 | })
21 | }
22 | }
23 |
24 | var getKey = (vdom) => {
25 | return vdom == null ? vdom : vdom.getAttribute ? vdom.getAttribute('key') : vdom.key
26 | }
27 |
28 | function patch(parent, node, vnode) {
29 | if (vnode.type === 3 && node.nodeType === 3) {
30 | if (node.nodeValue !== vnode.tag) {
31 | node.nodeValue = vnode.tag
32 | }
33 | } else if (node == null || node.nodeName.toLowerCase() !== vnode.tag) {
34 | parent.insertBefore(createNode(vnode))
35 | if (node != null) {
36 | parent.removeChild(node)
37 | }
38 | } else {
39 | let oldHead = 0,
40 | newHead = 0,
41 | oldKids = node.childNodes,
42 | newKids = vnode.children,
43 | oldTail = oldKids.length - 1,
44 | newTail = newKids.length - 1,
45 | oldKey
46 |
47 | updateNode(node, vnode)
48 |
49 | while (newHead <= newTail && oldHead <= oldTail) {
50 | if ((oldKey = getKey(oldKids[oldHead]) == null || oldKey !== getKey(newKids[newHead]))) {
51 | break
52 | }
53 | patch(node, oldKids[oldHead++], newKids[newHead++])
54 | }
55 |
56 | while (newHead <= newTail && oldHead <= oldTail) {
57 | if ((oldKey = getKey(oldKids[oldTail]) == null || oldKey !== getKey(newKids[newTail]))) {
58 | break
59 | }
60 | patch(node, oldKids[oldTail--], newKids[newTail--])
61 | }
62 |
63 | if (oldHead > oldTail) {
64 | while (newHead <= newTail) {
65 | node.insertBefore(createNode(newKids[newHead++]), oldKids[oldHead])
66 | }
67 | } else if (newHead > newTail) {
68 | while (oldHead <= oldTail) {
69 | node.removeChild(oldKids[oldHead++])
70 | }
71 | } else {
72 | for (var keyed = {}, newKeyed = {}, i = oldHead; i <= oldTail; i++) {
73 | if (oldKids[i].key != null) {
74 | keyed[oldKids[i].key] = oldKids[i]
75 | }
76 | }
77 | while (newHead <= newTail) {
78 | let oldKey = getKey(oldKids[oldHead])
79 | let newKey = getKey(newKids[newHead])
80 |
81 | if (newKeyed[oldKey] || (newKey != null && newKey === getKey(oldKids[oldHead + 1]))) {
82 | if (oldKey == null) {
83 | node.removeChild(oldKids[oldHead])
84 | }
85 | oldHead++
86 | continue
87 | }
88 |
89 | if (newKey == null) {
90 | if (oldKey == null) {
91 | patch(node, oldKids[oldHead], newKids[newHead])
92 | newHead++
93 | }
94 | oldHead++
95 | } else {
96 | if (oldKey === newKey) {
97 | patch(node, oldKids[oldHead], newKids[newHead])
98 | newKeyed[newKey] = true
99 | oldHead++
100 | } else {
101 | if (keyed[newKey] != null) {
102 | patch(node, node.insertBefore(keyed[newKey], oldKids[oldHead]), newKids[newHead])
103 | newKeyed[newKey] = true
104 | } else {
105 | patch(node, oldKids[oldHead], newKids[newHead])
106 | }
107 | }
108 | newHead++
109 | }
110 | }
111 |
112 | while (oldHead <= oldTail) {
113 | if (getKey(oldKids[oldHead++]) == null) {
114 | node.removeChild(oldKids[oldHead])
115 | }
116 | }
117 |
118 | for (const i in keyed) {
119 | if (newKeyed[i] == null) {
120 | node.removeChild(keyed[i])
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
127 | function updateNode(node, vnode) {
128 | for (const name in vnode.props) {
129 | // need diff
130 | if (name[0] === '$' || name === 'key') continue
131 | if (!(name in node.attributes) || node.getAttribute(name) !== vnode.props[name]) {
132 | if (name in node) {
133 | node[name] = vnode.props[name]
134 | } else {
135 | node.setAttribute(name, vnode.props[name])
136 | }
137 | }
138 | }
139 | }
140 |
141 | function createNode(vdom) {
142 | const dom = vdom.type === 3 ? document.createTextNode('') : document.createElement(vdom.tag)
143 |
144 | for (var i = 0; i < vdom.children.length; i++) {
145 | dom.appendChild(createNode(vdom.children[i]))
146 | }
147 | return dom
148 | }
149 |
--------------------------------------------------------------------------------
/src/s.mjs:
--------------------------------------------------------------------------------
1 | // 这里的实现并不好,正确应该是生成线性的结构
2 |
3 | export const s = {
4 | openTag(tag, attrs) {
5 | let code = ''
6 | code += `<${tag}`
7 | for (const name in attrs) {
8 | let value = attrs[name]
9 | if (typeof attrs[name] === 'object') {
10 | value = Object.values(attrs[name])[0].toString().replace(/[\s]/g, '')
11 | }
12 | code += ` ${name}="${value || ''}"`
13 | }
14 | code += '>'
15 | return code
16 | },
17 | closeTag(tag) {
18 | return `${tag}>`
19 | },
20 | expression(content) {
21 | if (typeof content === 'string' || typeof content === 'number') {
22 | return content.toString()
23 | } else if (Array.isArray(content)) {
24 | return content.join('')
25 | } else {
26 | return ''
27 | }
28 | },
29 | component(comp, attrs){
30 | let props = {}
31 | for (const name in attrs) {
32 | let value = attrs[name]
33 | if (typeof attrs[name] === 'object') {
34 | value = Object.values(attrs[name])[0]
35 | }
36 | props[name] = value
37 | }
38 | return comp(props)
39 | },
40 | empty(){
41 | return ''
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@arr/every@^1.0.0":
6 | version "1.0.1"
7 | resolved "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz#22fe1f8e6355beca6c7c7bde965eb15cf994387b"
8 | integrity sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==
9 |
10 | "@esbuild/android-arm@0.15.11":
11 | version "0.15.11"
12 | resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.11.tgz#bdd9c3e098183bdca97075aa4c3e0152ed3e10ee"
13 | integrity sha512-PzMcQLazLBkwDEkrNPi9AbjFt6+3I7HKbiYF2XtWQ7wItrHvEOeO3T8Am434zAozWtVP7lrTue1bEfc2nYWeCA==
14 |
15 | "@esbuild/linux-loong64@0.15.11":
16 | version "0.15.11"
17 | resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.11.tgz#2f4f9a1083dcb4fc65233b6f59003c406abf32e5"
18 | integrity sha512-geWp637tUhNmhL3Xgy4Bj703yXB9dqiLJe05lCUfjSFDrQf9C/8pArusyPUbUbPwlC/EAUjBw32sxuIl/11dZw==
19 |
20 | "@polka/url@^0.5.0":
21 | version "0.5.0"
22 | resolved "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31"
23 | integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==
24 |
25 | "@polka/url@^1.0.0-next.20":
26 | version "1.0.0-next.21"
27 | resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
28 | integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
29 |
30 | acorn@^8.8.0:
31 | version "8.8.0"
32 | resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
33 | integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
34 |
35 | chalk@^5.1.2:
36 | version "5.1.2"
37 | resolved "https://registry.npmjs.org/chalk/-/chalk-5.1.2.tgz#d957f370038b75ac572471e83be4c5ca9f8e8c45"
38 | integrity sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==
39 |
40 | es-module-lexer@^1.0.3:
41 | version "1.0.5"
42 | resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.0.5.tgz#d6d9de310a5e11cbf73b39a1d6f79e5c3df4d06f"
43 | integrity sha512-oxJ+R1DzAw6j4g1Lx70bIKgfoRCX67C51kH2Mx7J4bS7ZzWxkcivXskFspzgKHUj6JUwUTghQgUPy8zTp6mMBw==
44 |
45 | esbuild-android-64@0.15.11:
46 | version "0.15.11"
47 | resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.11.tgz#50402129c3e85bb06434e212374c5f693e4c5f01"
48 | integrity sha512-rrwoXEiuI1kaw4k475NJpexs8GfJqQUKcD08VR8sKHmuW9RUuTR2VxcupVvHdiGh9ihxL9m3lpqB1kju92Ialw==
49 |
50 | esbuild-android-arm64@0.15.11:
51 | version "0.15.11"
52 | resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.11.tgz#49bee35218ea2ccf1a0c5f187af77c1c0a5dee71"
53 | integrity sha512-/hDubOg7BHOhUUsT8KUIU7GfZm5bihqssvqK5PfO4apag7YuObZRZSzViyEKcFn2tPeHx7RKbSBXvAopSHDZJQ==
54 |
55 | esbuild-darwin-64@0.15.11:
56 | version "0.15.11"
57 | resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.11.tgz#89a90c8cf6f0029ac4169bfedd012a0412c1575f"
58 | integrity sha512-1DqHD0ms3AhiwkKnjRUzmiW7JnaJJr5FKrPiR7xuyMwnjDqvNWDdMq4rKSD9OC0piFNK6n0LghsglNMe2MwJtA==
59 |
60 | esbuild-darwin-arm64@0.15.11:
61 | version "0.15.11"
62 | resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.11.tgz#556f4385c6de806cc81132dd7b8af00fe9d292df"
63 | integrity sha512-OMzhxSbS0lwwrW40HHjRCeVIJTURdXFA8c3GU30MlHKuPCcvWNUIKVucVBtNpJySXmbkQMDJdJNrXzNDyvoqvQ==
64 |
65 | esbuild-freebsd-64@0.15.11:
66 | version "0.15.11"
67 | resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.11.tgz#fd86fd1b3b65366048f35b996d9cdf3547384eee"
68 | integrity sha512-8dKP26r0/Qyez8nTCwpq60QbuYKOeBygdgOAWGCRalunyeqWRoSZj9TQjPDnTTI9joxd3QYw3UhVZTKxO9QdRg==
69 |
70 | esbuild-freebsd-arm64@0.15.11:
71 | version "0.15.11"
72 | resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.11.tgz#d346bcacfe9779ebc1a11edac1bdedeff6dda3b1"
73 | integrity sha512-aSGiODiukLGGnSg/O9+cGO2QxEacrdCtCawehkWYTt5VX1ni2b9KoxpHCT9h9Y6wGqNHmXFnB47RRJ8BIqZgmQ==
74 |
75 | esbuild-linux-32@0.15.11:
76 | version "0.15.11"
77 | resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.11.tgz#64b50e774bf75af7dcc6a73ad509f2eb0ac4487b"
78 | integrity sha512-lsrAfdyJBGx+6aHIQmgqUonEzKYeBnyfJPkT6N2dOf1RoXYYV1BkWB6G02tjsrz1d5wZzaTc3cF+TKmuTo/ZwA==
79 |
80 | esbuild-linux-64@0.15.11:
81 | version "0.15.11"
82 | resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.11.tgz#fba3a78b95769772863f8f6dc316abca55cf8416"
83 | integrity sha512-Y2Rh+PcyVhQqXKBTacPCltINN3uIw2xC+dsvLANJ1SpK5NJUtxv8+rqWpjmBgaNWKQT1/uGpMmA9olALy9PLVA==
84 |
85 | esbuild-linux-arm64@0.15.11:
86 | version "0.15.11"
87 | resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.11.tgz#c0cb31980eee066bfd39a4593660a0ecebe926cb"
88 | integrity sha512-uhcXiTwTmD4OpxJu3xC5TzAAw6Wzf9O1XGWL448EE9bqGjgV1j+oK3lIHAfsHnuIn8K4nDW8yjX0Sv5S++oRuw==
89 |
90 | esbuild-linux-arm@0.15.11:
91 | version "0.15.11"
92 | resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.11.tgz#7824d20099977aa671016c7de7a5038c9870010f"
93 | integrity sha512-TJllTVk5aSyqPFvvcHTvf6Wu1ZKhWpJ/qNmZO8LL/XeB+LXCclm7HQHNEIz6MT7IX8PmlC1BZYrOiw2sXSB95A==
94 |
95 | esbuild-linux-mips64le@0.15.11:
96 | version "0.15.11"
97 | resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.11.tgz#10627331c90164e553429ed25e025184bba485b6"
98 | integrity sha512-WD61y/R1M4BLe4gxXRypoQ0Ci+Vjf714QYzcPNkiYv5I8K8WDz2ZR8Bm6cqKxd6rD+e/rZgPDbhQ9PCf7TMHmA==
99 |
100 | esbuild-linux-ppc64le@0.15.11:
101 | version "0.15.11"
102 | resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.11.tgz#be42679a36a5246b893fc8b898135ebacb5a0a14"
103 | integrity sha512-JVleZS9oPVLTlBhPTWgOwxFWU/wMUdlBwTbGA4GF8c38sLbS13cupj+C8bLq929jU7EMWry4SaL+tKGIaTlqKg==
104 |
105 | esbuild-linux-riscv64@0.15.11:
106 | version "0.15.11"
107 | resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.11.tgz#3ac2f328e3db73cbff833ada94314d8e79503e54"
108 | integrity sha512-9aLIalZ2HFHIOZpmVU11sEAS9F8TnHw49daEjcgMpBXHFF57VuT9f9/9LKJhw781Gda0P9jDkuCWJ0tFbErvJw==
109 |
110 | esbuild-linux-s390x@0.15.11:
111 | version "0.15.11"
112 | resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.11.tgz#e774e0df061b6847d86783bf3c8c4300a72e03ad"
113 | integrity sha512-sZHtiXXOKsLI3XGBGoYO4qKBzJlb8xNsWmvFiwFMHFzA4AXgDP1KDp7Dawe9C2pavTRBDvl+Ok4n/DHQ59oaTg==
114 |
115 | esbuild-netbsd-64@0.15.11:
116 | version "0.15.11"
117 | resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.11.tgz#55e265fa4489e3f396b16c81f6f5a11d6ca2a9a4"
118 | integrity sha512-hUC9yN06K9sg7ju4Vgu9ChAPdsEgtcrcLfyNT5IKwKyfpLvKUwCMZSdF+gRD3WpyZelgTQfJ+pDx5XFbXTlB0A==
119 |
120 | esbuild-openbsd-64@0.15.11:
121 | version "0.15.11"
122 | resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.11.tgz#bc04103ccfd8c2f2241e1add0b51a095955b73c4"
123 | integrity sha512-0bBo9SQR4t66Wd91LGMAqmWorzO0TTzVjYiifwoFtel8luFeXuPThQnEm5ztN4g0fnvcp7AnUPPzS/Depf17wQ==
124 |
125 | esbuild-sunos-64@0.15.11:
126 | version "0.15.11"
127 | resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz#ccd580305d31fde07b5c386da79c942aaf069013"
128 | integrity sha512-EuBdTGlsMTjEl1sQnBX2jfygy7iR6CKfvOzi+gEOfhDqbHXsmY1dcpbVtcwHAg9/2yUZSfMJHMAgf1z8M4yyyw==
129 |
130 | esbuild-windows-32@0.15.11:
131 | version "0.15.11"
132 | resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.11.tgz#40fe1d48f9b20a76f6db5109aaaf1511aed58c71"
133 | integrity sha512-O0/Wo1Wk6dc0rZSxkvGpmTNIycEznHmkObTFz2VHBhjPsO4ZpCgfGxNkCpz4AdAIeMczpTXt/8d5vdJNKEGC+Q==
134 |
135 | esbuild-windows-64@0.15.11:
136 | version "0.15.11"
137 | resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.11.tgz#80c58b1ef2ff030c78e3a06e7a922776cc4cb687"
138 | integrity sha512-x977Q4HhNjnHx00b4XLAnTtj5vfbdEvkxaQwC1Zh5AN8g5EX+izgZ6e5QgqJgpzyRNJqh4hkgIJF1pyy1be0mQ==
139 |
140 | esbuild-windows-arm64@0.15.11:
141 | version "0.15.11"
142 | resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.11.tgz#018624023b5c3f0cca334cc99f5ef7134d396333"
143 | integrity sha512-VwUHFACuBahrvntdcMKZteUZ9HaYrBRODoKe4tIWxguQRvvYoYb7iu5LrcRS/FQx8KPZNaa72zuqwVtHeXsITw==
144 |
145 | esbuild@^0.15.10:
146 | version "0.15.11"
147 | resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz#524d48612a9aa7edc1753c83459cb6fcae0cb66e"
148 | integrity sha512-OgHGuhlfZ//mToxjte1D5iiiQgWfJ2GByVMwEC/IuoXsBGkuyK1+KrjYu0laSpnN/L1UmLUCv0s25vObdc1bVg==
149 | optionalDependencies:
150 | "@esbuild/android-arm" "0.15.11"
151 | "@esbuild/linux-loong64" "0.15.11"
152 | esbuild-android-64 "0.15.11"
153 | esbuild-android-arm64 "0.15.11"
154 | esbuild-darwin-64 "0.15.11"
155 | esbuild-darwin-arm64 "0.15.11"
156 | esbuild-freebsd-64 "0.15.11"
157 | esbuild-freebsd-arm64 "0.15.11"
158 | esbuild-linux-32 "0.15.11"
159 | esbuild-linux-64 "0.15.11"
160 | esbuild-linux-arm "0.15.11"
161 | esbuild-linux-arm64 "0.15.11"
162 | esbuild-linux-mips64le "0.15.11"
163 | esbuild-linux-ppc64le "0.15.11"
164 | esbuild-linux-riscv64 "0.15.11"
165 | esbuild-linux-s390x "0.15.11"
166 | esbuild-netbsd-64 "0.15.11"
167 | esbuild-openbsd-64 "0.15.11"
168 | esbuild-sunos-64 "0.15.11"
169 | esbuild-windows-32 "0.15.11"
170 | esbuild-windows-64 "0.15.11"
171 | esbuild-windows-arm64 "0.15.11"
172 |
173 | fs-extra@^10.1.0:
174 | version "10.1.0"
175 | resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
176 | integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
177 | dependencies:
178 | graceful-fs "^4.2.0"
179 | jsonfile "^6.0.1"
180 | universalify "^2.0.0"
181 |
182 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
183 | version "4.2.10"
184 | resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
185 | integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
186 |
187 | jsonfile@^6.0.1:
188 | version "6.1.0"
189 | resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
190 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
191 | dependencies:
192 | universalify "^2.0.0"
193 | optionalDependencies:
194 | graceful-fs "^4.1.6"
195 |
196 | matchit@^1.0.0:
197 | version "1.1.0"
198 | resolved "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz#c4ccf17d9c824cc1301edbcffde9b75a61d10a7c"
199 | integrity sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==
200 | dependencies:
201 | "@arr/every" "^1.0.0"
202 |
203 | mrmime@^1.0.0:
204 | version "1.0.1"
205 | resolved "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
206 | integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==
207 |
208 | polka@^0.5.2:
209 | version "0.5.2"
210 | resolved "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz#588bee0c5806dbc6c64958de3a1251860e9f2e26"
211 | integrity sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==
212 | dependencies:
213 | "@polka/url" "^0.5.0"
214 | trouter "^2.0.1"
215 |
216 | sirv@^2.0.2:
217 | version "2.0.2"
218 | resolved "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz#128b9a628d77568139cff85703ad5497c46a4760"
219 | integrity sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==
220 | dependencies:
221 | "@polka/url" "^1.0.0-next.20"
222 | mrmime "^1.0.0"
223 | totalist "^3.0.0"
224 |
225 | totalist@^3.0.0:
226 | version "3.0.0"
227 | resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd"
228 | integrity sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==
229 |
230 | trouter@^2.0.1:
231 | version "2.0.1"
232 | resolved "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz#2726a5f8558e090d24c3a393f09eaab1df232df6"
233 | integrity sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==
234 | dependencies:
235 | matchit "^1.0.0"
236 |
237 | universalify@^2.0.0:
238 | version "2.0.0"
239 | resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
240 | integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
241 |
--------------------------------------------------------------------------------
评价
49 |50 | {comments.map(({ avatar, name, content }) => ( 51 |-
52 |
53 |
54 | {name}
55 |
56 |
58 | ))}
59 |
60 |{content}
57 |