diff --git a/demo/lit/render.ts b/demo/lit/render.ts
new file mode 100644
index 0000000..ec0cc57
--- /dev/null
+++ b/demo/lit/render.ts
@@ -0,0 +1,3 @@
+import '../../src/html-preview/kevisual-base';
+
+// console.log('KeVisualBase', KeVisualBase);
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6279ec8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ KeVisual Base Example
+
+
+
+
+
+
+
+ This paragraph should be red.
+ This span.
+
+
+ This paragraph should be blue.
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 98bd518..629f7e6 100644
--- a/package.json
+++ b/package.json
@@ -12,19 +12,23 @@
"author": "",
"license": "ISC",
"devDependencies": {
- "@rollup/plugin-commonjs": "^28.0.2",
- "@rollup/plugin-node-resolve": "^16.0.0",
+ "@cacheable/node-cache": "^1.5.3",
+ "@kevisual/tojs": "0.0.5",
+ "@rollup/plugin-commonjs": "^28.0.3",
+ "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@types/crypto-js": "^4.2.2",
- "@types/react": "^19.0.3",
+ "@types/react": "^19.0.10",
"rimraf": "^6.0.1",
- "rollup": "^4.30.0",
+ "rollup": "^4.36.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.1.1",
- "typescript": "^5.7.2"
+ "typescript": "^5.8.2",
+ "vite": "^6.2.2"
},
"dependencies": {
"fast-deep-equal": "^3.1.3",
+ "lit": "^3.2.1",
"lit-html": "^3.2.1"
},
"exports": {
diff --git a/src/html-preview/kevisual-base.ts b/src/html-preview/kevisual-base.ts
new file mode 100644
index 0000000..15818df
--- /dev/null
+++ b/src/html-preview/kevisual-base.ts
@@ -0,0 +1,119 @@
+import { unsafeCSS } from 'lit';
+import { LitElement, html, css } from '../lit';
+import { fnv1aHash16 } from '../utils/fnv1a-hash';
+
+const baseStyle = `:host {
+ display: block;
+ }
+ `;
+function fnv1aHash(str) {
+ const FNV_PRIME = 16777619;
+ const OFFSET_BASIS = 2166136261;
+
+ let hash = OFFSET_BASIS;
+ for (let i = 0; i < str.length; i++) {
+ hash ^= str.charCodeAt(i);
+ hash *= FNV_PRIME;
+ }
+
+ // 将结果转换为 32 位无符号整数
+ return hash >>> 0; // 确保结果是非负的 32 位整数
+}
+
+// 使用示例
+const hash = fnv1aHash('Hello, World!');
+console.log(hash.toString(16)); // 输出哈希值的十六进制表示
+// ${unsafeCSS(baseStyle)}
+
+export class EnvisionBase extends LitElement {
+ static styles = css``;
+ unsafeCssContent = '';
+ constructor() {
+ super();
+ }
+
+ render() {
+ return html` `;
+ }
+
+ handleSlotChange() {
+ const slot = this.shadowRoot.querySelector('slot');
+ const nodes = slot.assignedNodes({ flatten: true }); // 获取所有插槽内容
+ nodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const elementNode = node as HTMLElement;
+ if (elementNode.tagName.toLowerCase() === 'style') {
+ this.appendStyle(elementNode);
+ elementNode.remove();
+ } else if (elementNode.tagName.toLowerCase() === 'script') {
+ this.executeScript(elementNode as HTMLScriptElement);
+ elementNode.remove();
+ } else {
+ this.shadowRoot.appendChild(elementNode);
+ }
+ }
+ });
+ }
+
+ appendStyle(styleNode) {
+ console.log('this', this, this.classList);
+ console.dir(this);
+ this.unsafeCssContent = this.unsafeCssContent + styleNode.textContent;
+ const styleSheet = new CSSStyleSheet();
+ styleSheet.replaceSync(this.unsafeCssContent);
+ this.shadowRoot.adoptedStyleSheets.push(styleSheet);
+ }
+
+ async executeScript(scriptNode: HTMLScriptElement) {
+ const code = scriptNode.textContent;
+ const hash = fnv1aHash16(code);
+ const _code = code;
+ const url = `/api/s1/js/${hash}`;
+
+ const loadModule = async (url: string) => {
+ try {
+ return await import(/* @vite-ignore */ url);
+ } catch (error) {
+ console.error('import error', error);
+ return null;
+ }
+ };
+
+ // First attempt to load the module
+ let module = await loadModule(url);
+ if (module) {
+ console.log('module', module);
+ return;
+ }
+
+ // Post the code to the server
+ const addCache = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ code: _code }),
+ }).then((res) => res.json());
+
+ console.log('addCache', addCache);
+
+ // Wait for a short period before retrying
+ await new Promise((resolve) => setTimeout(resolve, 3000));
+
+ // Second attempt to load the module
+ module = await loadModule(url + '?test=2');
+ if (module) {
+ console.log('module2', module);
+ } else {
+ console.error('加载失败, 可能是build错误');
+ this.showError('加载失败');
+ }
+ module = await loadModule(url);
+ console.log('module3', module);
+ }
+ showError(msg?: string) {
+ this.shadowRoot.innerHTML = `Error: ${msg?.toString() || 'Unknown error'}
`;
+ }
+}
+
+customElements.define('ev-html', EnvisionBase);
diff --git a/src/lit.ts b/src/lit.ts
new file mode 100644
index 0000000..e8c1fb5
--- /dev/null
+++ b/src/lit.ts
@@ -0,0 +1,13 @@
+import * as lit from 'lit';
+
+// export { KeVisualBase } from './html-preview/kevisual-base';
+
+export { lit };
+
+export const html = lit.html;
+export const svg = lit.svg;
+export const mathml = lit.mathml;
+export const render = lit.render;
+export const nothing = lit.nothing;
+export const css = lit.css;
+export const LitElement = lit.LitElement;
diff --git a/src/utils/fnv1a-hash.ts b/src/utils/fnv1a-hash.ts
new file mode 100644
index 0000000..605f22d
--- /dev/null
+++ b/src/utils/fnv1a-hash.ts
@@ -0,0 +1,27 @@
+/**
+ * 将字符串转换为 32 位无符号整数
+ * @param str - 要转换的字符串
+ * @returns 32 位无符号整数
+ */
+export function fnv1aHash(str: string): number {
+ const FNV_PRIME = 16777619;
+ const OFFSET_BASIS = 2166136261;
+
+ let hash = OFFSET_BASIS;
+ for (let i = 0; i < str.length; i++) {
+ hash ^= str.charCodeAt(i);
+ hash *= FNV_PRIME;
+ }
+
+ // 将结果转换为 32 位无符号整数
+ return hash >>> 0; // 确保结果是非负的 32 位整数
+}
+
+/**
+ * 将字符串转换为 16 位无符号整数
+ * @param str - 要转换的字符串
+ * @returns 16 位无符号整数
+ */
+export const fnv1aHash16 = (str: string): string => {
+ return fnv1aHash(str).toString(16); // 取低 16 位
+};
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..a7c20c8
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,51 @@
+import { defineConfig } from 'vite';
+import { returnOriginalJs } from '@kevisual/tojs';
+import http from 'http';
+const server = http.createServer(async (req, res) => {
+ console.log('req', req.url, req.method);
+ const method = req.method;
+ let body2 = {};
+ if (method === 'POST') {
+ const body = await parseBody(req);
+ console.log('body', body);
+ body2 = JSON.parse(body);
+ }
+ returnOriginalJs(
+ {
+ req,
+ res,
+ query: {
+ ...body2,
+ },
+ },
+ {
+ prefixUrl: '/api/s1/js/',
+ },
+ );
+});
+server.listen(4005, () => {
+ console.log('server listen', server.address());
+});
+export const parseBody = async (req) => {
+ return new Promise((resolve, reject) => {
+ const arr = [];
+ req.on('data', (chunk) => {
+ arr.push(chunk);
+ });
+ req.on('end', () => {
+ resolve(Buffer.concat(arr).toString());
+ });
+ });
+};
+
+export default defineConfig({
+ server: {
+ port: 4003,
+ host: true,
+ proxy: {
+ '/api/s1/js': {
+ target: 'http://localhost:4005',
+ },
+ },
+ },
+});