├── .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 |

image

2 | 3 | 4 | # Asta [![NPM version](https://img.shields.io/npm/v/asta.svg)](https://npmjs.com/package/asta) [![NPM downloads](https://img.shields.io/npm/dt/eplayer.svg)](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(""])), 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 |
43 |

简介

44 |

{info}

45 |
46 | 47 |
48 |

评价

49 |
    50 | {comments.map(({ avatar, name, content }) => ( 51 |
  • 52 |
    53 | 54 | {name} 55 |
    56 |

    {content}

    57 |
  • 58 | ))} 59 |
60 |
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 ``; 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 `` 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 | --------------------------------------------------------------------------------