├── .prettierrc ├── packages ├── oreact │ ├── js │ │ └── .gitkeep │ ├── dist │ │ └── .gitkeep │ ├── babel.config.js │ ├── lib │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── util.ts │ │ ├── render.ts │ │ ├── create-element.ts │ │ ├── diff │ │ │ ├── props.ts │ │ │ ├── children.ts │ │ │ └── index.ts │ │ ├── type.d.ts │ │ └── component.ts │ ├── README.md │ ├── package.json │ └── tsconfig.json └── example │ ├── dist │ └── .gitkeep │ ├── README.md │ ├── src │ ├── index.html │ └── index.ts │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.js ├── .prettierignore ├── scripts ├── remove.sh └── dev.sh ├── lerna.json ├── .gitignore ├── package.json ├── .github └── workflows │ └── deploy.yml ├── LICENSE ├── CODE_OF_CONDUCT.md ├── README.md └── CONTRIBUTING.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/oreact/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/example/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/oreact/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/oreact/js/** 2 | packages/oreact/dist/** 3 | **/.DS_Store -------------------------------------------------------------------------------- /scripts/remove.sh: -------------------------------------------------------------------------------- 1 | rm -r ./packages/oreact/dist/* 2 | rm -r ./packages/oreact/js/* -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /packages/oreact/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /scripts/dev.sh: -------------------------------------------------------------------------------- 1 | lerna run --scope=oreact tsc 2 | lerna run --scope=oreact build:core 3 | lerna run --scope=example dev -------------------------------------------------------------------------------- /packages/example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | oreact の実装例 4 | 5 | ## Usage 6 | 7 | ``` 8 | npx webpack serve 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/oreact/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { render } from "./render"; 2 | export { createElement as h } from "./create-element"; 3 | export { Component } from "./component"; 4 | -------------------------------------------------------------------------------- /packages/oreact/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJ = {}; 2 | export const EMPTY_ARR = []; 3 | export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; 4 | -------------------------------------------------------------------------------- /packages/oreact/README.md: -------------------------------------------------------------------------------- 1 | # oreact 2 | 3 | preact を再実装する -俺の react- 4 | 5 | ## Usage 6 | 7 | build 8 | 9 | ```sh 10 | # tsc 11 | npx babel --extensions '.ts' ./lib -d js/ 12 | 13 | # bundle 14 | npx microbundle build --raw 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | **/dist/** 5 | */package-lock.json 6 | yarn.lock 7 | .vscode 8 | .idea 9 | test/ts/**/*.js 10 | coverage 11 | *.sw[op] 12 | *.log 13 | package/ 14 | preact-*.tgz 15 | **/js/** 16 | !.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "lerna": "^3.22.1", 6 | "prettier": "^2.1.2" 7 | }, 8 | "scripts": { 9 | "build-lib": "npx lerna run --scope=oreact tsc && npx lerna run --scope=oreact build:core", 10 | "build": "npx lerna run --scope=example build", 11 | "dev": "npx lerna run --scope=example dev", 12 | "format": "prettier --write 'packages/**'" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oreact example 7 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/oreact/lib/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign properties from `props` to `obj` 3 | * @template O, P The obj and props types 4 | * @param {O} obj The object to copy properties to 5 | * @param {P} props The object to copy properties from 6 | * @returns {O & P} 7 | */ 8 | export function assign(obj: Object, props: Object) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ obj; 11 | } 12 | 13 | /** 14 | * Remove a child node from its parent if attached. This is a workaround for 15 | * IE11 which doesn't support `Element.prototype.remove()`. Using this function 16 | * is smaller than including a dedicated polyfill. 17 | * @param {Node} node The node to remove 18 | */ 19 | export function removeNode(node: Node) { 20 | let parentNode = node.parentNode; 21 | if (parentNode) parentNode.removeChild(node); 22 | } 23 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "oreact の例", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "webpack serve", 9 | "build": "NODE_ENV='production' webpack" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ojisan-toybox/svganimation.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/ojisan-toybox/svganimation/issues" 19 | }, 20 | "homepage": "https://github.com/ojisan-toybox/svganimation#readme", 21 | "devDependencies": { 22 | "html-webpack-plugin": "^4.5.0", 23 | "ts-loader": "^8.0.8", 24 | "typescript": "^4.0.5", 25 | "webpack": "4", 26 | "webpack-cli": "^4.1.0", 27 | "webpack-dev-server": "^3.11.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: DEPLOY 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 12.x 16 | - name: npm install 17 | run: | 18 | npm install 19 | - name: bootstrap 20 | run: | 21 | npx lerna bootstrap 22 | - name: Build oreact 23 | run: npx lerna run --scope=oreact tsc && npx lerna run --scope=oreact build:core 24 | - name: Build App 25 | run: npx lerna run --scope=example build 26 | - name: App Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./packages/example/dist 31 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 4 | "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 5 | "strict": false /* Enable all strict type-checking options. */, 6 | "allowJs": true, 7 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 8 | "skipLibCheck": true /* Skip type checking of declaration files. */, 9 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 10 | "jsxFactory": "h", 11 | "moduleResolution": "Node" 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: "./src/index.ts", 7 | output: { 8 | path: path.resolve(__dirname, "./dist"), 9 | filename: "build.js", 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|ts)$/, 15 | use: [ 16 | { 17 | loader: "ts-loader", 18 | options: { 19 | transpileOnly: true, 20 | }, 21 | }, 22 | ], 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: [".js", ".ts"], 29 | alias: { 30 | react: "preact/compat", 31 | "react-dom/test-utils": "preact/test-utils", 32 | "react-dom": "preact/compat", 33 | }, 34 | }, 35 | plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })], 36 | devServer: { 37 | historyApiFallback: true, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/oreact/lib/render.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_OBJ, EMPTY_ARR } from "./constants"; 2 | import { commitRoot, diff } from "./diff/index"; 3 | import { createElement, Fragment } from "./create-element"; 4 | import { ComponentChild, ComponentClass, PreactElement } from "./type"; 5 | 6 | /** 7 | * renderエンドポイント. VNodeからDOMを構築する. 8 | * @param vnode 対象となるVNode(Element or Component) 9 | * @param parentDom マウントの対象 10 | */ 11 | export function render(vnode: ComponentChild, parentDom: PreactElement) { 12 | const initialVnode = createElement( 13 | (Fragment as any) as ComponentClass, 14 | null, 15 | [vnode] 16 | ); 17 | 18 | // 差分更新後の副作用を管理するリスト 19 | let commitQueue = []; 20 | 21 | parentDom._children = initialVnode; 22 | 23 | diff({ 24 | parentDom: parentDom, 25 | newVNode: initialVnode, 26 | oldVNode: EMPTY_OBJ, 27 | excessDomChildren: EMPTY_ARR.slice.call(parentDom.childNodes), 28 | commitQueue: commitQueue, 29 | oldDom: EMPTY_OBJ, 30 | }); 31 | 32 | // commitQueueにある副作用を実行 33 | commitRoot(commitQueue); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 sadnessOjisan 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. 22 | -------------------------------------------------------------------------------- /packages/oreact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sadness.ojisan/oreact", 3 | "version": "1.0.0", 4 | "description": "preact を再実装する -俺の react-", 5 | "main": "dist/preact.js", 6 | "module": "dist/preact.module.js", 7 | "umd:main": "dist/preact.umd.js", 8 | "unpkg": "dist/preact.min.js", 9 | "source": "js/index.js", 10 | "exports": { 11 | ".": { 12 | "browser": "./dist/preact.module.js", 13 | "umd": "./dist/preact.umd.js", 14 | "import": "./dist/preact.mjs", 15 | "require": "./dist/preact.js" 16 | }, 17 | "./package.json": "./package.json", 18 | "./": "./" 19 | }, 20 | "types": "src/index.d.ts", 21 | "files": [ 22 | "src", 23 | "js", 24 | "dist" 25 | ], 26 | "scripts": { 27 | "tsc": "babel --extensions '.ts' ./lib -d js/", 28 | "build:core": "microbundle build --raw", 29 | "build:core-min": "microbundle build --raw -f iife js/cjs.js -o dist/preact.min.js", 30 | "dev": "microbundle watch --raw --format cjs", 31 | "lint": "eslint src" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/sadnessOjisan/oreact.git" 36 | }, 37 | "author": "", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/sadnessOjisan/oreact/issues" 41 | }, 42 | "homepage": "https://github.com/sadnessOjisan/oreact#readme", 43 | "devDependencies": { 44 | "@babel/cli": "^7.12.1", 45 | "@babel/core": "^7.12.3", 46 | "@babel/preset-typescript": "^7.12.1", 47 | "microbundle": "^0.12.4", 48 | "typescript": "^4.0.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from "../../oreact/dist/preact"; 2 | 3 | class App extends Component { 4 | constructor() { 5 | this.state = { 6 | count: 10000000, 7 | data: [], 8 | }; 9 | } 10 | 11 | componentDidMount() { 12 | this.setState({ 13 | ...this.state, 14 | count: 0, 15 | data: [ 16 | { 17 | name: "taro", 18 | }, 19 | { 20 | name: "hanako", 21 | }, 22 | ], 23 | }); 24 | } 25 | 26 | componentWillReceiveProps(next) { 27 | console.log("next.props:", next.props); 28 | } 29 | 30 | render() { 31 | return h( 32 | "div", 33 | { 34 | style: { 35 | color: "blue", 36 | }, 37 | }, 38 | h( 39 | "section", 40 | null, 41 | h("h1", null, "counting area"), 42 | h("span", null, "count: "), 43 | h("span", null, this.state.count), 44 | h( 45 | "button", 46 | { 47 | onClick: () => 48 | this.setState({ ...this.state, count: this.state.count + 1 }), 49 | }, 50 | "add" 51 | ) 52 | ), 53 | h( 54 | "section", 55 | null, 56 | h("h1", null, "user data area"), 57 | h( 58 | "ul", 59 | null, 60 | this.state.data.map((d, i) => 61 | h(ListItem, { 62 | name: d.name, 63 | handleDelete: () => { 64 | this.setState({ 65 | ...this.state, 66 | data: this.state.data.filter((_, j) => { 67 | return i !== j; 68 | }), 69 | }); 70 | }, 71 | }) 72 | ) 73 | ), 74 | h( 75 | "form", 76 | { 77 | onSubmit: (e) => { 78 | e.preventDefault(); 79 | const userName = e.target["name"].value; 80 | this.setState({ 81 | ...this.state, 82 | data: [ 83 | ...this.state.data, 84 | { 85 | name: userName, 86 | }, 87 | ], 88 | }); 89 | }, 90 | }, 91 | h("input", { 92 | name: "name", 93 | }), 94 | h( 95 | "button", 96 | { 97 | type: "submit", 98 | }, 99 | "add" 100 | ) 101 | ) 102 | ) 103 | ); 104 | } 105 | } 106 | 107 | class ListItem extends Component { 108 | componentWillReceiveProps(nextProps, prevProps) { 109 | console.log("next.props:", nextProps); 110 | console.log("next.props:", prevProps); 111 | } 112 | 113 | render() { 114 | return h( 115 | "li", 116 | null, 117 | h("span", null, this.props.name), 118 | h( 119 | "button", 120 | { 121 | onClick: () => this.props.handleDelete(), 122 | }, 123 | "delete" 124 | ) 125 | ); 126 | } 127 | } 128 | 129 | render(h(App, null, null), document.body); 130 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@preactjs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /packages/oreact/lib/create-element.ts: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, Key, PropsType, VNode } from "./type"; 2 | 3 | /** 4 | * Create an virtual node (used for JSX) 5 | * @param {import('./internal').VNode["type"]} type The node name or Component 6 | * constructor for this virtual node 7 | * @param {object | null | undefined} [props] The properties of the virtual node 8 | * @param {Array} [children] The children of the virtual node 9 | * @returns {import('./internal').VNode} 10 | */ 11 | export function createElement( 12 | type: VNode["type"], 13 | props: PropsType, 14 | children: ComponentChildren 15 | ) { 16 | let normalizedProps: { children?: ComponentChildren } = {}, 17 | key, 18 | i; 19 | for (i in props) { 20 | if (i == "key") key = props[i]; 21 | else normalizedProps[i] = props[i]; 22 | } 23 | 24 | if (arguments.length > 3) { 25 | children = [children]; 26 | if (!Array.isArray(children)) { 27 | throw new Error(); 28 | } 29 | for (i = 3; i < arguments.length; i++) { 30 | children.push(arguments[i]); 31 | } 32 | } 33 | if (children != null) { 34 | normalizedProps.children = children; 35 | } 36 | 37 | // If a Component VNode, check for and apply defaultProps 38 | // Note: type may be undefined in development, must never error here. 39 | if (typeof type == "function" && type.defaultProps != null) { 40 | for (i in type.defaultProps) { 41 | if (normalizedProps[i] === undefined) { 42 | normalizedProps[i] = type.defaultProps[i]; 43 | } 44 | } 45 | } 46 | 47 | // key がなければ undefined のまま入る 48 | return createVNode( 49 | type, 50 | normalizedProps as { children: ComponentChildren }, 51 | key, 52 | undefined, 53 | null 54 | ); 55 | } 56 | 57 | /** 58 | * Create a VNode (used internally by Preact) 59 | * @param {import('./internal').VNode["type"]} type The node name or Component 60 | * Constructor for this virtual node 61 | * @param {object | string | number | null} props The properties of this virtual node. 62 | * If this virtual node represents a text node, this is the text of the node (string or number). 63 | * @param {string | number | null} key The key for this virtual node, used when 64 | * diffing it against its children 65 | * @param {import('./internal').VNode["ref"]} ref The ref property that will 66 | * receive a reference to its created child 67 | * @returns {import('./internal').VNode} 68 | */ 69 | export function createVNode( 70 | type: VNode["type"], 71 | props: VNode["props"] | string | number, 72 | key: Key, 73 | ref: undefined, 74 | original: VNode | null | string | number 75 | ) { 76 | // V8 seems to be better at detecting type shapes if the object is allocated from the same call site 77 | // Do not inline into createElement and coerceToVNode! 78 | const vnode: VNode = { 79 | type, 80 | props, 81 | key, 82 | ref, 83 | _children: null, 84 | _parent: null, 85 | _depth: 0, 86 | _dom: null, 87 | // _nextDom must be initialized to undefined b/c it will eventually 88 | // be set to dom.nextSibling which can return `null` and it is important 89 | // to be able to distinguish between an uninitialized _nextDom and 90 | // a _nextDom that has been set to `null` 91 | _nextDom: undefined, 92 | _component: null, 93 | _hydrating: null, 94 | constructor: undefined, 95 | _original: original, 96 | }; 97 | 98 | if (original == null) vnode._original = vnode; 99 | 100 | return vnode; 101 | } 102 | 103 | export function Fragment(props: VNode["props"]) { 104 | return props.children; 105 | } 106 | 107 | /** 108 | * Check if a the argument is a valid Preact VNode. 109 | * @param {*} vnode 110 | * @returns {vnode is import('./internal').VNode} 111 | */ 112 | export const isValidElement = (vnode: VNode) => 113 | vnode != null && vnode.constructor === undefined; 114 | -------------------------------------------------------------------------------- /packages/oreact/lib/diff/props.ts: -------------------------------------------------------------------------------- 1 | import { IS_NON_DIMENSIONAL } from "../constants"; 2 | import { PreactElement, PropsType } from "../type"; 3 | 4 | /** 5 | * props の diff を取る。 6 | * children は無視してくれる。 7 | * @param dom 8 | * @param newProps 9 | * @param oldProps 10 | */ 11 | export function diffProps( 12 | dom: PreactElement, 13 | newProps: PropsType, 14 | oldProps: PropsType 15 | ) { 16 | let i; 17 | 18 | for (i in oldProps) { 19 | if (i !== "children" && i !== "key" && !(i in newProps)) { 20 | setProperty(dom, i, null, oldProps[i]); 21 | } 22 | } 23 | 24 | for (i in newProps) { 25 | if ( 26 | i !== "children" && 27 | i !== "key" && 28 | i !== "value" && 29 | i !== "checked" && 30 | oldProps[i] !== newProps[i] 31 | ) { 32 | setProperty(dom, i, newProps[i], oldProps[i]); 33 | } 34 | } 35 | } 36 | 37 | function setStyle(style: CSSStyleDeclaration, key: string, value: any) { 38 | if (key[0] === "-") { 39 | style.setProperty(key, value); 40 | } else if (value == null) { 41 | style[key] = ""; 42 | } else if (typeof value != "number" || IS_NON_DIMENSIONAL.test(key)) { 43 | style[key] = value; 44 | } else { 45 | style[key] = value + "px"; 46 | } 47 | } 48 | 49 | /** 50 | * Set a property value on a DOM node 51 | * @param {import('../internal').PreactElement} dom The DOM node to modify 52 | * @param {string} name The name of the property to set 53 | * @param {*} value The value to set the property to 54 | * @param {*} oldValue The old value the property had 55 | */ 56 | export function setProperty( 57 | dom: PreactElement, 58 | name: string, 59 | value: any, 60 | oldValue: any 61 | ) { 62 | let useCapture, nameLower, proxy; 63 | 64 | if (name === "style") { 65 | if (typeof value == "string") { 66 | dom.style.cssText = value; 67 | } else { 68 | if (typeof oldValue == "string") { 69 | dom.style.cssText = oldValue = ""; 70 | } 71 | 72 | if (oldValue) { 73 | for (name in oldValue) { 74 | if (!(value && name in value)) { 75 | setStyle(dom.style, name, ""); 76 | } 77 | } 78 | } 79 | 80 | if (value) { 81 | for (name in value) { 82 | if (!oldValue || value[name] !== oldValue[name]) { 83 | setStyle(dom.style, name, value[name]); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6 90 | else if (name[0] === "o" && name[1] === "n") { 91 | useCapture = name !== (name = name.replace(/Capture$/, "")); 92 | nameLower = name.toLowerCase(); 93 | if (nameLower in dom) name = nameLower; 94 | name = name.slice(2); 95 | 96 | if (!dom._listeners) dom._listeners = {}; 97 | dom._listeners[name + useCapture] = value; 98 | 99 | proxy = useCapture ? eventProxyCapture : eventProxy; 100 | if (value) { 101 | if (!oldValue) dom.addEventListener(name, proxy, useCapture); 102 | } else { 103 | dom.removeEventListener(name, proxy, useCapture); 104 | } 105 | } else if ( 106 | name !== "list" && 107 | name !== "tagName" && 108 | // HTMLButtonElement.form and HTMLInputElement.form are read-only but can be set using 109 | // setAttribute 110 | name !== "form" && 111 | name !== "type" && 112 | name !== "size" && 113 | name !== "download" && 114 | name !== "href" && 115 | name in dom 116 | ) { 117 | dom[name] = value == null ? "" : value; 118 | } else if (typeof value != "function" && name !== "dangerouslySetInnerHTML") { 119 | dom.setAttribute(name, value); 120 | } 121 | } 122 | 123 | /** 124 | * Proxy an event to hooked event handlers 125 | * @param {Event} e The event object from the browser 126 | * @private 127 | */ 128 | function eventProxy(e) { 129 | this._listeners[e.type + false](e); 130 | } 131 | 132 | function eventProxyCapture(e) { 133 | this._listeners[e.type + true](e); 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oreact 2 | 3 | preact を再実装する -俺の react- 4 | 5 | slide: https://speakerdeck.com/sadnessojisan/preactfalseshi-zu-miwoli-jie-suruqing-liang-ban-jiao-yu-yong-preactwozuo-tuteruhua 6 | 7 | ## abst 8 | 9 | 教育用 preact です。state を書き換えると再レンダリングがされるというコアの部分だけを抜き出しています。 10 | 11 | ## dev 12 | 13 | ```sh 14 | npm install 15 | 16 | npx lerna bootstrap 17 | 18 | npm run dev 19 | ``` 20 | 21 | ビルド対象のコード例. 22 | jsx -> h への変換は[こちら](https://github.com/ojisan-toybox/preact-h-babel)から. 23 | 24 | 例では、 25 | 26 | - props 更新 27 | - state 更新 28 | - ライフサイクル 29 | - スタイリング 30 | 31 | をサポートしています。 32 | 33 | URL: https://sadnessojisan.github.io/oreact/ 34 | 35 | ```tsx 36 | import { h, render, Component } from "oreact"; 37 | 38 | class App extends Component { 39 | constructor() { 40 | this.state = { 41 | count: 10000000, 42 | data: [], 43 | }; 44 | } 45 | 46 | componentDidMount() { 47 | this.setState({ 48 | ...this.state, 49 | count: 0, 50 | data: [ 51 | { 52 | name: "taro", 53 | }, 54 | { 55 | name: "hanako", 56 | }, 57 | ], 58 | }); 59 | } 60 | 61 | componentWillReceiveProps(next) { 62 | console.log("next.props:", next.props); 63 | } 64 | 65 | render() { 66 | return h( 67 | "div", 68 | { 69 | style: { 70 | color: "blue", 71 | }, 72 | }, 73 | h( 74 | "section", 75 | null, 76 | h("h1", null, "counting area"), 77 | h("span", null, "count: "), 78 | h("span", null, this.state.count), 79 | h( 80 | "button", 81 | { 82 | onClick: () => 83 | this.setState({ ...this.state, count: this.state.count + 1 }), 84 | }, 85 | "add" 86 | ) 87 | ), 88 | h( 89 | "section", 90 | null, 91 | h("h1", null, "user data area"), 92 | h( 93 | "ul", 94 | null, 95 | this.state.data.map((d, i) => 96 | h(ListItem, { 97 | name: d.name, 98 | handleDelete: () => { 99 | this.setState({ 100 | ...this.state, 101 | data: this.state.data.filter((_, j) => { 102 | return i !== j; 103 | }), 104 | }); 105 | }, 106 | }) 107 | ) 108 | ), 109 | h( 110 | "form", 111 | { 112 | onSubmit: (e) => { 113 | e.preventDefault(); 114 | const userName = e.target["name"].value; 115 | this.setState({ 116 | ...this.state, 117 | data: [ 118 | ...this.state.data, 119 | { 120 | name: userName, 121 | }, 122 | ], 123 | }); 124 | }, 125 | }, 126 | h("input", { 127 | name: "name", 128 | }), 129 | h( 130 | "button", 131 | { 132 | type: "submit", 133 | }, 134 | "add" 135 | ) 136 | ) 137 | ) 138 | ); 139 | } 140 | } 141 | 142 | class ListItem extends Component { 143 | componentWillReceiveProps(nextProps, prevProps) { 144 | console.log("next.props:", nextProps); 145 | console.log("next.props:", prevProps); 146 | } 147 | 148 | render() { 149 | return h( 150 | "li", 151 | null, 152 | h("span", null, this.props.name), 153 | h( 154 | "button", 155 | { 156 | onClick: () => this.props.handleDelete(), 157 | }, 158 | "delete" 159 | ) 160 | ); 161 | } 162 | } 163 | 164 | render(h(App, null, null), document.body); 165 | ``` 166 | 167 | ## 意図的に消したもの 168 | 169 | - createContext 170 | - ref 171 | - dangerouslySetInnerHTML; 172 | - svg サポート 173 | - xlink 174 | - catch_error 175 | - getSnapShot 176 | - setState への関数わたし 177 | -------------------------------------------------------------------------------- /packages/oreact/lib/type.d.ts: -------------------------------------------------------------------------------- 1 | export type ComponentChild = 2 | | VNode 3 | | object 4 | | string 5 | | number 6 | | boolean 7 | | null 8 | | undefined; 9 | export type ComponentChildren = ComponentChild[] | ComponentChild; 10 | 11 | export interface ComponentClass

