diff --git a/README.md b/README.md index aaba2fa1e16eebb0ff68df9127e1afc6395c74d8..20040019323581de8e494dba58a6ab06a9fb5f27 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,19 @@ $ GIT_USER= yarn deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. + +### inula-docdemo + +In order to use inula-docdemo to show the preview of valid demos in the markdown files, install the dependencies first by: + +``` +$ npm install && cd docdemo && npm install && cd template && npm install && cd .. && cd .. +``` + +Then build demo and the website (in the root directory) + +``` +$ npm run docdemobuild +``` + +Adjust the settings in `docdemo/docdemo.config.js` if necessary. diff --git a/docdemo/.env.example b/docdemo/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..ef656dd325690d82e1b072ace6e366e9b91ac6fa --- /dev/null +++ b/docdemo/.env.example @@ -0,0 +1,5 @@ +# Do not share your OpenAI API key with anyone! It should remain a secret. (e.g. sk-xxxxxx) +OPENAI_API_KEY= + +# Proxy setting to connect to OpenAI API (e.g. http://127.0.0.1:7890) +PROXY= \ No newline at end of file diff --git a/docdemo/.gitignore b/docdemo/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0c30f04db7ba6493d343888e986f01b9878d21f1 --- /dev/null +++ b/docdemo/.gitignore @@ -0,0 +1,129 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Lock files +*-lock.* + +# JetBrains IDE +.idea + +# Project + +tmp/demo-src +demo-dist +tmp/ + diff --git a/docdemo/LICENSE b/docdemo/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..9e32cdef1625daed25cf365c865f01050877cff3 --- /dev/null +++ b/docdemo/LICENSE @@ -0,0 +1,127 @@ + 木兰宽松许可证, 第2版 + + 木兰宽松许可证, 第2版 + 2020年1月 http://license.coscl.org.cn/MulanPSL2 + + + 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + + 0. 定义 + + “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + + “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + + “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + + “法人实体”是指提交贡献的机构及其“关联实体”。 + + “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + + 1. 授予版权许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 + + 2. 授予专利许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 + + 3. 无商标许可 + + “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 + + 4. 分发限制 + + 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + + 5. 免责声明与责任限制 + + “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + + 6. 语言 + “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 + + 条款结束 + + 如何将木兰宽松许可证,第2版,应用到您的软件 + + 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + + 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + + 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + + 3, 请将如下声明文本放入每个源文件的头部注释中。 + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + + Mulan Permissive Software License,Version 2 + + Mulan Permissive Software License,Version 2 (Mulan PSL v2) + January 2020 http://license.coscl.org.cn/MulanPSL2 + + Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: + + 0. Definition + + Software means the program and related documents which are licensed under this License and comprise all Contribution(s). + + Contribution means the copyrightable work licensed by a particular Contributor under this License. + + Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + + Legal Entity means the entity making a Contribution and all its Affiliates. + + Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + + 1. Grant of Copyright License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. + + 2. Grant of Patent License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. + + 3. No Trademark License + + No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. + + 4. Distribution Restriction + + You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. + + 5. Disclaimer of Warranty and Limitation of Liability + + THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 6. Language + + THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. + + END OF THE TERMS AND CONDITIONS + + How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software + + To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: + + i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; + + ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; + + iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. + + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. diff --git a/docdemo/README.en.md b/docdemo/README.en.md new file mode 100644 index 0000000000000000000000000000000000000000..e45b47517eece815aaa3cf821e6cc1ce30ce0e22 --- /dev/null +++ b/docdemo/README.en.md @@ -0,0 +1,39 @@ +# inula-docdemo + +#### Introduction +Generate inula compilation results of code snippets in markdown documents + +#### Software Architecture + +![Software Architecture](img/architecture.png) + +1. Parse the Markdown document, extract the content of code blocks, and replace the code blocks with components containing previews in a new Markdown document. +2. Put the code block content into a sandbox and compile it using the latest version of openInula. Report any examples that cannot be compiled correctly and compare the report with previous compilation results for regression testing. +3. Temporarily replace the Markdown document with the new document, build the website, and then replace it back to the original document. +4. Integrate deployment in CI commands. + +#### Installation Tutorial + +```bash +npm install +cd template && npm install && cd .. +cd test && npm install && cd .. +``` + +#### Instructions for Use + +To use GPT, you need to create a `.env` file based on `.env.example` and fill in the `OPENAI_API_KEY`. Then, set `gpt` to `true` in `docdemo.config.js`. +If you want to keep the cached results on git, you need to manually +``` +git add -f tmp/demo-cache/* +``` +then commit. + +```bash +npm run start +npm run preview # optional +``` + +#### License + +Licensed under [Mulan Permissive Software License Version 2](LICENSE). diff --git a/docdemo/README.md b/docdemo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9403d83db238d7c1e30401973b655e20e434ec34 --- /dev/null +++ b/docdemo/README.md @@ -0,0 +1,40 @@ +# inula-docdemo + +#### 介绍 +生成 markdown 文档中代码片段的 inula 编译结果 + +#### 软件架构 + +![软件架构](img/architecture.png) + +1. 对 Markdown 文档进行解析,提取出代码块的内容,并对代码块在新的 Markdown 文档中替换为含有预览的组件。 +2. 将代码块内容放入沙箱中,使用最新版本的 openInula 进行编译,对于无法正确编译的示例进行报告,并可与之前的编译结果进行比较报告回归测试结果。 +3. 临时替换 Markdown 文档为新文档,构建网站,最后替换回原文档。 +4. 集成在 CI 命令中进行部署。 + +#### 安装教程 + +```bash +npm install +cd template && npm install && cd .. +cd test && npm install && cd .. +``` + +#### 使用说明 + +使用 GPT 需要在按照 `.env.example` 创建 `.env` 文件填写 `OPENAI_API_KEY`, +然后在 `docdemo.config.js` 中将 `gpt` 设置为 `true`。 +如果你希望保留缓存结果于 git 上,你需要手动 +``` +git add -f tmp/demo-cache/* +``` +然后再提交。 + +```bash +npm run start # 编译示例 +npm run preview # 预览效果(可选) +``` + +#### 许可证 + +遵循 [木兰宽松许可证第2版](LICENSE)。 diff --git a/docdemo/docdemo.config.js b/docdemo/docdemo.config.js new file mode 100644 index 0000000000000000000000000000000000000000..a74d66748e36b21b05b31743e2fcb65e0ab5b8b5 --- /dev/null +++ b/docdemo/docdemo.config.js @@ -0,0 +1,15 @@ +const config = { + siteFolder: '..', // website base folder + docsSrcFolder: '../docs', // folder contains markdown files + docsDistFolder: '../build', // website build result folder + cssFilePath: '../src/css/custom.css', // append to global css file + siteBuildCmd: 'npm run build', // website build command (relative to siteFolder) + templateFolder: 'template', // template folder (sandbox) + templateFiles: ['index.html', 'index.css'], // template files (sandbox) + tmpFolder: 'tmp', // temporary files folder (may contains cache) + gpt: false, // set .env file correctly, then use gpt to extend code demo + demoSrcCache: true, // cache code demo source files + demoSrcCacheDebug: false, // debug the cache. If you encounter failure on cache and do not want to fix it, set it to true, and build two times, the invalid cache will be ignored, and then set to false for regular set up. +}; + +module.exports = config; diff --git a/docdemo/img/architecture.png b/docdemo/img/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..1739254afdb55a19d8cbc15896526f507796030f Binary files /dev/null and b/docdemo/img/architecture.png differ diff --git a/docdemo/index.js b/docdemo/index.js new file mode 100644 index 0000000000000000000000000000000000000000..abf1ec0e53e316866d7bf8cf474d777ca0f76ea6 --- /dev/null +++ b/docdemo/index.js @@ -0,0 +1,25 @@ +// Setup environment variables +const dotenv = require('dotenv'); +dotenv.config(); + +const rawConfig = require('./docdemo.config.js'); +const configBuild = require('./src/configBuild.js'); +const processMarkdown = require('./src/processMarkdown.js'); +const compileCode = require('./src/compileCode.js'); +const buildSite = require('./src/buildSite.js'); + +const config = configBuild(rawConfig); + +console.log("Processing documentation Markdown files ...") +processMarkdown(config).then(() => { + + console.log("Building code ..."); + compileCode(config); + + console.log("Building website ..."); + buildSite(config); + + console.log("Generating Complete!"); + +}).catch((e) => { throw e; }); + diff --git a/docdemo/package.json b/docdemo/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d0c3341e070fea5adc8f501a89ebec7554a50a76 --- /dev/null +++ b/docdemo/package.json @@ -0,0 +1,35 @@ +{ + "name": "inula-docdemo", + "version": "0.2.0", + "description": "A tool for extracting code segements from markdown files and generating inula env compiled results", + "main": "index.js", + "scripts": { + "build": "cd test && npm run build", + "start": "node index.js", + "preview": "node preview.js" + }, + "repository": { + "type": "git", + "url": "https://gitee.com/LogCreative/inula-docdemo.git" + }, + "keywords": [ + "inula", + "doc", + "demo" + ], + "author": "Zilong Li , Huangji Wang <1114655801@qq.com>, Zhiteng Li ", + "license": "MulanPSL2", + "devDependencies": { + "@types/node": "^16.14" + }, + "dependencies": { + "@babel/parser": "^7.23.6", + "chalk": "<5", + "dotenv": "^16.3.1", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "https-proxy-agent": "^7.0.2", + "live-server": "^1.2.2", + "openai": "^4.24.1" + } +} diff --git a/docdemo/preview.js b/docdemo/preview.js new file mode 100644 index 0000000000000000000000000000000000000000..59297e85c4ced91ad857b31998a44f822335144a --- /dev/null +++ b/docdemo/preview.js @@ -0,0 +1,16 @@ +const liveServer = require('live-server'); +const config = require('./docdemo.config.js'); + +function startLiveServer(config) { + liveServer.start({ + port: 8080, + host: "localhost", + root: config.docsDistFolder, + open: false, + file: "404.html", + wait: 1000, + logLevel: 2 + }); +} + +startLiveServer(config); diff --git a/docdemo/src/buildSite.js b/docdemo/src/buildSite.js new file mode 100644 index 0000000000000000000000000000000000000000..68d6f28af718c978edd98e991a46a17d8cbf6a47 --- /dev/null +++ b/docdemo/src/buildSite.js @@ -0,0 +1,45 @@ +const fs = require("fs"); +const fsExtra = require("fs-extra"); +const path = require("path"); +const { execSync } = require("child_process"); + +function buildSite(config) { + + // Inject CSS codeblock styles + fs.copyFileSync(config.cssFilePath, path.join(config.tmpFolder, "old.css")); + let codeblockCss = fs.readFileSync(path.join(__dirname, "codeblock.css"), 'utf-8'); + let globalCss = fs.readFileSync(config.cssFilePath, 'utf-8'); + globalCss = globalCss + codeblockCss; + fs.writeFileSync(config.cssFilePath, globalCss); + + // Copy the new markdown files for doc build + fsExtra.copySync(config.docsTmpNewFolder, config.docsSrcFolder, { overwrite : true }); + + function recoverBack() { + // Copy the old markdown files back + fsExtra.copySync(config.docsTmpOldFolder, config.docsSrcFolder, { overwrite : true }); + // Recover CSS + fs.copyFileSync(path.join(config.tmpFolder, "old.css"), config.cssFilePath); + } + + // Build website + const websiteBuildCmd = config.siteBuildCmd; + try { + execSync(websiteBuildCmd, { cwd: config.siteFolder, stdio: 'inherit' }); + } catch (e) { + console.log("ERROR occurred when building the website, roll back the Markdown files ..."); + recoverBack(); + console.log("Markdown files rolled back complete, build failed."); + throw e; + } + + // Copy dist folder to the target demoDistFolder + fsExtra.emptydirSync(config.demoDistFolder); + fsExtra.copySync(config.demoDistFolderAux, config.demoDistFolder); + + // Copy the old markdown files back + recoverBack(); + +} + +module.exports = buildSite; \ No newline at end of file diff --git a/docdemo/src/codeblock.css b/docdemo/src/codeblock.css new file mode 100644 index 0000000000000000000000000000000000000000..3ead03f67c48f82844c28c00f0c5b709aa460911 --- /dev/null +++ b/docdemo/src/codeblock.css @@ -0,0 +1,50 @@ +.docDemoCodeblock { + /* 一层结构:包含代码部分、预览部分 */ + display: flex; + border: 1px solid #ddd; + border-radius: 10px; + margin-bottom: 20px; + /* width: 70vw; */ +} + +.docDemoCodeblock .docDemoCode { + /* 代码部分 */ + flex: 1; + padding: 10px; + /* background-color: #f6f8fa; */ + border-radius: 10px; + font-family: monospace; + overflow-x: auto; /* In case the code is wider than the container */ +} + +.docDemoCodeblock .docDemoPreview { + /* 预览部分 */ + flex: 1; + padding: 10px; + background-color: #fff; + border-radius: 10px; + border-left: 1px solid #ddd; +} + +.docDemoCodeblock .docPreviewBar { + /* background-color: #f4f4f4; */ + color: #42b883; + border-bottom: 3px solid #4569d4; + cursor: text; +} + +.docDemoCodeblock .docPreviewText { + font-family: Arial; + display: inline-block; + padding: 8px 10px 15px; + line-height: 20px; + font-size: 18px; + font-weight: bold; + color: #333333; +} + +iframe { + width: 100%; + height: 100%; + border: none; +} \ No newline at end of file diff --git a/docdemo/src/compileCode.js b/docdemo/src/compileCode.js new file mode 100644 index 0000000000000000000000000000000000000000..c4ec3003a15984c38c655a93926c4deefe5bb5ca --- /dev/null +++ b/docdemo/src/compileCode.js @@ -0,0 +1,149 @@ +const fs = require("fs"); +const fsExtra = require("fs-extra"); +const glob = require("glob"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const { getHash, getNpmProgram } = require('./utils'); + +class ViteBuildError extends Error { + constructor(message, codeFilename, codeLine, codeColumn) { + super(message+`\n at (${codeFilename}:${codeLine}:${codeColumn})`); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +function removeCache(filePath, debug=false) { + if (!debug) { + // remove the corresponding cache + fsExtra.removeSync(filePath); + } else { + // for debug, comment the cache file + const cacheFile = fs.readFileSync(filePath, 'utf8'); + fs.writeFileSync(filePath, cacheFile.replace(/\n/g, '\n// '), 'utf8'); + console.warn(`${path.join(process.cwd(), filePath)} cache file is commented, modify it manually or will not be rendered in the next run.`); + } +} + +function compileCode(config) { + + // Create template src folder if it doesn't exist, otherwise delete all files in it + const templateSrcFolder = path.join(config.templateFolder, 'src'); + fsExtra.emptydirSync(templateSrcFolder); + + // For every file in the demo source folder: + // Make a subfolder under the templateSrcFolder folder with name of the file without extension; + // Copy the file to the subfolder with name index.jsx; + // Copy additional files of config.templateFiles to the subfolder. + glob.globSync("**/*.*", { cwd: config.demoSrcFolder }).forEach(codeFile => { + + const codeFilePath = path.join(config.demoSrcFolder, codeFile); + const codeFileFolder = path.dirname(codeFile); + const codeFileExtension = path.extname(codeFile); + const codeFileName = path.basename(codeFile, codeFileExtension); + const templateSrcCodeFolder = path.join(templateSrcFolder, codeFileFolder, codeFileName); + + // Copy file to file folder + fsExtra.ensureDirSync(templateSrcCodeFolder); + const codeTargetFileName = `index${codeFileExtension}`; + const codeFileDestPath = path.join(templateSrcCodeFolder, codeTargetFileName); + fs.copyFileSync(codeFilePath, codeFileDestPath); + + // Copy additional files to file folder + config.templateFiles.forEach(templateFile => { + const templateFilePath = path.join(config.templateFolder, templateFile); + const templateFileDestPath = path.join(templateSrcCodeFolder, templateFile); + // in case it is tsx file or something else. + let templateFileData = fs.readFileSync(templateFilePath, 'utf8'); + templateFileData = templateFileData.replace(/src="(.*)index\..*"/g, `src="$1${codeTargetFileName}"`); + fs.writeFileSync(templateFileDestPath, templateFileData, 'utf8'); + }); + }); + + // Build code through Vite in the template folder + let child_process = spawnSync(getNpmProgram(), ['run', 'build'], { cwd: config.templateFolder, stdio: 'pipe' }); + if (child_process.status !== 0) { + const viteError = child_process.stderr.toString(); + console.error(viteError); + function getRealErrorPos(errorMdHex, errorLine, errorColumn) { + const errorMdCodeSplit = errorMdHex.lastIndexOf('-'); + const errorMarkdownSubPath = errorMdHex.substring(0, errorMdCodeSplit) + ".md"; + const errorMarkdownTmpOldFilePath = path.join(config.docsTmpOldFolder, errorMarkdownSubPath); + const errorMarkdownSrcFilePath = path.join(process.cwd(), config.docsSrcFolder, errorMarkdownSubPath); + const errorCodeSegmentHash = errorMdHex.substring(errorMdCodeSplit + 1); + + // find the codeSegment in errorMarkdownTmpOldFilePath + let errorMarkdownTmpOldFileContent = fs.readFileSync(errorMarkdownTmpOldFilePath, 'utf8'); + let markdownCodeRegex = /```(\w+)([\s\S]*?)```/g; + let result; + while((result = markdownCodeRegex.exec(errorMarkdownTmpOldFileContent)) !== null) { + const language = result[1]; + const codeSegment = result[2]; + const codeSegmentHash = getHash(codeSegment); + if (codeSegmentHash === errorCodeSegmentHash) { + // count how many lines before result.index in errorMarkdownTmpOldFileContent + const lines = errorMarkdownTmpOldFileContent.substring(0, result.index).split("\n"); + const fileErrorline = lines.length + errorLine - 1; + const fileErrorcolumn = errorColumn; + // throw custom error here + return [errorMarkdownSrcFilePath, fileErrorline, fileErrorcolumn]; + } + } + } + // find the templateSrcCodeFolder/(.*-[a-f\d]{8})\/index\..*:(\d+):(\d+) in viteError + // with the result codeFilename, codeLine, codeColumn + const matches = viteError.match(`${templateSrcFolder}\\/(.*-[a-f\\d]{8})\\/index\\..*:(\\d+):(\\d+): ERROR: (.*)`); + let errorMdHex, errorLine, errorColumn, errorReason; + if (matches === null) { + // might be tsx error + const viteOutput = child_process.stdout.toString(); + const tsxErrorRegex = /src\/(.*-[a-f0-9]{8})\/index\..*\((\d+),(\d+)\):/g; + const tsxErrors = viteOutput.match(tsxErrorRegex); + if (tsxErrors == null) { + console.log(viteOutput); + throw child_process.stderr.toString(); + } + let errorReplacingPairs = []; + let result; + let visited = new Set(); + while((result = tsxErrorRegex.exec(viteOutput)) !== null) { + errorMdHex = result[1]; + errorLine = parseInt(result[2]); + errorColumn = parseInt(result[3]); + let realErrorPos = getRealErrorPos(errorMdHex, errorLine, errorColumn); + errorReplacingPairs.push([result[0], `${realErrorPos[0]}(${realErrorPos[1]},${realErrorPos[2]}):`]); + if (!visited.has(result[1])){ + removeCache(path.join(config.demoCacheFolder, result[1] + '.tsx'), config.demoSrcCacheDebug); + visited.add(result[1]); + } + } + let newOutput = viteOutput; + errorReplacingPairs.forEach(errorReplacingPair => { + newOutput = newOutput.replace(errorReplacingPair[0], errorReplacingPair[1]); + }); + if (config.demoSrcCacheDebug) { + console.log(viteOutput); + } + console.log(newOutput); + throw new Error("tsc build error") + } else { + errorMdHex = matches[1]; + errorLine = parseInt(matches[2]); + errorColumn = parseInt(matches[3]); + errorReason = matches[4]; + removeCache(path.join(config.demoCacheFolder, matches[1] + '.jsx'), config.demoSrcCacheDebug); + throw new ViteBuildError(errorReason, ...getRealErrorPos(errorMdHex, errorLine, errorColumn)); + } + + } else { + console.log(child_process.stdout.toString()); + } + + // Copy dist folder to the demo dist tmp folder aux + const demoDistFolderAux = config.demoDistFolderAux; + fsExtra.emptyDirSync(demoDistFolderAux); + const templateDistFolder = path.join(config.templateFolder, 'dist'); + fsExtra.copySync(templateDistFolder, demoDistFolderAux); +} + +module.exports = compileCode; diff --git a/docdemo/src/configBuild.js b/docdemo/src/configBuild.js new file mode 100644 index 0000000000000000000000000000000000000000..3f99ef7e13eb09e25d6a64fa94b9d5a36920ca04 --- /dev/null +++ b/docdemo/src/configBuild.js @@ -0,0 +1,22 @@ +const path = require('path'); + +function configBuild(config) { + config.demoDistRelativeFolder = config.demoDistRelativeFolder || 'demos'; + config.demoSrcFolder = config.demoSrcFolder || path.join(config.tmpFolder, 'demo-src'); + config.demoDistFolder = config.demoDistFolder || path.join(config.docsDistFolder, config.demoDistRelativeFolder); + config.demoDistFolderAux = config.demoDistFolderAux || path.join(config.tmpFolder, 'demo-dist'); + config.docsTmpNewFolder = config.docsTmpNewFolder || path.join(config.tmpFolder, 'docs-new'); + config.docsTmpOldFolder = config.docsTmpOldFolder || path.join(config.tmpFolder, 'docs-old'); + + if (config.gpt == null) + config.gpt = false; + if (config.demoSrcCache == null) + config.demoSrcCache = true; + if (config.demoSrcCacheDebug == null) + config.demoSrcCacheDebug = false; + config.demoCacheFolder = config.demoCacheFolder || path.join(config.tmpFolder, 'demo-cache'); + + return config; +} + +module.exports = configBuild; \ No newline at end of file diff --git a/docdemo/src/gpt.js b/docdemo/src/gpt.js new file mode 100644 index 0000000000000000000000000000000000000000..1a67aaa4c14bd39dd0a273f1c173e68c5ac74e96 --- /dev/null +++ b/docdemo/src/gpt.js @@ -0,0 +1,56 @@ +// https://github.com/openai/openai-quickstart-node/blob/master/pages/api/generate.js +const os = require('os'); +const chalk = require('chalk'); +const OpenAI = require("openai"); +const { HttpsProxyAgent } = require('https-proxy-agent'); +const {sleep} = require("openai/core"); + +let openai; +const apiKey = process.env.OPENAI_API_KEY; +let httpAgent; +if (process.env.PROXY == null) { + httpAgent = null; +} else { + httpAgent = new HttpsProxyAgent(process.env.PROXY); +} +if (apiKey == null) { + openai = null; +} else { + openai = new OpenAI({apiKey: apiKey, httpAgent: httpAgent}); +} + +async function sendGPT (prompt) { + if (!apiKey) { + throw new Error("OpenAI API key not configured, please follow instructions in README.md"); + } + try { + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: prompt }], + model: "gpt-3.5-turbo", + }); + return completion.choices[0].message.content; + } catch(error) { + // Consider adjusting the error handling logic for your use case + if (error.response) { + console.error(error.response.status, error.response.data); + return null; + } else { + console.error(`Error with OpenAI API request: ${error.message}`); + return null; + } + } +} + +async function askCode(lang, code, codeFileName) { + const prompt = `只为这个${lang}语言的代码片段的结尾添加代码,使其使用 'openinula' 库的 render 语句渲染最主要的 Component 到 id 为 root 的 HTML DOM 上。必要时将 'openinula' 库中的 render 函数进行导入,只允许在代码结尾添加相关代码,只需返回增加部分的代码结果而不需要任何解释,如果做不到请直接返回 0:\n` + code; + try { + let res = await sendGPT(prompt); + if (res == null || res === "0") return null; + console.log(codeFileName + os.EOL + code + os.EOL + chalk.green(res) + os.EOL); + return code + "\n" + res; + } catch (e) { + return null; + } +} + +module.exports = { askCode }; \ No newline at end of file diff --git a/docdemo/src/processMarkdown.js b/docdemo/src/processMarkdown.js new file mode 100644 index 0000000000000000000000000000000000000000..222e8d3682541c14288dc335bf7a4eda2ca8915f --- /dev/null +++ b/docdemo/src/processMarkdown.js @@ -0,0 +1,183 @@ +const fs = require('fs'); +const fsExtra = require('fs-extra'); +const path = require('path'); +const glob = require('glob'); +const babelParser = require("@babel/parser"); +const { getHash, insertStrAt } = require('./utils'); +const { askCode } = require('./gpt'); + +function preprocessCode(lang, code) { + // remove jsx or tsx comments in code + return code.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, ''); +} + +function hasRender(lang, code) { + // if it has render(..., document.getElementById('root') ...); + if (code.match(/render([\s\S]*root[^)]*)/)==null) return false; + return true; +} + +function isCodeFileEmpty(lang, code) { + return preprocessCode(lang, code).replace(/[\r\n]/g,"").replace(/ /g,"") === ""; +} + +function tryParse(lang, code, codeFileName="") { + try { + const ast = babelParser.parse(code, { sourceType: "module", plugins : ["jsx", "typescript"], errorRecovery: true }); + if (ast.errors.length > 0){ + console.warn(`${codeFileName}: ${ast.errors[0].code}, ${ast.errors[0].reasonCode}`); + return false; + } + return true; + } catch (e) { + if (codeFileName !== "") { + console.warn(`${codeFileName}: ${e.message}`); + } + return false; + } +} + +async function analyzeCode(lang, code, codeFileName, config) { + // remove comments, this may breaks the error reporting, temporarily remove it. + newCode = preprocessCode(lang, code); + // global rules + if ( + newCode.match(/\.\//) != null // relative import + || newCode.match('function render') != null // define render function + || newCode.match(/<.*\/.*>/) == null // no HTML tag + || newCode.match('import') == null // no import + ) { + return null; // impossible to render Component. + } + // filter render code + if (!hasRender(lang, newCode)) { + code = code.replace(/export default\s+(.*)\s+=\(\)\s*=>\s*{/, "export default function $1() {"); // does not support anonymous function export here. + if (!tryParse(lang, code, codeFileName)) { + return code; // the original code is bad, expose it to the compile step for better error reporting. + } + if (config.demoSrcCacheDebug) { + return code; // we are debuging the cache, bypass gpt and you need to manually modify the cache or will be removed in the next run. + } + // ask GPT to generate render code + if (config.gpt) { + const res = await askCode(lang, code, codeFileName); + if (res !== null && (!tryParse(lang, res))) { + return null; // the gpt is bad, but will not report + } + return res; + } else return null; + } + return code; +} + +let totalCodeblocks, validCodeblocks; + +async function processMarkdownFile(markdownFileRelPath, config) { + const markdownFileFolder = path.dirname(markdownFileRelPath); + const markdownFileName = path.basename(markdownFileRelPath, path.extname(markdownFileRelPath)); + const markdownFilePath = path.join(config.docsSrcFolder, markdownFileRelPath); + + let data = fs.readFileSync(markdownFilePath, 'utf8'); + // remove markdown comments + data = data.replace(//g, ''); + + let markdownCodeRegex = /([ \t]*)```(\w+)([\s\S]*?)```/g; + let result; + while ((result = markdownCodeRegex.exec(data)) !== null) { + const indent = result[1]; + const language = result[2]; + const codeSegment = result[3].replace(/\r\n/g, "\n"); // normalize CRLF + + // to see if the language is supported + if (!(language === "jsx" || language === "tsx")) { + continue; + } + + // calculate sha-256 of codeSegment as part of the file name, + // which is prepared for further comparison if the code is the same. + const codeSegmentHash = getHash(codeSegment); + const codeFileName = `${markdownFileName}-${codeSegmentHash}`; + const codeFileFullName = `${codeFileName}.${language}`; + const codeFileFolder = markdownFileFolder; + + totalCodeblocks++; + + // write codeSegment to code file prepared for building + + const codeFolderPath = path.join(config.demoSrcFolder, codeFileFolder); + fsExtra.ensureDirSync(codeFolderPath); + const codeRelPath = path.join(codeFileFolder, codeFileFullName); + const codeFilePath = path.join(config.demoSrcFolder, codeRelPath); + const demoSrcCachePath = path.join(config.demoCacheFolder, codeRelPath); + if (config.demoSrcCache && fs.existsSync(demoSrcCachePath)) { + const demoSrcCacheFile = fs.readFileSync(demoSrcCachePath, 'utf8'); + if (isCodeFileEmpty(language, demoSrcCacheFile)) { + console.warn(`${path.join(process.cwd(), demoSrcCachePath)} is empty, ignore the demo.`) + continue; // it is declared as ignored file. + } + // use cached code file + fs.copyFileSync(demoSrcCachePath, codeFilePath); + } else { + // new code file + let extendedCodeSegment = codeSegment; + extendedCodeSegment = await analyzeCode(language, codeSegment, codeRelPath, config); + if (extendedCodeSegment == null) { + continue; + } + fs.writeFileSync(codeFilePath, extendedCodeSegment); + } + console.log(` valid: ${codeRelPath}`); + validCodeblocks++; + + // append iframe to the end of the code segment, + // two \n could help the Markdown parser running correctly. + // indent is to avoid bad list processing. + const demoFileRelativePath = path.join('/', config.demoDistRelativeFolder, 'src', codeFileFolder, codeFileName, 'index.html'); + const insertion = "\n" + "\n" + indent + `
Preview
`; + data = insertStrAt(data, result.index + result[0].length, insertion); + data = insertStrAt(data, result.index, indent + '
' + "\n" + "\n"); + } + + // Write the new data to the new markdown file + const markdownNewFileFolder = path.join(config.docsTmpNewFolder, markdownFileFolder); + fsExtra.ensureDirSync(markdownNewFileFolder); + const markdownNewFilePath = path.join(config.docsTmpNewFolder, markdownFileRelPath); + fs.writeFileSync(markdownNewFilePath, data); +} + +async function processMarkdown(config) { + + if (!config.demoSrcCache && config.gpt) { + console.warn("GPT is enabled but code cache is disabled, the cache is now legacy and will all be removed."); + fsExtra.removeSync(config.demoCacheFolder); + } + fsExtra.emptydirSync(config.demoSrcFolder); + fsExtra.emptydirSync(config.docsTmpOldFolder); + fsExtra.emptydirSync(config.docsTmpNewFolder); + + totalCodeblocks = 0; + validCodeblocks = 0; + + // Read all files in the docs folder recursively, and filter markdown files + const mdFiles = glob.globSync('**/*.md', { cwd: config.docsSrcFolder }); + // Process every markdown file not in parallel, avoid GPT rate limit. + for (const markdownFile of mdFiles) { + console.log(` Processing: ${markdownFile}`); + await processMarkdownFile(markdownFile, config); + } + + // Move the old markdown files to the doc tmp directory for backup + fsExtra.copySync(config.docsSrcFolder, config.docsTmpOldFolder); + + // Cache the code if needed + if (config.demoSrcCache) { + fsExtra.copySync(config.demoSrcFolder, config.demoCacheFolder); + } + + // Print statistics + console.log("Process complete."); + console.log(`Codeblock count summary:\n total: ${totalCodeblocks.toString().padStart(3, ' ')}\n valid: ${validCodeblocks.toString().padStart(3, ' ')}\n skip : ${(totalCodeblocks - validCodeblocks).toString().padStart(3, ' ')}`); + +} + +module.exports = processMarkdown; \ No newline at end of file diff --git a/docdemo/src/utils.js b/docdemo/src/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..a572657e0a047bb13badb88dfabf7fc84564cc7e --- /dev/null +++ b/docdemo/src/utils.js @@ -0,0 +1,22 @@ +const { createHash } = require("crypto"); + +// https://github.com/vitejs/vite/blob/5684fcd8d27110d098b3e1c19d851f44251588f1/packages/vite/src/node/utils.ts#L1045 +function getHash(data, length = 8) { + const h = createHash('sha256').update(data).digest('hex').substring(0, length) + if (length <= 64) return h + return h.padEnd(length, '_') +} + +function insertStrAt(str, index, insertion) { + return str.substring(0, index) + insertion + str.substring(index); +} + +function getNpmProgram() { + // For spawnSync(), `npm` should be `npm.cmd`: + // See https://stackoverflow.com/questions/37125619/spawnsyncnpm-install-gives-error-spawnsync-npm-enoent + // For execSync(), since it runs in a shell like in the regular commmand line, + // it does not matter. + return process.platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +module.exports = {getHash, insertStrAt, getNpmProgram}; \ No newline at end of file diff --git a/docdemo/template/.gitignore b/docdemo/template/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..80f75bc6c929536ec89c38efc732c1f6482d7dc9 --- /dev/null +++ b/docdemo/template/.gitignore @@ -0,0 +1,5 @@ +# src/ directory will hold all those files that will be generated from codeFileGen +src/ + +dummy.d.ts +dummy.jsx* \ No newline at end of file diff --git a/docdemo/template/dummy.tsx b/docdemo/template/dummy.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docdemo/template/index.css b/docdemo/template/index.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docdemo/template/index.html b/docdemo/template/index.html new file mode 100644 index 0000000000000000000000000000000000000000..e891e5140379a3028c7ee5a97a99c390b1f5e879 --- /dev/null +++ b/docdemo/template/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/docdemo/template/index.jsx b/docdemo/template/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..30584ef2acd3e8b1dee7cc5240d67befc02a99f0 --- /dev/null +++ b/docdemo/template/index.jsx @@ -0,0 +1,11 @@ + +import Inula from 'openinula'; +import './index.css'; + +function App() { + return ( +
+ ); +} + +Inula.render(, document.getElementById('root')); diff --git a/docdemo/template/package.json b/docdemo/template/package.json new file mode 100644 index 0000000000000000000000000000000000000000..aaa71297525bc5431e7c2905d05569343af42b05 --- /dev/null +++ b/docdemo/template/package.json @@ -0,0 +1,29 @@ +{ + "name": "inula-vite-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "vite", + "build": "tsc && vite build" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "glob": "^10.3.10", + "inula-intl": "latest", + "inula-router": "latest", + "openinula": "latest" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/preset-env": "^7.21.4", + "@babel/preset-react": "^7.18.6", + "@vitejs/plugin-react": "^3.1.0", + "@vitejs/plugin-react-refresh": "^1.3.6", + "babel-plugin-import": "^1.13.6", + "typescript": "4.9.3", + "vite": "^4.2.1" + } +} diff --git a/docdemo/template/tsconfig.json b/docdemo/template/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..2ee1b5fe2c04e4b36fa608392f4b0faeef9ebd29 --- /dev/null +++ b/docdemo/template/tsconfig.json @@ -0,0 +1,84 @@ +// Since there is still no official tsx template for openInula, +// this tsconfig is referenced from https://gitee.com/openInula/inula/blob/master/packages/inula-intl/tsconfig.json +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": false, /* Report errors in .js files. */ + "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": false, /* Report errors on unused locals. */ + "noUnusedParameters": false, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "resolveJsonModule": true, /* Include modules imported with '.json' extension */ + "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "allowUnreachableCode": true, /* Do not report errors on unreachable code. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "lib": ["dom", "ESNext", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020"] + }, + "include": [ + "src/**/*", + "dummy.tsx", /* dummy file: ignore error TS18003: No inputs were found in config file */ + ], + "types": [ + "node" + ] +} diff --git a/docdemo/template/vite.config.js b/docdemo/template/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..8c20807ae452cff7428f7f0e0d38e5b043776d88 --- /dev/null +++ b/docdemo/template/vite.config.js @@ -0,0 +1,38 @@ + +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import { globSync } from 'glob' + +let alias = { + react: 'openinula', // 新增 + 'react-dom': 'openinula', // 新增 + 'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime', +}; + +const searchDir = "./src"; + +// Get all subdirectories with index.html +const files = globSync("**/index.html", { cwd: searchDir }); + +// Generate input object +// "\\" Windows path spliter is a hazard for HTML embedding, +// we eliminate it manually. +const inputDict = files.reduce((acc, file) => { + const fileFolder = path.dirname(file).replace(/\\/g,'/'); + acc[fileFolder] = path.resolve(__dirname, path.join(searchDir, file)).replace(/\\/g,'/'); + return acc; +}, {}); + +export default defineConfig({ + plugins: [react()], + resolve: { + alias, + }, + build: { + rollupOptions: { + input: inputDict + } + }, + base: './', // relative path +}) diff --git a/docdemo/tmp/demo-cache/apis/core-09c352f0.jsx b/docdemo/tmp/demo-cache/apis/core-09c352f0.jsx new file mode 100644 index 0000000000000000000000000000000000000000..127e8af7972293ba2cc61cf6ce908c86392c2287 --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-09c352f0.jsx @@ -0,0 +1,34 @@ + +import Inula, { useState, useEffect } from "openinula"; + +function Timer() { + const [time, setTime] = useState(0); + + useEffect(() => { + const intervalId = setInterval(() => { + setTime(time + 1); + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, [time]); + + return
Time: {time}
; +} + +function App() { + const [showTimer, setShowTimer] = useState(true); + + return ( +
+ {showTimer && } + +
+ ); +} + +import { render } from "openinula"; +render(, document.getElementById("root")); \ No newline at end of file diff --git a/docdemo/tmp/demo-cache/apis/core-146ca16e.jsx b/docdemo/tmp/demo-cache/apis/core-146ca16e.jsx new file mode 100644 index 0000000000000000000000000000000000000000..915e55988bf91ebd7ecea73027d9737cb5024a4a --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-146ca16e.jsx @@ -0,0 +1,9 @@ + +// import Inula, { forwardRef, isForwardRef } from 'openinula'; + +// const ForwardedComponent = forwardRef((props, ref) => ( +//
Forwarded Component
+// )); + +// console.log(isForwardRef(ForwardedComponent)); +// console.log(isForwardRef(() => {})); diff --git a/docdemo/tmp/demo-cache/apis/core-155b1602.jsx b/docdemo/tmp/demo-cache/apis/core-155b1602.jsx new file mode 100644 index 0000000000000000000000000000000000000000..67e1bef955dc94e9d6b73f3d6350de1b24b7d1c3 --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-155b1602.jsx @@ -0,0 +1,33 @@ + +import Inula, { useRef, useImperativeHandle, forwardRef } from 'openinula'; + +const ChildComponent = forwardRef((props, ref) => { + const inputRef = useRef(); + + + useImperativeHandle(ref, () => ({ + focusInput: () => { + inputRef.current.focus(); + } + })); + + return ; +}); + +function App() { + const childRef = useRef(); + + const handleFocusInput = () => { + childRef.current.focusInput(); + }; + + return ( +
+ + +
+ ); +} + +import { render } from 'openinula'; +render(, document.getElementById('root')); \ No newline at end of file diff --git a/docdemo/tmp/demo-cache/apis/core-2515692f.tsx b/docdemo/tmp/demo-cache/apis/core-2515692f.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f2f9f563025a6f13af8385790e478bb4572b5b68 --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-2515692f.tsx @@ -0,0 +1,7 @@ + +// import Inula, { isValidElement } from 'openinula'; + +// const element =
Hello, World!
; + +// console.log(isValidElement(element)); +// console.log(isValidElement('Hello World!')); diff --git a/docdemo/tmp/demo-cache/apis/core-27a0fcbc.jsx b/docdemo/tmp/demo-cache/apis/core-27a0fcbc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c968473771189f55ad91e7a86596ede421d1c70e --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-27a0fcbc.jsx @@ -0,0 +1,21 @@ + +import Inula, { useRef } from 'openinula'; + +function RefDemo() { + const inputRef = useRef(null); + + const handleButtonClick = () => { + + inputRef.current.focus(); + }; + + return ( +
+ + +
+ ); +} + +import { render } from 'openinula'; +render(, document.getElementById('root')); \ No newline at end of file diff --git a/docdemo/tmp/demo-cache/apis/core-2f561eef.jsx b/docdemo/tmp/demo-cache/apis/core-2f561eef.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6a4a0814af871f68af78dfb8a52d4a56c12b3d5d --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-2f561eef.jsx @@ -0,0 +1,25 @@ + +import Inula, { Fragment } from 'openinula'; + +function App() { + return ( +
+ {} + +
Element 4
+
Element 5
+
Element 6
+
+ + {} + <> +
Element 7
+
Element 8
+
Element 9
+ +
+ ); +} + +import { render } from 'openinula'; +render(, document.getElementById('root')) \ No newline at end of file diff --git a/docdemo/tmp/demo-cache/apis/core-307645a5.jsx b/docdemo/tmp/demo-cache/apis/core-307645a5.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a7d2fafebf6c3a7ce4d6966e9d7d3924479006a2 --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-307645a5.jsx @@ -0,0 +1,26 @@ + +import Inula, { useState, useEffect, useLayoutEffect } from 'openinula'; + +const App = () => { + const [count, setCount] = useState(0); + + useEffect(() => { + console.log('useEffect'); + document.title = `Count: ${count}`; + }, [count]); + + useLayoutEffect(() => { + console.log('useLayoutEffect'); + document.title = `Count: ${count}`; + }, [count]); + + return ( +
+

Count: {count}

+ +
+ ); +}; + +import { render } from 'openinula'; +render(, document.getElementById('root')); \ No newline at end of file diff --git a/docdemo/tmp/demo-cache/apis/core-31a8e6f4.jsx b/docdemo/tmp/demo-cache/apis/core-31a8e6f4.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2c268fa49e81580a2b90bd3d2bb506b79af6183a --- /dev/null +++ b/docdemo/tmp/demo-cache/apis/core-31a8e6f4.jsx @@ -0,0 +1,21 @@ + +import { createElement, cloneElement } from 'openinula'; + +function Time({ time }) { + return createElement( + 'h1', + { className: 'clock' }, + 'Time is ', + createElement('i', null, time), + ); +} +function Clock() { + const time = new Date().toLocaleTimeString(); + return cloneElement( +