diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..864b1f8 --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-typescript"], + "plugins": [ + [ + "@babel/plugin-transform-react-jsx", + { + "pragma": "h" + } + ] + ] +} diff --git a/index.html b/index.html index 4c93138..3023797 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + diff --git a/jsx.d.ts b/jsx.d.ts new file mode 100644 index 0000000..ea6be5d --- /dev/null +++ b/jsx.d.ts @@ -0,0 +1,5 @@ +declare namespace JSX { + interface IntrinsicElements { + [elementName: string]: any; + } +} diff --git a/package.json b/package.json index ff687e2..e51f238 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,22 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "build": "babel src --out-dir dist --extensions '.ts,.tsx'", + "start": "vite", + "babel": "babel src/main.tsx --out-file dist/bundle.js" }, "devDependencies": { + "@babel/cli": "^7.26.4", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx": "^7.25.9", + "@babel/preset-env": "^7.26.0", + "@babel/preset-react": "^7.26.3", + "@babel/preset-typescript": "^7.26.0", "typescript": "~5.6.2", - "vite": "^6.0.3" + "vite": "^6.0.6" + }, + "dependencies": { + "jsx-runtime": "link:@/lib/jsx/jsx-runtime" } } diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 62ed0f3..0000000 --- a/src/main.ts +++ /dev/null @@ -1 +0,0 @@ -// Your Code diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..d6615c2 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,123 @@ +type RecordType = Record; + +interface VirtualNode { + type: string; + props: RecordType; + children: (VirtualNode | string)[]; +} + +function h(type: string, props: RecordType | null, ...children: any[]): VirtualNode { + return { + type, + props: props || {}, + children: children.flat().filter((child) => child != null), + }; +} + +function updateElement(parent: Node, oldNode: Node | null, newNode: Node | null) { + if (!newNode && oldNode && oldNode instanceof HTMLElement) { + oldNode.remove(); + return; + } + + if (newNode && !oldNode) { + parent.appendChild(newNode); + return; + } + + if (!oldNode || !newNode) return; + + if (newNode instanceof Text && oldNode instanceof Text) { + if (newNode.nodeValue !== oldNode.nodeValue) { + oldNode.nodeValue = newNode.nodeValue; + } + return; + } + + if (oldNode instanceof Element && newNode instanceof Element) { + if (newNode.nodeName !== oldNode.nodeName) { + oldNode.replaceWith(newNode); + return; + } + + updateAttributes(oldNode, newNode); + + const newChildren = Array.from(newNode.childNodes); + const oldChildren = Array.from(oldNode.childNodes); + const maxLength = Math.max(newChildren.length, oldChildren.length); + + for (let i = 0; i < maxLength; i++) { + updateElement(oldNode, oldChildren[i] || null, newChildren[i] || null); + } + } +} + +function updateAttributes(oldNode: Element, newNode: Element) { + const oldProps = Array.from(oldNode.attributes); + const newProps = Array.from(newNode.attributes); + + for (const { name, value } of newProps) { + if (oldNode.getAttribute(name) !== value) { + oldNode.setAttribute(name, value); + } + } + + for (const { name } of oldProps) { + if (!newNode.hasAttribute(name)) { + oldNode.removeAttribute(name); + } + } +} + +const render = (state: RecordType[]) => { + const element = document.createElement('div'); + element.innerHTML = ` +
+ +
+ + +
+
+ `.trim(); + + return element.firstElementChild; +}; + +const oldState = [ + { id: 1, completed: false, content: 'todo list item 1' }, + { id: 2, completed: true, content: 'todo list item 2' }, +]; + +const newState = [ + { id: 1, completed: true, content: 'todo list item 1 updated' }, + { id: 2, completed: true, content: 'todo list item 2' }, + { id: 3, completed: false, content: 'todo list item 3' }, +]; + +const $root = document.createElement('div'); +document.body.appendChild($root); + +const oldNode = render(oldState); +if (oldNode) { + $root.appendChild(oldNode); +} + +setTimeout(() => { + const newNode = render(newState); + if (oldNode && newNode) { + updateElement($root, oldNode, newNode); + } +}, 1000); diff --git a/tsconfig.json b/tsconfig.json index a4883f2..a4969b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,30 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES5", "useDefineForClassFields": true, - "module": "ESNext", + "module": "CommonJS", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, + "esModuleInterop": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "jsx": "preserve", + "jsxFactory": "h", + "jsxImportSource": "@/lib/jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } }, - "include": ["src"] + "include": ["src", "jsx.d.ts"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..36c4622 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 3000, + }, + build: { + outDir: 'dist', + }, +});