{ 12 | new (props: P, context?: any): Component; 13 | displayName?: string; 14 | defaultProps?: Partial

; 15 | getDerivedStateFromProps?( 16 | props: Readonly

, 17 | state: Readonly 18 | ): Partial | null; 19 | getDerivedStateFromError?(error: any): Partial | null; 20 | } 21 | 22 | export type ComponentType

= ComponentClass

| FunctionComponent

; 23 | 24 | export type PropsType = { 25 | // createElementのoption 26 | is?: string; 27 | // form 28 | checked?: any; 29 | // form 30 | value?: any; 31 | }; 32 | 33 | export type Key = string | number | any; 34 | 35 | interface FunctionComponent

{ 36 | (props: RenderableProps

, context?: any): VNode | null; 37 | displayName?: string; 38 | defaultProps?: Partial

; 39 | } 40 | 41 | interface Attributes { 42 | key?: Key; 43 | jsx?: boolean; 44 | } 45 | 46 | type RenderableProps = P & 47 | Readonly; 48 | 49 | export interface FunctionalComponent

extends FunctionComponent

{ 50 | // Define getDerivedStateFromProps as undefined on FunctionalComponent 51 | // to get rid of some errors in `diff()` 52 | getDerivedStateFromProps?: undefined; 53 | } 54 | 55 | export type ComponentFactory

= ComponentClass

| FunctionalComponent

; 56 | 57 | interface VNode

{ 58 | key: Key; 59 | /** 60 | * The time this `vnode` started rendering. Will only be set when 61 | * the devtools are attached. 62 | * Default value: `0` 63 | */ 64 | startTime?: number; 65 | /** 66 | * The time that the rendering of this `vnode` was completed. Will only be 67 | * set when the devtools are attached. 68 | * Default value: `-1` 69 | */ 70 | endTime?: number; 71 | type: string | ComponentFactory

; 72 | props: P & { children: ComponentChildren }; 73 | _children: Array> | null; 74 | _parent: VNode | null; 75 | _depth: number | null; 76 | /** 77 | * The [first (for Fragments)] DOM child of a VNode 78 | */ 79 | _dom: PreactElement | null; 80 | /** 81 | * The last dom child of a Fragment, or components that return a Fragment 82 | */ 83 | _nextDom: PreactElement | null; 84 | _component: Component | null; 85 | _hydrating: boolean | null; 86 | constructor: undefined; 87 | // 初回レンダリングでは与えられないが、renderComponent から詰め込まれていく 88 | _original?: VNode | null | string | number; 89 | } 90 | 91 | interface Context { 92 | Consumer: Consumer; 93 | Provider: Provider; 94 | } 95 | 96 | interface Consumer 97 | extends FunctionComponent<{ 98 | children: (value: T) => ComponentChildren; 99 | }> {} 100 | interface PreactConsumer extends Consumer {} 101 | 102 | interface Provider 103 | extends FunctionComponent<{ 104 | value: T; 105 | children: ComponentChildren; 106 | }> {} 107 | 108 | export interface Component

{ 109 | // When component is functional component, this is reset to functional component 110 | constructor: ComponentType

; 111 | state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks 112 | base?: PreactElement; 113 | 114 | _dirty: boolean; 115 | _force?: boolean; 116 | _renderCallbacks: Array; // Component は実質 () => void 117 | _globalContext?: any; 118 | _vnode?: VNode

| null; 119 | // setStateが呼ばれるとこの値に置き換える 120 | _nextState?: S | null; // Only class components 121 | /** Only used in the devtools to later dirty check if state has changed */ 122 | _prevState?: S | null; 123 | /** 124 | * Pointer to the parent dom node. This is only needed for top-level Fragment 125 | * components or array returns. 126 | */ 127 | _parentDom?: PreactElement | null; 128 | // Always read, set only when handling error 129 | _processingException?: Component | null; 130 | // Always read, set only when handling error. This is used to indicate at diffTime to set _processingException 131 | _pendingError?: Component | null; 132 | } 133 | 134 | export interface PreactElement extends HTMLElement, Text { 135 | _children?: VNode | null; 136 | /** Event listeners to support event delegation */ 137 | _listeners: Record void>; 138 | 139 | // Preact uses this attribute to detect SVG nodes 140 | ownerSVGElement?: SVGElement | null; 141 | 142 | // style: HTMLElement["style"]; // From HTMLElement 143 | 144 | data?: string | number; // From Text node 145 | } 146 | -------------------------------------------------------------------------------- /packages/oreact/lib/component.ts: -------------------------------------------------------------------------------- 1 | import { assign } from "./util"; 2 | import { diff, commitRoot } from "./diff/index"; 3 | import { Fragment } from "./create-element"; 4 | import { Component as ComponentType, PropsType, VNode } from "./type"; 5 | 6 | /** 7 | * コンポーネント 8 | * @param props props 9 | */ 10 | export function Component(props: PropsType) { 11 | this.props = props; 12 | } 13 | 14 | /** 15 | * setState メソッド. 16 | * component._nextState に次の状態を保存し、enqueueRenderを呼び出して再レンダリングをトリガーする 17 | */ 18 | Component.prototype.setState = function (update: Object) { 19 | // only clone state when copying to nextState the first time. 20 | let s; 21 | if (this._nextState != null && this._nextState !== this.state) { 22 | s = this._nextState; 23 | } else { 24 | s = this._nextState = assign({}, this.state); 25 | } 26 | 27 | if (update) { 28 | assign(s, update); 29 | } 30 | 31 | // Skip update if updater function returned null 32 | if (update == null) return; 33 | 34 | if (this._vnode) { 35 | enqueueRender(this); 36 | } 37 | }; 38 | 39 | /** 40 | * render が呼ばれると、Fragment() つまり、props.childrenを返す 41 | */ 42 | Component.prototype.render = Fragment; 43 | 44 | /** 45 | * 兄弟DOMを取得する 46 | * @param vnode 対象となるVNode 47 | * @param childIndex 何個目の兄弟かの番号 48 | */ 49 | export function getDomSibling(vnode: VNode, childIndex: number | null) { 50 | if (childIndex == null) { 51 | return vnode._parent 52 | ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1) 53 | : null; 54 | } 55 | 56 | let sibling; 57 | for (; childIndex < vnode._children.length; childIndex++) { 58 | sibling = vnode._children[childIndex]; 59 | 60 | if (sibling != null && sibling._dom != null) { 61 | return sibling._dom; 62 | } 63 | } 64 | 65 | // vnode's children からDOMが見つからなかった時の処理 66 | return typeof vnode.type == "function" 67 | ? getDomSibling(vnode, undefined) 68 | : null; 69 | } 70 | 71 | /** 72 | * 際レンダリングのトリガー, diffを呼び出す 73 | * @param component rerender したいコンポーネント 74 | */ 75 | function renderComponent(component: ComponentType) { 76 | let vnode = component._vnode, 77 | oldDom = vnode._dom, 78 | parentDom = component._parentDom; 79 | 80 | if (parentDom) { 81 | let commitQueue = []; 82 | const oldVNode = assign({}, vnode) as VNode; 83 | oldVNode._original = oldVNode; 84 | 85 | let newDom = diff({ 86 | parentDom: parentDom, 87 | newVNode: vnode, 88 | oldVNode: oldVNode, 89 | excessDomChildren: null, 90 | commitQueue: commitQueue, 91 | oldDom: oldDom == null ? getDomSibling(vnode, undefined) : oldDom, 92 | }); 93 | 94 | commitRoot(commitQueue); 95 | 96 | if (newDom != oldDom) { 97 | updateParentDomPointers(vnode); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * @param {import('./internal').VNode} vnode 104 | */ 105 | function updateParentDomPointers(vnode: VNode) { 106 | if ((vnode = vnode._parent) != null && vnode._component != null) { 107 | vnode._dom = vnode._component.base = null; 108 | for (let i = 0; i < vnode._children.length; i++) { 109 | let child = vnode._children[i]; 110 | if (child != null && child._dom != null) { 111 | vnode._dom = vnode._component.base = child._dom; 112 | break; 113 | } 114 | } 115 | 116 | return updateParentDomPointers(vnode); 117 | } 118 | } 119 | 120 | // 差分更新後の副作用を管理するリスト 121 | let rerenderQueue: ComponentType[] = []; 122 | 123 | // callbackの非同期スケジューラー 124 | const defer: (cb: () => void) => void = 125 | typeof Promise == "function" 126 | ? Promise.prototype.then.bind(Promise.resolve()) 127 | : setTimeout; 128 | 129 | /** 130 | * Enqueue a rerender of a component 131 | * @param {import('./internal').Component} c The component to rerender 132 | */ 133 | 134 | /** 135 | * setStateが呼び出すトリガー. 136 | * データの破壊的操作があるので注意 137 | * @param c コンポーネント 138 | */ 139 | export function enqueueRender(c: ComponentType) { 140 | if ( 141 | (!c._dirty && 142 | (c._dirty = true) && 143 | // queueの操作 144 | rerenderQueue.push(c) && 145 | // counterの増加操作 146 | !process._rerenderCount++) || 147 | true 148 | ) { 149 | defer(process); 150 | } 151 | } 152 | 153 | /** 154 | * renderQueueを実行する 155 | */ 156 | function process() { 157 | let queue; 158 | while ((process._rerenderCount = rerenderQueue.length)) { 159 | queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); 160 | rerenderQueue = []; 161 | // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary 162 | // process() calls from getting scheduled while `queue` is still being consumed. 163 | queue.some((c) => { 164 | if (c._dirty) renderComponent(c); 165 | }); 166 | } 167 | } 168 | process._rerenderCount = 0; 169 | -------------------------------------------------------------------------------- /packages/oreact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": ["dom", "es5"], 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./js" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": ["lib/"] 64 | } 65 | -------------------------------------------------------------------------------- /packages/oreact/lib/diff/children.ts: -------------------------------------------------------------------------------- 1 | import { diff, unmount } from "./index"; 2 | import { createVNode, Fragment } from "../create-element"; 3 | import { EMPTY_OBJ, EMPTY_ARR } from "../constants"; 4 | import { getDomSibling } from "../component"; 5 | import { Component, ComponentChildren, PreactElement, VNode } from "../type"; 6 | 7 | type DiffChildrenArgType = { 8 | parentDom: PreactElement; 9 | /** diffElementNodesからchildrenが渡される */ 10 | renderResult: ComponentChildren[]; 11 | /** [renderResult]がdiffから渡される */ 12 | newParentVNode: VNode; 13 | /** diff が持ってる oldVNode が渡される. 呼び出されるたびに */ 14 | oldParentVNode: VNode; 15 | excessDomChildren: PreactElement; 16 | commitQueue: Component[]; 17 | oldDom: Element | Text | typeof EMPTY_OBJ; 18 | }; 19 | 20 | /** 21 | * VNodeのchildren比較を行う 22 | */ 23 | export function diffChildren(arg: DiffChildrenArgType) { 24 | let { 25 | parentDom, 26 | renderResult, 27 | newParentVNode, 28 | oldParentVNode, 29 | excessDomChildren, 30 | commitQueue, 31 | oldDom, 32 | } = arg; 33 | let i, 34 | j, 35 | oldVNode, 36 | childVNode, 37 | newDom, 38 | firstChildDom, 39 | filteredOldDom: Element | Text | null; 40 | 41 | let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; 42 | let oldChildrenLength = oldChildren.length; 43 | 44 | // top level の render か Fragmentかの識別 45 | if (oldDom == EMPTY_OBJ) { 46 | if (oldChildrenLength) { 47 | filteredOldDom = getDomSibling(oldParentVNode, 0); 48 | } else { 49 | filteredOldDom = null; 50 | } 51 | } 52 | 53 | // renderResult から newVNode として扱いたい childVNode を作り出し、それを配列に詰め込んで newParentVNode._children を作り出す 54 | newParentVNode._children = []; 55 | for (i = 0; i < renderResult.length; i++) { 56 | childVNode = renderResult[i]; 57 | 58 | if (childVNode == null || typeof childVNode == "boolean") { 59 | childVNode = newParentVNode._children[i] = null; 60 | } else if (typeof childVNode == "string" || typeof childVNode == "number") { 61 | // child が primitive の場合 62 | childVNode = newParentVNode._children[i] = createVNode( 63 | null, 64 | childVNode, 65 | null, 66 | null, 67 | childVNode 68 | ); 69 | } else if (Array.isArray(childVNode)) { 70 | // child が 配列 の場合 71 | childVNode = newParentVNode._children[i] = createVNode( 72 | Fragment, 73 | { children: childVNode }, 74 | null, 75 | null, 76 | null 77 | ); 78 | } else if (childVNode._dom != null || childVNode._component != null) { 79 | // child が element の場合 80 | childVNode = newParentVNode._children[i] = createVNode( 81 | childVNode.type, 82 | childVNode.props, 83 | childVNode.key, 84 | null, 85 | childVNode._original 86 | ); 87 | } else { 88 | // child が コンポーネントの場合 89 | childVNode = newParentVNode._children[i] = childVNode; 90 | } 91 | 92 | // Terser removes the `continue` here and wraps the loop body 93 | // in a `if (childVNode) { ... } condition 94 | if (childVNode == null) { 95 | continue; 96 | } 97 | 98 | childVNode._parent = newParentVNode; 99 | childVNode._depth = newParentVNode._depth + 1; 100 | 101 | // Check if we find a corresponding element in oldChildren. 102 | // If found, delete the array item by setting to `undefined`. 103 | // We use `undefined`, as `null` is reserved for empty placeholders 104 | // (holes). 105 | oldVNode = oldChildren[i]; 106 | 107 | if ( 108 | oldVNode === null || 109 | (oldVNode && 110 | childVNode.key == oldVNode.key && 111 | childVNode.type === oldVNode.type) 112 | ) { 113 | oldChildren[i] = undefined; 114 | } else { 115 | // Either oldVNode === undefined or oldChildrenLength > 0, 116 | // so after this loop oldVNode == null or oldVNode is a valid value. 117 | for (j = 0; j < oldChildrenLength; j++) { 118 | oldVNode = oldChildren[j]; 119 | // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key. 120 | // We always match by type (in either case). 121 | if ( 122 | oldVNode && 123 | childVNode.key == oldVNode.key && 124 | childVNode.type === oldVNode.type 125 | ) { 126 | oldChildren[j] = undefined; 127 | break; 128 | } 129 | oldVNode = null; 130 | } 131 | } 132 | 133 | oldVNode = oldVNode || EMPTY_OBJ; 134 | 135 | // Morph the old element into the new one, but don't append it to the dom yet 136 | newDom = diff({ 137 | parentDom: parentDom, // diff から渡された parentDom を使ってまた diff を呼び出す. 138 | newVNode: childVNode, // diff の renderResult の要素を newVNode として diff に渡す. 139 | oldVNode: oldVNode, // oldVNode はおやから渡されたもの or EMPTY_OBJ. key不一致ならEMPTY_OBJが渡される. 140 | excessDomChildren: excessDomChildren, 141 | commitQueue: commitQueue, 142 | oldDom: filteredOldDom, 143 | }); 144 | 145 | // 新しいDOMがあれば挿入する 146 | if (newDom != null) { 147 | if (firstChildDom == null) { 148 | firstChildDom = newDom; 149 | } 150 | 151 | filteredOldDom = placeChild({ 152 | parentDom: parentDom, 153 | childVNode: childVNode, 154 | oldVNode: oldVNode, 155 | oldChildren: oldChildren, 156 | excessDomChildren: excessDomChildren, 157 | newDom: newDom, 158 | oldDom: filteredOldDom, 159 | }); 160 | 161 | if (typeof newParentVNode.type == "function") { 162 | newParentVNode._nextDom = filteredOldDom as PreactElement; 163 | } 164 | } 165 | } 166 | 167 | newParentVNode._dom = firstChildDom; 168 | 169 | // Remove remaining oldChildren if there are any. 170 | for (i = oldChildrenLength; i--; ) { 171 | if (oldChildren[i] != null) unmount(oldChildren[i], oldChildren[i]); 172 | } 173 | } 174 | 175 | type PlaceChildArgType = { 176 | parentDom: PreactElement; 177 | childVNode: VNode; 178 | oldVNode: VNode; 179 | oldChildren: ComponentChildren; 180 | excessDomChildren: ComponentChildren; 181 | newDom: Node | Text; 182 | oldDom: Node | Text; 183 | }; 184 | 185 | /** 186 | * parentDOMにnewDOMを挿入する関数 187 | * @param arg 188 | */ 189 | export function placeChild(arg: PlaceChildArgType): PreactElement { 190 | let { 191 | parentDom, 192 | childVNode, 193 | oldVNode, 194 | oldChildren, 195 | excessDomChildren, 196 | newDom, 197 | oldDom, 198 | } = arg; 199 | 200 | let nextDom; 201 | if (childVNode._nextDom !== undefined) { 202 | nextDom = childVNode._nextDom; 203 | 204 | childVNode._nextDom = undefined; 205 | } else if ( 206 | excessDomChildren == oldVNode || 207 | newDom != oldDom || 208 | newDom.parentNode == null 209 | ) { 210 | outer: if (oldDom == null || oldDom.parentNode !== parentDom) { 211 | // 親が異なるなら兄弟ではないので子要素を追加 212 | parentDom.appendChild(newDom); 213 | nextDom = null; 214 | } else { 215 | // 親が同じなら兄弟要素を追加 216 | if (!Array.isArray(oldChildren)) { 217 | throw new Error("配列であるべき"); 218 | } 219 | for ( 220 | let sibDom = oldDom, j = 0; 221 | (sibDom = sibDom.nextSibling) && j < oldChildren.length; 222 | j += 2 223 | ) { 224 | if (sibDom == newDom) { 225 | break outer; 226 | } 227 | } 228 | parentDom.insertBefore(newDom, oldDom); 229 | nextDom = oldDom; 230 | } 231 | } 232 | 233 | if (nextDom !== undefined) { 234 | oldDom = nextDom; 235 | } else { 236 | oldDom = newDom.nextSibling; 237 | } 238 | 239 | return oldDom as PreactElement; 240 | } 241 | -------------------------------------------------------------------------------- /packages/oreact/lib/diff/index.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_OBJ } from "../constants"; 2 | import { Component } from "../component"; 3 | import { Fragment } from "../create-element"; 4 | import { diffChildren } from "./children"; 5 | import { diffProps } from "./props"; 6 | import { removeNode } from "../util"; 7 | import { 8 | Component as ComponentType, 9 | PreactElement, 10 | PropsType, 11 | VNode, 12 | } from "../type"; 13 | 14 | type DiffArgType = { 15 | /** マウント対象のDOM. このうえにVNodeを反映させていく. 初回実行では render から渡されたものが入るが、diff 自体は再帰的に呼ばれ parentDom も置き換えられたりするので様々な値が入りうる。 */ 16 | parentDom: PreactElement; 17 | /** 置き換えに使うvnode, renderから呼ばれるときは事前にcreateElementされている。diffCHildrenから呼ばれるときはchildNode(つまり一つ階層を降っている) */ 18 | newVNode: VNode; 19 | /** 初回実行ならnullが渡されれる(hydrateされていないなら) */ 20 | oldVNode: VNode | typeof EMPTY_OBJ; 21 | excessDomChildren: PreactElement[]; 22 | /** commitRoot時に実行されるcallbackを持つコンポーネントのリスト */ 23 | commitQueue: ComponentType[]; 24 | /** 初回レンダリングではreplaceNodeがそのまま渡される(初回レンダリングでは大抵の場合はEMPTY_OBJECT),ただしdiff 自体は再帰的に呼ばれ oldDom も置き換えられたりするので様々な値が入りうる。 */ 25 | oldDom: Element | Text | typeof EMPTY_OBJ; 26 | }; 27 | 28 | /** 29 | * 与えられた新旧VNodeの差分をとって、その差分DOMに適用してそのDOMを返す関数 30 | */ 31 | export function diff(arg: DiffArgType) { 32 | console.log("diff", arguments); 33 | let { 34 | parentDom, 35 | newVNode, 36 | oldVNode, 37 | excessDomChildren, 38 | commitQueue, 39 | oldDom, 40 | } = arg; 41 | let tmp, 42 | newType = newVNode.type; 43 | 44 | // createElement は constructor を undefined で設定するtので、そこ経由で作られたことを保証する。 45 | if (newVNode.constructor !== undefined) return null; 46 | 47 | // newVNode がコンポーネントかエレメントかで処理が分岐 48 | if (typeof newType == "function") { 49 | // newVNode がコンポーネントの時の分岐 50 | let c, isNew, oldProps, oldState; 51 | let newProps = newVNode.props; 52 | 53 | let componentContext = EMPTY_OBJ; 54 | 55 | // コンポーネントオブジェクトの作成 56 | if (oldVNode._component) { 57 | // すでにコンポーネントがある時(例えばsetStateのとき) 58 | c = newVNode._component = oldVNode._component; 59 | } else { 60 | if ("prototype" in newType && newType.prototype.render) { 61 | newVNode._component = c = new newType(newProps); 62 | } else { 63 | newVNode._component = c = new Component(newProps); 64 | c.constructor = newType; 65 | c.render = doRender; 66 | } 67 | 68 | c.props = newProps; 69 | if (!c.state) c.state = {}; 70 | isNew = c._dirty = true; 71 | c._renderCallbacks = []; 72 | } 73 | 74 | // Invoke getDerivedStateFromProps 75 | if (c._nextState == null) { 76 | c._nextState = c.state; 77 | } 78 | 79 | oldProps = c.props; 80 | oldState = c.state; 81 | 82 | // 差分更新前(diff中)に呼び出されるライフサイクルイベントを実行 83 | if (isNew) { 84 | if (c.componentDidMount != null) { 85 | c._renderCallbacks.push(c.componentDidMount); 86 | } 87 | } else { 88 | if ( 89 | newType.getDerivedStateFromProps == null && 90 | newProps !== oldProps && 91 | c.componentWillReceiveProps != null 92 | ) { 93 | c.componentWillReceiveProps(newProps, componentContext); 94 | } 95 | } 96 | 97 | c.props = newProps; 98 | c.state = c._nextState; 99 | 100 | c._dirty = false; 101 | c._vnode = newVNode; 102 | c._parentDom = parentDom; 103 | 104 | tmp = c.render(c.props); 105 | 106 | // Handle setState called in render, see #2553 107 | c.state = c._nextState; 108 | 109 | let isTopLevelFragment = 110 | tmp != null && tmp.type == Fragment && tmp.key == null; 111 | let renderResult = isTopLevelFragment ? tmp.props.children : tmp; 112 | 113 | diffChildren({ 114 | parentDom: parentDom, 115 | renderResult: Array.isArray(renderResult) ? renderResult : [renderResult], 116 | newParentVNode: newVNode, 117 | oldParentVNode: oldVNode, 118 | excessDomChildren: excessDomChildren, 119 | commitQueue: commitQueue, 120 | oldDom: oldDom, 121 | }); 122 | 123 | // FIXME: 消しても問題ない 124 | c.base = newVNode._dom; 125 | 126 | // We successfully rendered this VNode, unset any stored hydration/bailout state: 127 | newVNode._hydrating = null; 128 | 129 | if (c._renderCallbacks.length) { 130 | commitQueue.push(c); 131 | } 132 | 133 | c._force = false; 134 | } else if ( 135 | excessDomChildren == null && 136 | newVNode._original === oldVNode._original 137 | ) { 138 | // FIXME: このブロックも消して問題ない 139 | newVNode._children = oldVNode._children; 140 | newVNode._dom = oldVNode._dom; 141 | } else { 142 | // newVNode が Element の時の分岐 143 | newVNode._dom = diffElementNodes({ 144 | dom: oldVNode._dom, // この分岐に入るとparentDomではなくoldVNode._domを見るようになる。(階層を下る) 145 | newVNode: newVNode, 146 | oldVNode: oldVNode, 147 | excessDomChildren: excessDomChildren, 148 | commitQueue: commitQueue, 149 | }) as PreactElement; 150 | } 151 | 152 | return newVNode._dom; 153 | } 154 | 155 | /** 156 | * commitQueueを実行する関数 157 | * @param commitQueue コールバックリストを持ったcomponentのリスト 158 | */ 159 | export function commitRoot(commitQueue: ComponentType[]) { 160 | commitQueue.some((c) => { 161 | commitQueue = c._renderCallbacks; 162 | c._renderCallbacks = []; 163 | commitQueue.some((cb) => { 164 | cb.call(c); 165 | }); 166 | }); 167 | } 168 | 169 | type DiffElementArgType = { 170 | /** diffのoldVNode._domが渡される */ 171 | dom: PreactElement; 172 | newVNode: VNode; 173 | oldVNode: VNode; 174 | excessDomChildren: any; 175 | commitQueue: ComponentType[]; 176 | }; 177 | 178 | /** 179 | * newVNode と oldVNode を比較して dom に反映する。 180 | * ツリーではなくDOM Node のプロパティ比較が責務。 181 | */ 182 | function diffElementNodes(arg: DiffElementArgType) { 183 | let { dom, newVNode, oldVNode, excessDomChildren, commitQueue } = arg; 184 | let i; 185 | let oldProps = oldVNode.props; 186 | let newProps = newVNode.props; 187 | 188 | if (dom == null) { 189 | // oldVNode._dom は 初回レンダリングはnullなのでこの分岐 190 | if (newVNode.type === null) { 191 | // primitive値であることが保証されているのでキャストする 192 | // 3 も '3' も '3' として扱う 193 | const value = String(newProps); 194 | return document.createTextNode(value); 195 | } 196 | 197 | // 初回レンダリングでDOMツリーを作る 198 | dom = document.createElement( 199 | newVNode.type, 200 | newProps.is && { is: newProps.is } 201 | ); 202 | // 新しく親を作ったので既存の子は使いまわさない 203 | excessDomChildren = null; 204 | } 205 | 206 | if (newVNode.type === null) { 207 | // newVNode が primitive の場合 208 | const textNodeProps = (newProps as any) as string | number; 209 | if (oldProps !== newProps && dom.data !== textNodeProps) { 210 | dom.data = textNodeProps; 211 | } 212 | } else { 213 | // newVNode が element の場合 214 | const props: Partial["props"]> = 215 | oldVNode.props || EMPTY_OBJ; 216 | 217 | // VNodeの差分を取る。domは破壊的操作がされる 218 | diffProps(dom, newProps, props); 219 | 220 | // VNode の children に diff を取るためにchildrenを抽出 221 | i = newVNode.props.children; 222 | 223 | // newVNodeがComponentの入れ子でなくてもElementの入れ子の可能性があるので、childrenの比較も行う 224 | diffChildren({ 225 | parentDom: dom, 226 | renderResult: Array.isArray(i) ? i : [i], 227 | newParentVNode: newVNode, 228 | oldParentVNode: oldVNode, 229 | excessDomChildren: excessDomChildren, 230 | commitQueue: commitQueue, 231 | oldDom: EMPTY_OBJ, 232 | }); 233 | } 234 | 235 | return dom; 236 | } 237 | 238 | /** 239 | * componentWillUnmount の実行と、DOMツリーからNodeをremoveする 240 | * @param vnode 241 | * @param parentVNode 242 | * @param skipRemove 243 | */ 244 | export function unmount(vnode, parentVNode, skipRemove) { 245 | let r; 246 | 247 | let dom; 248 | if (!skipRemove && typeof vnode.type != "function") { 249 | skipRemove = (dom = vnode._dom) != null; 250 | } 251 | 252 | // Must be set to `undefined` to properly clean up `_nextDom` 253 | // for which `null` is a valid value. See comment in `create-element.js` 254 | vnode._dom = vnode._nextDom = undefined; 255 | 256 | if ((r = vnode._component) != null) { 257 | if (r.componentWillUnmount) { 258 | r.componentWillUnmount(); 259 | } 260 | 261 | r.base = r._parentDom = null; 262 | } 263 | 264 | if ((r = vnode._children)) { 265 | for (let i = 0; i < r.length; i++) { 266 | if (r[i]) unmount(r[i], parentVNode, skipRemove); 267 | } 268 | } 269 | 270 | if (dom != null) removeNode(dom); 271 | } 272 | 273 | /** The `.render()` method for a PFC backing instance. */ 274 | function doRender(props) { 275 | return this.constructor(props); 276 | } 277 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This document is intended for developers interest in making contributions to Preact and document our internal processes like releasing a new version. 4 | 5 | ## Getting Started 6 | 7 | This steps will help you to set up your development environment. That includes all dependencies we use to build Preact and developer tooling like git commit hooks. 8 | 9 | 1. Clone the git repository: `git clone git@github.com:preactjs/preact.git` 10 | 2. Go into the cloned folder: `cd preact/` 11 | 3. Install all dependencies: `npm install` 12 | 13 | ## The Repo Structure 14 | 15 | This repository contains Preact itself, as well as several addons like the debugging package for example. This is reflected in the directory structure of this repository. Each package has a `src/` folder where the source code can be found, a `test` folder for all sorts of tests that check if the code in `src/` is correct, and a `dist/` folder where you can find the bundled artifacts. Note that the `dist/` folder may not be present initially. It will be created as soon as you run any of the build scripts inside `package.json`. More on that later ;) 16 | 17 | A quick overview of our repository: 18 | 19 | ```bash 20 | # The repo root (folder where you cloned the repo into) 21 | / 22 | src/ # Source code of our core 23 | test/ # Unit tests for core 24 | dist/ # Build artifacts for publishing on npm (may not be present) 25 | 26 | # Sub-package, can be imported via `preact/compat` by users. 27 | # Compat stands for react-compatibility layer which tries to mirror the 28 | # react API as close as possible (mostly legacy APIs) 29 | compat/ 30 | src/ # Source code of the compat addon 31 | test/ # Tests related to the compat addon 32 | dist/ # Build artifacts for publishing on npm (may not be present) 33 | 34 | # Sub-package, can be imported via `preact/hooks` by users. 35 | # The hooks API is an effect based API to deal with component lifcycles. 36 | # It's similar to hooks in React 37 | hooks/ 38 | src/ # Source code of the hooks addon 39 | test/ # Tests related to the hooks addon 40 | dist/ # Build artifacts for publishing on npm (may not be present) 41 | 42 | # Sub-package, can be imported via `preact/debug` by users. 43 | # Includes debugging warnings and error messages for common mistakes found 44 | # in Preact application. Also hosts the devtools bridge 45 | debug/ 46 | src/ # Source code of the debug addon 47 | test/ # Tests related to the debug addon 48 | dist/ # Build artifacts for publishing on npm (may not be present) 49 | 50 | # Sub-package, can be imported via `preact/test-utils` by users. 51 | # Provides helpers to make testing Preact applications easier 52 | test-utils/ 53 | src/ # Source code of the test-utils addon 54 | test/ # Tests related to the test-utils addon 55 | dist/ # Build artifacts for publishing on npm (may not be present) 56 | 57 | # A demo application that we use to debug tricky errors and play with new 58 | # features. 59 | demo/ 60 | 61 | # Contains build scripts and dependencies for development 62 | package.json 63 | ``` 64 | 65 | _Note: The code for rendering Preact on the server lives in another repo and is a completely separate npm package. It can be found here: [https://github.com/preactjs/preact-render-to-string](https://github.com/preactjs/preact-render-to-string)_ 66 | 67 | ### What does `mangle.json` do? 68 | 69 | It's a special file that can be used to specify how `terser` (previously known as `uglify`) will minify variable names. Because each sub-package has it's own distribution files we need to ensure that the variable names stay consistent across bundles. 70 | 71 | ## What does `options.js` do? 72 | 73 | Unique to Preact we do support several ways to hook into our renderer. All our addons use that to inject code at different stages of a render process. They are documented in our typings in `internal.d.ts`. The core itself doesn't make use of them, which is why the file only contains an empty `object`. 74 | 75 | ## Important Branches 76 | 77 | We merge every PR into the `master` branch which is the one that we'll use to publish code to npm. For the previous Preact release line we have a branch called `8` which is in maintenance mode. As a new contributor you won't have to deal with that ;) 78 | 79 | ## Creating your first Pull-Request 80 | 81 | We try to make it as easy as possible to contribute to Preact and make heavy use of GitHub's "Draft PR" feature which tags Pull-Requests (short = PR) as work in progress. PRs tend to be published as soon as there is an idea that the developer deems worthwhile to include into Preact and has written some rough code. The PR doesn't have to be perfect or anything really ;) 82 | 83 | Once a PR or a Draft PR has been created our community typically joins the discussion about the proposed change. Sometimes that includes ideas for test cases or even different ways to go about implementing a feature. Often this also includes ideas on how to make the code smaller. We usually refer to the latter as "code-golfing" or just "golfing". 84 | 85 | When everything is good to go someone will approve the PR and the changes will be merged into the `master` branch and we usually cut a release a few days/ a week later. 86 | 87 | _The big takeaway for you here is, that we will guide you along the way. We're here to help to make a PR ready for approval!_ 88 | 89 | The short summary is: 90 | 91 | 1. Make changes and submit a PR 92 | 2. Modify change according to feedback (if there is any) 93 | 3. PR will be merged into `master` 94 | 4. A new release will be cut (every 2-3 weeks). 95 | 96 | ## Commonly used scripts for contributions 97 | 98 | Scripts can be executed via `npm run [script]` or `yarn [script]` respectively. 99 | 100 | - `build` - compiles all packages ready for publishing to npm 101 | - `build:core` - builds just Preact itself 102 | - `build:debug` - builds the debug addon only 103 | - `build:hooks` - builds the hook addon only 104 | - `build:test-utils` - builds the test-utils addon only 105 | - `test:ts` - Run all tests for TypeScript definitions 106 | - `test:karma` - Run all unit/integration tests. 107 | - `test:karma:watch` - Same as above, but it will automatically re-run the test suite if a code change was detected. 108 | 109 | But to be fair, the only ones we use ourselves are `build` and `test:karma:watch`. The other ones are mainly used on our CI pipeline and we rarely use them. 110 | 111 | _Note: Both `test:karma` and `test:karma:watch` listen to the environment variable `COVERAGE=true`. Disabling code coverage can significantly speed up the time it takes to complete the test suite._ 112 | 113 | _Note2: The test suite is based on `karma` and `mocha`. Individual tests can be executed by appending `.only`:_ 114 | 115 | ```jsx 116 | it.only('should test something', () => { 117 | expect(1).to.equal(1); 118 | }); 119 | ``` 120 | 121 | ## Common terminology and variable names 122 | 123 | - `vnode` -> shorthand for `virtual-node` which is an object that specifies how a Component or DOM-node looks like 124 | - `commit` -> A commit is the moment in time when you flush all changes to the DOM 125 | - `c` -> The variable `c` always refers to a `component` instance throughout our code base. 126 | - `diff/diffing` -> Diffing describes the process of comparing two "things". In our case we compare the previous `vnode` tree with the new one and apply the delta to the DOM. 127 | - `root` -> The topmost node of a `vnode` tree 128 | 129 | ## Tips for getting to know the code base 130 | 131 | - Check the JSDoc block right above the function definition to understand what it does. It contains a short description of each function argument and what it does. 132 | - Check the callsites of a function to understand how it's used. Modern editors/IDEs allow you to quickly find those, or use the plain old search feature instead. 133 | 134 | ## FAQ 135 | 136 | ### Why does the JSDoc use TypeScript syntax to specify types? 137 | 138 | Several members of the team are very fond of TypeScript and we wanted to leverage as many of its advantages, like improved autocompletion, for Preact. We even attempted to port Preact to TypeScript a few times, but we ran into many issues with the DOM typings. Those would force us to fill our codebase with many `any` castings, making our code very noisy. 139 | 140 | Luckily TypeScript has a mode where it can somewhat reliably typecheck JavaScript code by reusing the types defined in JSDoc blocks. It's not perfect and it often has trouble inferring the correct types the further one strays away from the function arguments, but it's good enough that it helps us a lot with autocompletion. Another plus is that we can make sure that our TypeScript definitons are correct at the same time. 141 | 142 | Check out the [official TypeScript documentation](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html) for more information. 143 | 144 | _Note that we have separate tests for our TypeScript definition files. We only use `ts-check` for local development and don't check it anywhere else like on the CI._ 145 | 146 | ### Why does the code base often use `let` instead of `const`? 147 | 148 | There is no real reason for that other a historical one. Back before auto-formatting via prettier was a thing and minifiers weren't as advanced as they are today we used a pretty terse code-style. The code-style deliberately was aimed at making code look as concise and short as possible. The `let` keyword is a bit shorter than `const` to write, so we only used that. This was done only for stylistic reasons. 149 | 150 | This helped our minds to not lose sight of focusing on size, but made it difficult for newcomers to start contributing to Preact. For that reason alone we switched to `prettier` and loosened our rule regarding usage of `let` or `const`. Today we use both, but you can still find many existing places where `let` is still in use. 151 | 152 | In the end there is no effect on size regardless if you use `const`, `let` or use both. Our code is downtranspiled to `ES5` for npm so both will be replaced with `var` anyways. Therefore it doesn't really matter at all which one is used in our codebase. 153 | 154 | This will only become important once shipping modern JavaScript code on npm becomes a thing and bundlers follow suit. 155 | 156 | ## How to create a good bug report 157 | 158 | To be able to fix issues we need to see them on our machine. This is only possible when we can reproduce the error. The easiest way to do that is narrow down the problem to specific components or combination of them. This can be done by removing as much unrelated code as possible. 159 | 160 | The perfect way to do that is to make a [codesandbox](https://codesandbox.io/). That way you can easily share the problematic code and ensure that others can see the same issue you are seeing. 161 | 162 | For us a [codesandbox](https://codesandbox.io/) says more than a 1000 words :tada: 163 | 164 | ## I have more questions on how to contribute to Preact. How can I reach you? 165 | 166 | We closely watch our issues and have a pretty active [Slack workspace](https://chat.preactjs.com/). Nearly all our communication happens via these two forms of communication. 167 | 168 | ## Releasing Preact (Maintainers only) 169 | 170 | This guide is intended for core team members that have the necessary 171 | rights to publish new releases on npm. 172 | 173 | 1. [Write the release notes](#writing-release-notes) and keep them as a draft in GitHub 174 | 1. I'd recommend writing them in an offline editor because each edit to a draft will change the URL in GitHub. 175 | 2. Make a PR where **only** the version number is incremented in `package.json` (note: We follow `SemVer` conventions) 176 | 3. Wait until the PR is approved and merged. 177 | 4. Switch back to the `master` branch and pull the merged PR 178 | 5. Run `npm run build && npm publish` 179 | 1. Make sure you have 2FA enabled in npm, otherwise the above command will fail. 180 | 2. If you're doing a pre-release add `--tag next` to the `npm publish` command to publish it under a different tag (default is `latest`) 181 | 6. Publish the release notes and create the correct git tag. 182 | 7. Tweet it out 183 | 184 | ## Legacy Releases (8.x) 185 | 186 | > **ATTENTION:** Make sure that you've cleared the project correctly 187 | > when switching from a 10.x branch. 188 | 189 | 0. Run `rm -rf dist node_modules && npm i` to make sure to have the correct dependencies. 190 | 191 | Apart from that it's the same as above. 192 | 193 | ## Writing release notes 194 | 195 | The release notes have become a sort of tiny blog post about what's 196 | happening in preact-land. The title usually has this format: 197 | 198 | ```txt 199 | Version Name 200 | ``` 201 | 202 | Example: 203 | 204 | ```txt 205 | 10.0.0-beta.1 Los Compresseros 206 | ``` 207 | 208 | The name is optional, we just have fun finding creative names :wink: 209 | 210 | To keep them interesting we try to be as 211 | concise as possible and to just reflect where we are. There are some 212 | rules we follow while writing them: 213 | 214 | - Be nice, use a positive tone. Avoid negative words 215 | - Show, don't just tell. 216 | - Be honest. 217 | - Don't write too much, keep it simple and short. 218 | - Avoid making promises and don't overpromise. That leads to unhappy users 219 | - Avoid framework comparisons if possible 220 | - Highlight awesome community contributions (or great issue reports) 221 | - If in doubt, praise the users. 222 | 223 | After this section we typically follow with a changelog part that's 224 | divided into 4 groups in order of importance for the user: 225 | 226 | - Features 227 | - Bug Fixes 228 | - Typings 229 | - Maintenance 230 | 231 | We generate it via this handy cli program: [changelogged](https://github.com/marvinhagemeister/changelogged). It will collect and format 232 | the descriptions of all PRs that have been merged between two tags. 233 | The usual command is `changelogged 10.0.0-rc.2..HEAD` similar to how 234 | you'd diff two points in time with git. This will get you 90% there, 235 | but you still need to divide it into groups. It's also a good idea 236 | to unify the formatting of the descriptions, so that they're easier 237 | to read and don't look like a mess. 238 | --------------------------------------------------------------------------------