├── .editorconfig ├── .eslintignore ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── benchmarks.yml │ ├── build-test.yml │ ├── ci.yml │ ├── pr-reporter.yml │ ├── release.yml │ ├── run-bench.yml │ ├── single-bench.yml │ └── size.yml ├── .gitignore ├── .gitmodules ├── .husky └── pre-commit ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── biome.json ├── compat ├── LICENSE ├── client.d.ts ├── client.js ├── client.mjs ├── jsx-dev-runtime.js ├── jsx-dev-runtime.mjs ├── jsx-runtime.js ├── jsx-runtime.mjs ├── mangle.json ├── package.json ├── scheduler.js ├── scheduler.mjs ├── server.browser.js ├── server.js ├── server.mjs ├── src │ ├── Children.js │ ├── PureComponent.js │ ├── forwardRef.js │ ├── hooks.js │ ├── index.d.ts │ ├── index.js │ ├── internal.d.ts │ ├── memo.js │ ├── portals.js │ ├── render.js │ ├── suspense-list.d.ts │ ├── suspense-list.js │ ├── suspense.d.ts │ ├── suspense.js │ └── util.js ├── test-utils.js └── test │ ├── browser │ ├── Children.test.js │ ├── PureComponent.test.js │ ├── cloneElement.test.js │ ├── compat.options.test.js │ ├── component.test.js │ ├── componentDidCatch.test.js │ ├── context.test.js │ ├── createElement.test.js │ ├── createFactory.test.js │ ├── events.test.js │ ├── exports.test.js │ ├── findDOMNode.test.js │ ├── forwardRef.test.js │ ├── hooks.test.js │ ├── hydrate.test.js │ ├── isFragment.test.js │ ├── isMemo.test.js │ ├── isValidElement.test.js │ ├── memo.test.js │ ├── portals.test.js │ ├── render.test.js │ ├── scheduler.test.js │ ├── select.test.js │ ├── suspense-hydration.test.js │ ├── suspense-list.test.js │ ├── suspense-utils.js │ ├── suspense.test.js │ ├── svg.test.js │ ├── testUtils.js │ ├── textarea.test.js │ ├── unmountComponentAtNode.test.js │ ├── unstable_batchedUpdates.test.js │ └── useSyncExternalStore.test.js │ └── ts │ ├── forward-ref.tsx │ ├── index.tsx │ ├── lazy.tsx │ ├── memo.tsx │ ├── react-default.tsx │ ├── react-star.tsx │ ├── scheduler.ts │ ├── suspense.tsx │ ├── tsconfig.json │ └── utils.ts ├── config ├── codemod-const.js ├── codemod-let-name.js ├── codemod-strip-tdz.js ├── compat-entries.js └── node-13-exports.js ├── debug ├── LICENSE ├── mangle.json ├── package.json ├── src │ ├── check-props.js │ ├── component-stack.js │ ├── constants.js │ ├── debug.js │ ├── index.d.ts │ ├── index.js │ ├── internal.d.ts │ └── util.js └── test │ └── browser │ ├── component-stack-2.test.js │ ├── component-stack.test.js │ ├── debug-compat.test.js │ ├── debug-hooks.test.js │ ├── debug-suspense.test.js │ ├── debug.options.test.js │ ├── debug.test.js │ ├── fakeDevTools.js │ ├── prop-types.test.js │ ├── serializeVNode.test.js │ └── validateHookArgs.test.js ├── demo ├── contenteditable.jsx ├── context.jsx ├── devtools.jsx ├── fragments.jsx ├── index.html ├── index.jsx ├── key_bug.jsx ├── list.jsx ├── logger.jsx ├── mobx.jsx ├── nested-suspense │ ├── addnewcomponent.jsx │ ├── component-container.jsx │ ├── dropzone.jsx │ ├── editor.jsx │ ├── index.jsx │ └── subcomponent.jsx ├── old.js.bak ├── package-lock.json ├── package.json ├── people │ ├── Readme.md │ ├── index.tsx │ ├── profile.tsx │ ├── router.tsx │ ├── store.ts │ └── styles │ │ ├── animations.scss │ │ ├── app.scss │ │ ├── avatar.scss │ │ ├── button.scss │ │ ├── index.scss │ │ └── profile.scss ├── preact.jsx ├── profiler.jsx ├── pythagoras │ ├── index.jsx │ └── pythagoras.jsx ├── redux-toolkit.jsx ├── redux.jsx ├── reduxUpdate.jsx ├── reorder.jsx ├── spiral.jsx ├── stateOrderBug.jsx ├── style.css ├── style.scss ├── styled-components.jsx ├── suspense-router │ ├── bye.jsx │ ├── hello.jsx │ ├── index.jsx │ └── simple-router.jsx ├── suspense.jsx ├── textFields.jsx ├── todo.jsx ├── tsconfig.json ├── vite.config.js └── zustand.jsx ├── devtools ├── LICENSE ├── mangle.json ├── package.json ├── src │ ├── devtools.js │ ├── index.d.ts │ └── index.js └── test │ └── browser │ └── addHookName.test.js ├── hooks ├── LICENSE ├── mangle.json ├── package.json ├── src │ ├── index.d.ts │ ├── index.js │ └── internal.d.ts └── test │ ├── _util │ └── useEffectUtil.js │ └── browser │ ├── combinations.test.js │ ├── componentDidCatch.test.js │ ├── errorBoundary.test.js │ ├── hooks.options.test.js │ ├── useCallback.test.js │ ├── useContext.test.js │ ├── useDebugValue.test.js │ ├── useEffect.test.js │ ├── useEffectAssertions.js │ ├── useId.test.js │ ├── useImperativeHandle.test.js │ ├── useLayoutEffect.test.js │ ├── useMemo.test.js │ ├── useReducer.test.js │ ├── useRef.test.js │ └── useState.test.js ├── jsconfig-lint.json ├── jsconfig.json ├── jsx-runtime ├── LICENSE ├── mangle.json ├── package.json ├── src │ ├── index.d.ts │ ├── index.js │ └── utils.js └── test │ └── browser │ └── jsx-runtime.test.js ├── mangle.json ├── oxlint.json ├── package-lock.json ├── package.json ├── scripts └── release │ ├── create-gh-release.js │ ├── publish.mjs │ └── upload-gh-asset.js ├── sizereport.config.js ├── src ├── cjs.js ├── clone-element.js ├── component.js ├── constants.js ├── create-context.js ├── create-element.js ├── diff │ ├── catch-error.js │ ├── children.js │ ├── index.js │ └── props.js ├── index-5.d.ts ├── index.d.ts ├── index.js ├── internal.d.ts ├── jsx.d.ts ├── options.js ├── render.js └── util.js ├── test-utils ├── package.json ├── src │ ├── index.d.ts │ └── index.js └── test │ └── shared │ ├── act.test.js │ └── rerender.test.js ├── test ├── _util │ ├── dom.js │ ├── helpers.js │ ├── logCall.js │ └── optionSpies.js ├── browser │ ├── cloneElement.test.js │ ├── components.test.js │ ├── context.test.js │ ├── createContext.test.js │ ├── customBuiltInElements.test.js │ ├── events.test.js │ ├── focus.test.js │ ├── fragments.test.js │ ├── getDomSibling.test.js │ ├── hydrate.test.js │ ├── isValidElement.test.js │ ├── keys.test.js │ ├── lifecycles │ │ ├── componentDidCatch.test.js │ │ ├── componentDidMount.test.js │ │ ├── componentDidUpdate.test.js │ │ ├── componentWillMount.test.js │ │ ├── componentWillReceiveProps.test.js │ │ ├── componentWillUnmount.test.js │ │ ├── componentWillUpdate.test.js │ │ ├── getDerivedStateFromError.test.js │ │ ├── getDerivedStateFromProps.test.js │ │ ├── getSnapshotBeforeUpdate.test.js │ │ ├── lifecycle.test.js │ │ └── shouldComponentUpdate.test.js │ ├── mathml.test.js │ ├── placeholders.test.js │ ├── refs.test.js │ ├── render.test.js │ ├── replaceNode.test.js │ ├── select.test.js │ ├── spec.test.js │ ├── style.test.js │ ├── svg.test.js │ └── toChildArray.test.js ├── fixtures │ └── preact.js ├── node │ ├── index.test.js │ └── package.json ├── shared │ ├── createContext.test.js │ ├── createElement.test.js │ ├── exports.test.js │ ├── isValidElement.test.js │ ├── isValidElementTests.js │ └── package.json └── ts │ ├── Component-test.tsx │ ├── VNode-test.tsx │ ├── custom-elements.tsx │ ├── dom-attributes-test.tsx │ ├── hoc-test.tsx │ ├── jsx-namespacce-test.tsx │ ├── package.json │ ├── preact-global-test.tsx │ ├── preact.tsx │ ├── refs.tsx │ └── tsconfig.json ├── types └── events.d.ts ├── vitest.config.mjs └── vitest.setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{*.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | benchmarks 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [preactjs] 2 | open_collective: preact 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | - [ ] Check if updating to the latest Preact version resolves the issue 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | If possible, please provide a link to a StackBlitz/CodeSandbox/Codepen project or a GitHub repository that demonstrates the issue. You can use the following template on StackBlitz to get started: https://stackblitz.com/edit/create-preact-starter 17 | 18 | Steps to reproduce the behavior: 19 | 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. See error 23 | 24 | **Expected behavior** 25 | What should have happened when following the steps above? 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Describe the feature you'd love to see** 10 | A clear and concise description of what you'd love to see added to Preact. 11 | 12 | **Additional context (optional)** 13 | Add any other context or screenshots about the feature request here. 14 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | 7 | jobs: 8 | prepare: 9 | name: Prepare environment 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | steps: 13 | - name: Download locally built preact package 14 | uses: actions/download-artifact@v4 15 | with: 16 | name: npm-package 17 | - run: mv preact.tgz preact-local.tgz 18 | - name: Download base package 19 | uses: andrewiggins/download-base-artifact@v3 20 | with: 21 | artifact: npm-package 22 | workflow: ci.yml 23 | required: true 24 | - run: mv preact.tgz preact-main.tgz 25 | - name: Upload locally build & base preact package 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: bench-environment 29 | path: | 30 | preact-local.tgz 31 | preact-main.tgz 32 | 33 | bench_todo: 34 | name: Bench todo 35 | uses: ./.github/workflows/run-bench.yml 36 | needs: prepare 37 | with: 38 | benchmark: todo/todo 39 | timeout: 10 40 | 41 | bench_text_update: 42 | name: Bench text-update 43 | uses: ./.github/workflows/run-bench.yml 44 | needs: prepare 45 | with: 46 | benchmark: text-update/text-update 47 | timeout: 10 48 | 49 | bench_many_updates: 50 | name: Bench many-updates 51 | uses: ./.github/workflows/run-bench.yml 52 | needs: prepare 53 | with: 54 | benchmark: many-updates/many-updates 55 | timeout: 10 56 | 57 | bench_replace1k: 58 | name: Bench replace1k 59 | uses: ./.github/workflows/run-bench.yml 60 | needs: prepare 61 | with: 62 | benchmark: table-app/replace1k 63 | 64 | bench_update10th1k: 65 | name: Bench 03_update10th1k_x16 66 | uses: ./.github/workflows/run-bench.yml 67 | needs: prepare 68 | with: 69 | benchmark: table-app/update10th1k 70 | 71 | bench_create10k: 72 | name: Bench create10k 73 | uses: ./.github/workflows/run-bench.yml 74 | needs: prepare 75 | with: 76 | benchmark: table-app/create10k 77 | 78 | bench_hydrate1k: 79 | name: Bench hydrate1k 80 | uses: ./.github/workflows/run-bench.yml 81 | needs: prepare 82 | with: 83 | benchmark: table-app/hydrate1k 84 | 85 | bench_filter_list: 86 | name: Bench filter-list 87 | uses: ./.github/workflows/run-bench.yml 88 | needs: prepare 89 | with: 90 | benchmark: filter-list/filter-list 91 | timeout: 10 -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | inputs: 7 | ref: 8 | description: 'Branch or tag ref to check out' 9 | type: string 10 | required: false 11 | default: '' 12 | artifact_name: 13 | description: 'Name of the artifact to upload' 14 | type: string 15 | required: false 16 | default: 'npm-package' 17 | 18 | jobs: 19 | build_test: 20 | name: Build & Test 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | ref: ${{ inputs.ref || '' }} 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version-file: 'package.json' 29 | cache: 'npm' 30 | cache-dependency-path: '**/package-lock.json' 31 | - run: npm ci 32 | - name: test 33 | env: 34 | CI: true 35 | COVERAGE: true 36 | FLAKEY: false 37 | # Not using `npm test` since it rebuilds source which npm ci has already done 38 | run: npm run lint && npm run test:unit 39 | - name: Coveralls GitHub Action 40 | uses: coverallsapp/github-action@v2.3.0 41 | timeout-minutes: 2 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | fail-on-error: false 45 | - name: Package 46 | # Use --ignore-scripts here to avoid re-building again before pack 47 | run: | 48 | npm pack --ignore-scripts 49 | mv preact-*.tgz preact.tgz 50 | - name: Upload npm package 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: ${{ inputs.artifact_name || 'npm-package' }} 54 | path: preact.tgz 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - '**' 8 | push: 9 | branches: 10 | - main 11 | - restructure 12 | - v11 13 | 14 | jobs: 15 | filter_jobs: 16 | name: Filter jobs 17 | runs-on: ubuntu-latest 18 | outputs: 19 | jsChanged: ${{ steps.filter.outputs.jsChanged }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: dorny/paths-filter@v3 23 | id: filter 24 | with: 25 | # Should be kept in sync with the filter in the PR Reporter workflow 26 | predicate-quantifier: 'every' 27 | filters: | 28 | jsChanged: 29 | - '**/src/**/*.js' 30 | - '!devtools/src/devtools.js' 31 | 32 | compressed_size: 33 | name: Compressed Size 34 | needs: filter_jobs 35 | if: ${{ needs.filter_jobs.outputs.jsChanged == 'true' }} 36 | uses: ./.github/workflows/size.yml 37 | 38 | build_test: 39 | name: Build & Test 40 | needs: filter_jobs 41 | uses: ./.github/workflows/build-test.yml 42 | 43 | benchmarks: 44 | name: Benchmarks 45 | needs: build_test 46 | if: ${{ needs.filter_jobs.outputs.jsChanged == 'true' }} 47 | uses: ./.github/workflows/benchmarks.yml -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: create 3 | 4 | jobs: 5 | build: 6 | if: github.ref_type == 'tag' 7 | uses: preactjs/preact/.github/workflows/build-test.yml@main 8 | 9 | release: 10 | runs-on: ubuntu-latest 11 | needs: build 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/download-artifact@v4 15 | with: 16 | name: npm-package 17 | - name: Create draft release 18 | id: create-release 19 | uses: actions/github-script@v6 20 | with: 21 | script: | 22 | const script = require('./scripts/release/create-gh-release.js') 23 | return script({ github, context }) 24 | - name: Upload release artifact 25 | uses: actions/github-script@v6 26 | with: 27 | script: | 28 | const script = require('./scripts/release/upload-gh-asset.js') 29 | return script({ require, github, context, glob, release: ${{ steps.create-release.outputs.result }} }) -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version-file: 'package.json' 14 | cache: 'npm' 15 | cache-dependency-path: '**/package-lock.json' 16 | - uses: preactjs/compressed-size-action@v2 17 | with: 18 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 19 | # Our `prepare` script already builds the app post-install, 20 | # building it again would be redundant 21 | build-script: 'npm run --if-present noop' -------------------------------------------------------------------------------- /.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 | preact.tgz 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "benchmarks"] 2 | path = benchmarks 3 | url = https://github.com/preactjs/benchmarks 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx nano-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 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 | preact.tgz 16 | 17 | package-lock.json 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | 4 | const noModules = String(process.env.BABEL_NO_MODULES) === 'true'; 5 | 6 | const rename = {}; 7 | const mangle = require('./mangle.json'); 8 | for (let prop in mangle.props.props) { 9 | let name = prop; 10 | if (name[0] === '$') { 11 | name = name.slice(1); 12 | } 13 | 14 | rename[name] = mangle.props.props[prop]; 15 | } 16 | 17 | return { 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | loose: true, 23 | // Don't transform modules when using esbuild 24 | modules: noModules ? false : 'auto', 25 | exclude: ['@babel/plugin-transform-typeof-symbol'], 26 | targets: { 27 | browsers: ['last 2 versions', 'IE >= 9'] 28 | } 29 | } 30 | ] 31 | ], 32 | plugins: [ 33 | '@babel/plugin-transform-react-jsx', 34 | ['babel-plugin-transform-rename-properties', { rename }] 35 | ], 36 | include: ['**/src/**/*.js', '**/test/**/*.js'], 37 | overrides: [ 38 | { 39 | test: /(component-stack|debug)\.test\.js$/, 40 | plugins: ['@babel/plugin-transform-react-jsx-source'] 41 | } 42 | ] 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatter": { 3 | "enabled": true, 4 | "formatWithErrors": false, 5 | "indentStyle": "tab", 6 | "indentWidth": 2, 7 | "lineEnding": "lf", 8 | "lineWidth": 80, 9 | "attributePosition": "auto", 10 | "ignore": [ 11 | "**/.DS_Store", 12 | "**/node_modules", 13 | "**/npm-debug.log", 14 | "**/dist", 15 | "*/package-lock.json", 16 | "**/yarn.lock", 17 | "**/.vscode", 18 | "**/.idea", 19 | "test/ts/**/*.js", 20 | "**/coverage", 21 | "**/*.sw[op]", 22 | "**/*.log", 23 | "**/package/", 24 | "**/preact-*.tgz", 25 | "**/preact.tgz", 26 | "**/package-lock.json" 27 | ] 28 | }, 29 | "organizeImports": { "enabled": true }, 30 | "linter": { "enabled": true, "rules": { "recommended": true } }, 31 | "javascript": { 32 | "formatter": { 33 | "jsxQuoteStyle": "double", 34 | "quoteProperties": "asNeeded", 35 | "trailingCommas": "none", 36 | "semicolons": "always", 37 | "arrowParentheses": "asNeeded", 38 | "bracketSpacing": true, 39 | "bracketSameLine": false, 40 | "quoteStyle": "single", 41 | "attributePosition": "auto" 42 | } 43 | }, 44 | "overrides": [ 45 | { 46 | "include": ["*.json", ".*rc", "*.yml"], 47 | "formatter": { "indentWidth": 2, "indentStyle": "space" } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /compat/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /compat/client.d.ts: -------------------------------------------------------------------------------- 1 | // Intentionally not using a relative path to take advantage of 2 | // the TS version resolution mechanism 3 | import * as preact from 'preact'; 4 | 5 | export function createRoot(container: preact.ContainerNode): { 6 | render(children: preact.ComponentChild): void; 7 | unmount(): void; 8 | }; 9 | 10 | export function hydrateRoot( 11 | container: preact.ContainerNode, 12 | children: preact.ComponentChild 13 | ): ReturnType; 14 | -------------------------------------------------------------------------------- /compat/client.js: -------------------------------------------------------------------------------- 1 | const { render, hydrate, unmountComponentAtNode } = require('preact/compat'); 2 | 3 | function createRoot(container) { 4 | return { 5 | // eslint-disable-next-line 6 | render: function (children) { 7 | render(children, container); 8 | }, 9 | // eslint-disable-next-line 10 | unmount: function () { 11 | unmountComponentAtNode(container); 12 | } 13 | }; 14 | } 15 | 16 | exports.createRoot = createRoot; 17 | 18 | exports.hydrateRoot = function (container, children) { 19 | hydrate(children, container); 20 | return createRoot(container); 21 | }; 22 | -------------------------------------------------------------------------------- /compat/client.mjs: -------------------------------------------------------------------------------- 1 | import { render, hydrate, unmountComponentAtNode } from 'preact/compat'; 2 | 3 | export function createRoot(container) { 4 | return { 5 | // eslint-disable-next-line 6 | render: function (children) { 7 | render(children, container); 8 | }, 9 | // eslint-disable-next-line 10 | unmount: function () { 11 | unmountComponentAtNode(container); 12 | } 13 | }; 14 | } 15 | 16 | export function hydrateRoot(container, children) { 17 | hydrate(children, container); 18 | return createRoot(container); 19 | } 20 | 21 | export default { 22 | createRoot, 23 | hydrateRoot 24 | }; 25 | -------------------------------------------------------------------------------- /compat/jsx-dev-runtime.js: -------------------------------------------------------------------------------- 1 | require('preact/compat'); 2 | 3 | module.exports = require('preact/jsx-runtime'); 4 | -------------------------------------------------------------------------------- /compat/jsx-dev-runtime.mjs: -------------------------------------------------------------------------------- 1 | import 'preact/compat'; 2 | 3 | export * from 'preact/jsx-runtime'; 4 | -------------------------------------------------------------------------------- /compat/jsx-runtime.js: -------------------------------------------------------------------------------- 1 | require('preact/compat'); 2 | 3 | module.exports = require('preact/jsx-runtime'); 4 | -------------------------------------------------------------------------------- /compat/jsx-runtime.mjs: -------------------------------------------------------------------------------- 1 | import 'preact/compat'; 2 | 3 | export * from 'preact/jsx-runtime'; 4 | -------------------------------------------------------------------------------- /compat/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /compat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-compat", 3 | "amdName": "preactCompat", 4 | "version": "4.0.0", 5 | "private": true, 6 | "description": "A React compatibility layer for Preact", 7 | "main": "dist/compat.js", 8 | "module": "dist/compat.module.js", 9 | "umd:main": "dist/compat.umd.js", 10 | "source": "src/index.js", 11 | "types": "src/index.d.ts", 12 | "license": "MIT", 13 | "mangle": { 14 | "regex": "^_" 15 | }, 16 | "peerDependencies": { 17 | "preact": "^10.0.0" 18 | }, 19 | "exports": { 20 | ".": { 21 | "types": "./src/index.d.ts", 22 | "browser": "./dist/compat.module.js", 23 | "umd": "./dist/compat.umd.js", 24 | "import": "./dist/compat.mjs", 25 | "require": "./dist/compat.js" 26 | }, 27 | "./client": { 28 | "types": "./client.d.ts", 29 | "import": "./client.mjs", 30 | "require": "./client.js" 31 | }, 32 | "./server": { 33 | "browser": "./server.browser.js", 34 | "import": "./server.mjs", 35 | "require": "./server.js" 36 | }, 37 | "./jsx-runtime": { 38 | "import": "./jsx-runtime.mjs", 39 | "require": "./jsx-runtime.js" 40 | }, 41 | "./jsx-dev-runtime": { 42 | "import": "./jsx-dev-runtime.mjs", 43 | "require": "./jsx-dev-runtime.js" 44 | }, 45 | "./scheduler": { 46 | "import": "./scheduler.mjs", 47 | "require": "./scheduler.js" 48 | }, 49 | "./package.json": "./package.json" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /compat/scheduler.js: -------------------------------------------------------------------------------- 1 | // see scheduler.mjs 2 | 3 | function unstable_runWithPriority(priority, callback) { 4 | return callback(); 5 | } 6 | 7 | module.exports = { 8 | unstable_ImmediatePriority: 1, 9 | unstable_UserBlockingPriority: 2, 10 | unstable_NormalPriority: 3, 11 | unstable_LowPriority: 4, 12 | unstable_IdlePriority: 5, 13 | unstable_runWithPriority, 14 | unstable_now: performance.now.bind(performance) 15 | }; 16 | -------------------------------------------------------------------------------- /compat/scheduler.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // This file includes experimental React APIs exported from the "scheduler" 4 | // npm package. Despite being explicitely marked as unstable some libraries 5 | // already make use of them. This file is not a full replacement for the 6 | // scheduler package, but includes the necessary shims to make those libraries 7 | // work with Preact. 8 | 9 | export var unstable_ImmediatePriority = 1; 10 | export var unstable_UserBlockingPriority = 2; 11 | export var unstable_NormalPriority = 3; 12 | export var unstable_LowPriority = 4; 13 | export var unstable_IdlePriority = 5; 14 | 15 | /** 16 | * @param {number} priority 17 | * @param {() => void} callback 18 | */ 19 | export function unstable_runWithPriority(priority, callback) { 20 | return callback(); 21 | } 22 | 23 | export var unstable_now = performance.now.bind(performance); 24 | -------------------------------------------------------------------------------- /compat/server.browser.js: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'preact-render-to-string'; 2 | 3 | export { 4 | renderToString, 5 | renderToString as renderToStaticMarkup 6 | } from 'preact-render-to-string'; 7 | 8 | export default { 9 | renderToString, 10 | renderToStaticMarkup: renderToString 11 | }; 12 | -------------------------------------------------------------------------------- /compat/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var renderToString; 3 | try { 4 | const mod = require('preact-render-to-string'); 5 | renderToString = mod.default || mod.renderToString || mod; 6 | } catch (e) { 7 | throw Error( 8 | 'renderToString() error: missing "preact-render-to-string" dependency.' 9 | ); 10 | } 11 | 12 | var renderToReadableStream; 13 | try { 14 | const mod = require('preact-render-to-string/stream'); 15 | renderToReadableStream = mod.default || mod.renderToReadableStream || mod; 16 | } catch (e) { 17 | throw Error( 18 | 'renderToReadableStream() error: update "preact-render-to-string" dependency to at least 6.5.0.' 19 | ); 20 | } 21 | var renderToPipeableStream; 22 | try { 23 | const mod = require('preact-render-to-string/stream-node'); 24 | renderToPipeableStream = mod.default || mod.renderToPipeableStream || mod; 25 | } catch (e) { 26 | throw Error( 27 | 'renderToPipeableStream() error: update "preact-render-to-string" dependency to at least 6.5.0.' 28 | ); 29 | } 30 | 31 | module.exports = { 32 | renderToString: renderToString, 33 | renderToStaticMarkup: renderToString, 34 | renderToPipeableStream: renderToPipeableStream, 35 | renderToReadableStream: renderToReadableStream 36 | }; 37 | -------------------------------------------------------------------------------- /compat/server.mjs: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'preact-render-to-string'; 2 | import { renderToPipeableStream } from 'preact-render-to-string/stream-node'; 3 | import { renderToReadableStream } from 'preact-render-to-string/stream'; 4 | 5 | export { 6 | renderToString, 7 | renderToString as renderToStaticMarkup 8 | } from 'preact-render-to-string'; 9 | 10 | export { renderToPipeableStream } from 'preact-render-to-string/stream-node'; 11 | export { renderToReadableStream } from 'preact-render-to-string/stream'; 12 | export default { 13 | renderToString, 14 | renderToStaticMarkup: renderToString, 15 | renderToPipeableStream, 16 | renderToReadableStream 17 | }; 18 | -------------------------------------------------------------------------------- /compat/src/Children.js: -------------------------------------------------------------------------------- 1 | import { toChildArray } from 'preact'; 2 | 3 | const mapFn = (children, fn) => { 4 | if (children == null) return null; 5 | return toChildArray(toChildArray(children).map(fn)); 6 | }; 7 | 8 | // This API is completely unnecessary for Preact, so it's basically passthrough. 9 | export const Children = { 10 | map: mapFn, 11 | forEach: mapFn, 12 | count(children) { 13 | return children ? toChildArray(children).length : 0; 14 | }, 15 | only(children) { 16 | const normalized = toChildArray(children); 17 | if (normalized.length !== 1) throw 'Children.only'; 18 | return normalized[0]; 19 | }, 20 | toArray: toChildArray 21 | }; 22 | -------------------------------------------------------------------------------- /compat/src/PureComponent.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | import { shallowDiffers } from './util'; 3 | 4 | /** 5 | * Component class with a predefined `shouldComponentUpdate` implementation 6 | */ 7 | export function PureComponent(p, c) { 8 | this.props = p; 9 | this.context = c; 10 | } 11 | PureComponent.prototype = new Component(); 12 | // Some third-party libraries check if this property is present 13 | PureComponent.prototype.isPureReactComponent = true; 14 | PureComponent.prototype.shouldComponentUpdate = function (props, state) { 15 | return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); 16 | }; 17 | -------------------------------------------------------------------------------- /compat/src/forwardRef.js: -------------------------------------------------------------------------------- 1 | import { options } from 'preact'; 2 | import { assign } from './util'; 3 | 4 | let oldDiffHook = options._diff; 5 | options._diff = vnode => { 6 | if (vnode.type && vnode.type._forwarded && vnode.ref) { 7 | vnode.props.ref = vnode.ref; 8 | vnode.ref = null; 9 | } 10 | if (oldDiffHook) oldDiffHook(vnode); 11 | }; 12 | 13 | export const REACT_FORWARD_SYMBOL = 14 | (typeof Symbol != 'undefined' && 15 | Symbol.for && 16 | Symbol.for('react.forward_ref')) || 17 | 0xf47; 18 | 19 | /** 20 | * Pass ref down to a child. This is mainly used in libraries with HOCs that 21 | * wrap components. Using `forwardRef` there is an easy way to get a reference 22 | * of the wrapped component instead of one of the wrapper itself. 23 | * @param {import('./index').ForwardFn} fn 24 | * @returns {import('./internal').FunctionComponent} 25 | */ 26 | export function forwardRef(fn) { 27 | function Forwarded(props) { 28 | let clone = assign({}, props); 29 | delete clone.ref; 30 | return fn(clone, props.ref || null); 31 | } 32 | 33 | // mobx-react checks for this being present 34 | Forwarded.$$typeof = REACT_FORWARD_SYMBOL; 35 | // mobx-react heavily relies on implementation details. 36 | // It expects an object here with a `render` property, 37 | // and prototype.render will fail. Without this 38 | // mobx-react throws. 39 | Forwarded.render = Forwarded; 40 | 41 | Forwarded.prototype.isReactComponent = Forwarded._forwarded = true; 42 | Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; 43 | return Forwarded; 44 | } 45 | -------------------------------------------------------------------------------- /compat/src/hooks.js: -------------------------------------------------------------------------------- 1 | import { useState, useLayoutEffect, useEffect } from 'preact/hooks'; 2 | import { is } from './util'; 3 | 4 | /** 5 | * This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84 6 | * on a high level this cuts out the warnings, ... and attempts a smaller implementation 7 | * @typedef {{ _value: any; _getSnapshot: () => any }} Store 8 | */ 9 | export function useSyncExternalStore(subscribe, getSnapshot) { 10 | const value = getSnapshot(); 11 | 12 | /** 13 | * @typedef {{ _instance: Store }} StoreRef 14 | * @type {[StoreRef, (store: StoreRef) => void]} 15 | */ 16 | const [{ _instance }, forceUpdate] = useState({ 17 | _instance: { _value: value, _getSnapshot: getSnapshot } 18 | }); 19 | 20 | useLayoutEffect(() => { 21 | _instance._value = value; 22 | _instance._getSnapshot = getSnapshot; 23 | 24 | if (didSnapshotChange(_instance)) { 25 | forceUpdate({ _instance }); 26 | } 27 | }, [subscribe, value, getSnapshot]); 28 | 29 | useEffect(() => { 30 | if (didSnapshotChange(_instance)) { 31 | forceUpdate({ _instance }); 32 | } 33 | 34 | return subscribe(() => { 35 | if (didSnapshotChange(_instance)) { 36 | forceUpdate({ _instance }); 37 | } 38 | }); 39 | }, [subscribe]); 40 | 41 | return value; 42 | } 43 | 44 | /** @type {(inst: Store) => boolean} */ 45 | function didSnapshotChange(inst) { 46 | const latestGetSnapshot = inst._getSnapshot; 47 | const prevValue = inst._value; 48 | try { 49 | const nextValue = latestGetSnapshot(); 50 | return !is(prevValue, nextValue); 51 | } catch (error) { 52 | return true; 53 | } 54 | } 55 | 56 | export function startTransition(cb) { 57 | cb(); 58 | } 59 | 60 | export function useDeferredValue(val) { 61 | return val; 62 | } 63 | 64 | export function useTransition() { 65 | return [false, startTransition]; 66 | } 67 | 68 | // TODO: in theory this should be done after a VNode is diffed as we want to insert 69 | // styles/... before it attaches 70 | export const useInsertionEffect = useLayoutEffect; 71 | -------------------------------------------------------------------------------- /compat/src/internal.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component as PreactComponent, 3 | VNode as PreactVNode, 4 | FunctionComponent as PreactFunctionComponent, 5 | PreactElement 6 | } from '../../src/internal'; 7 | import { SuspenseProps } from './suspense'; 8 | 9 | export { ComponentChildren } from '../..'; 10 | 11 | export { PreactElement }; 12 | 13 | export interface Component

extends PreactComponent { 14 | isReactComponent?: object; 15 | isPureReactComponent?: true; 16 | _patchedLifecycles?: true; 17 | 18 | // Suspense internal properties 19 | _childDidSuspend?(error: Promise, suspendingVNode: VNode): void; 20 | _suspended: (vnode: VNode) => (unsuspend: () => void) => void; 21 | _onResolve?(): void; 22 | 23 | // Portal internal properties 24 | _temp: any; 25 | _container: PreactElement; 26 | } 27 | 28 | export interface FunctionComponent

extends PreactFunctionComponent

{ 29 | shouldComponentUpdate?(nextProps: Readonly

): boolean; 30 | _forwarded?: boolean; 31 | _patchedLifecycles?: true; 32 | } 33 | 34 | export interface VNode extends PreactVNode { 35 | $$typeof?: symbol | string; 36 | preactCompatNormalized?: boolean; 37 | } 38 | 39 | export interface SuspenseState { 40 | _suspended?: null | VNode; 41 | } 42 | 43 | export interface SuspenseComponent 44 | extends PreactComponent { 45 | _pendingSuspensionCount: number; 46 | _suspenders: Component[]; 47 | _detachOnNextRender: null | VNode; 48 | } 49 | -------------------------------------------------------------------------------- /compat/src/memo.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import { shallowDiffers } from './util'; 3 | 4 | /** 5 | * Memoize a component, so that it only updates when the props actually have 6 | * changed. This was previously known as `React.pure`. 7 | * @param {import('./internal').FunctionComponent} c functional component 8 | * @param {(prev: object, next: object) => boolean} [comparer] Custom equality function 9 | * @returns {import('./internal').FunctionComponent} 10 | */ 11 | export function memo(c, comparer) { 12 | function shouldUpdate(nextProps) { 13 | let ref = this.props.ref; 14 | let updateRef = ref == nextProps.ref; 15 | if (!updateRef && ref) { 16 | ref.call ? ref(null) : (ref.current = null); 17 | } 18 | 19 | if (!comparer) { 20 | return shallowDiffers(this.props, nextProps); 21 | } 22 | 23 | return !comparer(this.props, nextProps) || !updateRef; 24 | } 25 | 26 | function Memoed(props) { 27 | this.shouldComponentUpdate = shouldUpdate; 28 | return createElement(c, props); 29 | } 30 | Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; 31 | Memoed.prototype.isReactComponent = true; 32 | Memoed._forwarded = true; 33 | return Memoed; 34 | } 35 | -------------------------------------------------------------------------------- /compat/src/portals.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | 3 | /** 4 | * @param {import('../../src/index').RenderableProps<{ context: any }>} props 5 | */ 6 | function ContextProvider(props) { 7 | this.getChildContext = () => props.context; 8 | return props.children; 9 | } 10 | 11 | /** 12 | * Portal component 13 | * @this {import('./internal').Component} 14 | * @param {object | null | undefined} props 15 | * 16 | * TODO: use createRoot() instead of fake root 17 | */ 18 | function Portal(props) { 19 | const _this = this; 20 | let container = props._container; 21 | 22 | _this.componentWillUnmount = function () { 23 | render(null, _this._temp); 24 | _this._temp = null; 25 | _this._container = null; 26 | }; 27 | 28 | // When we change container we should clear our old container and 29 | // indicate a new mount. 30 | if (_this._container && _this._container !== container) { 31 | _this.componentWillUnmount(); 32 | } 33 | 34 | if (!_this._temp) { 35 | // Ensure the element has a mask for useId invocations 36 | let root = _this._vnode; 37 | while (root !== null && !root._mask && root._parent !== null) { 38 | root = root._parent; 39 | } 40 | 41 | _this._container = container; 42 | 43 | // Create a fake DOM parent node that manages a subset of `container`'s children: 44 | _this._temp = { 45 | nodeType: 1, 46 | parentNode: container, 47 | childNodes: [], 48 | _children: { _mask: root._mask }, 49 | contains: () => true, 50 | insertBefore(child, before) { 51 | this.childNodes.push(child); 52 | _this._container.insertBefore(child, before); 53 | }, 54 | removeChild(child) { 55 | this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1); 56 | _this._container.removeChild(child); 57 | } 58 | }; 59 | } 60 | 61 | // Render our wrapping element into temp. 62 | render( 63 | createElement(ContextProvider, { context: _this.context }, props._vnode), 64 | _this._temp 65 | ); 66 | } 67 | 68 | /** 69 | * Create a `Portal` to continue rendering the vnode tree at a different DOM node 70 | * @param {import('./internal').VNode} vnode The vnode to render 71 | * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to. 72 | */ 73 | export function createPortal(vnode, container) { 74 | const el = createElement(Portal, { _vnode: vnode, _container: container }); 75 | el.containerInfo = container; 76 | return el; 77 | } 78 | -------------------------------------------------------------------------------- /compat/src/suspense-list.d.ts: -------------------------------------------------------------------------------- 1 | // Intentionally not using a relative path to take advantage of 2 | // the TS version resolution mechanism 3 | import { Component, ComponentChild, ComponentChildren } from 'preact'; 4 | 5 | // 6 | // SuspenseList 7 | // ----------------------------------- 8 | 9 | export interface SuspenseListProps { 10 | children?: ComponentChildren; 11 | revealOrder?: 'forwards' | 'backwards' | 'together'; 12 | } 13 | 14 | export class SuspenseList extends Component { 15 | render(): ComponentChild; 16 | } 17 | -------------------------------------------------------------------------------- /compat/src/suspense.d.ts: -------------------------------------------------------------------------------- 1 | // Intentionally not using a relative path to take advantage of 2 | // the TS version resolution mechanism 3 | import { Component, ComponentChild, ComponentChildren } from 'preact'; 4 | 5 | // 6 | // Suspense/lazy 7 | // ----------------------------------- 8 | export function lazy( 9 | loader: () => Promise<{ default: T } | T> 10 | ): T extends { default: infer U } ? U : T; 11 | 12 | export interface SuspenseProps { 13 | children?: ComponentChildren; 14 | fallback: ComponentChildren; 15 | } 16 | 17 | export class Suspense extends Component { 18 | render(): ComponentChild; 19 | } 20 | -------------------------------------------------------------------------------- /compat/src/util.js: -------------------------------------------------------------------------------- 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, props) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ (obj); 11 | } 12 | 13 | /** 14 | * Check if two objects have a different shape 15 | * @param {object} a 16 | * @param {object} b 17 | * @returns {boolean} 18 | */ 19 | export function shallowDiffers(a, b) { 20 | for (let i in a) if (i !== '__source' && !(i in b)) return true; 21 | for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true; 22 | return false; 23 | } 24 | 25 | /** 26 | * Check if two values are the same value 27 | * @param {*} x 28 | * @param {*} y 29 | * @returns {boolean} 30 | */ 31 | export function is(x, y) { 32 | return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); 33 | } 34 | -------------------------------------------------------------------------------- /compat/test-utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('preact/test-utils'); 2 | -------------------------------------------------------------------------------- /compat/test/browser/compat.options.test.js: -------------------------------------------------------------------------------- 1 | import { vnodeSpy, eventSpy } from '../../../test/_util/optionSpies'; 2 | import React, { 3 | createElement, 4 | render, 5 | Component, 6 | createRef 7 | } from 'preact/compat'; 8 | import { setupRerender } from 'preact/test-utils'; 9 | import { 10 | setupScratch, 11 | teardown, 12 | createEvent 13 | } from '../../../test/_util/helpers'; 14 | 15 | describe('compat options', () => { 16 | /** @type {HTMLDivElement} */ 17 | let scratch; 18 | 19 | /** @type {() => void} */ 20 | let rerender; 21 | 22 | /** @type {() => void} */ 23 | let increment; 24 | 25 | /** @type {import('../../src/index').PropRef} */ 26 | let buttonRef; 27 | 28 | beforeEach(() => { 29 | scratch = setupScratch(); 30 | rerender = setupRerender(); 31 | 32 | vnodeSpy.resetHistory(); 33 | eventSpy.resetHistory(); 34 | 35 | buttonRef = createRef(); 36 | }); 37 | 38 | afterEach(() => { 39 | teardown(scratch); 40 | }); 41 | 42 | class ClassApp extends Component { 43 | constructor() { 44 | super(); 45 | this.state = { count: 0 }; 46 | increment = () => 47 | this.setState(({ count }) => ({ 48 | count: count + 1 49 | })); 50 | } 51 | 52 | render() { 53 | return ( 54 | 57 | ); 58 | } 59 | } 60 | 61 | it('should call old options on mount', () => { 62 | render(, scratch); 63 | 64 | expect(vnodeSpy).to.have.been.called; 65 | }); 66 | 67 | it('should call old options on event and update', () => { 68 | render(, scratch); 69 | expect(scratch.innerHTML).to.equal(''); 70 | 71 | buttonRef.current.dispatchEvent(createEvent('click')); 72 | rerender(); 73 | expect(scratch.innerHTML).to.equal(''); 74 | 75 | expect(vnodeSpy).to.have.been.called; 76 | expect(eventSpy).to.have.been.called; 77 | }); 78 | 79 | it('should call old options on unmount', () => { 80 | render(, scratch); 81 | render(null, scratch); 82 | 83 | expect(vnodeSpy).to.have.been.called; 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /compat/test/browser/componentDidCatch.test.js: -------------------------------------------------------------------------------- 1 | import React, { render, Component } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { act } from 'preact/test-utils'; 4 | 5 | describe('componentDidCatch', () => { 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should pass errorInfo in compat', () => { 17 | let info; 18 | let update; 19 | class Receiver extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { error: null, i: 0 }; 23 | update = this.setState.bind(this); 24 | } 25 | componentDidCatch(error, errorInfo) { 26 | info = errorInfo; 27 | this.setState({ error }); 28 | } 29 | render() { 30 | if (this.state.error) return

; 31 | // @ts-expect-error 32 | if (this.state.i === 0) return ; 33 | return null; 34 | } 35 | } 36 | 37 | function ThrowErr() { 38 | throw new Error('fail'); 39 | } 40 | 41 | act(() => { 42 | render(, scratch); 43 | }); 44 | 45 | act(() => { 46 | update({ i: 1 }); 47 | }); 48 | 49 | expect(info).to.deep.equal({}); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /compat/test/browser/createElement.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, render } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { getSymbol } from './testUtils'; 4 | 5 | describe('compat createElement()', () => { 6 | /** @type {HTMLDivElement} */ 7 | let scratch; 8 | 9 | beforeEach(() => { 10 | scratch = setupScratch(); 11 | }); 12 | 13 | afterEach(() => { 14 | teardown(scratch); 15 | }); 16 | 17 | it('should normalize vnodes', () => { 18 | let vnode = ( 19 |
20 | t 21 |
22 | ); 23 | 24 | const $$typeof = getSymbol('react.element', 0xeac7); 25 | expect(vnode).to.have.property('$$typeof', $$typeof); 26 | expect(vnode).to.have.property('type', 'div'); 27 | expect(vnode).to.have.property('props').that.is.an('object'); 28 | expect(vnode.props).to.have.property('children'); 29 | expect(vnode.props.children).to.have.property('$$typeof', $$typeof); 30 | expect(vnode.props.children).to.have.property('type', 'a'); 31 | expect(vnode.props.children).to.have.property('props').that.is.an('object'); 32 | expect(vnode.props.children.props).to.eql({ children: 't' }); 33 | }); 34 | 35 | it('should not normalize text nodes', () => { 36 | // @ts-expect-error 37 | String.prototype.capFLetter = function () { 38 | return this.charAt(0).toUpperCase() + this.slice(1); 39 | }; 40 | let vnode =
hi buddy
; 41 | 42 | render(vnode, scratch); 43 | 44 | expect(scratch.innerHTML).to.equal('
hi buddy
'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /compat/test/browser/createFactory.test.js: -------------------------------------------------------------------------------- 1 | import React, { render, createElement, createFactory } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('createFactory', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should create a DOM element', () => { 17 | render(createFactory('span')({ class: 'foo' }, '1'), scratch); 18 | expect(scratch.innerHTML).to.equal('1'); 19 | }); 20 | 21 | it('should create a component', () => { 22 | const Foo = ({ id, children }) =>
foo {children}
; 23 | render(createFactory(Foo)({ id: 'value' }, 'bar'), scratch); 24 | expect(scratch.innerHTML).to.equal('
foo bar
'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /compat/test/browser/findDOMNode.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, findDOMNode } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('findDOMNode()', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | class Helper extends React.Component { 9 | render({ something }) { 10 | if (something == null) return null; 11 | if (something === false) return null; 12 | return
; 13 | } 14 | } 15 | 16 | beforeEach(() => { 17 | scratch = setupScratch(); 18 | }); 19 | 20 | afterEach(() => { 21 | teardown(scratch); 22 | }); 23 | 24 | it.skip('should return DOM Node if render is not false nor null', () => { 25 | const helper = React.render(, scratch); 26 | expect(findDOMNode(helper)).to.be.instanceof(Node); 27 | }); 28 | 29 | it('should return null if given null', () => { 30 | expect(findDOMNode(null)).to.be.null; 31 | }); 32 | 33 | it('should return a regular DOM Element if given a regular DOM Element', () => { 34 | let scratch = document.createElement('div'); 35 | expect(findDOMNode(scratch)).to.equalNode(scratch); 36 | }); 37 | 38 | // NOTE: React.render() returning false or null has the component pointing 39 | // to no DOM Node, in contrast, Preact always render an empty Text DOM Node. 40 | it('should return null if render returns false', () => { 41 | const helper = React.render(, scratch); 42 | expect(findDOMNode(helper)).to.be.null; 43 | }); 44 | 45 | // NOTE: React.render() returning false or null has the component pointing 46 | // to no DOM Node, in contrast, Preact always render an empty Text DOM Node. 47 | it('should return null if render returns null', () => { 48 | const helper = React.render(, scratch); 49 | expect(findDOMNode(helper)).to.be.null; 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /compat/test/browser/hydrate.test.js: -------------------------------------------------------------------------------- 1 | import React, { hydrate } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('compat hydrate', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should render react-style jsx', () => { 17 | const input = document.createElement('input'); 18 | scratch.appendChild(input); 19 | input.focus(); 20 | expect(document.activeElement).to.equalNode(input); 21 | 22 | hydrate(, scratch); 23 | expect(document.activeElement).to.equalNode(input); 24 | }); 25 | 26 | it('should call the callback', () => { 27 | scratch.innerHTML = '
'; 28 | 29 | let spy = sinon.spy(); 30 | hydrate(
, scratch, spy); 31 | expect(spy).to.be.calledOnce; 32 | expect(spy).to.be.calledWithExactly(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /compat/test/browser/isFragment.test.js: -------------------------------------------------------------------------------- 1 | import { createElement as preactCreateElement, Fragment } from 'preact'; 2 | import React, { isFragment } from 'preact/compat'; 3 | 4 | describe('isFragment', () => { 5 | it('should check return false for invalid arguments', () => { 6 | expect(isFragment(null)).to.equal(false); 7 | expect(isFragment(false)).to.equal(false); 8 | expect(isFragment(true)).to.equal(false); 9 | expect(isFragment('foo')).to.equal(false); 10 | expect(isFragment(123)).to.equal(false); 11 | expect(isFragment([])).to.equal(false); 12 | expect(isFragment({})).to.equal(false); 13 | }); 14 | 15 | it('should detect a preact vnode', () => { 16 | expect(isFragment(preactCreateElement(Fragment, {}))).to.equal(true); 17 | }); 18 | 19 | it('should detect a compat vnode', () => { 20 | expect(isFragment(React.createElement(Fragment, {}))).to.equal(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /compat/test/browser/isMemo.test.js: -------------------------------------------------------------------------------- 1 | import { createElement as preactCreateElement, Fragment } from 'preact'; 2 | import React, { createElement, isMemo, memo } from 'preact/compat'; 3 | 4 | describe('isMemo', () => { 5 | it('should check return false for invalid arguments', () => { 6 | expect(isMemo(null)).to.equal(false); 7 | expect(isMemo(false)).to.equal(false); 8 | expect(isMemo(true)).to.equal(false); 9 | expect(isMemo('foo')).to.equal(false); 10 | expect(isMemo(123)).to.equal(false); 11 | expect(isMemo([])).to.equal(false); 12 | expect(isMemo({})).to.equal(false); 13 | }); 14 | 15 | it('should detect a preact memo', () => { 16 | function Foo() { 17 | return

Hello World

; 18 | } 19 | let App = memo(Foo); 20 | expect(isMemo(App)).to.equal(true); 21 | }); 22 | 23 | it('should not detect a normal element', () => { 24 | function Foo() { 25 | return

Hello World

; 26 | } 27 | expect(isMemo(Foo)).to.equal(false); 28 | }); 29 | 30 | it('should detect a preact vnode as false', () => { 31 | expect(isMemo(preactCreateElement(Fragment, {}))).to.equal(false); 32 | }); 33 | 34 | it('should detect a compat vnode as false', () => { 35 | expect(isMemo(React.createElement(Fragment, {}))).to.equal(false); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /compat/test/browser/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement as preactCreateElement } from 'preact'; 2 | import React, { isValidElement } from 'preact/compat'; 3 | 4 | describe('isValidElement', () => { 5 | it('should check return false for invalid arguments', () => { 6 | expect(isValidElement(null)).to.equal(false); 7 | expect(isValidElement(false)).to.equal(false); 8 | expect(isValidElement(true)).to.equal(false); 9 | expect(isValidElement('foo')).to.equal(false); 10 | expect(isValidElement(123)).to.equal(false); 11 | expect(isValidElement([])).to.equal(false); 12 | expect(isValidElement({})).to.equal(false); 13 | }); 14 | 15 | it('should detect a preact vnode', () => { 16 | expect(isValidElement(preactCreateElement('div', {}))).to.equal(true); 17 | }); 18 | 19 | it('should detect a compat vnode', () => { 20 | expect(isValidElement(React.createElement('div', {}))).to.equal(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /compat/test/browser/scheduler.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | unstable_runWithPriority, 3 | unstable_NormalPriority, 4 | unstable_LowPriority, 5 | unstable_IdlePriority, 6 | unstable_UserBlockingPriority, 7 | unstable_ImmediatePriority, 8 | unstable_now 9 | } from 'preact/compat/scheduler'; 10 | 11 | describe('scheduler', () => { 12 | describe('runWithPriority', () => { 13 | it('should call callback ', () => { 14 | const spy = sinon.spy(); 15 | unstable_runWithPriority(unstable_IdlePriority, spy); 16 | expect(spy.callCount).to.equal(1); 17 | 18 | unstable_runWithPriority(unstable_LowPriority, spy); 19 | expect(spy.callCount).to.equal(2); 20 | 21 | unstable_runWithPriority(unstable_NormalPriority, spy); 22 | expect(spy.callCount).to.equal(3); 23 | 24 | unstable_runWithPriority(unstable_UserBlockingPriority, spy); 25 | expect(spy.callCount).to.equal(4); 26 | 27 | unstable_runWithPriority(unstable_ImmediatePriority, spy); 28 | expect(spy.callCount).to.equal(5); 29 | }); 30 | }); 31 | 32 | describe('unstable_now', () => { 33 | it('should return number', () => { 34 | const res = unstable_now(); 35 | expect(res).is.a('number'); 36 | expect(res > 0).to.equal(true); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /compat/test/browser/select.test.js: -------------------------------------------------------------------------------- 1 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 2 | import React, { createElement, render } from 'preact/compat'; 3 | 4 | describe('Select', () => { 5 | let scratch; 6 | 7 | beforeEach(() => { 8 | scratch = setupScratch(); 9 | }); 10 | 11 | afterEach(() => { 12 | teardown(scratch); 13 | }); 14 | 15 | it('should work with multiple selected (array of values)', () => { 16 | function App() { 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | render(, scratch); 27 | const options = scratch.firstChild.children; 28 | expect(options[0]).to.have.property('selected', false); 29 | expect(options[1]).to.have.property('selected', true); 30 | expect(options[2]).to.have.property('selected', true); 31 | expect(scratch.firstChild.value).to.equal('B'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /compat/test/browser/testUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve a Symbol if supported or use the fallback value 3 | * @param {string} name The name of the Symbol to look up 4 | * @param {number} fallback Fallback value if Symbols are not supported 5 | */ 6 | export function getSymbol(name, fallback) { 7 | let out = fallback; 8 | 9 | try { 10 | // eslint-disable-next-line 11 | if ( 12 | Function.prototype.toString 13 | .call((0, eval)('Symbol.for')) 14 | .match(/\[native code\]/) 15 | ) { 16 | // Concatenate these string literals to prevent the test 17 | // harness and/or Babel from modifying the symbol value. 18 | // eslint-disable-next-line 19 | out = (0, eval)('Sym' + 'bol.for("' + name + '")'); 20 | } 21 | } catch (e) {} 22 | 23 | return out; 24 | } 25 | -------------------------------------------------------------------------------- /compat/test/browser/unmountComponentAtNode.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, unmountComponentAtNode } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('unmountComponentAtNode', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should unmount a root node', () => { 17 | const App = () =>
foo
; 18 | React.render(, scratch); 19 | 20 | expect(unmountComponentAtNode(scratch)).to.equal(true); 21 | expect(scratch.innerHTML).to.equal(''); 22 | }); 23 | 24 | it('should do nothing if root is not mounted', () => { 25 | expect(unmountComponentAtNode(scratch)).to.equal(false); 26 | expect(scratch.innerHTML).to.equal(''); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /compat/test/browser/unstable_batchedUpdates.test.js: -------------------------------------------------------------------------------- 1 | import { unstable_batchedUpdates, flushSync } from 'preact/compat'; 2 | 3 | describe('unstable_batchedUpdates', () => { 4 | it('should call the callback', () => { 5 | const spy = sinon.spy(); 6 | unstable_batchedUpdates(spy); 7 | expect(spy).to.be.calledOnce; 8 | }); 9 | 10 | it('should call callback with only one arg', () => { 11 | const spy = sinon.spy(); 12 | // @ts-expect-error 13 | unstable_batchedUpdates(spy, 'foo', 'bar'); 14 | expect(spy).to.be.calledWithExactly('foo'); 15 | }); 16 | }); 17 | 18 | describe('flushSync', () => { 19 | it('should invoke the given callback', () => { 20 | const returnValue = {}; 21 | const spy = sinon.spy(() => returnValue); 22 | const result = flushSync(spy); 23 | expect(spy).to.have.been.calledOnce; 24 | expect(result).to.equal(returnValue); 25 | }); 26 | 27 | it('should invoke the given callback with the given argument', () => { 28 | const returnValue = {}; 29 | const spy = sinon.spy(() => returnValue); 30 | const result = flushSync(spy, 'foo'); 31 | expect(spy).to.be.calledWithExactly('foo'); 32 | expect(result).to.equal(returnValue); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /compat/test/ts/forward-ref.tsx: -------------------------------------------------------------------------------- 1 | import React from '../../src'; 2 | 3 | const MyInput: React.ForwardFn<{ id: string }, { focus(): void }> = ( 4 | props, 5 | ref 6 | ) => { 7 | const inputRef = React.useRef(null); 8 | 9 | React.useImperativeHandle(ref, () => ({ 10 | focus: () => { 11 | if (inputRef.current) { 12 | inputRef.current.focus(); 13 | } 14 | } 15 | })); 16 | 17 | return ; 18 | }; 19 | 20 | export const foo = React.forwardRef(MyInput); 21 | 22 | export const Bar = React.forwardRef( 23 | (props, ref) => { 24 | return
{props.children}
; 25 | } 26 | ); 27 | 28 | export const baz = ( 29 | ref: React.ForwardedRef 30 | ): React.Ref => ref; 31 | -------------------------------------------------------------------------------- /compat/test/ts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from '../../src'; 2 | 3 | React.render(
, document.createElement('div')); 4 | React.render(
, document.createDocumentFragment()); 5 | React.render(
, document.body.shadowRoot!); 6 | 7 | React.hydrate(
, document.createElement('div')); 8 | React.hydrate(
, document.createDocumentFragment()); 9 | React.hydrate(
, document.body.shadowRoot!); 10 | 11 | React.unmountComponentAtNode(document.createElement('div')); 12 | React.unmountComponentAtNode(document.createDocumentFragment()); 13 | React.unmountComponentAtNode(document.body.shadowRoot!); 14 | 15 | React.createPortal(
, document.createElement('div')); 16 | React.createPortal(
, document.createDocumentFragment()); 17 | React.createPortal(
, document.body.shadowRoot!); 18 | 19 | const Ctx = React.createContext({ contextValue: '' }); 20 | class SimpleComponentWithContextAsProvider extends React.Component { 21 | componentProp = 'componentProp'; 22 | render() { 23 | // Render inside div to ensure standard JSX elements still work 24 | return ( 25 | 26 |
27 | {/* Ensure context still works */} 28 | 29 | {({ contextValue }) => contextValue.toLowerCase()} 30 | 31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | React.render( 38 | , 39 | document.createElement('div') 40 | ); 41 | -------------------------------------------------------------------------------- /compat/test/ts/lazy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from '../../src'; 2 | 3 | export interface LazyProps { 4 | isProp: boolean; 5 | } 6 | 7 | interface LazyState { 8 | forState: string; 9 | } 10 | export default class IsLazyComponent extends React.Component< 11 | LazyProps, 12 | LazyState 13 | > { 14 | render({ isProp }: LazyProps) { 15 | return
{isProp ? 'Super Lazy TRUE' : 'Super Lazy FALSE'}
; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /compat/test/ts/memo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from '../../src'; 2 | import { expectType } from './utils'; 3 | 4 | interface MemoProps { 5 | required: string; 6 | optional?: string; 7 | defaulted: string; 8 | } 9 | 10 | interface MemoPropsExceptDefaults { 11 | required: string; 12 | optional?: string; 13 | } 14 | 15 | const ComponentExceptDefaults = () =>
; 16 | 17 | const ReadonlyBaseComponent = (props: Readonly) => ( 18 |
{props.required + props.optional + props.defaulted}
19 | ); 20 | ReadonlyBaseComponent.defaultProps = { defaulted: '' }; 21 | 22 | const BaseComponent = (props: MemoProps) => ( 23 |
{props.required + props.optional + props.defaulted}
24 | ); 25 | BaseComponent.defaultProps = { defaulted: '' }; 26 | 27 | // memo for readonly component with default comparison 28 | const MemoedReadonlyComponent = React.memo(ReadonlyBaseComponent); 29 | expectType>(MemoedReadonlyComponent); 30 | export const memoedReadonlyComponent = ( 31 | 32 | ); 33 | 34 | // memo for non-readonly component with default comparison 35 | const MemoedComponent = React.memo(BaseComponent); 36 | expectType>(MemoedComponent); 37 | export const memoedComponent = ; 38 | 39 | // memo with custom comparison 40 | const CustomMemoedComponent = React.memo(BaseComponent, (a, b) => { 41 | expectType(a); 42 | expectType(b); 43 | return a.required === b.required; 44 | }); 45 | expectType>(CustomMemoedComponent); 46 | export const customMemoedComponent = ; 47 | 48 | const MemoedComponentExceptDefaults = React.memo( 49 | ComponentExceptDefaults 50 | ); 51 | expectType>( 52 | MemoedComponentExceptDefaults 53 | ); 54 | export const memoedComponentExceptDefaults = ( 55 | 56 | ); 57 | -------------------------------------------------------------------------------- /compat/test/ts/react-default.tsx: -------------------------------------------------------------------------------- 1 | import React from '../../src'; 2 | class ReactIsh extends React.Component { 3 | render() { 4 | return
Text
; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /compat/test/ts/react-star.tsx: -------------------------------------------------------------------------------- 1 | // import React from '../../src'; 2 | import * as React from '../../src'; 3 | class ReactIsh extends React.Component { 4 | render() { 5 | return
Text
; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compat/test/ts/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unstable_runWithPriority, 3 | unstable_NormalPriority, 4 | unstable_LowPriority, 5 | unstable_IdlePriority, 6 | unstable_UserBlockingPriority, 7 | unstable_ImmediatePriority, 8 | unstable_now 9 | } from '../../src'; 10 | 11 | const noop = () => null; 12 | unstable_runWithPriority(unstable_IdlePriority, noop); 13 | unstable_runWithPriority(unstable_LowPriority, noop); 14 | unstable_runWithPriority(unstable_NormalPriority, noop); 15 | unstable_runWithPriority(unstable_UserBlockingPriority, noop); 16 | unstable_runWithPriority(unstable_ImmediatePriority, noop); 17 | 18 | if (typeof unstable_now() === 'number') { 19 | } 20 | -------------------------------------------------------------------------------- /compat/test/ts/suspense.tsx: -------------------------------------------------------------------------------- 1 | import * as React from '../../src'; 2 | 3 | interface LazyProps { 4 | isProp: boolean; 5 | } 6 | 7 | const IsLazyFunctional = (props: LazyProps) => ( 8 |
{props.isProp ? 'Super Lazy TRUE' : 'Super Lazy FALSE'}
9 | ); 10 | 11 | const FallBack = () =>
Still working...
; 12 | /** 13 | * Have to mock dynamic import as import() throws a syntax error in the test runner 14 | */ 15 | const componentPromise = new Promise<{ default: typeof IsLazyFunctional }>( 16 | resolve => { 17 | setTimeout(() => { 18 | resolve({ default: IsLazyFunctional }); 19 | }, 800); 20 | } 21 | ); 22 | 23 | /** 24 | * For usage with import: 25 | * const IsLazyComp = lazy(() => import('./lazy')); 26 | */ 27 | const IsLazyFunc = React.lazy(() => componentPromise); 28 | 29 | // Suspense using lazy component 30 | class ReactSuspensefulFunc extends React.Component { 31 | render() { 32 | return ( 33 | }> 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | //SuspenseList using lazy components 41 | function ReactSuspenseListTester(_props: any) { 42 | return ( 43 | 44 | }> 45 | 46 | 47 | }> 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | const Comp = () =>

Hello world

; 55 | 56 | const importComponent = async () => { 57 | return { MyComponent: Comp }; 58 | }; 59 | 60 | const Lazy = React.lazy(() => 61 | importComponent().then(mod => ({ default: mod.MyComponent })) 62 | ); 63 | 64 | // eslint-disable-next-line 65 | function App() { 66 | return ; 67 | } 68 | -------------------------------------------------------------------------------- /compat/test/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom"], 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react", 10 | "noEmit": true, 11 | "allowSyntheticDefaultImports": true, 12 | "paths": { 13 | "preact": ["../../../src/index.d.ts"] 14 | } 15 | }, 16 | "include": ["./**/*.ts", "./**/*.tsx"] 17 | } -------------------------------------------------------------------------------- /compat/test/ts/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assert the parameter is of a specific type. 3 | */ 4 | export const expectType = (_: T): void => undefined; 5 | -------------------------------------------------------------------------------- /config/codemod-const.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | /** Find constants (identified by ALL_CAPS_DECLARATIONS), and inline them globally. 4 | * This is safe because Preact *only* uses global constants. 5 | */ 6 | export default (file, api) => { 7 | let j = api.jscodeshift, 8 | code = j(file.source), 9 | constants = {}, 10 | found = 0; 11 | 12 | code 13 | .find(j.VariableDeclaration) 14 | .filter(decl => { 15 | for (let i = decl.value.declarations.length; i--; ) { 16 | let node = decl.value.declarations[i], 17 | name = node.id && node.id.name, 18 | init = node.init; 19 | if (name && init && name.match(/^[A-Z0-9_$]+$/g) && !init.regex) { 20 | if (init.type === 'Literal') { 21 | // console.log(`Inlining constant: ${name}=${init.raw}`); 22 | found++; 23 | constants[name] = init; 24 | // remove declaration 25 | decl.value.declarations.splice(i, 1); 26 | // if it's the last, we'll remove the whole statement 27 | return !decl.value.declarations.length; 28 | } 29 | } 30 | } 31 | return false; 32 | }) 33 | .remove(); 34 | 35 | code 36 | .find(j.Identifier) 37 | .filter( 38 | path => path.value.name && constants.hasOwnProperty(path.value.name) 39 | ) 40 | .replaceWith(path => (found++, constants[path.value.name])); 41 | 42 | return found ? code.toSource({ quote: 'single' }) : null; 43 | }; 44 | -------------------------------------------------------------------------------- /config/codemod-let-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Restores var names transformed by babel's let block scoping 3 | */ 4 | export default (file, api) => { 5 | let j = api.jscodeshift; 6 | let code = j(file.source); 7 | 8 | // @TODO unsafe, but without it we gain 20b gzipped: https://www.diffchecker.com/bVrOJWTO 9 | code 10 | .findVariableDeclarators() 11 | .filter(d => /^_i/.test(d.value.id.name)) 12 | .renameTo('i'); 13 | code.findVariableDeclarators('_key').renameTo('key'); 14 | 15 | return code.toSource({ quote: 'single' }); 16 | }; 17 | -------------------------------------------------------------------------------- /config/codemod-strip-tdz.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | // parent node types that we don't want to remove pointless initializations from (because it breaks hoisting) 4 | const BLOCKED = ['ForStatement', 'WhileStatement']; // 'IfStatement', 'SwitchStatement' 5 | 6 | /** Removes var initialization to `void 0`, which Babel adds for TDZ strictness. */ 7 | export default (file, api) => { 8 | let { jscodeshift } = api, 9 | found = 0; 10 | 11 | let code = jscodeshift(file.source) 12 | .find(jscodeshift.VariableDeclaration) 13 | .forEach(handleDeclaration); 14 | 15 | function handleDeclaration(decl) { 16 | let p = decl, 17 | remove = true; 18 | 19 | while ((p = p.parentPath)) { 20 | if (~BLOCKED.indexOf(p.value.type)) { 21 | remove = false; 22 | break; 23 | } 24 | } 25 | 26 | decl.value.declarations.filter(isPointless).forEach(node => { 27 | if (remove === false) { 28 | console.log( 29 | `> Skipping removal of undefined init for "${node.id.name}": within ${p.value.type}` 30 | ); 31 | } else { 32 | removeNodeInitialization(node); 33 | } 34 | }); 35 | } 36 | 37 | function removeNodeInitialization(node) { 38 | node.init = null; 39 | found++; 40 | } 41 | 42 | function isPointless(node) { 43 | let { init } = node; 44 | if (init) { 45 | if ( 46 | init.type === 'UnaryExpression' && 47 | init.operator === 'void' && 48 | init.argument.value == 0 49 | ) { 50 | return true; 51 | } 52 | if (init.type === 'Identifier' && init.name === 'undefined') { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | return found ? code.toSource({ quote: 'single' }) : null; 60 | }; 61 | -------------------------------------------------------------------------------- /config/compat-entries.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const kl = require('kolorist'); 4 | 5 | const pkgFiles = new Set(require('../package.json').files); 6 | const compatDir = path.join(__dirname, '..', 'compat'); 7 | const files = fs.readdirSync(compatDir); 8 | 9 | let missing = 0; 10 | for (const file of files) { 11 | const expected = 'compat/' + file; 12 | if (/\.(js|mjs)$/.test(file) && !pkgFiles.has(expected)) { 13 | missing++; 14 | 15 | const filePath = kl.cyan('compat/' + file); 16 | const label = kl.inverse(kl.red(' ERROR ')); 17 | console.error( 18 | `${label} File ${filePath} is missing in "files" entry in package.json` 19 | ); 20 | } 21 | } 22 | 23 | if (missing > 0) { 24 | process.exit(1); 25 | } 26 | -------------------------------------------------------------------------------- /config/node-13-exports.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const subRepositories = [ 4 | 'compat', 5 | 'debug', 6 | 'devtools', 7 | 'hooks', 8 | 'jsx-runtime', 9 | 'test-utils' 10 | ]; 11 | const snakeCaseToCamelCase = str => 12 | str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '')); 13 | 14 | const copyPreact = () => { 15 | // Copy .module.js --> .mjs for Node 13 compat. 16 | fs.writeFileSync( 17 | `${process.cwd()}/dist/preact.mjs`, 18 | fs.readFileSync(`${process.cwd()}/dist/preact.module.js`) 19 | ); 20 | }; 21 | 22 | const copy = name => { 23 | // Copy .module.js --> .mjs for Node 13 compat. 24 | const filename = name.includes('-') ? snakeCaseToCamelCase(name) : name; 25 | fs.writeFileSync( 26 | `${process.cwd()}/${name}/dist/${filename}.mjs`, 27 | fs.readFileSync(`${process.cwd()}/${name}/dist/${filename}.module.js`) 28 | ); 29 | }; 30 | 31 | copyPreact(); 32 | subRepositories.forEach(copy); 33 | -------------------------------------------------------------------------------- /debug/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /debug/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-debug", 3 | "amdName": "preactDebug", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact extensions for development", 7 | "main": "dist/debug.js", 8 | "module": "dist/debug.module.js", 9 | "umd:main": "dist/debug.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "mangle": { 13 | "regex": "^(?!_renderer)^_" 14 | }, 15 | "peerDependencies": { 16 | "preact": "^10.0.0" 17 | }, 18 | "exports": { 19 | ".": { 20 | "types": "./src/index.d.ts", 21 | "browser": "./dist/debug.module.js", 22 | "umd": "./dist/debug.umd.js", 23 | "import": "./dist/debug.mjs", 24 | "require": "./dist/debug.js" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /debug/src/check-props.js: -------------------------------------------------------------------------------- 1 | const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; 2 | 3 | let loggedTypeFailures = {}; 4 | 5 | /** 6 | * Reset the history of which prop type warnings have been logged. 7 | */ 8 | export function resetPropWarnings() { 9 | loggedTypeFailures = {}; 10 | } 11 | 12 | /** 13 | * Assert that the values match with the type specs. 14 | * Error messages are memorized and will only be shown once. 15 | * 16 | * Adapted from https://github.com/facebook/prop-types/blob/master/checkPropTypes.js 17 | * 18 | * @param {object} typeSpecs Map of name to a ReactPropType 19 | * @param {object} values Runtime values that need to be type-checked 20 | * @param {string} location e.g. "prop", "context", "child context" 21 | * @param {string} componentName Name of the component for error messages. 22 | * @param {?Function} getStack Returns the component stack. 23 | */ 24 | export function checkPropTypes( 25 | typeSpecs, 26 | values, 27 | location, 28 | componentName, 29 | getStack 30 | ) { 31 | Object.keys(typeSpecs).forEach(typeSpecName => { 32 | let error; 33 | try { 34 | error = typeSpecs[typeSpecName]( 35 | values, 36 | typeSpecName, 37 | componentName, 38 | location, 39 | null, 40 | ReactPropTypesSecret 41 | ); 42 | } catch (e) { 43 | error = e; 44 | } 45 | if (error && !(error.message in loggedTypeFailures)) { 46 | loggedTypeFailures[error.message] = true; 47 | console.error( 48 | `Failed ${location} type: ${error.message}${ 49 | (getStack && `\n${getStack()}`) || '' 50 | }` 51 | ); 52 | } 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /debug/src/constants.js: -------------------------------------------------------------------------------- 1 | export const ELEMENT_NODE = 1; 2 | export const DOCUMENT_NODE = 9; 3 | export const DOCUMENT_FRAGMENT_NODE = 11; 4 | -------------------------------------------------------------------------------- /debug/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reset the history of which prop type warnings have been logged. 3 | */ 4 | export function resetPropWarnings(): void; 5 | -------------------------------------------------------------------------------- /debug/src/index.js: -------------------------------------------------------------------------------- 1 | import { initDebug } from './debug'; 2 | import 'preact/devtools'; 3 | 4 | initDebug(); 5 | 6 | export { resetPropWarnings } from './check-props'; 7 | -------------------------------------------------------------------------------- /debug/src/internal.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, PreactElement, VNode, Options } from '../../src/internal'; 2 | 3 | export { Component, PreactElement, VNode, Options }; 4 | 5 | export interface DevtoolsInjectOptions { 6 | /** 1 = DEV, 0 = production */ 7 | bundleType: 1 | 0; 8 | /** The devtools enable different features for different versions of react */ 9 | version: string; 10 | /** Informative string, currently unused in the devtools */ 11 | rendererPackageName: string; 12 | /** Find the root dom node of a vnode */ 13 | findHostInstanceByFiber(vnode: VNode): HTMLElement | null; 14 | /** Find the closest vnode given a dom node */ 15 | findFiberByHostInstance(instance: HTMLElement): VNode | null; 16 | } 17 | 18 | export interface DevtoolsUpdater { 19 | setState(objOrFn: any): void; 20 | forceUpdate(): void; 21 | setInState(path: Array, value: any): void; 22 | setInProps(path: Array, value: any): void; 23 | setInContext(): void; 24 | } 25 | 26 | export type NodeType = 'Composite' | 'Native' | 'Wrapper' | 'Text'; 27 | 28 | export interface DevtoolData { 29 | nodeType: NodeType; 30 | // Component type 31 | type: any; 32 | name: string; 33 | ref: any; 34 | key: string | number; 35 | updater: DevtoolsUpdater | null; 36 | text: string | number | null; 37 | state: any; 38 | props: any; 39 | children: VNode[] | string | number | null; 40 | publicInstance: PreactElement | Text | Component; 41 | memoizedInteractions: any[]; 42 | 43 | actualDuration: number; 44 | actualStartTime: number; 45 | treeBaseDuration: number; 46 | } 47 | 48 | export type EventType = 49 | | 'unmount' 50 | | 'rootCommitted' 51 | | 'root' 52 | | 'mount' 53 | | 'update' 54 | | 'updateProfileTimes'; 55 | 56 | export interface DevtoolsEvent { 57 | data?: DevtoolData; 58 | internalInstance: VNode; 59 | renderer: string; 60 | type: EventType; 61 | } 62 | 63 | export interface DevtoolsHook { 64 | _renderers: Record; 65 | _roots: Set; 66 | on(ev: string, listener: () => void): void; 67 | emit(ev: string, data?: object): void; 68 | helpers: Record; 69 | getFiberRoots(rendererId: string): Set; 70 | inject(config: DevtoolsInjectOptions): string; 71 | onCommitFiberRoot(rendererId: string, root: VNode): void; 72 | onCommitFiberUnmount(rendererId: string, vnode: VNode): void; 73 | } 74 | 75 | export interface DevtoolsWindow extends Window { 76 | /** 77 | * If the devtools extension is installed it will inject this object into 78 | * the dom. This hook handles all communications between preact and the 79 | * devtools panel. 80 | */ 81 | __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevtoolsHook; 82 | } 83 | -------------------------------------------------------------------------------- /debug/src/util.js: -------------------------------------------------------------------------------- 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, props) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ (obj); 11 | } 12 | 13 | export function isNaN(value) { 14 | return value !== value; 15 | } 16 | -------------------------------------------------------------------------------- /debug/test/browser/component-stack-2.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import 'preact/debug'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | // This test is not part of component-stack.test.js to avoid it being 8 | // transpiled with '@babel/plugin-transform-react-jsx-source' enabled. 9 | 10 | describe('component stack', () => { 11 | /** @type {HTMLDivElement} */ 12 | let scratch; 13 | 14 | let errors = []; 15 | let warnings = []; 16 | 17 | beforeEach(() => { 18 | scratch = setupScratch(); 19 | 20 | errors = []; 21 | warnings = []; 22 | sinon.stub(console, 'error').callsFake(e => errors.push(e)); 23 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 24 | }); 25 | 26 | afterEach(() => { 27 | console.error.restore(); 28 | console.warn.restore(); 29 | teardown(scratch); 30 | }); 31 | 32 | it('should print a warning when "@babel/plugin-transform-react-jsx-source" is not installed', () => { 33 | function Foo() { 34 | return ; 35 | } 36 | 37 | class Thrower extends Component { 38 | constructor(props) { 39 | super(props); 40 | this.setState({ foo: 1 }); 41 | } 42 | 43 | render() { 44 | return
foo
; 45 | } 46 | } 47 | 48 | render(, scratch); 49 | 50 | expect( 51 | warnings[0].indexOf('@babel/plugin-transform-react-jsx-source') > -1 52 | ).to.equal(true); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /debug/test/browser/component-stack.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import 'preact/debug'; 3 | import { vi } from 'vitest'; 4 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 5 | 6 | /** @jsx createElement */ 7 | 8 | describe('component stack', () => { 9 | /** @type {HTMLDivElement} */ 10 | let scratch; 11 | 12 | let errors = []; 13 | let warnings = []; 14 | 15 | const getStack = arr => arr[0].split('\n\n')[1]; 16 | 17 | beforeEach(() => { 18 | scratch = setupScratch(); 19 | 20 | errors = []; 21 | warnings = []; 22 | vi.spyOn(console, 'error').mockImplementation(e => errors.push(e)); 23 | vi.spyOn(console, 'warn').mockImplementation(w => warnings.push(w)); 24 | }); 25 | 26 | afterEach(() => { 27 | vi.resetAllMocks(); 28 | teardown(scratch); 29 | }); 30 | 31 | it('should print component stack', () => { 32 | function Foo() { 33 | return ; 34 | } 35 | 36 | class Thrower extends Component { 37 | constructor(props) { 38 | super(props); 39 | this.setState({ foo: 1 }); 40 | } 41 | 42 | render() { 43 | return
foo
; 44 | } 45 | } 46 | 47 | render(, scratch); 48 | 49 | // This has a JSX transform warning, so we need to remove it 50 | warnings.shift(); 51 | let lines = getStack(warnings).split('\n'); 52 | expect(lines[0].indexOf('Thrower') > -1).to.equal(true); 53 | expect(lines[1].indexOf('Foo') > -1).to.equal(true); 54 | }); 55 | 56 | it('should only print owners', () => { 57 | function Foo(props) { 58 | return
{props.children}
; 59 | } 60 | 61 | function Bar() { 62 | return ( 63 | 64 | 65 | 66 | ); 67 | } 68 | 69 | class Thrower extends Component { 70 | render() { 71 | return ( 72 | 73 | foo 75 | 76 |
74 |
77 | ); 78 | } 79 | } 80 | 81 | render(, scratch); 82 | 83 | let lines = getStack(errors).split('\n'); 84 | expect(lines[0].indexOf('tr') > -1).to.equal(true); 85 | expect(lines[1].indexOf('Thrower') > -1).to.equal(true); 86 | expect(lines[2].indexOf('Bar') > -1).to.equal(true); 87 | }); 88 | 89 | it('should not print a warning when "@babel/plugin-transform-react-jsx-source" is installed', () => { 90 | function Thrower() { 91 | throw new Error('foo'); 92 | } 93 | 94 | try { 95 | render(, scratch); 96 | } catch {} 97 | 98 | expect(warnings.join(' ')).to.not.include( 99 | '@babel/plugin-transform-react-jsx-source' 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /debug/test/browser/debug-compat.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, createRef } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import './fakeDevTools'; 4 | import 'preact/debug'; 5 | import * as PropTypes from 'prop-types'; 6 | 7 | // eslint-disable-next-line no-duplicate-imports 8 | import { resetPropWarnings } from 'preact/debug'; 9 | import { forwardRef, createPortal } from 'preact/compat'; 10 | 11 | const h = createElement; 12 | /** @jsx createElement */ 13 | 14 | describe('debug compat', () => { 15 | let scratch; 16 | let root; 17 | let errors = []; 18 | let warnings = []; 19 | 20 | beforeEach(() => { 21 | errors = []; 22 | warnings = []; 23 | scratch = setupScratch(); 24 | sinon.stub(console, 'error').callsFake(e => errors.push(e)); 25 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 26 | 27 | root = document.createElement('div'); 28 | document.body.appendChild(root); 29 | }); 30 | 31 | afterEach(() => { 32 | /** @type {*} */ 33 | console.error.restore(); 34 | console.warn.restore(); 35 | teardown(scratch); 36 | 37 | document.body.removeChild(root); 38 | }); 39 | 40 | describe('portals', () => { 41 | it('should not throw an invalid render argument for a portal.', () => { 42 | function Foo(props) { 43 | return
{createPortal(props.children, root)}
; 44 | } 45 | expect(() => render(foobar, scratch)).not.to.throw(); 46 | }); 47 | }); 48 | 49 | describe('PropTypes', () => { 50 | beforeEach(() => { 51 | resetPropWarnings(); 52 | }); 53 | 54 | it('should not fail if ref is passed to comp wrapped in forwardRef', () => { 55 | // This test ensures compat with airbnb/prop-types-exact, mui exact prop types util, etc. 56 | 57 | const Foo = forwardRef(function Foo(props, ref) { 58 | return

{props.text}

; 59 | }); 60 | 61 | Foo.propTypes = { 62 | text: PropTypes.string.isRequired, 63 | ref(props) { 64 | if ('ref' in props) { 65 | throw new Error( 66 | 'ref should not be passed to prop-types valiation!' 67 | ); 68 | } 69 | } 70 | }; 71 | 72 | const ref = createRef(); 73 | 74 | render(, scratch); 75 | 76 | expect(console.error).not.been.called; 77 | 78 | expect(ref.current).to.not.be.undefined; 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /debug/test/browser/fakeDevTools.js: -------------------------------------------------------------------------------- 1 | window.__PREACT_DEVTOOLS__ = { attachPreact: sinon.spy() }; 2 | -------------------------------------------------------------------------------- /debug/test/browser/serializeVNode.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | import { serializeVNode } from '../../src/debug'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('serializeVNode', () => { 7 | it("should prefer a function component's displayName", () => { 8 | function Foo() { 9 | return
; 10 | } 11 | Foo.displayName = 'Bar'; 12 | 13 | expect(serializeVNode()).to.equal(''); 14 | }); 15 | 16 | it("should prefer a class component's displayName", () => { 17 | class Bar extends Component { 18 | render() { 19 | return
; 20 | } 21 | } 22 | Bar.displayName = 'Foo'; 23 | 24 | expect(serializeVNode()).to.equal(''); 25 | }); 26 | 27 | it('should serialize vnodes without children', () => { 28 | expect(serializeVNode(
)).to.equal('
'); 29 | }); 30 | 31 | it('should serialize vnodes with children', () => { 32 | expect(serializeVNode(
Hello World
)).to.equal('
..
'); 33 | }); 34 | 35 | it('should serialize components', () => { 36 | function Foo() { 37 | return
; 38 | } 39 | expect(serializeVNode()).to.equal(''); 40 | }); 41 | 42 | it('should serialize props', () => { 43 | expect(serializeVNode(
)).to.equal('
'); 44 | 45 | // Ensure that we have a predictable function name. Our test runner 46 | // creates an all inclusive bundle per file and the identifier 47 | // "noop" may have already been used. 48 | // eslint-disable-next-line func-style 49 | let noop = function noopFn() {}; 50 | expect(serializeVNode(
)).to.equal( 51 | '
' 52 | ); 53 | 54 | function Foo(props) { 55 | return props.foo; 56 | } 57 | 58 | expect(serializeVNode()).to.equal( 59 | '' 60 | ); 61 | 62 | expect(serializeVNode(
)).to.equal( 63 | '
' 64 | ); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /debug/test/browser/validateHookArgs.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, createRef } from 'preact'; 2 | import { 3 | useState, 4 | useEffect, 5 | useLayoutEffect, 6 | useCallback, 7 | useMemo, 8 | useImperativeHandle 9 | } from 'preact/hooks'; 10 | import { setupRerender } from 'preact/test-utils'; 11 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 12 | import 'preact/debug'; 13 | 14 | /** @jsx createElement */ 15 | 16 | describe('Hook argument validation', () => { 17 | /** 18 | * @param {string} name 19 | * @param {(arg: number) => void} hook 20 | */ 21 | function validateHook(name, hook) { 22 | const TestComponent = ({ initialValue }) => { 23 | const [value, setValue] = useState(initialValue); 24 | hook(value); 25 | 26 | return ( 27 | 30 | ); 31 | }; 32 | 33 | it(`should error if ${name} is mounted with NaN as an argument`, async () => { 34 | render(, scratch); 35 | expect(console.warn).to.be.calledOnce; 36 | expect(console.warn.args[0]).to.match( 37 | /Hooks should not be called with NaN in the dependency array/ 38 | ); 39 | }); 40 | 41 | it(`should error if ${name} is updated with NaN as an argument`, async () => { 42 | render(, scratch); 43 | 44 | scratch.querySelector('button').click(); 45 | rerender(); 46 | 47 | expect(console.warn).to.be.calledOnce; 48 | expect(console.warn.args[0]).to.match( 49 | /Hooks should not be called with NaN in the dependency array/ 50 | ); 51 | }); 52 | } 53 | 54 | /** @type {HTMLElement} */ 55 | let scratch; 56 | /** @type {() => void} */ 57 | let rerender; 58 | let warnings = []; 59 | 60 | beforeEach(() => { 61 | scratch = setupScratch(); 62 | rerender = setupRerender(); 63 | warnings = []; 64 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 65 | }); 66 | 67 | afterEach(() => { 68 | teardown(scratch); 69 | console.warn.restore(); 70 | }); 71 | 72 | validateHook('useEffect', arg => useEffect(() => {}, [arg])); 73 | validateHook('useLayoutEffect', arg => useLayoutEffect(() => {}, [arg])); 74 | validateHook('useCallback', arg => useCallback(() => {}, [arg])); 75 | validateHook('useMemo', arg => useMemo(() => {}, [arg])); 76 | 77 | const ref = createRef(); 78 | validateHook('useImperativeHandle', arg => { 79 | useImperativeHandle(ref, () => undefined, [arg]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /demo/contenteditable.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'preact/hooks'; 2 | 3 | export default function Contenteditable() { 4 | const [value, setValue] = useState("Hey there
I'm editable!"); 5 | 6 | return ( 7 |
8 |
9 | 10 |
11 |
setValue(e.currentTarget.innerHTML)} 20 | dangerouslySetInnerHTML={{ __html: value }} 21 | /> 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /demo/context.jsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { Component, createContext } from 'preact'; 3 | const { Provider, Consumer } = createContext(); 4 | 5 | class ThemeProvider extends Component { 6 | state = { 7 | value: this.props.value 8 | }; 9 | 10 | onClick = () => { 11 | this.setState(prev => ({ 12 | value: 13 | prev.value === this.props.value ? this.props.next : this.props.value 14 | })); 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 | 21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | 27 | class Child extends Component { 28 | shouldComponentUpdate() { 29 | return false; 30 | } 31 | 32 | render() { 33 | return ( 34 | <> 35 |

(blocked update)

36 | {this.props.children} 37 | 38 | ); 39 | } 40 | } 41 | 42 | export default class ContextDemo extends Component { 43 | render() { 44 | return ( 45 | 46 | 47 | 48 | {data => ( 49 |
50 |

51 | current theme: {data} 52 |

53 | 54 | 55 | {data => ( 56 |

57 | current sub theme: {data} 58 |

59 | )} 60 |
61 |
62 |
63 | )} 64 |
65 |
66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demo/devtools.jsx: -------------------------------------------------------------------------------- 1 | import { Component, memo, Suspense, lazy } from 'react'; 2 | 3 | function Foo() { 4 | return
I'm memoed
; 5 | } 6 | 7 | function LazyComp() { 8 | return
I'm (fake) lazy loaded
; 9 | } 10 | 11 | const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); 12 | 13 | const Memoed = memo(Foo); 14 | 15 | export default class DevtoolsDemo extends Component { 16 | render() { 17 | return ( 18 |
19 |

memo()

20 |

21 | functional component: 22 |

23 | 24 |

lazy()

25 |

26 | functional component: 27 |

28 | Loading (fake) lazy loaded component...
}> 29 | 30 | 31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/fragments.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | 3 | export default class FragmentComp extends Component { 4 | state = { number: 0 }; 5 | 6 | componentDidMount() { 7 | setInterval(_ => this.updateChildren(), 1000); 8 | } 9 | 10 | updateChildren() { 11 | this.setState(state => ({ number: state.number + 1 })); 12 | } 13 | 14 | render(props, state) { 15 | return ( 16 |
17 |
{state.number}
18 | <> 19 |
one
20 |
{state.number}
21 |
three
22 | 23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Preact Demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/key_bug.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | 3 | function Foo(props) { 4 | return
This is: {props.children}
; 5 | } 6 | 7 | export default class KeyBug extends Component { 8 | constructor() { 9 | super(); 10 | this.onClick = this.onClick.bind(this); 11 | this.state = { active: false }; 12 | } 13 | 14 | onClick() { 15 | this.setState(prev => ({ active: !prev.active })); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | {this.state.active && foo} 22 |

Hello World

23 |
24 | 25 | bar bar 26 | 27 |
28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/list.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import htm from 'htm'; 3 | import './style.css'; 4 | 5 | const html = htm.bind(h); 6 | const createRoot = parent => ({ 7 | render: v => render(v, parent) 8 | }); 9 | 10 | function List({ items, renders, useKeys, useCounts, update }) { 11 | const toggleKeys = () => update({ useKeys: !useKeys }); 12 | const toggleCounts = () => update({ useCounts: !useCounts }); 13 | const swap = () => { 14 | const u = { items: items.slice() }; 15 | u.items[1] = items[8]; 16 | u.items[8] = items[1]; 17 | update(u); 18 | }; 19 | return html` 20 |
21 | 22 | 23 | 27 | 31 |
    32 | ${items.map( 33 | (item, i) => html` 34 |
  • 38 | ${item.name} ${useCounts ? ` (${renders} renders)` : ''} 39 |
  • 40 | ` 41 | )} 42 |
43 |
44 | `; 45 | } 46 | 47 | const root = createRoot(document.body); 48 | 49 | let data = { 50 | items: new Array(1000).fill(null).map((x, i) => ({ name: `Item ${i + 1}` })), 51 | renders: 0, 52 | useKeys: false, 53 | useCounts: false 54 | }; 55 | 56 | function update(partial) { 57 | if (partial) Object.assign(data, partial); 58 | data.renders++; 59 | data.update = update; 60 | root.render(List(data)); 61 | } 62 | 63 | update(); 64 | -------------------------------------------------------------------------------- /demo/mobx.jsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useRef, useState } from 'react'; 2 | import { decorate, observable } from 'mobx'; 3 | import { observer, useObserver } from 'mobx-react'; 4 | import 'mobx-react-lite/batchingForReactDom'; 5 | 6 | class Todo { 7 | constructor() { 8 | this.id = Math.random(); 9 | this.title = 'initial'; 10 | this.finished = false; 11 | } 12 | } 13 | decorate(Todo, { 14 | title: observable, 15 | finished: observable 16 | }); 17 | 18 | const Forward = observer( 19 | // eslint-disable-next-line react/display-name 20 | forwardRef(({ todo }, ref) => { 21 | return ( 22 |

23 | Forward: "{todo.title}" {'' + todo.finished} 24 |

25 | ); 26 | }) 27 | ); 28 | 29 | const todo = new Todo(); 30 | 31 | const TodoView = observer(({ todo }) => { 32 | return ( 33 |

34 | Todo View: "{todo.title}" {'' + todo.finished} 35 |

36 | ); 37 | }); 38 | 39 | const HookView = ({ todo }) => { 40 | return useObserver(() => { 41 | return ( 42 |

43 | Todo View: "{todo.title}" {'' + todo.finished} 44 |

45 | ); 46 | }); 47 | }; 48 | 49 | export function MobXDemo() { 50 | const ref = useRef(null); 51 | let [v, set] = useState(0); 52 | 53 | const success = ref.current && ref.current.nodeName === 'P'; 54 | 55 | return ( 56 |
57 | { 61 | todo.title = e.target.value; 62 | set(v + 1); 63 | }} 64 | /> 65 |

66 | 67 | {success ? 'SUCCESS' : 'FAIL'} 68 | 69 |

70 | 71 | 72 | 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /demo/nested-suspense/addnewcomponent.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function AddNewComponent({ appearance }) { 4 | return
AddNewComponent (component #{appearance})
; 5 | } 6 | -------------------------------------------------------------------------------- /demo/nested-suspense/component-container.jsx: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | const pause = timeout => 4 | new Promise(d => setTimeout(d, timeout), console.log(timeout)); 5 | 6 | const SubComponent = lazy(() => 7 | pause(Math.random() * 1000).then(() => import('./subcomponent.jsx')) 8 | ); 9 | 10 | export default function ComponentContainer({ appearance }) { 11 | return ( 12 |
13 | GenerateComponents (component #{appearance}) 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /demo/nested-suspense/dropzone.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function DropZone({ appearance }) { 4 | return
DropZone (component #{appearance})
; 5 | } 6 | -------------------------------------------------------------------------------- /demo/nested-suspense/editor.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function Editor({ children }) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /demo/nested-suspense/index.jsx: -------------------------------------------------------------------------------- 1 | import { createElement, Suspense, lazy, Component } from 'react'; 2 | 3 | const Loading = function () { 4 | return
Loading...
; 5 | }; 6 | const Error = function ({ resetState }) { 7 | return ( 8 |
9 | Error!  10 | 11 | Reset app 12 | 13 |
14 | ); 15 | }; 16 | 17 | const pause = timeout => 18 | new Promise(d => setTimeout(d, timeout), console.log(timeout)); 19 | 20 | const DropZone = lazy(() => 21 | pause(Math.random() * 1000).then(() => import('./dropzone.jsx')) 22 | ); 23 | const Editor = lazy(() => 24 | pause(Math.random() * 1000).then(() => import('./editor.jsx')) 25 | ); 26 | const AddNewComponent = lazy(() => 27 | pause(Math.random() * 1000).then(() => import('./addnewcomponent.jsx')) 28 | ); 29 | const GenerateComponents = lazy(() => 30 | pause(Math.random() * 1000).then(() => import('./component-container.jsx')) 31 | ); 32 | 33 | export default class App extends Component { 34 | state = { hasError: false }; 35 | 36 | static getDerivedStateFromError(error) { 37 | // Update state so the next render will show the fallback UI. 38 | console.warn(error); 39 | return { hasError: true }; 40 | } 41 | 42 | render() { 43 | return this.state.hasError ? ( 44 | this.setState({ hasError: false })} /> 45 | ) : ( 46 | }> 47 | 48 | 49 |
50 | }> 51 | 52 | 53 | 54 |
55 | 63 |
64 | 65 |
Footer here
66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demo/nested-suspense/subcomponent.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function SubComponent({ onClick }) { 4 | return
Lazy loaded sub component
; 5 | } 6 | -------------------------------------------------------------------------------- /demo/old.js.bak: -------------------------------------------------------------------------------- 1 | 2 | // function createRoot(title) { 3 | // let div = document.createElement('div'); 4 | // let h2 = document.createElement('h2'); 5 | // h2.textContent = title; 6 | // div.appendChild(h2); 7 | // document.body.appendChild(div); 8 | // return div; 9 | // } 10 | 11 | 12 | /* 13 | function logCall(obj, method, name) { 14 | let old = obj[method]; 15 | obj[method] = function(...args) { 16 | console.log(`<${this.localName}>.`+(name || `${method}(${args})`)); 17 | return old.apply(this, args); 18 | }; 19 | } 20 | 21 | logCall(HTMLElement.prototype, 'appendChild'); 22 | logCall(HTMLElement.prototype, 'removeChild'); 23 | logCall(HTMLElement.prototype, 'insertBefore'); 24 | logCall(HTMLElement.prototype, 'replaceChild'); 25 | logCall(HTMLElement.prototype, 'setAttribute'); 26 | logCall(HTMLElement.prototype, 'removeAttribute'); 27 | let d = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue'); 28 | Object.defineProperty(Text.prototype, 'nodeValue', { 29 | get() { 30 | let value = d.get.call(this); 31 | console.log('get Text#nodeValue: ', value); 32 | return value; 33 | }, 34 | set(v) { 35 | console.log('set Text#nodeValue', v); 36 | return d.set.call(this, v); 37 | } 38 | }); 39 | 40 | 41 | render(( 42 |
43 |

This is a test.

44 | 45 | 46 |
47 | ), createRoot('Stateful component update demo:')); 48 | 49 | 50 | class Foo extends Component { 51 | componentDidMount() { 52 | console.log('mounted'); 53 | this.timer = setInterval( () => { 54 | this.setState({ time: Date.now() }); 55 | }, 5000); 56 | } 57 | componentWillUnmount() { 58 | clearInterval(this.timer); 59 | } 60 | render(props, state, context) { 61 | // console.log('rendering', props, state, context); 62 | return 63 | } 64 | } 65 | 66 | 67 | render(( 68 |
69 |

This is a test.

70 | 71 | 72 |
73 | ), createRoot('Stateful component update demo:')); 74 | 75 | 76 | let items = []; 77 | let count = 0; 78 | let three = createRoot('Top-level render demo:'); 79 | 80 | setInterval( () => { 81 | if (count++ %20 < 10 ) { 82 | items.push(
  • item #{items.length}
  • ); 87 | } 88 | else { 89 | items.shift(); 90 | } 91 | 92 | render(( 93 |
    94 |

    This is a test.

    95 | 96 |
      {items}
    97 |
    98 | ), three); 99 | }, 5000); 100 | 101 | // Mount the top-level component to the DOM: 102 | render(
    , document.body); 103 | */ 104 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "vite", 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55", 12 | "@babel/plugin-proposal-decorators": "^7.4.0", 13 | "vite": "^5.4.10" 14 | }, 15 | "dependencies": { 16 | "@material-ui/core": "4.9.5", 17 | "@reduxjs/toolkit": "^2.2.3", 18 | "d3-scale": "^1.0.7", 19 | "d3-selection": "^1.2.0", 20 | "htm": "2.1.1", 21 | "mobx": "^5.15.4", 22 | "mobx-react": "^6.2.2", 23 | "mobx-state-tree": "^3.16.0", 24 | "preact-render-to-string": "^5.0.2", 25 | "preact-router": "^3.0.0", 26 | "react-redux": "^7.1.0", 27 | "react-router": "^5.0.1", 28 | "react-router-dom": "^5.0.1", 29 | "redux": "^4.0.1", 30 | "styled-components": "^4.2.0", 31 | "zustand": "^4.5.2" 32 | }, 33 | "volta": { 34 | "extends": "../package.json" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/people/Readme.md: -------------------------------------------------------------------------------- 1 | # People demo page 2 | 3 | This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app 4 | -------------------------------------------------------------------------------- /demo/people/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react'; 2 | import { Component } from 'preact'; 3 | import { Profile } from './profile'; 4 | import { Link, Route, Router } from './router'; 5 | import { store } from './store'; 6 | 7 | import './styles/index.scss'; 8 | 9 | @observer 10 | export default class App extends Component { 11 | componentDidMount() { 12 | store.loadUsers().catch(console.error); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 |
    19 | 50 |
    51 | 52 | 53 | 54 |
    55 |
    56 |
    57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo/people/profile.tsx: -------------------------------------------------------------------------------- 1 | import { computed, observable } from 'mobx'; 2 | import { observer } from 'mobx-react'; 3 | import { Component } from 'preact'; 4 | import { RouteChildProps } from './router'; 5 | import { store } from './store'; 6 | 7 | export type ProfileProps = RouteChildProps; 8 | @observer 9 | export class Profile extends Component { 10 | @observable id = ''; 11 | @observable busy = false; 12 | 13 | componentDidMount() { 14 | this.id = this.props.route; 15 | } 16 | 17 | componentWillReceiveProps(props: ProfileProps) { 18 | this.id = props.route; 19 | } 20 | 21 | render() { 22 | const user = this.user; 23 | if (user == null) return null; 24 | return ( 25 |
    26 | 27 |

    28 | {user.name.first} {user.name.last} 29 |

    30 |
    31 |

    32 | {user.gender === 'female' ? '👩' : '👨'} {user.id} 33 |

    34 |

    🖂 {user.email}

    35 |
    36 |

    37 | 44 |

    45 |
    46 | ); 47 | } 48 | 49 | @computed get user() { 50 | return store.users.find(u => u.id === this.id); 51 | } 52 | 53 | remove = async () => { 54 | this.busy = true; 55 | await new Promise(cb => setTimeout(cb, 1500)); 56 | store.deleteUser(this.id); 57 | this.busy = false; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /demo/people/store.ts: -------------------------------------------------------------------------------- 1 | import { flow, Instance, types } from 'mobx-state-tree'; 2 | 3 | const cmp = 4 | (fn: (x: T) => U) => 5 | (a: T, b: T): number => 6 | fn(a) > fn(b) ? 1 : -1; 7 | 8 | const User = types.model({ 9 | email: types.string, 10 | gender: types.enumeration(['male', 'female']), 11 | id: types.identifier, 12 | name: types.model({ 13 | first: types.string, 14 | last: types.string 15 | }), 16 | picture: types.model({ 17 | large: types.string 18 | }) 19 | }); 20 | 21 | const Store = types 22 | .model({ 23 | users: types.array(User), 24 | usersOrder: types.enumeration(['name', 'id']) 25 | }) 26 | .views(self => ({ 27 | getSortedUsers() { 28 | if (self.usersOrder === 'name') 29 | return self.users.slice().sort(cmp(x => x.name.first)); 30 | if (self.usersOrder === 'id') 31 | return self.users.slice().sort(cmp(x => x.id)); 32 | throw Error(`Unknown ordering ${self.usersOrder}`); 33 | } 34 | })) 35 | .actions(self => ({ 36 | addUser: flow(function* () { 37 | const data = yield fetch('https://randomuser.me/api?results=1') 38 | .then(res => res.json()) 39 | .then(data => 40 | data.results.map((user: any) => ({ 41 | ...user, 42 | id: user.login.username 43 | })) 44 | ); 45 | self.users.push(...data); 46 | }), 47 | loadUsers: flow(function* () { 48 | const data = yield fetch( 49 | `https://randomuser.me/api?seed=${12321}&results=12` 50 | ) 51 | .then(res => res.json()) 52 | .then(data => 53 | data.results.map((user: any) => ({ 54 | ...user, 55 | id: user.login.username 56 | })) 57 | ); 58 | self.users.replace(data); 59 | }), 60 | deleteUser(id: string) { 61 | const user = self.users.find(u => u.id === id); 62 | if (user != null) self.users.remove(user); 63 | }, 64 | setUsersOrder(order: 'name' | 'id') { 65 | self.usersOrder = order; 66 | } 67 | })); 68 | 69 | export type StoreType = Instance; 70 | export const store = Store.create({ 71 | usersOrder: 'name', 72 | users: [] 73 | }); 74 | 75 | // const { Provider, Consumer } = createContext(undefined as any) 76 | 77 | // export const StoreProvider: FunctionalComponent = props => { 78 | // const store = Store.create({}) 79 | // return 80 | // } 81 | 82 | // export type StoreProps = {store: StoreType} 83 | // export function injectStore(Child: AnyComponent): FunctionalComponent { 84 | // return props => }/> 85 | // } 86 | -------------------------------------------------------------------------------- /demo/people/styles/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes popup { 2 | from { 3 | box-shadow: 0 0 0 black; 4 | opacity: 0; 5 | transform: scale(0.9); 6 | } 7 | to { 8 | box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5); 9 | opacity: 1; 10 | transform: none; 11 | } 12 | } 13 | 14 | @keyframes zoom { 15 | from { 16 | opacity: 0; 17 | transform: scale(0.8); 18 | } 19 | to { 20 | opacity: 1; 21 | transform: none; 22 | } 23 | } 24 | 25 | @keyframes appear-from-left { 26 | from { 27 | opacity: 0; 28 | transform: translateX(-25px); 29 | } 30 | to { 31 | opacity: 1; 32 | transform: none; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/people/styles/app.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | position: relative; 3 | overflow: hidden; 4 | min-height: 100vh; 5 | animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards; 6 | background: var(--app-background); 7 | --menu-width: 260px; 8 | --menu-item-height: 50px; 9 | 10 | @media (min-width: 1280px) { 11 | max-width: 1280px; 12 | min-height: calc(100vh - 64px); 13 | margin: 32px auto; 14 | border-radius: 10px; 15 | } 16 | 17 | > nav { 18 | position: absolute; 19 | display: flow-root; 20 | width: var(--menu-width); 21 | height: 100%; 22 | background-color: var(--app-background-secondary); 23 | overflow-x: hidden; 24 | overflow-y: auto; 25 | } 26 | 27 | > nav h4 { 28 | padding-left: 16px; 29 | font-weight: normal; 30 | text-transform: uppercase; 31 | } 32 | 33 | > nav ul { 34 | position: relative; 35 | } 36 | 37 | > nav li { 38 | position: absolute; 39 | width: 100%; 40 | animation: zoom 200ms forwards; 41 | opacity: 0; 42 | transition: top 200ms; 43 | } 44 | 45 | > nav li > a { 46 | position: relative; 47 | display: flex; 48 | overflow: hidden; 49 | flex-flow: row; 50 | align-items: center; 51 | margin-left: 16px; 52 | border-right: 2px solid transparent; 53 | border-bottom-left-radius: 48px; 54 | border-top-left-radius: 48px; 55 | text-transform: capitalize; 56 | transition: border 500ms; 57 | } 58 | 59 | > nav li > a:hover { 60 | background-color: var(--app-highlight); 61 | } 62 | 63 | > nav li > a::after { 64 | position: absolute; 65 | top: 0; 66 | right: -2px; 67 | bottom: 0; 68 | left: 0; 69 | background-image: radial-gradient( 70 | circle, 71 | var(--app-ripple) 1%, 72 | transparent 1% 73 | ); 74 | background-position: center; 75 | background-repeat: no-repeat; 76 | background-size: 10000%; 77 | content: ''; 78 | opacity: 0; 79 | transition: opacity 700ms, background 300ms; 80 | } 81 | 82 | > nav li > a:active::after { 83 | background-size: 100%; 84 | opacity: 0.5; 85 | transition: none; 86 | } 87 | 88 | > nav li > a.active { 89 | border-color: var(--app-primary); 90 | background-color: var(--app-highlight); 91 | } 92 | 93 | > nav li > a > * { 94 | margin: 8px; 95 | } 96 | 97 | #people-main { 98 | padding-left: var(--menu-width); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo/people/styles/avatar.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | .avatar { 3 | display: inline-block; 4 | overflow: hidden; 5 | width: var(--avatar-size, 32px); 6 | height: var(--avatar-size, 32px); 7 | background-color: var(--avatar-color, var(--app-primary)); 8 | border-radius: 50%; 9 | font-size: calc(var(--avatar-size, 32px) * 0.5); 10 | line-height: var(--avatar-size, 32px); 11 | object-fit: cover; 12 | text-align: center; 13 | text-transform: uppercase; 14 | white-space: nowrap; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/people/styles/profile.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | .profile { 3 | display: flex; 4 | flex-flow: column; 5 | align-items: center; 6 | margin: 32px 0; 7 | animation: appear-from-left 0.5s forwards; 8 | --avatar-size: 80px; 9 | } 10 | 11 | .profile h2 { 12 | text-transform: capitalize; 13 | } 14 | 15 | .profile .details { 16 | display: flex; 17 | flex-flow: column; 18 | align-items: stretch; 19 | margin: 16px auto; 20 | } 21 | 22 | .profile .details p { 23 | margin-top: 8px; 24 | margin-bottom: 8px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/preact.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | options, 3 | createElement, 4 | cloneElement, 5 | Component as CevicheComponent, 6 | render 7 | } from 'preact'; 8 | 9 | options.vnode = vnode => { 10 | vnode.nodeName = vnode.type; 11 | vnode.attributes = vnode.props; 12 | vnode.children = vnode._children || [].concat(vnode.props.children || []); 13 | }; 14 | 15 | function asArray(arr) { 16 | return Array.isArray(arr) ? arr : [arr]; 17 | } 18 | 19 | function normalize(obj) { 20 | if (Array.isArray(obj)) { 21 | return obj.map(normalize); 22 | } 23 | if ('type' in obj && !('attributes' in obj)) { 24 | obj.attributes = obj.props; 25 | } 26 | return obj; 27 | } 28 | 29 | export function Component(props, context) { 30 | CevicheComponent.call(this, props, context); 31 | const render = this.render; 32 | this.render = function (props, state, context) { 33 | if (props.children) props.children = asArray(normalize(props.children)); 34 | return render.call(this, props, state, context); 35 | }; 36 | } 37 | Component.prototype = new CevicheComponent(); 38 | 39 | export { createElement, createElement as h, cloneElement, render }; 40 | -------------------------------------------------------------------------------- /demo/profiler.jsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component, options } from 'preact'; 2 | 3 | function getPrimes(max) { 4 | let sieve = [], 5 | i, 6 | j, 7 | primes = []; 8 | for (i = 2; i <= max; ++i) { 9 | if (!sieve[i]) { 10 | // i has not been marked -- it is prime 11 | primes.push(i); 12 | for (j = i << 1; j <= max; j += i) { 13 | sieve[j] = true; 14 | } 15 | } 16 | } 17 | return primes.join(''); 18 | } 19 | 20 | function Foo(props) { 21 | return
    {props.children}
    ; 22 | } 23 | 24 | function Bar() { 25 | getPrimes(10000); 26 | return ( 27 |
    28 | ...yet another component 29 |
    30 | ); 31 | } 32 | 33 | function PrimeNumber(props) { 34 | // Slow down rendering of this component 35 | getPrimes(10); 36 | 37 | return ( 38 |
    39 | I'm a slow component 40 |
    41 | {props.children} 42 |
    43 | ); 44 | } 45 | 46 | export default class ProfilerDemo extends Component { 47 | constructor() { 48 | super(); 49 | this.onClick = this.onClick.bind(this); 50 | this.state = { counter: 0 }; 51 | } 52 | 53 | componentDidMount() { 54 | options._diff = vnode => (vnode.startTime = performance.now()); 55 | options.diffed = vnode => (vnode.endTime = performance.now()); 56 | } 57 | 58 | componentWillUnmount() { 59 | delete options._diff; 60 | delete options.diffed; 61 | } 62 | 63 | onClick() { 64 | this.setState(prev => ({ counter: ++prev.counter })); 65 | } 66 | 67 | render() { 68 | return ( 69 |
    70 |

    ⚛ Preact

    71 |

    72 | Devtools Profiler integration 🕒 73 |

    74 | 75 | 76 | I'm a fast component 77 | 78 | 79 | 80 | I'm the fastest component 🎉 81 | Counter: {this.state.counter} 82 |
    83 |
    84 | 85 |
    86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /demo/pythagoras/index.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | import { select as d3select, mouse as d3mouse } from 'd3-selection'; 3 | import { scaleLinear } from 'd3-scale'; 4 | import Pythagoras from './pythagoras'; 5 | 6 | export default class PythagorasDemo extends Component { 7 | svg = { 8 | width: 1280, 9 | height: 600 10 | }; 11 | 12 | state = { 13 | currentMax: 0, 14 | baseW: 80, 15 | heightFactor: 0, 16 | lean: 0 17 | }; 18 | 19 | realMax = 11; 20 | 21 | svgRef = c => { 22 | this.svgElement = c; 23 | }; 24 | 25 | scaleFactor = scaleLinear().domain([this.svg.height, 0]).range([0, 0.8]); 26 | 27 | scaleLean = scaleLinear() 28 | .domain([0, this.svg.width / 2, this.svg.width]) 29 | .range([0.5, 0, -0.5]); 30 | 31 | onMouseMove = event => { 32 | let [x, y] = d3mouse(this.svgElement); 33 | 34 | this.setState({ 35 | heightFactor: this.scaleFactor(y), 36 | lean: this.scaleLean(x) 37 | }); 38 | }; 39 | 40 | restart = () => { 41 | this.setState({ currentMax: 0 }); 42 | this.next(); 43 | }; 44 | 45 | next = () => { 46 | let { currentMax } = this.state; 47 | 48 | if (currentMax < this.realMax) { 49 | this.setState({ currentMax: currentMax + 1 }); 50 | this.timer = setTimeout(this.next, 500); 51 | } 52 | }; 53 | 54 | componentDidMount() { 55 | this.selected = d3select(this.svgElement).on('mousemove', this.onMouseMove); 56 | this.next(); 57 | } 58 | 59 | componentWillUnmount() { 60 | this.selected.on('mousemove', null); 61 | clearTimeout(this.timer); 62 | } 63 | 64 | render({}, { currentMax, baseW, heightFactor, lean }) { 65 | let { width, height } = this.svg; 66 | 67 | return ( 68 |
    69 | 70 | 80 | 81 |
    82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /demo/pythagoras/pythagoras.jsx: -------------------------------------------------------------------------------- 1 | import { interpolateViridis } from 'd3-scale'; 2 | 3 | Math.deg = function (radians) { 4 | return radians * (180 / Math.PI); 5 | }; 6 | 7 | const memoizedCalc = (function () { 8 | const memo = {}; 9 | 10 | const key = ({ w, heightFactor, lean }) => `${w}-${heightFactor}-${lean}`; 11 | 12 | return args => { 13 | let memoKey = key(args); 14 | 15 | if (memo[memoKey]) { 16 | return memo[memoKey]; 17 | } 18 | 19 | let { w, heightFactor, lean } = args; 20 | let trigH = heightFactor * w; 21 | 22 | let result = { 23 | nextRight: Math.sqrt(trigH ** 2 + (w * (0.5 + lean)) ** 2), 24 | nextLeft: Math.sqrt(trigH ** 2 + (w * (0.5 - lean)) ** 2), 25 | A: Math.deg(Math.atan(trigH / ((0.5 - lean) * w))), 26 | B: Math.deg(Math.atan(trigH / ((0.5 + lean) * w))) 27 | }; 28 | 29 | memo[memoKey] = result; 30 | return result; 31 | }; 32 | })(); 33 | 34 | export default function Pythagoras({ 35 | w, 36 | x, 37 | y, 38 | heightFactor, 39 | lean, 40 | left, 41 | right, 42 | lvl, 43 | maxlvl 44 | }) { 45 | if (lvl >= maxlvl || w < 1) { 46 | return null; 47 | } 48 | 49 | const { nextRight, nextLeft, A, B } = memoizedCalc({ 50 | w, 51 | heightFactor, 52 | lean 53 | }); 54 | 55 | let rotate = ''; 56 | 57 | if (left) { 58 | rotate = `rotate(${-A} 0 ${w})`; 59 | } else if (right) { 60 | rotate = `rotate(${B} ${w} ${w})`; 61 | } 62 | 63 | return ( 64 | 65 | 72 | 73 | 83 | 84 | 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /demo/redux-toolkit.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import { Provider, useSelector } from 'react-redux'; 3 | import { configureStore, createSlice } from '@reduxjs/toolkit'; 4 | 5 | const initialState = { 6 | value: 0 7 | }; 8 | const counterSlice = createSlice({ 9 | name: 'counter', 10 | initialState, 11 | reducers: { 12 | increment: state => { 13 | state.value += 1; 14 | }, 15 | decrement: state => { 16 | state.value -= 1; 17 | } 18 | } 19 | }); 20 | const store = configureStore({ 21 | reducer: { 22 | counter: counterSlice.reducer 23 | } 24 | }); 25 | 26 | function Counter({ number }) { 27 | const count = useSelector(state => state.counter.value); 28 | return ( 29 |
    30 | Counter #{number}:{count} 31 |
    32 | ); 33 | } 34 | 35 | export default function ReduxToolkit() { 36 | function increment() { 37 | store.dispatch(counterSlice.actions.increment()); 38 | } 39 | function decrement() { 40 | store.dispatch(counterSlice.actions.decrement()); 41 | } 42 | function incrementAsync() { 43 | setTimeout(() => { 44 | store.dispatch(counterSlice.actions.increment()); 45 | }, 1000); 46 | } 47 | return ( 48 | 49 |
    50 |

    Redux Toolkit

    51 |

    Counter

    52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /demo/redux.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import React from 'react'; 3 | import { createStore } from 'redux'; 4 | import { connect, Provider } from 'react-redux'; 5 | 6 | const store = createStore((state = { value: 0 }, action) => { 7 | switch (action.type) { 8 | case 'increment': 9 | return { value: state.value + 1 }; 10 | case 'decrement': 11 | return { value: state.value - 1 }; 12 | default: 13 | return state; 14 | } 15 | }); 16 | 17 | class Child extends React.Component { 18 | render() { 19 | return ( 20 |
    21 |
    Child #1: {this.props.foo}
    22 | 23 |
    24 | ); 25 | } 26 | } 27 | const ConnectedChild = connect(store => ({ foo: store.value }))(Child); 28 | 29 | class Child2 extends React.Component { 30 | render() { 31 | return
    Child #2: {this.props.foo}
    ; 32 | } 33 | } 34 | const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2); 35 | 36 | export default function Redux() { 37 | return ( 38 |
    39 |

    Counter

    40 | 41 | 42 | 43 |
    44 | 45 | 46 |
    47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /demo/reduxUpdate.jsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | import { connect, Provider } from 'react-redux'; 3 | import { createStore } from 'redux'; 4 | import { HashRouter, Route, Link } from 'react-router-dom'; 5 | 6 | const store = createStore( 7 | (state, action) => ({ ...state, display: action.display }), 8 | { display: false } 9 | ); 10 | 11 | function _Redux({ showMe, counter }) { 12 | if (!showMe) return null; 13 | return
    showMe {counter}
    ; 14 | } 15 | const Redux = connect( 16 | state => console.log('injecting', state.display) || { showMe: state.display } 17 | )(_Redux); 18 | 19 | let display = false; 20 | class Test extends Component { 21 | componentDidUpdate(prevProps) { 22 | if (this.props.start != prevProps.start) { 23 | this.setState({ f: (this.props.start || 0) + 1 }); 24 | setTimeout(() => this.setState({ i: (this.state.i || 0) + 1 })); 25 | } 26 | } 27 | 28 | render() { 29 | const { f } = this.state; 30 | return ( 31 |
    32 | 40 | Click me 41 | 42 | 43 |
    44 | ); 45 | } 46 | } 47 | 48 | function App() { 49 | return ( 50 | 51 | 52 | } 55 | /> 56 | 57 | 58 | ); 59 | } 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /demo/stateOrderBug.jsx: -------------------------------------------------------------------------------- 1 | import htm from 'htm'; 2 | import { h } from 'preact'; 3 | import { useState, useCallback } from 'preact/hooks'; 4 | 5 | const html = htm.bind(h); 6 | 7 | // configuration used to show behavior vs. workaround 8 | let childFirst = true; 9 | const Config = () => html` 10 | 20 | `; 21 | 22 | const Child = ({ items, setItems }) => { 23 | let [pendingId, setPendingId] = useState(null); 24 | if (!pendingId) { 25 | setPendingId((pendingId = Math.random().toFixed(20).slice(2))); 26 | } 27 | 28 | const onInput = useCallback( 29 | evt => { 30 | let val = evt.target.value, 31 | _items = [...items, { _id: pendingId, val }]; 32 | if (childFirst) { 33 | setPendingId(null); 34 | setItems(_items); 35 | } else { 36 | setItems(_items); 37 | setPendingId(null); 38 | } 39 | }, 40 | [childFirst, setPendingId, setItems, items, pendingId] 41 | ); 42 | 43 | return html` 44 |
    45 | ${items.map( 46 | (item, idx) => html` 47 | { 51 | let val = evt.target.value, 52 | _items = [...items]; 53 | _items.splice(idx, 1, { ...item, val }); 54 | setItems(_items); 55 | }} 56 | /> 57 | ` 58 | )} 59 | 60 | 65 |
    66 | `; 67 | }; 68 | 69 | const Parent = () => { 70 | let [items, setItems] = useState([]); 71 | return html` 72 |
    <${Config} /><${Child} items=${items} setItems=${setItems} />
    73 | `; 74 | }; 75 | 76 | export default Parent; 77 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font: 14px system-ui, sans-serif; 4 | } 5 | .list { 6 | list-style: none; 7 | padding: 0; 8 | } 9 | .list > li { 10 | position: relative; 11 | padding: 5px 10px; 12 | animation: fadeIn 1s ease; 13 | } 14 | @keyframes fadeIn { 15 | 0% { 16 | box-shadow: inset 0 0 2px 2px red, 0 0 2px 2px red; 17 | } 18 | } 19 | .list > .odd { 20 | background-color: #def; 21 | } 22 | .list > .even { 23 | background-color: #fed; 24 | } 25 | -------------------------------------------------------------------------------- /demo/styled-components.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | const Button = styled.button` 5 | background: transparent; 6 | border-radius: 3px; 7 | border: 2px solid palevioletred; 8 | color: palevioletred; 9 | margin: 0.5em 1em; 10 | padding: 0.25em 1em; 11 | 12 | ${props => 13 | props.primary && 14 | css` 15 | background: palevioletred; 16 | color: white; 17 | `} 18 | `; 19 | 20 | const Container = styled.div` 21 | text-align: center; 22 | `; 23 | 24 | export default function StyledComp() { 25 | return ( 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /demo/suspense-router/bye.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from './simple-router'; 2 | 3 | export default function Bye() { 4 | return ( 5 |
    6 | Bye! Go to Hello! 7 |
    8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /demo/suspense-router/hello.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from './simple-router'; 2 | 3 | export default function Hello() { 4 | return ( 5 |
    6 | Hello! Go to Bye! 7 |
    8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /demo/suspense-router/index.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from 'react'; 2 | 3 | import { Router, Route, Switch } from './simple-router'; 4 | 5 | let Hello = lazy(() => import('./hello.jsx')); 6 | let Bye = lazy(() => import('./bye.jsx')); 7 | 8 | function Loading() { 9 | return
    Hey! This is a fallback because we're loading things! :D
    ; 10 | } 11 | 12 | export default function SuspenseRouterBug() { 13 | return ( 14 | 15 |

    Suspense Router bug

    16 | }> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /demo/suspense-router/simple-router.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useState, 4 | useContext, 5 | Children, 6 | useLayoutEffect 7 | } from 'react'; 8 | 9 | const memoryHistory = { 10 | /** 11 | * @typedef {{ pathname: string }} Location 12 | * @typedef {(location: Location) => void} HistoryListener 13 | * @type {HistoryListener[]} 14 | */ 15 | listeners: [], 16 | 17 | /** 18 | * @param {HistoryListener} listener 19 | */ 20 | listen(listener) { 21 | const newLength = this.listeners.push(listener); 22 | return () => this.listeners.splice(newLength - 1, 1); 23 | }, 24 | 25 | /** 26 | * @param {Location} to 27 | */ 28 | navigate(to) { 29 | this.listeners.forEach(listener => listener(to)); 30 | } 31 | }; 32 | 33 | /** @type {import('react').Context<{ history: typeof memoryHistory; location: Location }>} */ 34 | const RouterContext = createContext(null); 35 | 36 | export function Router({ history = memoryHistory, children }) { 37 | const [location, setLocation] = useState({ pathname: '/' }); 38 | 39 | useLayoutEffect(() => { 40 | return history.listen(newLocation => setLocation(newLocation)); 41 | }, []); 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | } 49 | 50 | export function Switch(props) { 51 | const { location } = useContext(RouterContext); 52 | 53 | let element = null; 54 | Children.forEach(props.children, child => { 55 | if (element == null && child.props.path == location.pathname) { 56 | element = child; 57 | } 58 | }); 59 | 60 | return element; 61 | } 62 | 63 | /** 64 | * @param {{ children: any; path: string; exact?: boolean; }} props 65 | */ 66 | export function Route({ children, path, exact }) { 67 | return children; 68 | } 69 | 70 | export function Link({ to, children }) { 71 | const { history } = useContext(RouterContext); 72 | const onClick = event => { 73 | event.preventDefault(); 74 | event.stopPropagation(); 75 | history.navigate({ pathname: to }); 76 | }; 77 | 78 | return ( 79 | 80 | {children} 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /demo/suspense.jsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { 3 | createElement, 4 | Component, 5 | memo, 6 | Fragment, 7 | Suspense, 8 | lazy 9 | } from 'react'; 10 | 11 | function LazyComp() { 12 | return
    I'm (fake) lazy loaded
    ; 13 | } 14 | 15 | const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); 16 | 17 | function createSuspension(name, timeout, error) { 18 | let done = false; 19 | let prom; 20 | 21 | return { 22 | name, 23 | timeout, 24 | start: () => { 25 | if (!prom) { 26 | prom = new Promise((res, rej) => { 27 | setTimeout(() => { 28 | done = true; 29 | if (error) { 30 | rej(error); 31 | } else { 32 | res(); 33 | } 34 | }, timeout); 35 | }); 36 | } 37 | 38 | return prom; 39 | }, 40 | getPromise: () => prom, 41 | isDone: () => done 42 | }; 43 | } 44 | 45 | function CustomSuspense({ isDone, start, timeout, name }) { 46 | if (!isDone()) { 47 | throw start(); 48 | } 49 | 50 | return ( 51 |
    52 | Hello from CustomSuspense {name}, loaded after {timeout / 1000}s 53 |
    54 | ); 55 | } 56 | 57 | function init() { 58 | return { 59 | s1: createSuspension('1', 1000, null), 60 | s2: createSuspension('2', 2000, null), 61 | s3: createSuspension('3', 3000, null) 62 | }; 63 | } 64 | 65 | export default class DevtoolsDemo extends Component { 66 | constructor(props) { 67 | super(props); 68 | this.state = init(); 69 | this.onRerun = this.onRerun.bind(this); 70 | } 71 | 72 | onRerun() { 73 | this.setState(init()); 74 | } 75 | 76 | render(props, state) { 77 | return ( 78 |
    79 |

    lazy()

    80 | Loading (fake) lazy loaded component...
    }> 81 | 82 | 83 |

    Suspense

    84 |
    85 | 86 |
    87 | Fallback 1
    }> 88 | 89 | Fallback 2
    }> 90 | 91 | 92 | 93 | 94 |
    95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /demo/textFields.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | 4 | const PatchedTextField = props => { 5 | const [value, set] = useState(props.value); 6 | return ( 7 | set(e.target.value)} /> 8 | ); 9 | }; 10 | 11 | const TextFields = () => ( 12 |
    13 | 19 | 25 | 32 |
    33 | ); 34 | 35 | export default TextFields; 36 | -------------------------------------------------------------------------------- /demo/todo.jsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | 3 | let counter = 0; 4 | 5 | export default class TodoList extends Component { 6 | state = { todos: [], text: '' }; 7 | 8 | setText = e => { 9 | this.setState({ text: e.target.value }); 10 | }; 11 | 12 | addTodo = () => { 13 | let { todos, text } = this.state; 14 | todos = todos.concat({ text, id: ++counter }); 15 | this.setState({ todos, text: '' }); 16 | }; 17 | 18 | removeTodo = e => { 19 | let id = e.target.getAttribute('data-id'); 20 | this.setState({ todos: this.state.todos.filter(t => t.id != id) }); 21 | }; 22 | 23 | render({}, { todos, text }) { 24 | return ( 25 |
    26 | 27 | 28 |
      29 | 30 |
    31 |
    32 | ); 33 | } 34 | } 35 | 36 | class TodoItems extends Component { 37 | render({ todos, removeTodo }) { 38 | return todos.map(todo => ( 39 |
  • 40 | {' '} 43 | {todo.text} 44 |
  • 45 | )); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "jsx": "react", 5 | "jsxFactory": "h", 6 | "baseUrl": ".", 7 | "target": "es2018", 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "preact/hooks": ["../hooks/src/index.js"], 12 | "preact": ["../src/index.js"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'node:path'; 3 | 4 | const root = path.join(__dirname, '..'); 5 | const resolvePkg = (...parts) => path.join(root, ...parts, 'src', 'index.js'); 6 | 7 | // https://vitejs.dev/config/ 8 | /** @type {import('vite').UserConfig} */ 9 | export default defineConfig({ 10 | optimizeDeps: { 11 | exclude: [ 12 | 'preact', 13 | 'preact/compat', 14 | 'preact/debug', 15 | 'preact/hooks', 16 | 'preact/devtools', 17 | 'preact/jsx-runtime', 18 | 'preact/jsx-dev-runtime', 19 | 'preact-router', 20 | 'react', 21 | 'react-dom' 22 | ] 23 | }, 24 | resolve: { 25 | alias: { 26 | 'preact/debug/src/debug': path.join(root, 'debug', 'src', 'debug'), 27 | 'preact/devtools/src/devtools': path.join( 28 | root, 29 | 'devtools', 30 | 'src', 31 | 'devtools' 32 | ), 33 | //'preact/debug': resolvePkg('debug'), 34 | 'preact/devtools': resolvePkg('devtools'), 35 | 'preact/hooks': resolvePkg('hooks'), 36 | 'preact/jsx-runtime': resolvePkg('jsx-runtime'), 37 | 'preact/jsx-dev-runtime': resolvePkg('jsx-runtime'), 38 | preact: resolvePkg(''), 39 | 'react-dom': resolvePkg('compat'), 40 | react: resolvePkg('compat') 41 | } 42 | }, 43 | esbuild: { 44 | jsx: 'automatic', 45 | jsxImportSource: 'preact' 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /demo/zustand.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import create from 'zustand'; 3 | 4 | const useStore = create(set => ({ 5 | value: 0, 6 | text: 'John', 7 | setText: text => set(state => ({ ...state, text })), 8 | increment: () => set(state => ({ value: state.value + 1 })), 9 | decrement: () => set(state => ({ value: state.value - 1 })), 10 | incrementAsync: async () => { 11 | await new Promise(resolve => setTimeout(resolve, 1000)); 12 | set(state => ({ value: state.value + 1 })); 13 | } 14 | })); 15 | 16 | function Counter({ number }) { 17 | const value = useStore(state => state.value); 18 | return ( 19 |
    20 | Counter #{number}: {value} 21 |
    22 | ); 23 | } 24 | function Text() { 25 | const text = useStore(state => state.text); 26 | const { setText } = useStore(); 27 | function handleInput(e) { 28 | setText(e.target.value); 29 | } 30 | return ( 31 |
    32 | Text: {text} 33 | 34 |
    35 | ); 36 | } 37 | 38 | export default function ZustandComponent() { 39 | const { increment, decrement, incrementAsync } = useStore(); 40 | 41 | return ( 42 |
    43 |

    Zustand

    44 |

    Counter

    45 | 46 | 47 | 48 | 49 | 50 | 51 |
    52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /devtools/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /devtools/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-devtools", 3 | "amdName": "preactDevtools", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact bridge for Preact devtools", 7 | "main": "dist/devtools.js", 8 | "module": "dist/devtools.module.js", 9 | "umd:main": "dist/devtools.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "types": "src/index.d.ts", 13 | "peerDependencies": { 14 | "preact": "^10.0.0" 15 | }, 16 | "exports": { 17 | ".": { 18 | "types": "./src/index.d.ts", 19 | "browser": "./dist/devtools.module.js", 20 | "umd": "./dist/devtools.umd.js", 21 | "import": "./dist/devtools.mjs", 22 | "require": "./dist/devtools.js" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /devtools/src/devtools.js: -------------------------------------------------------------------------------- 1 | import { Component, Fragment, options } from 'preact'; 2 | 3 | export function initDevTools() { 4 | const globalVar = 5 | typeof globalThis !== 'undefined' 6 | ? globalThis 7 | : typeof window !== 'undefined' 8 | ? window 9 | : undefined; 10 | 11 | if ( 12 | globalVar !== null && 13 | globalVar !== undefined && 14 | globalVar.__PREACT_DEVTOOLS__ 15 | ) { 16 | globalVar.__PREACT_DEVTOOLS__.attachPreact('10.26.8', options, { 17 | Fragment, 18 | Component 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devtools/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize the displayed name of a useState, useReducer or useRef hook 3 | * in the devtools panel. 4 | * 5 | * @param value Wrapped native hook. 6 | * @param name Custom name 7 | */ 8 | export function addHookName(value: T, name: string): T; 9 | -------------------------------------------------------------------------------- /devtools/src/index.js: -------------------------------------------------------------------------------- 1 | import { options } from 'preact'; 2 | import { initDevTools } from './devtools'; 3 | 4 | initDevTools(); 5 | 6 | /** 7 | * Display a custom label for a custom hook for the devtools panel 8 | * @type {(value: T, name: string) => T} 9 | */ 10 | export function addHookName(value, name) { 11 | if (options._addHookName) { 12 | options._addHookName(name); 13 | } 14 | return value; 15 | } 16 | -------------------------------------------------------------------------------- /devtools/test/browser/addHookName.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, options } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useState } from 'preact/hooks'; 4 | import { addHookName } from 'preact/devtools'; 5 | 6 | /** @jsx createElement */ 7 | 8 | describe('addHookName', () => { 9 | /** @type {HTMLDivElement} */ 10 | let scratch; 11 | 12 | beforeEach(() => { 13 | scratch = setupScratch(); 14 | }); 15 | 16 | afterEach(() => { 17 | teardown(scratch); 18 | delete options._addHookName; 19 | }); 20 | 21 | it('should do nothing when no options hook is present', () => { 22 | function useFoo() { 23 | return addHookName(useState(0), 'foo'); 24 | } 25 | 26 | function App() { 27 | let [v] = useFoo(); 28 | return
    {v}
    ; 29 | } 30 | 31 | expect(() => render(, scratch)).to.not.throw(); 32 | }); 33 | 34 | it('should call options hook with value', () => { 35 | let spy = (options._addHookName = sinon.spy()); 36 | 37 | function useFoo() { 38 | return addHookName(useState(0), 'foo'); 39 | } 40 | 41 | function App() { 42 | let [v] = useFoo(); 43 | return
    {v}
    ; 44 | } 45 | 46 | render(, scratch); 47 | 48 | expect(spy).to.be.calledOnce; 49 | expect(spy).to.be.calledWith('foo'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /hooks/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /hooks/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-hooks", 3 | "amdName": "preactHooks", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "Hook addon for Preact", 7 | "main": "dist/hooks.js", 8 | "module": "dist/hooks.module.js", 9 | "umd:main": "dist/hooks.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "types": "src/index.d.ts", 13 | "scripts": { 14 | "build": "microbundle build --raw", 15 | "dev": "microbundle watch --raw --format cjs", 16 | "test": "npm-run-all build --parallel test:karma", 17 | "test:karma": "karma start test/karma.conf.js --single-run", 18 | "test:karma:watch": "karma start test/karma.conf.js --no-single-run" 19 | }, 20 | "peerDependencies": { 21 | "preact": "^10.0.0" 22 | }, 23 | "mangle": { 24 | "regex": "^_" 25 | }, 26 | "exports": { 27 | ".": { 28 | "types": "./src/index.d.ts", 29 | "browser": "./dist/hooks.module.js", 30 | "umd": "./dist/hooks.umd.js", 31 | "import": "./dist/hooks.mjs", 32 | "require": "./dist/hooks.js" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /hooks/test/_util/useEffectUtil.js: -------------------------------------------------------------------------------- 1 | export function scheduleEffectAssert(assertFn) { 2 | return new Promise(resolve => { 3 | requestAnimationFrame(() => 4 | setTimeout(() => { 5 | assertFn(); 6 | resolve(); 7 | }, 0) 8 | ); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /hooks/test/browser/componentDidCatch.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { act } from 'preact/test-utils'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | import { useEffect } from 'preact/hooks'; 5 | 6 | /** @jsx createElement */ 7 | 8 | describe('errorInfo', () => { 9 | /** @type {HTMLDivElement} */ 10 | let scratch; 11 | 12 | beforeEach(() => { 13 | scratch = setupScratch(); 14 | }); 15 | 16 | afterEach(() => { 17 | teardown(scratch); 18 | }); 19 | 20 | it('should pass errorInfo on hook unmount error', () => { 21 | let info; 22 | let update; 23 | class Receiver extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { error: null, i: 0 }; 27 | update = this.setState.bind(this); 28 | } 29 | componentDidCatch(error, errorInfo) { 30 | info = errorInfo; 31 | this.setState({ error }); 32 | } 33 | render() { 34 | if (this.state.error) return
    ; 35 | if (this.state.i === 0) return ; 36 | return null; 37 | } 38 | } 39 | 40 | function ThrowErr() { 41 | useEffect(() => { 42 | return () => { 43 | throw new Error('fail'); 44 | }; 45 | }, []); 46 | 47 | return

    ; 48 | } 49 | 50 | act(() => { 51 | render(, scratch); 52 | }); 53 | 54 | act(() => { 55 | update({ i: 1 }); 56 | }); 57 | 58 | expect(info).to.deep.equal({}); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /hooks/test/browser/useCallback.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useCallback } from 'preact/hooks'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('useCallback', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('only recomputes the callback when inputs change', () => { 20 | const callbacks = []; 21 | 22 | function Comp({ a, b }) { 23 | const cb = useCallback(() => a + b, [a, b]); 24 | callbacks.push(cb); 25 | return null; 26 | } 27 | 28 | render(, scratch); 29 | render(, scratch); 30 | 31 | expect(callbacks[0]).to.equal(callbacks[1]); 32 | expect(callbacks[0]()).to.equal(2); 33 | 34 | render(, scratch); 35 | render(, scratch); 36 | 37 | expect(callbacks[1]).to.not.equal(callbacks[2]); 38 | expect(callbacks[2]).to.equal(callbacks[3]); 39 | expect(callbacks[2]()).to.equal(3); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /hooks/test/browser/useDebugValue.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, options } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useDebugValue, useState } from 'preact/hooks'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('useDebugValue', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | delete options.useDebugValue; 18 | }); 19 | 20 | it('should do nothing when no options hook is present', () => { 21 | function useFoo() { 22 | useDebugValue('foo'); 23 | return useState(0); 24 | } 25 | 26 | function App() { 27 | let [v] = useFoo(); 28 | return
    {v}
    ; 29 | } 30 | 31 | expect(() => render(, scratch)).to.not.throw(); 32 | }); 33 | 34 | it('should call options hook with value', () => { 35 | let spy = (options.useDebugValue = sinon.spy()); 36 | 37 | function useFoo() { 38 | useDebugValue('foo'); 39 | return useState(0); 40 | } 41 | 42 | function App() { 43 | let [v] = useFoo(); 44 | return
    {v}
    ; 45 | } 46 | 47 | render(, scratch); 48 | 49 | expect(spy).to.be.calledOnce; 50 | expect(spy).to.be.calledWith('foo'); 51 | }); 52 | 53 | it('should apply optional formatter', () => { 54 | let spy = (options.useDebugValue = sinon.spy()); 55 | 56 | function useFoo() { 57 | useDebugValue('foo', x => x + 'bar'); 58 | return useState(0); 59 | } 60 | 61 | function App() { 62 | let [v] = useFoo(); 63 | return
    {v}
    ; 64 | } 65 | 66 | render(, scratch); 67 | 68 | expect(spy).to.be.calledOnce; 69 | expect(spy).to.be.calledWith('foobar'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /hooks/test/browser/useRef.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useRef } from 'preact/hooks'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('useRef', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('provides a stable reference', () => { 20 | const values = []; 21 | 22 | function Comp() { 23 | const ref = useRef(1); 24 | values.push(ref.current); 25 | ref.current = 2; 26 | return null; 27 | } 28 | 29 | render(, scratch); 30 | render(, scratch); 31 | 32 | expect(values).to.deep.equal([1, 2]); 33 | }); 34 | 35 | it('defaults to undefined', () => { 36 | const values = []; 37 | 38 | function Comp() { 39 | const ref = useRef(); 40 | values.push(ref.current); 41 | ref.current = 2; 42 | return null; 43 | } 44 | 45 | render(, scratch); 46 | render(, scratch); 47 | 48 | expect(values).to.deep.equal([undefined, 2]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /jsconfig-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./jsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "noEmit": true, 6 | "typeRoots": ["./node_modules/@types", "./types"] 7 | }, 8 | "include": [ 9 | "src/**/*", 10 | "hooks/src/**/*", 11 | "compat/**/*.d.ts", 12 | "jsx-runtime/**/*.d.ts", 13 | "types/**/*.d.ts" 14 | ], 15 | "exclude": ["**/node_modules/**", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "checkJs": true, 5 | "jsx": "react", 6 | "jsxFactory": "createElement", 7 | "jsxFragmentFactory": "Fragment", 8 | "lib": ["dom", "es5"], 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "paths": { 12 | "preact": ["."], 13 | "preact/*": ["./*"] 14 | }, 15 | "target": "es5", 16 | "noEmit": true, 17 | "skipLibCheck": false 18 | }, 19 | "exclude": ["**/node_modules/**", "**/dist/**", "coverage", "demo"] 20 | } 21 | -------------------------------------------------------------------------------- /jsx-runtime/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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 | -------------------------------------------------------------------------------- /jsx-runtime/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jsx-runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-runtime", 3 | "amdName": "jsxRuntime", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact JSX runtime", 7 | "main": "dist/jsxRuntime.js", 8 | "module": "dist/jsxRuntime.module.js", 9 | "umd:main": "dist/jsxRuntime.umd.js", 10 | "source": "src/index.js", 11 | "types": "src/index.d.ts", 12 | "license": "MIT", 13 | "peerDependencies": { 14 | "preact": "^10.0.0" 15 | }, 16 | "mangle": { 17 | "regex": "^_" 18 | }, 19 | "exports": { 20 | ".": { 21 | "types": "./src/index.d.ts", 22 | "browser": "./dist/jsxRuntime.module.js", 23 | "umd": "./dist/jsxRuntime.umd.js", 24 | "import": "./dist/jsxRuntime.mjs", 25 | "require": "./dist/jsxRuntime.js" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jsx-runtime/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Intentionally not using a relative path to take advantage of 2 | // the TS version resolution mechanism 3 | export { Fragment } from 'preact'; 4 | import { 5 | ComponentType, 6 | ComponentChild, 7 | ComponentChildren, 8 | VNode, 9 | Attributes 10 | } from 'preact'; 11 | import { JSXInternal } from '../../src/jsx'; 12 | 13 | export function jsx( 14 | type: string, 15 | props: JSXInternal.HTMLAttributes & 16 | JSXInternal.SVGAttributes & 17 | Record & { children?: ComponentChild }, 18 | key?: string 19 | ): VNode; 20 | export function jsx

    ( 21 | type: ComponentType

    , 22 | props: Attributes & P & { children?: ComponentChild }, 23 | key?: string 24 | ): VNode; 25 | 26 | export function jsxs( 27 | type: string, 28 | props: JSXInternal.HTMLAttributes & 29 | JSXInternal.SVGAttributes & 30 | Record & { children?: ComponentChild[] }, 31 | key?: string 32 | ): VNode; 33 | export function jsxs

    ( 34 | type: ComponentType

    , 35 | props: Attributes & P & { children?: ComponentChild[] }, 36 | key?: string 37 | ): VNode; 38 | 39 | export function jsxDEV( 40 | type: string, 41 | props: JSXInternal.HTMLAttributes & 42 | JSXInternal.SVGAttributes & 43 | Record & { children?: ComponentChildren }, 44 | key?: string 45 | ): VNode; 46 | export function jsxDEV

    ( 47 | type: ComponentType

    , 48 | props: Attributes & P & { children?: ComponentChildren }, 49 | key?: string 50 | ): VNode; 51 | 52 | // These are not expected to be used manually, but by a JSX transform 53 | export function jsxTemplate( 54 | template: string[], 55 | ...expressions: any[] 56 | ): VNode; 57 | export function jsxAttr(name: string, value: any): string | null; 58 | export function jsxEscape( 59 | value: T 60 | ): string | null | VNode | Array; 61 | 62 | export { JSXInternal as JSX }; 63 | -------------------------------------------------------------------------------- /jsx-runtime/src/utils.js: -------------------------------------------------------------------------------- 1 | const ENCODED_ENTITIES = /["&<]/; 2 | 3 | /** @param {string} str */ 4 | export function encodeEntities(str) { 5 | // Skip all work for strings with no entities needing encoding: 6 | if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str; 7 | 8 | let last = 0, 9 | i = 0, 10 | out = '', 11 | ch = ''; 12 | 13 | // Seek forward in str until the next entity char: 14 | for (; i < str.length; i++) { 15 | switch (str.charCodeAt(i)) { 16 | case 34: 17 | ch = '"'; 18 | break; 19 | case 38: 20 | ch = '&'; 21 | break; 22 | case 60: 23 | ch = '<'; 24 | break; 25 | default: 26 | continue; 27 | } 28 | // Append skipped/buffered characters and the encoded entity: 29 | if (i !== last) out += str.slice(last, i); 30 | out += ch; 31 | // Start the next seek/buffer after the entity's offset: 32 | last = i + 1; 33 | } 34 | if (i !== last) out += str.slice(last, i); 35 | return out; 36 | } 37 | -------------------------------------------------------------------------------- /mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | }, 20 | "compress": { 21 | "hoist_vars": true, 22 | "reduce_funcs": false 23 | } 24 | }, 25 | "props": { 26 | "cname": 6, 27 | "props": { 28 | "$_hasScuFromHooks": "__f", 29 | "$_listeners": "l", 30 | "$_cleanup": "__c", 31 | "$__hooks": "__H", 32 | "$_hydrationMismatch": "__m", 33 | "$_list": "__", 34 | "$_pendingEffects": "__h", 35 | "$_value": "__", 36 | "$_nextValue": "__N", 37 | "$_original": "__v", 38 | "$_args": "__H", 39 | "$_factory": "__h", 40 | "$_depth": "__b", 41 | "$_dirty": "__d", 42 | "$_mask": "__m", 43 | "$_detachOnNextRender": "__b", 44 | "$_force": "__e", 45 | "$_nextState": "__s", 46 | "$_renderCallbacks": "__h", 47 | "$_stateCallbacks": "_sb", 48 | "$_vnode": "__v", 49 | "$_children": "__k", 50 | "$_pendingSuspensionCount": "__u", 51 | "$_childDidSuspend": "__c", 52 | "$_onResolve": "__R", 53 | "$_suspended": "__a", 54 | "$_dom": "__e", 55 | "$_component": "__c", 56 | "$_index": "__i", 57 | "$_flags": "__u", 58 | "$__html": "__html", 59 | "$_parent": "__", 60 | "$_pendingError": "__E", 61 | "$_processingException": "__", 62 | "$_globalContext": "__n", 63 | "$_context": "c", 64 | "$_defaultValue": "__", 65 | "$_id": "__c", 66 | "$_contextRef": "__l", 67 | "$_parentDom": "__P", 68 | "$_originalParentDom": "__O", 69 | "$_prevState": "__u", 70 | "$_root": "__", 71 | "$_diff": "__b", 72 | "$_commit": "__c", 73 | "$_addHookName": "__a", 74 | "$_render": "__r", 75 | "$_hook": "__h", 76 | "$_catchError": "__e", 77 | "$_unmount": "__u", 78 | "$_owner": "__o", 79 | "$_skipEffects": "__s", 80 | "$_rerenderCount": "__r", 81 | "$_forwarded": "__f", 82 | "$_isSuspended": "__i" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /oxlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "camelcase": [ 4 | 1, 5 | { 6 | "allow": ["__test__*", "unstable_*", "UNSAFE_*"] 7 | } 8 | ], 9 | "no-unused-vars": [ 10 | 2, 11 | { 12 | "args": "none", 13 | "caughtErrors": "none", 14 | "varsIgnorePattern": "^h|React|createElement|Fragment$" 15 | } 16 | ], 17 | "typescript/no-namespace": 0, 18 | "no-constant-binary-expression": 0, 19 | "no-useless-catch": 0, 20 | "no-empty-pattern": 0, 21 | "prefer-rest-params": 0, 22 | "prefer-spread": 0, 23 | "no-cond-assign": 0, 24 | "react/jsx-no-bind": 0, 25 | "react/no-danger": 0, 26 | "react/no-danger-with-children": 0, 27 | "react/prefer-stateless-function": 0, 28 | "react/sort-comp": 0, 29 | "jest/valid-expect": 0, 30 | "jest/no-disabled-tests": 0, 31 | "jest/no-test-callback": 0, 32 | "jest/expect-expect": 0, 33 | "jest/no-standalone-expect": 0, 34 | "jest/no-export": 0, 35 | "react/no-find-dom-node": 0, 36 | "react/no-direct-mutation-state": 0, 37 | "react/no-children-prop": 0, 38 | "react/jsx-key": 0, 39 | "react/no-string-refs": 0, 40 | "react/require-render-return": 0, 41 | "unicorn/no-new-array": 0, 42 | "unicorn/prefer-string-starts-ends-with": 0 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/release/create-gh-release.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('@octokit/openapi-types').components["schemas"]["release"]} Release 3 | * 4 | * @typedef Params 5 | * @property {ReturnType} github 6 | * @property {typeof import("@actions/github").context} context 7 | * 8 | * @param {Params} params 9 | * @returns {Promise} 10 | */ 11 | async function create({ github, context }) { 12 | const commitSha = process.env.GITHUB_SHA; 13 | const tag_name = process.env.GITHUB_REF_NAME; 14 | console.log('tag:', tag_name); 15 | 16 | let releaseResult; 17 | 18 | let releasePages = github.paginate.iterator( 19 | github.rest.repos.listReleases, 20 | context.repo 21 | ); 22 | 23 | for await (const page of releasePages) { 24 | for (let release of page.data) { 25 | if (release.tag_name == tag_name) { 26 | console.log('Existing release found:', release); 27 | releaseResult = release; 28 | break; 29 | } 30 | } 31 | } 32 | 33 | if (!releaseResult) { 34 | console.log('No existing release found. Creating a new draft...'); 35 | 36 | // No existing release for this tag found so let's create a release 37 | // API Documentation: https://docs.github.com/en/rest/reference/repos#create-a-release 38 | // Octokit Documentation: https://octokit.github.io/rest.js/v18#repos-create-release 39 | const createReleaseRes = await github.rest.repos.createRelease({ 40 | ...context.repo, 41 | tag_name, 42 | name: tag_name, 43 | body: '', // TODO: Maybe run changelogged and prefill the body? 44 | draft: true, 45 | prerelease: tag_name.includes('-'), 46 | target_commitish: commitSha 47 | }); 48 | 49 | console.log('Created release:', createReleaseRes.data); 50 | releaseResult = createReleaseRes.data; 51 | } else if (!releaseResult.draft) { 52 | console.error('Found existing release but it was not in draft mode'); 53 | process.exit(1); 54 | } 55 | 56 | return releaseResult; 57 | } 58 | 59 | module.exports = create; 60 | -------------------------------------------------------------------------------- /scripts/release/upload-gh-asset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('@octokit/openapi-types').components["schemas"]["release"]} Release 3 | * 4 | * @typedef Params 5 | * @property {typeof require} require 6 | * @property {ReturnType} github 7 | * @property {typeof import("@actions/github").context} context 8 | * @property {typeof import("@actions/glob")} glob 9 | * @property {Release} release 10 | * 11 | * @param {Params} params 12 | */ 13 | async function upload({ require, github, context, glob, release }) { 14 | const fs = require('fs'); 15 | 16 | // Find artifact to upload 17 | const artifactPattern = 'preact.tgz'; 18 | const globber = await glob.create(artifactPattern, { 19 | matchDirectories: false 20 | }); 21 | 22 | const results = await globber.glob(); 23 | if (results.length == 0) { 24 | throw new Error( 25 | `No release artifact found matching pattern: ${artifactPattern}` 26 | ); 27 | } else if (results.length > 1) { 28 | throw new Error( 29 | `More than one artifact matching pattern found. Expected only one. Found ${results.length}.` 30 | ); 31 | } 32 | 33 | const assetPath = results[0]; 34 | const assetName = `preact-${release.tag_name.replace(/^v/, '')}.tgz`; 35 | const assetRegex = /^preact-.+\.tgz$/; 36 | 37 | for (let asset of release.assets) { 38 | if (assetRegex.test(asset.name)) { 39 | console.log( 40 | `Found existing asset matching asset pattern: ${asset.name}. Removing...` 41 | ); 42 | await github.rest.repos.deleteReleaseAsset({ 43 | ...context.repo, 44 | asset_id: asset.id 45 | }); 46 | } 47 | } 48 | 49 | console.log(`Uploading ${assetName} from ${assetPath}...`); 50 | 51 | // Upload a release asset 52 | // API Documentation: https://docs.github.com/en/rest/reference/repos#upload-a-release-asset 53 | // Octokit Documentation: https://octokit.github.io/rest.js/v18#repos-upload-release-asset 54 | const uploadAssetResponse = await github.rest.repos.uploadReleaseAsset({ 55 | ...context.repo, 56 | release_id: release.id, 57 | name: assetName, 58 | data: fs.readFileSync(assetPath) 59 | }); 60 | 61 | console.log('Asset:', uploadAssetResponse.data); 62 | return uploadAssetResponse.data; 63 | } 64 | 65 | module.exports = upload; 66 | -------------------------------------------------------------------------------- /sizereport.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | repo: 'preactjs/preact', 3 | path: ['./{compat,debug,hooks,}/dist/**/!(*.map)'], 4 | branch: 'main' 5 | }; 6 | -------------------------------------------------------------------------------- /src/cjs.js: -------------------------------------------------------------------------------- 1 | import * as preact from './index.js'; 2 | if (typeof module < 'u') module.exports = preact; 3 | else self.preact = preact; 4 | -------------------------------------------------------------------------------- /src/clone-element.js: -------------------------------------------------------------------------------- 1 | import { assign, slice } from './util'; 2 | import { createVNode } from './create-element'; 3 | import { NULL, UNDEFINED } from './constants'; 4 | 5 | /** 6 | * Clones the given VNode, optionally adding attributes/props and replacing its 7 | * children. 8 | * @param {import('./internal').VNode} vnode The virtual DOM element to clone 9 | * @param {object} props Attributes/props to add when cloning 10 | * @param {Array} rest Any additional arguments will be used 11 | * as replacement children. 12 | * @returns {import('./internal').VNode} 13 | */ 14 | export function cloneElement(vnode, props, children) { 15 | let normalizedProps = assign({}, vnode.props), 16 | key, 17 | ref, 18 | i; 19 | 20 | let defaultProps; 21 | 22 | if (vnode.type && vnode.type.defaultProps) { 23 | defaultProps = vnode.type.defaultProps; 24 | } 25 | 26 | for (i in props) { 27 | if (i == 'key') key = props[i]; 28 | else if (i == 'ref') ref = props[i]; 29 | else if (props[i] === UNDEFINED && defaultProps != UNDEFINED) { 30 | normalizedProps[i] = defaultProps[i]; 31 | } else { 32 | normalizedProps[i] = props[i]; 33 | } 34 | } 35 | 36 | if (arguments.length > 2) { 37 | normalizedProps.children = 38 | arguments.length > 3 ? slice.call(arguments, 2) : children; 39 | } 40 | 41 | return createVNode( 42 | vnode.type, 43 | normalizedProps, 44 | key || vnode.key, 45 | ref || vnode.ref, 46 | NULL 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** Normal hydration that attaches to a DOM tree but does not diff it. */ 2 | export const MODE_HYDRATE = 1 << 5; 3 | /** Signifies this VNode suspended on the previous render */ 4 | export const MODE_SUSPENDED = 1 << 7; 5 | /** Indicates that this node needs to be inserted while patching children */ 6 | export const INSERT_VNODE = 1 << 2; 7 | /** Indicates a VNode has been matched with another VNode in the diff */ 8 | export const MATCHED = 1 << 1; 9 | 10 | /** Reset all mode flags */ 11 | export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED); 12 | 13 | export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 14 | export const XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 15 | export const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 16 | 17 | export const NULL = null; 18 | export const UNDEFINED = undefined; 19 | export const EMPTY_OBJ = /** @type {any} */ ({}); 20 | export const EMPTY_ARR = []; 21 | export const IS_NON_DIMENSIONAL = 22 | /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; 23 | -------------------------------------------------------------------------------- /src/create-context.js: -------------------------------------------------------------------------------- 1 | import { enqueueRender } from './component'; 2 | import { NULL } from './constants'; 3 | 4 | export let i = 0; 5 | 6 | export function createContext(defaultValue) { 7 | function Context(props) { 8 | if (!this.getChildContext) { 9 | /** @type {Set | null} */ 10 | let subs = new Set(); 11 | let ctx = {}; 12 | ctx[Context._id] = this; 13 | 14 | this.getChildContext = () => ctx; 15 | 16 | this.componentWillUnmount = () => { 17 | subs = NULL; 18 | }; 19 | 20 | this.shouldComponentUpdate = function (_props) { 21 | // @ts-expect-error even 22 | if (this.props.value != _props.value) { 23 | subs.forEach(c => { 24 | c._force = true; 25 | enqueueRender(c); 26 | }); 27 | } 28 | }; 29 | 30 | this.sub = c => { 31 | subs.add(c); 32 | let old = c.componentWillUnmount; 33 | c.componentWillUnmount = () => { 34 | if (subs) { 35 | subs.delete(c); 36 | } 37 | if (old) old.call(c); 38 | }; 39 | }; 40 | } 41 | 42 | return props.children; 43 | } 44 | 45 | Context._id = '__cC' + i++; 46 | Context._defaultValue = defaultValue; 47 | 48 | /** @type {import('./internal').FunctionComponent} */ 49 | Context.Consumer = (props, contextValue) => { 50 | return props.children(contextValue); 51 | }; 52 | 53 | // we could also get rid of _contextRef entirely 54 | Context.Provider = 55 | Context._contextRef = 56 | Context.Consumer.contextType = 57 | Context; 58 | 59 | return Context; 60 | } 61 | -------------------------------------------------------------------------------- /src/diff/catch-error.js: -------------------------------------------------------------------------------- 1 | import { NULL } from '../constants'; 2 | 3 | /** 4 | * Find the closest error boundary to a thrown error and call it 5 | * @param {object} error The thrown value 6 | * @param {import('../internal').VNode} vnode The vnode that threw the error that was caught (except 7 | * for unmounting when this parameter is the highest parent that was being 8 | * unmounted) 9 | * @param {import('../internal').VNode} [oldVNode] 10 | * @param {import('../internal').ErrorInfo} [errorInfo] 11 | */ 12 | export function _catchError(error, vnode, oldVNode, errorInfo) { 13 | /** @type {import('../internal').Component} */ 14 | let component, 15 | /** @type {import('../internal').ComponentType} */ 16 | ctor, 17 | /** @type {boolean} */ 18 | handled; 19 | 20 | for (; (vnode = vnode._parent); ) { 21 | if ((component = vnode._component) && !component._processingException) { 22 | try { 23 | ctor = component.constructor; 24 | 25 | if (ctor && ctor.getDerivedStateFromError != NULL) { 26 | component.setState(ctor.getDerivedStateFromError(error)); 27 | handled = component._dirty; 28 | } 29 | 30 | if (component.componentDidCatch != NULL) { 31 | component.componentDidCatch(error, errorInfo || {}); 32 | handled = component._dirty; 33 | } 34 | 35 | // This is an error boundary. Mark it as having bailed out, and whether it was mid-hydration. 36 | if (handled) { 37 | return (component._pendingError = component); 38 | } 39 | } catch (e) { 40 | error = e; 41 | } 42 | } 43 | } 44 | 45 | throw error; 46 | } 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { render, hydrate } from './render'; 2 | export { 3 | createElement, 4 | createElement as h, 5 | Fragment, 6 | createRef, 7 | isValidElement 8 | } from './create-element'; 9 | export { BaseComponent as Component } from './component'; 10 | export { cloneElement } from './clone-element'; 11 | export { createContext } from './create-context'; 12 | export { toChildArray } from './diff/children'; 13 | export { default as options } from './options'; 14 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import { _catchError } from './diff/catch-error'; 2 | 3 | /** 4 | * The `option` object can potentially contain callback functions 5 | * that are called during various stages of our renderer. This is the 6 | * foundation on which all our addons like `preact/debug`, `preact/compat`, 7 | * and `preact/hooks` are based on. See the `Options` type in `internal.d.ts` 8 | * for a full list of available option hooks (most editors/IDEs allow you to 9 | * ctrl+click or cmd+click on mac the type definition below). 10 | * @type {import('./internal').Options} 11 | */ 12 | const options = { 13 | _catchError 14 | }; 15 | 16 | export default options; 17 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { EMPTY_ARR } from './constants'; 2 | 3 | export const isArray = Array.isArray; 4 | 5 | /** 6 | * Assign properties from `props` to `obj` 7 | * @template O, P The obj and props types 8 | * @param {O} obj The object to copy properties to 9 | * @param {P} props The object to copy properties from 10 | * @returns {O & P} 11 | */ 12 | export function assign(obj, props) { 13 | // @ts-expect-error We change the type of `obj` to be `O & P` 14 | for (let i in props) obj[i] = props[i]; 15 | return /** @type {O & P} */ (obj); 16 | } 17 | 18 | /** 19 | * Remove a child node from its parent if attached. This is a workaround for 20 | * IE11 which doesn't support `Element.prototype.remove()`. Using this function 21 | * is smaller than including a dedicated polyfill. 22 | * @param {import('./index').ContainerNode} node The node to remove 23 | */ 24 | export function removeNode(node) { 25 | if (node && node.parentNode) node.parentNode.removeChild(node); 26 | } 27 | 28 | export const slice = EMPTY_ARR.slice; 29 | -------------------------------------------------------------------------------- /test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-utils", 3 | "amdName": "preactTestUtils", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "Test-utils for Preact", 7 | "main": "dist/testUtils.js", 8 | "module": "dist/testUtils.module.js", 9 | "umd:main": "dist/testUtils.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "types": "src/index.d.ts", 13 | "peerDependencies": { 14 | "preact": "^10.0.0" 15 | }, 16 | "mangle": { 17 | "regex": "^_" 18 | }, 19 | "exports": { 20 | ".": { 21 | "types": "./src/index.d.ts", 22 | "browser": "./dist/testUtils.module.js", 23 | "umd": "./dist/testUtils.umd.js", 24 | "import": "./dist/testUtils.mjs", 25 | "require": "./dist/testUtils.js" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-utils/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export function setupRerender(): () => void; 2 | export function act(callback: () => void | Promise): Promise; 3 | export function teardown(): void; 4 | -------------------------------------------------------------------------------- /test-utils/test/shared/rerender.test.js: -------------------------------------------------------------------------------- 1 | import { options, createElement, render, Component } from 'preact'; 2 | import { teardown, setupRerender } from 'preact/test-utils'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('setupRerender & teardown', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = document.createElement('div'); 12 | }); 13 | 14 | it('should restore previous debounce', () => { 15 | let spy = (options.debounceRendering = sinon.spy()); 16 | 17 | setupRerender(); 18 | teardown(); 19 | 20 | expect(options.debounceRendering).to.equal(spy); 21 | }); 22 | 23 | it('teardown should flush the queue', () => { 24 | /** @type {() => void} */ 25 | let increment; 26 | class Counter extends Component { 27 | constructor(props) { 28 | super(props); 29 | 30 | this.state = { count: 0 }; 31 | increment = () => this.setState({ count: this.state.count + 1 }); 32 | } 33 | 34 | render() { 35 | return

    {this.state.count}
    ; 36 | } 37 | } 38 | 39 | sinon.spy(Counter.prototype, 'render'); 40 | 41 | // Setup rerender 42 | setupRerender(); 43 | 44 | // Initial render 45 | render(, scratch); 46 | expect(Counter.prototype.render).to.have.been.calledOnce; 47 | expect(scratch.innerHTML).to.equal('
    0
    '); 48 | 49 | // queue rerender 50 | increment(); 51 | expect(Counter.prototype.render).to.have.been.calledOnce; 52 | expect(scratch.innerHTML).to.equal('
    0
    '); 53 | 54 | // Pretend test forgot to call rerender. Teardown should do that 55 | teardown(); 56 | expect(Counter.prototype.render).to.have.been.calledTwice; 57 | expect(scratch.innerHTML).to.equal('
    1
    '); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/_util/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize contents 3 | * @typedef {string | number | Array} Contents 4 | * @param {Contents} contents 5 | */ 6 | const serialize = contents => 7 | Array.isArray(contents) ? contents.join('') : contents.toString(); 8 | 9 | /** 10 | * A helper to generate innerHTML validation strings containing spans 11 | * @param {Contents} contents The contents of the span, as a string 12 | */ 13 | export const span = contents => `${serialize(contents)}`; 14 | 15 | /** 16 | * A helper to generate innerHTML validation strings containing divs 17 | * @param {Contents} contents The contents of the div, as a string 18 | */ 19 | export const div = contents => `
    ${serialize(contents)}
    `; 20 | 21 | /** 22 | * A helper to generate innerHTML validation strings containing p 23 | * @param {Contents} contents The contents of the p, as a string 24 | */ 25 | export const p = contents => `

    ${serialize(contents)}

    `; 26 | 27 | /** 28 | * A helper to generate innerHTML validation strings containing sections 29 | * @param {Contents} contents The contents of the section, as a string 30 | */ 31 | export const section = contents => `
    ${serialize(contents)}
    `; 32 | 33 | /** 34 | * A helper to generate innerHTML validation strings containing uls 35 | * @param {Contents} contents The contents of the ul, as a string 36 | */ 37 | export const ul = contents => `
      ${serialize(contents)}
    `; 38 | 39 | /** 40 | * A helper to generate innerHTML validation strings containing ols 41 | * @param {Contents} contents The contents of the ol, as a string 42 | */ 43 | export const ol = contents => `
      ${serialize(contents)}
    `; 44 | 45 | /** 46 | * A helper to generate innerHTML validation strings containing lis 47 | * @param {Contents} contents The contents of the li, as a string 48 | */ 49 | export const li = contents => `
  • ${serialize(contents)}
  • `; 50 | 51 | /** 52 | * A helper to generate innerHTML validation strings containing inputs 53 | */ 54 | export const input = () => ``; 55 | 56 | /** 57 | * A helper to generate innerHTML validation strings containing h1 58 | * @param {Contents} contents The contents of the h1 59 | */ 60 | export const h1 = contents => `

    ${serialize(contents)}

    `; 61 | 62 | /** 63 | * A helper to generate innerHTML validation strings containing h2 64 | * @param {Contents} contents The contents of the h2 65 | */ 66 | export const h2 = contents => `

    ${serialize(contents)}

    `; 67 | -------------------------------------------------------------------------------- /test/_util/logCall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize an object 3 | * @param {Object} obj 4 | * @return {string} 5 | */ 6 | function serialize(obj) { 7 | if (obj instanceof Text) return '#text'; 8 | if (obj instanceof Element) return `<${obj.localName}>${obj.textContent}`; 9 | if (obj === document) return 'document'; 10 | if (typeof obj == 'string') return obj; 11 | return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, ''); 12 | } 13 | 14 | /** @type {string[]} */ 15 | let log = []; 16 | 17 | /** 18 | * Modify obj's original method to log calls and arguments on logger object 19 | * @template T 20 | * @param {T} obj 21 | * @param {keyof T} method 22 | */ 23 | export function logCall(obj, method) { 24 | let old = obj[method]; 25 | obj[method] = function (...args) { 26 | let c = ''; 27 | for (let i = 0; i < args.length; i++) { 28 | if (c) c += ', '; 29 | c += serialize(args[i]); 30 | } 31 | 32 | let operation; 33 | switch (method) { 34 | case 'removeChild': { 35 | operation = `${serialize(c)}.remove()`; 36 | break; 37 | } 38 | case 'insertBefore': { 39 | if (args[1] === null && args.length === 2) { 40 | operation = `${serialize(this)}.appendChild(${serialize(args[0])})`; 41 | } else { 42 | operation = `${serialize(this)}.${String(method)}(${c})`; 43 | } 44 | break; 45 | } 46 | default: { 47 | operation = `${serialize(this)}.${String(method)}(${c})`; 48 | break; 49 | } 50 | } 51 | 52 | log.push(operation); 53 | return old.apply(this, args); 54 | }; 55 | 56 | return () => (obj[method] = old); 57 | } 58 | 59 | /** 60 | * Return log object 61 | * @return {string[]} log 62 | */ 63 | export function getLog() { 64 | return log; 65 | } 66 | 67 | /** Clear log object */ 68 | export function clearLog() { 69 | log = []; 70 | } 71 | 72 | export function getLogSummary() { 73 | /** @type {{ [key: string]: number }} */ 74 | const summary = {}; 75 | 76 | for (let entry of log) { 77 | summary[entry] = (summary[entry] || 0) + 1; 78 | } 79 | 80 | return summary; 81 | } 82 | -------------------------------------------------------------------------------- /test/_util/optionSpies.js: -------------------------------------------------------------------------------- 1 | import { options as rawOptions } from 'preact'; 2 | 3 | /** @type {import('preact/src/internal').Options} */ 4 | let options = rawOptions; 5 | 6 | let oldVNode = options.vnode; 7 | let oldEvent = options.event || (e => e); 8 | let oldAfterDiff = options.diffed; 9 | let oldUnmount = options.unmount; 10 | 11 | let oldRoot = options._root; 12 | let oldBeforeDiff = options._diff; 13 | let oldBeforeRender = options._render; 14 | let oldBeforeCommit = options._commit; 15 | let oldHook = options._hook; 16 | let oldCatchError = options._catchError; 17 | 18 | export const vnodeSpy = sinon.spy(oldVNode); 19 | export const eventSpy = sinon.spy(oldEvent); 20 | export const afterDiffSpy = sinon.spy(oldAfterDiff); 21 | export const unmountSpy = sinon.spy(oldUnmount); 22 | 23 | export const rootSpy = sinon.spy(oldRoot); 24 | export const beforeDiffSpy = sinon.spy(oldBeforeDiff); 25 | export const beforeRenderSpy = sinon.spy(oldBeforeRender); 26 | export const beforeCommitSpy = sinon.spy(oldBeforeCommit); 27 | export const hookSpy = sinon.spy(oldHook); 28 | export const catchErrorSpy = sinon.spy(oldCatchError); 29 | 30 | options.vnode = vnodeSpy; 31 | options.event = eventSpy; 32 | options.diffed = afterDiffSpy; 33 | options.unmount = unmountSpy; 34 | options._root = rootSpy; 35 | options._diff = beforeDiffSpy; 36 | options._render = beforeRenderSpy; 37 | options._commit = beforeCommitSpy; 38 | options._hook = hookSpy; 39 | options._catchError = catchErrorSpy; 40 | -------------------------------------------------------------------------------- /test/browser/customBuiltInElements.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown } from '../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | const runSuite = typeof customElements == 'undefined' ? xdescribe : describe; 7 | 8 | runSuite('customised built-in elements', () => { 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('should create built in elements correctly', () => { 20 | class Foo extends Component { 21 | render() { 22 | return
    ; 23 | } 24 | } 25 | 26 | const spy = sinon.spy(); 27 | 28 | class BuiltIn extends HTMLDivElement { 29 | connectedCallback() { 30 | spy(); 31 | } 32 | } 33 | 34 | customElements.define('built-in', BuiltIn, { extends: 'div' }); 35 | 36 | render(, scratch); 37 | 38 | expect(spy).to.have.been.calledOnce; 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/browser/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, isValidElement, Component } from 'preact'; 2 | import { isValidElementTests } from '../shared/isValidElementTests'; 3 | 4 | isValidElementTests(expect, isValidElement, createElement, Component); 5 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentDidMount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupRerender } from 'preact/test-utils'; 3 | import { setupScratch, teardown } from '../../_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('Lifecycle methods', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | let rerender; 11 | 12 | beforeEach(() => { 13 | scratch = setupScratch(); 14 | rerender = setupRerender(); 15 | }); 16 | 17 | afterEach(() => { 18 | teardown(scratch); 19 | }); 20 | 21 | describe('#componentDidMount', () => { 22 | it('is invoked after refs are set', () => { 23 | const spy = sinon.spy(); 24 | 25 | class App extends Component { 26 | componentDidMount() { 27 | expect(spy).to.have.been.calledOnceWith(scratch.firstChild); 28 | } 29 | 30 | render() { 31 | return
    ; 32 | } 33 | } 34 | 35 | render(, scratch); 36 | expect(spy).to.have.been.calledOnceWith(scratch.firstChild); 37 | }); 38 | 39 | it('supports multiple setState callbacks', () => { 40 | const spy = sinon.spy(); 41 | 42 | class App extends Component { 43 | constructor(props) { 44 | super(props); 45 | this.state = { count: 0 }; 46 | } 47 | 48 | componentDidMount() { 49 | // eslint-disable-next-line 50 | this.setState({ count: 1 }, spy); 51 | // eslint-disable-next-line 52 | this.setState({ count: 2 }, spy); 53 | } 54 | 55 | render() { 56 | return
    ; 57 | } 58 | } 59 | 60 | render(, scratch); 61 | 62 | rerender(); 63 | expect(spy).to.have.been.calledTwice; 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentWillMount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupRerender } from 'preact/test-utils'; 3 | import { setupScratch, teardown } from '../../_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('Lifecycle methods', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | /** @type {() => void} */ 12 | let rerender; 13 | 14 | beforeEach(() => { 15 | scratch = setupScratch(); 16 | rerender = setupRerender(); 17 | }); 18 | 19 | afterEach(() => { 20 | teardown(scratch); 21 | }); 22 | 23 | describe('#componentWillMount', () => { 24 | it('should update state when called setState in componentWillMount', () => { 25 | let componentState; 26 | 27 | class Foo extends Component { 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | value: 0 32 | }; 33 | } 34 | componentWillMount() { 35 | this.setState({ value: 1 }); 36 | } 37 | render() { 38 | componentState = this.state; 39 | return
    ; 40 | } 41 | } 42 | 43 | render(, scratch); 44 | 45 | expect(componentState).to.deep.equal({ value: 1 }); 46 | }); 47 | 48 | it('should invoke setState callbacks when setState is called in componentWillMount', () => { 49 | let componentState; 50 | let callback = sinon.spy(); 51 | 52 | class Foo extends Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = { 56 | value: 0 57 | }; 58 | } 59 | componentWillMount() { 60 | this.setState({ value: 1 }, callback); 61 | this.setState({ value: 2 }, () => { 62 | callback(); 63 | this.setState({ value: 3 }, callback); 64 | }); 65 | } 66 | render() { 67 | componentState = this.state; 68 | return
    ; 69 | } 70 | } 71 | 72 | render(, scratch); 73 | 74 | expect(componentState).to.deep.equal({ value: 2 }); 75 | expect(callback).to.have.been.calledTwice; 76 | 77 | rerender(); 78 | 79 | expect(componentState).to.deep.equal({ value: 3 }); 80 | expect(callback).to.have.been.calledThrice; 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentWillUnmount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown } from '../../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Lifecycle methods', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = setupScratch(); 12 | }); 13 | 14 | afterEach(() => { 15 | teardown(scratch); 16 | }); 17 | 18 | describe('top-level componentWillUnmount', () => { 19 | it('should invoke componentWillUnmount for top-level components', () => { 20 | class Foo extends Component { 21 | componentDidMount() {} 22 | componentWillUnmount() {} 23 | render() { 24 | return 'foo'; 25 | } 26 | } 27 | class Bar extends Component { 28 | componentDidMount() {} 29 | componentWillUnmount() {} 30 | render() { 31 | return 'bar'; 32 | } 33 | } 34 | sinon.spy(Foo.prototype, 'componentDidMount'); 35 | sinon.spy(Foo.prototype, 'componentWillUnmount'); 36 | sinon.spy(Foo.prototype, 'render'); 37 | 38 | sinon.spy(Bar.prototype, 'componentDidMount'); 39 | sinon.spy(Bar.prototype, 'componentWillUnmount'); 40 | sinon.spy(Bar.prototype, 'render'); 41 | 42 | render(, scratch); 43 | expect(Foo.prototype.componentDidMount, 'initial render').to.have.been 44 | .calledOnce; 45 | 46 | render(, scratch); 47 | expect(Foo.prototype.componentWillUnmount, 'when replaced').to.have.been 48 | .calledOnce; 49 | expect(Bar.prototype.componentDidMount, 'when replaced').to.have.been 50 | .calledOnce; 51 | 52 | render(
    , scratch); 53 | expect(Bar.prototype.componentWillUnmount, 'when removed').to.have.been 54 | .calledOnce; 55 | }); 56 | 57 | it('should only remove dom after componentWillUnmount was called', () => { 58 | class Foo extends Component { 59 | componentWillUnmount() { 60 | expect(document.getElementById('foo')).to.not.equal(null); 61 | } 62 | 63 | render() { 64 | return
    ; 65 | } 66 | } 67 | 68 | render(, scratch); 69 | render(null, scratch); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/browser/mathml.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component, render } from 'preact'; 2 | import { setupRerender } from 'preact/test-utils'; 3 | import { setupScratch, teardown } from '../_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('mathml', () => { 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = setupScratch(); 12 | }); 13 | 14 | afterEach(() => { 15 | teardown(scratch); 16 | }); 17 | 18 | it('should render with the correct namespace URI', () => { 19 | render(, scratch); 20 | 21 | let namespace = scratch.querySelector('math').namespaceURI; 22 | 23 | expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); 24 | }); 25 | 26 | it('should render children with the correct namespace URI', () => { 27 | render( 28 | 29 | 30 | , 31 | scratch 32 | ); 33 | 34 | let namespace = scratch.querySelector('mrow').namespaceURI; 35 | 36 | expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); 37 | }); 38 | 39 | it('should inherit correct namespace URI from parent', () => { 40 | const math = document.createElementNS( 41 | 'http://www.w3.org/1998/Math/MathML', 42 | 'math' 43 | ); 44 | scratch.appendChild(math); 45 | 46 | render(, scratch.firstChild); 47 | 48 | let namespace = scratch.querySelector('mrow').namespaceURI; 49 | expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); 50 | }); 51 | 52 | it('should inherit correct namespace URI from parent upon updating', () => { 53 | setupRerender(); 54 | 55 | const math = document.createElementNS( 56 | 'http://www.w3.org/1998/Math/MathML', 57 | 'math' 58 | ); 59 | scratch.appendChild(math); 60 | 61 | class App extends Component { 62 | state = { show: true }; 63 | componentDidMount() { 64 | // eslint-disable-next-line 65 | this.setState({ show: false }, () => { 66 | expect(scratch.querySelector('mo').namespaceURI).to.equal( 67 | 'http://www.w3.org/1998/Math/MathML' 68 | ); 69 | }); 70 | } 71 | render() { 72 | return this.state.show ? 1 : 2; 73 | } 74 | } 75 | 76 | render(, scratch.firstChild); 77 | }); 78 | 79 | it('should transition from DOM to MathML and back', () => { 80 | render( 81 |
    82 | 83 | 84 | c 85 | 2 86 | 87 | 88 |
    , 89 | scratch 90 | ); 91 | 92 | expect(scratch.firstChild).to.be.an('HTMLDivElement'); 93 | expect(scratch.firstChild.firstChild).to.be.an('MathMLElement'); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/browser/select.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Select', () => { 7 | let scratch; 8 | 9 | beforeEach(() => { 10 | scratch = setupScratch(); 11 | }); 12 | 13 | afterEach(() => { 14 | teardown(scratch); 15 | }); 16 | 17 | it('should set 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(, scratch); 29 | expect(scratch.firstChild.value).to.equal('B'); 30 | }); 31 | 32 | it('should set value with selected', () => { 33 | function App() { 34 | return ( 35 | 42 | ); 43 | } 44 | 45 | render(, scratch); 46 | expect(scratch.firstChild.value).to.equal('B'); 47 | }); 48 | 49 | it('should work with multiple selected', () => { 50 | function App() { 51 | return ( 52 | 61 | ); 62 | } 63 | 64 | render(, scratch); 65 | Array.prototype.slice.call(scratch.firstChild.childNodes).forEach(node => { 66 | if (node.value === 'B' || node.value === 'C') { 67 | expect(node.selected).to.equal(true); 68 | } 69 | }); 70 | expect(scratch.firstChild.value).to.equal('B'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/node/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as preact from '../../'; 3 | 4 | describe('build artifact', () => { 5 | // #1075 Check that the build artifact has the correct exports 6 | it('should have exported properties', () => { 7 | expect(typeof preact).to.equal('object'); 8 | expect(preact).to.have.property('createElement'); 9 | expect(preact).to.have.property('h'); 10 | expect(preact).to.have.property('Component'); 11 | expect(preact).to.have.property('render'); 12 | expect(preact).to.have.property('hydrate'); 13 | // expect(preact).to.have.property('options'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/shared/createContext.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, createContext } from '../../src/index'; 2 | import { expect } from 'chai'; 3 | 4 | /** @jsx createElement */ 5 | /* eslint-env browser, mocha */ 6 | 7 | describe('createContext', () => { 8 | it('should return a Provider and a Consumer', () => { 9 | const context = createContext(); 10 | expect(context).to.have.property('Provider'); 11 | expect(context).to.have.property('Consumer'); 12 | }); 13 | 14 | it('should return a valid Provider Component', () => { 15 | const { Provider } = createContext(); 16 | const contextValue = { value: 'test' }; 17 | const children = [
    child1
    ,
    child2
    ]; 18 | 19 | const providerComponent = {children}; 20 | //expect(providerComponent).to.have.property('tag', 'Provider'); 21 | expect(providerComponent.props.value).to.equal(contextValue.value); 22 | expect(providerComponent.props.children).to.equal(children); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/shared/exports.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | h, 4 | createContext, 5 | Component, 6 | Fragment, 7 | render, 8 | hydrate, 9 | cloneElement, 10 | options, 11 | createRef, 12 | toChildArray, 13 | isValidElement 14 | } from '../../src/index'; 15 | import { expect } from 'chai'; 16 | 17 | describe('preact', () => { 18 | it('should be available as named exports', () => { 19 | expect(h).to.be.a('function'); 20 | expect(createElement).to.be.a('function'); 21 | expect(Component).to.be.a('function'); 22 | expect(Fragment).to.exist; 23 | expect(render).to.be.a('function'); 24 | expect(hydrate).to.be.a('function'); 25 | expect(cloneElement).to.be.a('function'); 26 | expect(createContext).to.be.a('function'); 27 | expect(options).to.exist.and.be.an('object'); 28 | expect(createRef).to.be.a('function'); 29 | expect(isValidElement).to.be.a('function'); 30 | expect(toChildArray).to.be.a('function'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/shared/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, isValidElement, Component } from '../../src/index'; 2 | import { expect } from 'chai'; 3 | import { isValidElementTests } from './isValidElementTests'; 4 | 5 | isValidElementTests(expect, isValidElement, createElement, Component); 6 | -------------------------------------------------------------------------------- /test/shared/isValidElementTests.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | 3 | export function isValidElementTests( 4 | expect, 5 | isValidElement, 6 | createElement, 7 | Component 8 | ) { 9 | describe('isValidElement', () => { 10 | it('should check if the argument is a valid vnode', () => { 11 | // Failure cases 12 | expect(isValidElement(123)).to.equal(false); 13 | expect(isValidElement(0)).to.equal(false); 14 | expect(isValidElement('')).to.equal(false); 15 | expect(isValidElement('abc')).to.equal(false); 16 | expect(isValidElement(null)).to.equal(false); 17 | expect(isValidElement(undefined)).to.equal(false); 18 | expect(isValidElement(true)).to.equal(false); 19 | expect(isValidElement(false)).to.equal(false); 20 | expect(isValidElement([])).to.equal(false); 21 | expect(isValidElement([123])).to.equal(false); 22 | expect(isValidElement([null])).to.equal(false); 23 | 24 | // Success cases 25 | expect(isValidElement(
    )).to.equal(true); 26 | 27 | const Foo = () => 123; 28 | expect(isValidElement()).to.equal(true); 29 | class Bar extends Component { 30 | render() { 31 | return
    ; 32 | } 33 | } 34 | expect(isValidElement()).to.equal(true); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/ts/dom-attributes-test.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Fragment, JSX } from 'preact'; 2 | 3 | function createSignal(value: T): JSX.SignalLike { 4 | return { 5 | value, 6 | peek() { 7 | return value; 8 | }, 9 | subscribe() { 10 | return () => {}; 11 | } 12 | }; 13 | } 14 | 15 | // @ts-expect-error We should correctly type aria attributes like autocomplete 16 | const badAriaValues =
    ; 17 | const validAriaValues =
    ; 18 | const undefAriaValues =
    ; 19 | const noAriaValues =
    ; 20 | 21 | const signalBadAriaValues = ( 22 | // @ts-expect-error We should correctly type aria attributes like autocomplete 23 |
    24 | ); 25 | const signalValidAriaValues = ( 26 |
    27 | ); 28 | const signalValidAriaValues2 = ( 29 |
    32 | )} 33 | /> 34 | ); 35 | 36 | const validRole =
    ; 37 | // @ts-expect-error We should correctly type aria roles 38 | const invalidRole =
    ; 39 | // @ts-expect-error We should disallow `generic` as it should not ever be explicitly set 40 | const invalidRole2 =
    ; 41 | const fallbackRole =
    ; 42 | 43 | const booleanishTest = ( 44 | <> 45 |
    46 |
    47 |
    48 |
    49 |
    50 | 51 | ); 52 | -------------------------------------------------------------------------------- /test/ts/hoc-test.tsx: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | createElement, 4 | ComponentFactory, 5 | ComponentConstructor, 6 | Component 7 | } from '../../'; 8 | import { SimpleComponent, SimpleComponentProps } from './Component-test'; 9 | 10 | export interface highlightedProps { 11 | isHighlighted: boolean; 12 | } 13 | 14 | export function highlighted( 15 | Wrappable: ComponentFactory 16 | ): ComponentConstructor { 17 | return class extends Component { 18 | constructor(props: T & highlightedProps) { 19 | super(props); 20 | } 21 | 22 | render() { 23 | let className = this.props.isHighlighted ? 'highlighted' : ''; 24 | return ( 25 |
    26 | 27 |
    28 | ); 29 | } 30 | 31 | toString() { 32 | return `Highlighted ${Wrappable.name}`; 33 | } 34 | }; 35 | } 36 | 37 | const HighlightedSimpleComponent = 38 | highlighted(SimpleComponent); 39 | 40 | describe('hoc', () => { 41 | it('wraps the given component', () => { 42 | const highlight = new HighlightedSimpleComponent({ 43 | initialName: 'initial name', 44 | isHighlighted: true 45 | }); 46 | 47 | expect(highlight.toString()).to.eq('Highlighted SimpleComponent'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/ts/jsx-namespacce-test.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from '../../'; 2 | 3 | // declare global JSX types that should not be mixed with preact's internal types 4 | declare global { 5 | namespace JSX { 6 | interface Element { 7 | unknownProperty: string; 8 | } 9 | } 10 | } 11 | 12 | class SimpleComponent extends Component { 13 | render() { 14 | return
    It works
    ; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/ts/preact-global-test.tsx: -------------------------------------------------------------------------------- 1 | import { createElement } from '../../src'; 2 | 3 | // Test that preact types are available via the global `preact` namespace. 4 | 5 | let component: preact.ComponentChild; 6 | component =
    Hello World
    ; 7 | -------------------------------------------------------------------------------- /test/ts/refs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | Component, 4 | createRef, 5 | FunctionalComponent, 6 | Fragment, 7 | RefObject, 8 | RefCallback 9 | } from '../../'; 10 | 11 | // Test Fixtures 12 | const Foo: FunctionalComponent = () => Foo; 13 | class Bar extends Component { 14 | render() { 15 | return Bar; 16 | } 17 | } 18 | 19 | // Using Refs 20 | class CallbackRef extends Component { 21 | divRef: RefCallback = div => { 22 | if (div !== null) { 23 | console.log(div.tagName); 24 | } 25 | }; 26 | fooRef: RefCallback = foo => { 27 | if (foo !== null) { 28 | console.log(foo.base); 29 | } 30 | }; 31 | barRef: RefCallback = bar => { 32 | if (bar !== null) { 33 | console.log(bar.base); 34 | } 35 | }; 36 | 37 | render() { 38 | return ( 39 | 40 |
    41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | class CreateRefComponent extends Component { 49 | private divRef: RefObject = createRef(); 50 | private fooRef: RefObject = createRef(); 51 | private barRef: RefObject = createRef(); 52 | 53 | componentDidMount() { 54 | if (this.divRef.current != null) { 55 | console.log(this.divRef.current.tagName); 56 | } 57 | 58 | if (this.fooRef.current != null) { 59 | console.log(this.fooRef.current.base); 60 | } 61 | 62 | if (this.barRef.current != null) { 63 | console.log(this.barRef.current.base); 64 | } 65 | } 66 | 67 | render() { 68 | return ( 69 | 70 |
    71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom"], 7 | "strict": true, 8 | "typeRoots": ["../../"], 9 | "types": [], 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "jsxFactory": "createElement", 13 | "jsxFragmentFactory": "Fragment", 14 | "paths": { 15 | "preact": ["../../"], 16 | "preact/*": ["../../*"] 17 | } 18 | }, 19 | "include": ["./**/*.ts", "./**/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /types/events.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type declaration file to fix the events module errors 3 | */ 4 | 5 | declare module 'events' { 6 | export class EventEmitter { 7 | // Methods 8 | addListener( 9 | event: string | symbol, 10 | listener: (...args: any[]) => void 11 | ): this; 12 | on(event: string | symbol, listener: (...args: any[]) => void): this; 13 | once(event: string | symbol, listener: (...args: any[]) => void): this; 14 | removeListener( 15 | event: string | symbol, 16 | listener: (...args: any[]) => void 17 | ): this; 18 | off(event: string | symbol, listener: (...args: any[]) => void): this; 19 | removeAllListeners(event?: string | symbol): this; 20 | setMaxListeners(n: number): this; 21 | getMaxListeners(): number; 22 | listeners(event: string | symbol): Function[]; 23 | rawListeners(event: string | symbol): Function[]; 24 | emit(event: string | symbol, ...args: any[]): boolean; 25 | listenerCount(event: string | symbol): number; 26 | prependListener( 27 | event: string | symbol, 28 | listener: (...args: any[]) => void 29 | ): this; 30 | prependOnceListener( 31 | event: string | symbol, 32 | listener: (...args: any[]) => void 33 | ): this; 34 | eventNames(): (string | symbol)[]; 35 | 36 | // For TypeScript errors in events module 37 | static listenerCount(emitter: EventEmitter, event: string | symbol): number; 38 | static defaultMaxListeners: number; 39 | } 40 | 41 | // Extended Error type to include properties used in events.js 42 | interface ErrorWithEventProperties extends Error { 43 | context?: any; 44 | emitter?: any; 45 | type?: string | symbol; 46 | count?: number; 47 | } 48 | 49 | // For onceWrapper 50 | interface OnceWrapper extends Function { 51 | listener?: Function; 52 | target?: any; 53 | type?: string | symbol; 54 | wrapFn?: Function; 55 | } 56 | 57 | export default EventEmitter; 58 | } 59 | -------------------------------------------------------------------------------- /vitest.setup.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { expect, chai, describe } from 'vitest'; 3 | import sinonChai from 'sinon-chai'; 4 | 5 | chai.use(sinonChai); 6 | 7 | globalThis.context = describe; 8 | globalThis.sinon = sinon; 9 | 10 | window.addEventListener('error', () => {}) 11 | 12 | // Something that's loaded before this file polyfills Symbol object. 13 | // We need to verify that it works in IE without that. 14 | if (/Trident/.test(window.navigator.userAgent)) { 15 | window.Symbol = undefined; 16 | } 17 | 18 | // Fix Function#name on browsers that do not support it (IE). 19 | // Taken from: https://stackoverflow.com/a/17056530/755391 20 | if (!function f() {}.name) { 21 | Object.defineProperty(Function.prototype, 'name', { 22 | get() { 23 | let name = (this.toString().match(/^function\s*([^\s(]+)/) || [])[1]; 24 | // For better performance only parse once, and then cache the 25 | // result through a new accessor for repeated access. 26 | Object.defineProperty(this, 'name', { value: name }); 27 | return name; 28 | } 29 | }); 30 | } 31 | 32 | expect.extend({ 33 | equalNode: (obj, expected) => { 34 | if (expected == null) { 35 | return { 36 | pass: obj == null, 37 | message: () => `expected node to "== null" but got ${obj} instead.` 38 | }; 39 | } else { 40 | return { 41 | pass: obj.tagName === expected.tagName && obj === expected, 42 | message: () => 43 | `expected node to have tagName ${expected.tagName} but got ${obj.tagName} instead.` 44 | }; 45 | } 46 | } 47 | }); 48 | --------------------------------------------------------------------------------