├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── config │ ├── codeql.yml │ └── snyk.yml └── workflows │ ├── browser.yml │ ├── codeql.yml │ ├── deno.yml │ ├── eslint.yml │ ├── nodejs.yml │ ├── prettier.yml │ └── snyk.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bundles ├── nano.core.min.js ├── nano.full.min.js ├── nano.slim.min.js └── nano.ui.min.js ├── deno.dev.tsx ├── deno_lib ├── LICENSE ├── README.md ├── component.ts ├── components │ ├── helmet.ts │ ├── img.ts │ ├── index.ts │ ├── link.ts │ ├── router.ts │ ├── suspense.ts │ └── visible.ts ├── context.ts ├── core.ts ├── customElementsMode.ts ├── fragment.ts ├── helpers.ts ├── hooks │ ├── index.ts │ └── useState.ts ├── htm.ts ├── htm │ ├── LICENSE │ ├── README.md │ ├── build.ts │ ├── constants.ts │ └── index.ts ├── index.ts ├── jsx-runtime │ └── index.ts ├── jsx.ts ├── lazy.ts ├── mod.ts ├── regexDom.ts ├── ssr.ts ├── state.ts ├── store.ts ├── types.ts ├── ui │ ├── README.md │ ├── _config.ts │ ├── _helpers.ts │ ├── appBar.ts │ ├── banner.ts │ ├── button.ts │ ├── dialog.ts │ ├── fab.ts │ ├── icon.ts │ ├── index.ts │ ├── list.ts │ ├── menu.ts │ ├── navigation.ts │ ├── sheet.ts │ ├── snackbar.ts │ ├── tabs.ts │ └── toolbar.ts ├── version.ts └── withStyles.ts ├── dev ├── bundled-core.html ├── bundled-dev.html ├── dev.html ├── font-awesome │ ├── LICENSE │ ├── ellipsis-v-solid.svg │ ├── heart-solid.svg │ ├── home-solid.svg │ └── user-solid.svg ├── img │ └── placeholder.png └── ui.html ├── jest.config.json ├── package-lock.json ├── package.json ├── readme ├── logo-drawing.svg ├── nano-jsx-logo-512.png ├── nano-jsx-logo-dark.svg ├── nano-jsx-logo.png ├── nano-jsx-logo.svg └── thumbnail.png ├── scripts ├── browserTest │ ├── README.md │ ├── index.mjs │ ├── mime.mjs │ ├── nyc.mjs │ ├── requestListener.mjs │ ├── tester.ts │ └── tsconfig.json └── packageType.mjs ├── src ├── bundles │ ├── bundle.core.ts │ ├── bundle.full.ts │ ├── bundle.slim.ts │ └── bundle.ui.ts ├── component.ts ├── components │ ├── helmet.ts │ ├── img.ts │ ├── index.ts │ ├── link.ts │ ├── router.ts │ ├── suspense.ts │ └── visible.ts ├── context.ts ├── core.ts ├── customElementsMode.ts ├── dev │ ├── components │ │ └── isomorphic.tsx │ ├── dev.tsx │ └── devSSR.tsx ├── fragment.ts ├── helpers.ts ├── hooks │ ├── index.ts │ └── useState.ts ├── htm.ts ├── htm │ ├── LICENSE │ ├── README.md │ ├── build.ts │ ├── constants.ts │ └── index.ts ├── index.ts ├── jsx-runtime │ └── index.ts ├── jsx.ts ├── lazy.ts ├── mod.ts ├── regexDom.ts ├── ssr.ts ├── state.ts ├── store.ts ├── types.ts ├── ui │ ├── README.md │ ├── _config.ts │ ├── _helpers.ts │ ├── appBar.ts │ ├── banner.ts │ ├── button.ts │ ├── dialog.ts │ ├── fab.ts │ ├── icon.ts │ ├── index.ts │ ├── list.ts │ ├── menu.ts │ ├── navigation.ts │ ├── sheet.ts │ ├── snackbar.ts │ ├── tabs.ts │ └── toolbar.ts ├── version.ts └── withStyles.ts ├── test.jsx-runtime └── nodejs │ ├── client.test.tsx │ └── server.tsx ├── test ├── browser │ ├── className.html │ ├── component.test.html │ ├── dangerous.test.html │ ├── fragment.test.html │ ├── link.test.html │ ├── router.test.html │ ├── simple.test.html │ ├── svg.test.html │ ├── webComponent.html │ └── withStyles.test.html ├── deno │ ├── deno.test.tsx │ ├── nanossr.test.tsx │ └── svg.test.tsx ├── helpers.mjs ├── nodejs │ ├── children.test.tsx │ ├── className.test.tsx │ ├── component.test.tsx │ ├── components │ │ ├── helmet.ssr.test.tsx │ │ ├── helmet.test.tsx │ │ ├── img.test.tsx │ │ ├── link.test.tsx │ │ └── visible.test.tsx │ ├── context.ssr.test.tsx │ ├── context.test.tsx │ ├── customElementsMode.ssr.test.tsx │ ├── customElementsMode.test.tsx │ ├── e2e │ │ ├── assets │ │ │ ├── index.html │ │ │ └── toggle.html │ │ └── core.test.tsx │ ├── events.ssr.test.tsx │ ├── events.test.tsx │ ├── exeptions.test.tsx │ ├── fetchAPI.test.tsx │ ├── fragment.test.tsx │ ├── functionLess.test.tsx │ ├── helpers.ts │ ├── hooks │ │ └── useState.test.tsx │ ├── htm.test.ts │ ├── hydrateLazy.test.tsx │ ├── jsx-runtime.test.tsx │ ├── lifecycles.test.tsx │ ├── nestedRenderOutputs.test.tsx │ ├── no-jsx.test.tsx │ ├── parentElement.test.tsx │ ├── partialHydration.test.tsx │ ├── props.test.tsx │ ├── ref.test.tsx │ ├── regexDom.ssr.test.tsx │ ├── renderNothing.test.tsx │ ├── replaceParent.test.tsx │ ├── router.ssr.test.tsx │ ├── router.test.tsx │ ├── ssr.test.tsx │ ├── state.test.tsx │ ├── store.ssr.test.tsx │ ├── store.test.tsx │ ├── stringArray.test.tsx │ ├── style.test.tsx │ ├── suspense.ssr.test.tsx │ ├── suspense.test.tsx │ ├── svg.ssr.test.tsx │ ├── svg.test.tsx │ └── withStyles.test.tsx └── scripts.mjs ├── tsconfig.deno.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.test.json ├── tsconfig.test.jsx-runtime.json └── webpack ├── webpack.bundle.dev.js ├── webpack.bundle.instrumented.js ├── webpack.bundle.prod.js └── webpack.dev.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@yandeu/eslint-config", 4 | "prettier" 5 | ], 6 | "globals": { 7 | "globalThis": "off" 8 | }, 9 | "rules": { 10 | "no-undef": "off", 11 | "sort-imports": "off" 12 | } 13 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: yandeu 2 | -------------------------------------------------------------------------------- /.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 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Have a question?** 14 | Join the [discussions](https://github.com/nanojsx/nano/discussions) instead. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/nanojsx/nano/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/config/codeql.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - src 3 | -------------------------------------------------------------------------------- /.github/config/snyk.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - src 3 | -------------------------------------------------------------------------------- /.github/workflows/browser.yml: -------------------------------------------------------------------------------- 1 | # syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 2 | name: Browser 3 | 4 | on: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | - '.vscode/**' 9 | - 'bundles/**' 10 | - 'dev/**' 11 | - 'readme/**' 12 | pull_request: 13 | paths-ignore: 14 | - '**.md' 15 | - '.vscode/**' 16 | - 'bundles/**' 17 | - 'dev/**' 18 | - 'readme/**' 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | name: Browser 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v4 29 | 30 | - name: NodeJS 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: '20' 34 | 35 | - name: Install 36 | run: | 37 | rm -f package.json 38 | rm -f package-lock.json 39 | npm init -y 40 | npm i -D rimraf typescript puppeteer 41 | npm i -D webpack webpack-cli ts-loader nyc coverage-istanbul-loader 42 | 43 | - name: Prepare 44 | run: | 45 | node_modules/.bin/rimraf .nyc_output coverage 46 | node_modules/.bin/tsc -p tsconfig.json 47 | node_modules/.bin/tsc -p scripts/browserTest/tsconfig.json 48 | node_modules/.bin/webpack -c webpack/webpack.bundle.instrumented.js 49 | 50 | - name: Test 51 | run: | 52 | node scripts/browserTest/index.mjs --coverage 53 | 54 | - name: Coverage 55 | run: | 56 | node_modules/.bin/nyc report --exclude "src/{bundles,dev,htm,ui}/**" --exclude "src/mod.ts" --reporter=text --reporter=html --reporter=lcov 57 | 58 | - name: Upload coverage to Codecov 59 | if: ${{ github.ref == 'refs/heads/master' }} 60 | uses: codecov/codecov-action@v2 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - '.github/**' 8 | pull_request: 9 | paths: 10 | - 'src/**' 11 | - '.github/**' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v2 24 | with: 25 | languages: typescript 26 | config-file: ./.github/config/codeql.yml 27 | 28 | - name: Perform CodeQL Analysis 29 | uses: github/codeql-action/analyze@v2 30 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | # syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 2 | name: Deno 3 | 4 | on: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | - '.vscode/**' 9 | - 'bundles/**' 10 | - 'dev/**' 11 | - 'readme/**' 12 | pull_request: 13 | paths-ignore: 14 | - '**.md' 15 | - '.vscode/**' 16 | - 'bundles/**' 17 | - 'dev/**' 18 | - 'readme/**' 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | deno: ['v1.x'] 27 | node-version: ['20.x'] 28 | 29 | name: Deno ${{ matrix.deno }} 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | # install node to run denoify.land 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | 41 | # install deno to run the test 42 | - name: Setup Deno 43 | uses: denolib/setup-deno@v2 44 | with: 45 | deno-version: ${{ matrix.deno }} 46 | 47 | - name: Install Dependencies 48 | run: | 49 | rm -f package.json 50 | rm -f package-lock.json 51 | npm init -y 52 | npm i -D typescript 53 | npm i -D denoify 54 | npm i -D rimraf 55 | 56 | - name: Denoify 57 | run: | 58 | node_modules/.bin/rimraf deno_lib 59 | node_modules/.bin/denoify 60 | node_modules/.bin/rimraf deno_lib/bundles deno_lib/dev 61 | 62 | - name: Run Tests 63 | run: | 64 | deno --version 65 | deno test -c tsconfig.deno.json test/deno/** 66 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 2 | name: ESLint 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | name: Linting 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '20' 17 | - run: rm -f package.json 18 | - run: rm -f package-lock.json 19 | - run: npm init -y 20 | - run: npm i -D @yandeu/eslint-config 21 | - run: npm i -D eslint 22 | - run: node node_modules/.bin/eslint src --ext .ts,.tsx 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 2 | name: NodeJS 3 | 4 | on: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | - '.vscode/**' 9 | - 'bundles/**' 10 | - 'dev/**' 11 | - 'readme/**' 12 | pull_request: 13 | paths-ignore: 14 | - '**.md' 15 | - '.vscode/**' 16 | - 'bundles/**' 17 | - 'dev/**' 18 | - 'readme/**' 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | node-version: [18.x, 20.x, 22.x] 27 | 28 | name: Node ${{ matrix.node-version }} 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Use Node.js ${{ matrix.node-version }} 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | 39 | - name: Install Dependencies 40 | run: npm install 41 | 42 | - name: Build Packages 43 | run: npm run build 44 | 45 | - name: Bundle Package 46 | run: npm run bundle 47 | 48 | - name: Run Tests 49 | run: npm test 50 | 51 | - name: Upload coverage to Codecov 52 | if: ${{ github.ref == 'refs/heads/master' }} 53 | uses: codecov/codecov-action@v2 54 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | # syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 2 | name: Prettier 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | name: Format 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '20' 17 | - run: rm -f package.json 18 | - run: rm -f package-lock.json 19 | - run: npm init -y 20 | - run: npm i -D @yandeu/prettier-config 21 | - run: npm i -D prettier 22 | - run: node node_modules/.bin/prettier --check src 23 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Snyk 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - '.github/**' 8 | pull_request: 9 | paths: 10 | - 'src/**' 11 | - '.github/**' 12 | 13 | jobs: 14 | snyk: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: Run Snyk to check for vulnerabilities 19 | uses: snyk/actions/node@master 20 | continue-on-error: true # To make sure that SARIF upload gets called 21 | env: 22 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 23 | with: 24 | args: --sarif-file-output=snyk.sarif 25 | - name: Upload result to GitHub Code Scanning 26 | uses: github/codeql-action/upload-sarif@v2 27 | continue-on-error: true 28 | with: 29 | sarif_file: snyk.sarif 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /bundles/nano.dev.min.js 3 | /bundles/nano.instrumented.min.js 4 | /bundles/nano.instrumented.min.js.map 5 | /coverage 6 | /debug.log 7 | /dev/dev.js 8 | /esm 9 | /lib 10 | /node_modules 11 | /test/**/*.js 12 | /test.jsx-runtime/**/*.js 13 | /scripts/browserTest/tester.js 14 | /deno.lock -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !/.npmrc 4 | !/LICENSE 5 | !/README.md 6 | !/bundles 7 | !/esm 8 | !/lib 9 | !/package.json 10 | !/readme 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.json 3 | *.md 4 | *.min.js 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@yandeu/prettier-config" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/nanojsx/nano 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 | -------------------------------------------------------------------------------- /deno.dev.tsx: -------------------------------------------------------------------------------- 1 | import { h, Helmet, renderSSR, Component } from './deno_lib/mod.ts' 2 | import { Application, Router } from 'https://deno.land/x/oak@v10.2.1/mod.ts' 3 | 4 | const comments = ['Comment One', 'Comment Two'] 5 | 6 | class Comments extends Component { 7 | render() { 8 | return ( 9 | 14 | ) 15 | } 16 | } 17 | 18 | const App = () => ( 19 |
20 | 21 | Nano JSX SSR 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

Comments

30 |
31 | 32 |
33 |
34 | ) 35 | 36 | const ssr = renderSSR() 37 | const { body, head, footer } = Helmet.SSR(ssr) 38 | 39 | console.log(body) 40 | 41 | const html = ` 42 | 43 | 44 | 45 | 46 | 47 | ${head.join('\n')} 48 | 49 | 50 | ${body} 51 | ${footer.join('\n')} 52 | 53 | ` 54 | 55 | const router = new Router() 56 | router.get('/', context => { 57 | context.response.body = html 58 | }) 59 | 60 | const app = new Application() 61 | app.use(router.routes()) 62 | app.use(router.allowedMethods()) 63 | 64 | app.addEventListener('listen', ({ port }) => { 65 | console.log(`Listening on: http://localhost:${port}`) 66 | }) 67 | 68 | await app.listen({ port: 5000 }) 69 | -------------------------------------------------------------------------------- /deno_lib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/nanojsx/nano 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 | -------------------------------------------------------------------------------- /deno_lib/components/img.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { h, strToHash } from '../core.ts' 3 | 4 | interface Props { 5 | [key: string]: any 6 | src: string 7 | height?: number | string 8 | width?: number | string 9 | lazy?: boolean 10 | placeholder?: any 11 | } 12 | 13 | /** 14 | * A useful Image component 15 | * Add { 20 | constructor(props: Props) { 21 | super(props) 22 | 23 | const { src, key } = props 24 | 25 | // id has to be unique 26 | this.id = `${strToHash(src)}-${strToHash(JSON.stringify(props))}` 27 | if (key) this.id += `key-${key}` 28 | 29 | // this could also be done in willMount() 30 | if (!this.state) this.setState({ isLoaded: false, image: undefined }) 31 | } 32 | 33 | didMount() { 34 | const { lazy = true, placeholder, children, key, ref, ...rest } = this.props 35 | 36 | if (typeof lazy === 'boolean' && lazy === false) return 37 | 38 | const observer = new IntersectionObserver( 39 | (entries, observer) => { 40 | entries.forEach(entry => { 41 | if (entry.isIntersecting) { 42 | observer.disconnect() 43 | this.state.image = h('img', { ...rest }) as HTMLImageElement 44 | if (this.state.image.complete) { 45 | this.state.isLoaded = true 46 | this.update() 47 | } else { 48 | this.state.image.onload = () => { 49 | this.state.isLoaded = true 50 | this.update() 51 | } 52 | } 53 | } 54 | }) 55 | }, 56 | { threshold: [0, 1] } 57 | ) 58 | observer.observe(this.elements[0]) 59 | } 60 | render() { 61 | const { src, placeholder, children, lazy = true, key, ref, ...rest } = this.props 62 | 63 | // return the img tag if not lazy loaded 64 | if (typeof lazy === 'boolean' && lazy === false) { 65 | this.state.image = h('img', { src, ...rest }) as HTMLImageElement 66 | return this.state.image 67 | } 68 | 69 | // if it is visible and loaded, show the image 70 | if (this.state.isLoaded) { 71 | return this.state.image 72 | // if the placeholder is an image src 73 | } else if (placeholder && typeof placeholder === 'string') { 74 | return h('img', { src: placeholder, ...rest }) 75 | // if the placeholder is an JSX element 76 | } else if (placeholder && typeof placeholder === 'function') { 77 | return placeholder() 78 | } else { 79 | // render a simple box 80 | const style: Record = {} 81 | if (rest.width) style.width = `${rest.width}px` 82 | if (rest.height) style.height = `${rest.height}px` 83 | const { width, height, ...others } = rest 84 | return h('div', { style, ...others }) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /deno_lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Helmet } from './helmet.ts' 2 | export { Img } from './img.ts' 3 | export { Link } from './link.ts' 4 | export * as Router from './router.ts' 5 | export { Suspense } from './suspense.ts' 6 | export { Visible } from './visible.ts' 7 | -------------------------------------------------------------------------------- /deno_lib/components/suspense.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { isSSR, strToHash } from '../core.ts' 3 | 4 | interface Props { 5 | fallback: any 6 | cache?: boolean 7 | [key: string]: any 8 | } 9 | 10 | export class Suspense extends Component { 11 | ready = false 12 | 13 | constructor(props: Props) { 14 | super(props) 15 | 16 | // get props promises in ...rest 17 | const { children, fallback, cache = false, ...rest } = this.props 18 | 19 | // stringify ...rest 20 | const str = JSON.stringify(rest, function (_key, val) { 21 | if (typeof val === 'function') return `${val}` // implicitly `toString` it 22 | return val 23 | }) 24 | 25 | // create unique id based on ...rest 26 | this.id = strToHash(JSON.stringify(str)) 27 | } 28 | 29 | async didMount() { 30 | // get props promises in ...rest 31 | const { children, fallback, cache = false, ...rest } = this.props 32 | 33 | // set initial state to [] 34 | if (cache) this.initState = {} 35 | 36 | // check if we already cached the results in this.state 37 | if (this.loadFromCache(cache)) return 38 | 39 | // resolve the promises 40 | const promises = Object.values(rest).map(p => p()) 41 | const resolved = await Promise.all(promises) 42 | 43 | // prepare data 44 | const data = this.prepareData(rest, resolved, cache) 45 | 46 | // add data to children 47 | this.addDataToChildren(data) 48 | 49 | // update the component 50 | this.ready = true 51 | this.update() 52 | } 53 | 54 | ssr() { 55 | // get props promises in ...rest 56 | const { children, fallback, cache = false, ...rest } = this.props 57 | 58 | // execute the functions 59 | const functions = Object.values(rest).map(p => p()) 60 | 61 | // prepare data 62 | const data = this.prepareData(rest, functions, false) 63 | 64 | // add data to children 65 | this.addDataToChildren(data) 66 | } 67 | 68 | loadFromCache(cache: boolean) { 69 | const hasCachedProps = this.state && cache && Object.keys(this.state).length > 0 70 | 71 | if (hasCachedProps) { 72 | this.addDataToChildren(this.state) 73 | this.ready = true 74 | } 75 | 76 | return hasCachedProps 77 | } 78 | 79 | prepareData(rest: any, fnc: Function[], cache: boolean) { 80 | const data = Object.keys(rest).reduce((obj, item, index) => { 81 | if (cache) this.state = { ...this.state, [item]: fnc[index] } 82 | return { 83 | ...obj, 84 | [item]: fnc[index] 85 | } 86 | }, {}) 87 | return data 88 | } 89 | 90 | addDataToChildren(data: any) { 91 | // add data as props to children 92 | this.props.children.forEach((child: any) => { 93 | if (child.props) child.props = { ...child.props, ...data } 94 | }) 95 | } 96 | 97 | render() { 98 | if (!isSSR()) { 99 | const { cache = false } = this.props 100 | this.loadFromCache(cache) 101 | return !this.ready ? this.props.fallback : this.props.children 102 | } else { 103 | this.ssr() 104 | return this.props.children 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /deno_lib/components/visible.ts: -------------------------------------------------------------------------------- 1 | import { h, render } from '../core.ts' 2 | import { Component } from '../component.ts' 3 | 4 | export class Visible extends Component { 5 | isVisible = false 6 | 7 | didMount() { 8 | const observer = new IntersectionObserver( 9 | (entries, observer) => { 10 | entries.forEach(entry => { 11 | if (entry.isIntersecting) { 12 | observer.disconnect() 13 | this.isVisible = true 14 | this.update() 15 | } 16 | }) 17 | }, 18 | { threshold: [0, 1] } 19 | ) 20 | observer.observe(this.elements[0]) 21 | } 22 | 23 | render() { 24 | if (!this.isVisible) { 25 | return h('div', { 'data-visible': false, visibility: 'hidden' }) 26 | } else { 27 | if (this.props.onVisible) this.props.onVisible() 28 | return render(this.props.component || this.props.children[0]) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deno_lib/context.ts: -------------------------------------------------------------------------------- 1 | export const createContext = (ctx: any) => { 2 | let _ctx = ctx 3 | return { 4 | Provider: (props: any) => { 5 | if (props.value) _ctx = props.value 6 | return props.children 7 | }, 8 | Consumer: (props: any) => { 9 | return { component: props.children[0](_ctx), props: { ...props, context: _ctx } } 10 | }, 11 | get: () => _ctx, 12 | set: (ctx: any) => (_ctx = ctx) 13 | } 14 | } 15 | 16 | export const useContext = (ctx: any) => { 17 | const _ctx = ctx 18 | if (_ctx && typeof _ctx.get === 'function') { 19 | return _ctx.get() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /deno_lib/fragment.ts: -------------------------------------------------------------------------------- 1 | export const Fragment = (props: any) => { 2 | return props.children 3 | } 4 | -------------------------------------------------------------------------------- /deno_lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { VERSION } from './version.ts' 2 | 3 | /** Creates a new Task using setTimeout() */ 4 | export const task = (task: () => void) => setTimeout(task, 0) 5 | 6 | export const nodeToString = (node: Node) => { 7 | const tmpNode = document.createElement('div') 8 | tmpNode.appendChild(node.cloneNode(true)) 9 | return tmpNode.innerHTML 10 | } 11 | 12 | export const detectSSR = (): boolean => { 13 | // @ts-ignore 14 | const isDeno = typeof Deno !== 'undefined' 15 | const hasWindow = typeof window !== 'undefined' ? true : false 16 | return (typeof _nano !== 'undefined' && _nano.isSSR) || isDeno || !hasWindow 17 | } 18 | 19 | function isDescendant(desc: ParentNode | null, root: Node): boolean { 20 | // @ts-ignore 21 | return !!desc && (desc === root || isDescendant(desc.parentNode, root)) 22 | } 23 | 24 | // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 25 | export const onNodeRemove = (element: HTMLElement, callback: () => void) => { 26 | let observer = new MutationObserver(mutationsList => { 27 | mutationsList.forEach(mutation => { 28 | mutation.removedNodes.forEach(removed => { 29 | if (isDescendant(element, removed)) { 30 | callback() 31 | if (observer) { 32 | // allow garbage collection 33 | observer.disconnect() 34 | // @ts-ignore 35 | observer = undefined 36 | } 37 | } 38 | }) 39 | }) 40 | }) 41 | observer.observe(document, { 42 | childList: true, 43 | subtree: true 44 | }) 45 | return observer 46 | } 47 | 48 | // https://stackoverflow.com/a/6234804 49 | export const escapeHtml = (unsafe: string) => { 50 | if (unsafe && typeof unsafe === 'string') 51 | return unsafe 52 | .replace(/&/g, '&') 53 | .replace(//g, '>') 55 | .replace(/"/g, '"') 56 | .replace(/'/g, ''') 57 | return unsafe 58 | } 59 | 60 | export const printVersion = () => { 61 | const info = `Powered by nano JSX v${VERSION}` 62 | console.log( 63 | `%c %c %c %c %c ${info} %c http://nanojsx.io`, 64 | 'background: #ff0000', 65 | 'background: #ffff00', 66 | 'background: #00ff00', 67 | 'background: #00ffff', 68 | 'color: #fff; background: #000000;', 69 | 'background: none' 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /deno_lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useState, getState, setState } from './useState.ts' 2 | -------------------------------------------------------------------------------- /deno_lib/hooks/useState.ts: -------------------------------------------------------------------------------- 1 | import { _state } from '../state.ts' 2 | 3 | export const useState = (state: T, id: string): readonly [T, (state: T) => void] => { 4 | const s = { 5 | setState(state: T) { 6 | if (state !== null) _state.set(id, state) 7 | }, 8 | get state(): T { 9 | return _state.get(id) 10 | } 11 | } 12 | 13 | if (!_state.has(id)) _state.set(id, state) 14 | 15 | return [s.state, s.setState] 16 | } 17 | 18 | export const getState = (id: string) => { 19 | return _state.get(id) 20 | } 21 | 22 | export const setState = (id: string, state: any) => { 23 | return _state.set(id, state) 24 | } 25 | -------------------------------------------------------------------------------- /deno_lib/htm.ts: -------------------------------------------------------------------------------- 1 | import htm from './htm/index.ts' 2 | export default htm 3 | -------------------------------------------------------------------------------- /deno_lib/htm/README.md: -------------------------------------------------------------------------------- 1 | All files in this folder are copied from version 3.0.4 of htm from https://github.com/developit/htm and ported to TypeScript to make it compatible with Deno. 2 | 3 | Please take a look at the [LICENSE](./LICENSE) file. 4 | -------------------------------------------------------------------------------- /deno_lib/htm/constants.ts: -------------------------------------------------------------------------------- 1 | export const MINI = false 2 | -------------------------------------------------------------------------------- /deno_lib/htm/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { MINI } from './constants.ts' 15 | import { build, evaluate } from './build.ts' 16 | 17 | const CACHES = new Map() 18 | 19 | const regular = function (this: HTMLElement, statics: TemplateStringsArray) { 20 | let tmp = CACHES.get(this) 21 | if (!tmp) { 22 | tmp = new Map() 23 | CACHES.set(this, tmp) 24 | } 25 | tmp = evaluate(this, tmp.get(statics) || (tmp.set(statics, (tmp = build(statics))), tmp), arguments, []) 26 | return tmp.length > 1 ? tmp : tmp[0] 27 | } 28 | 29 | // export as htm 30 | export default MINI ? build : regular 31 | -------------------------------------------------------------------------------- /deno_lib/index.ts: -------------------------------------------------------------------------------- 1 | // core 2 | export { h, render, hydrate, tick } from './core.ts' 3 | export type { FC } from './core.ts' 4 | 5 | // component 6 | export { Component } from './component.ts' 7 | 8 | // built-in Components 9 | export * from './components/index.ts' 10 | 11 | // export some defaults (Nano) 12 | import { h, render, hydrate, isSSR } from './core.ts' 13 | import { renderSSR } from './ssr.ts' 14 | export default { h, render, hydrate, renderSSR, isSSR } 15 | 16 | // other 17 | export { isSSR } 18 | export { jsx } from './jsx.ts' 19 | export { hydrateLazy } from './lazy.ts' 20 | export { nodeToString, task } from './helpers.ts' 21 | export { renderSSR } from './ssr.ts' 22 | export { Fragment } from './fragment.ts' 23 | export { Store } from './store.ts' 24 | export { createContext, useContext } from './context.ts' 25 | export { withStyles } from './withStyles.ts' 26 | export { defineAsCustomElements } from './customElementsMode.ts' 27 | 28 | // version 29 | export { printVersion } from './helpers.ts' 30 | export { VERSION } from './version.ts' 31 | -------------------------------------------------------------------------------- /deno_lib/jsx-runtime/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | export { Fragment } from '../fragment.ts' 3 | import { h } from '../core.ts' 4 | 5 | const createNode: (type: any, props: any, key: string, source?: string, self?: string) => any = function (type, props) { 6 | let { children = [], ..._props } = props 7 | if (!Array.isArray(children)) children = [children] 8 | return h(type, _props, ...children) 9 | } 10 | 11 | export { createNode as jsx } 12 | export { createNode as jsxs } 13 | export { createNode as jsxDev } 14 | -------------------------------------------------------------------------------- /deno_lib/jsx.ts: -------------------------------------------------------------------------------- 1 | import { h } from './core.ts' 2 | import htm from './htm.ts' 3 | const jsx = htm.bind(h) 4 | 5 | export { jsx } 6 | -------------------------------------------------------------------------------- /deno_lib/lazy.ts: -------------------------------------------------------------------------------- 1 | import { hydrate, h } from './core.ts' 2 | import { Visible } from './components/visible.ts' 3 | 4 | export const hydrateLazy = (component: any, parent: HTMLElement | null = null, removeChildNodes = true) => { 5 | const c = h(Visible, null, component) 6 | return hydrate(c, parent, removeChildNodes) 7 | } 8 | -------------------------------------------------------------------------------- /deno_lib/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './index.ts' 2 | -------------------------------------------------------------------------------- /deno_lib/ssr.ts: -------------------------------------------------------------------------------- 1 | import { isSSR, render } from './core.ts' 2 | import { documentSSR } from './regexDom.ts' 3 | import { _state } from './state.ts' 4 | import { detectSSR } from './helpers.ts' 5 | 6 | // functions that should only be available on the server-side 7 | const ssrTricks = { 8 | isWebComponent: (tagNameOrComponent: any) => { 9 | return ( 10 | typeof tagNameOrComponent === 'string' && 11 | tagNameOrComponent.includes('-') && 12 | _nano.customElements.has(tagNameOrComponent) 13 | ) 14 | }, 15 | renderWebComponent: (tagNameOrComponent: any, props: any, children: any, _render: any) => { 16 | const customElement = _nano.customElements.get(tagNameOrComponent) 17 | const component = _render({ component: customElement, props: { ...props, children: children } }) 18 | // get the html tag and the innerText from string 19 | // match[1]: HTMLTag 20 | // match[2]: innerText 21 | const match = component.toString().match(/^<(?[a-z]+)>(.*)<\/\k>$/) 22 | if (match) { 23 | const element = document.createElement(match[1]) 24 | element.innerText = match[2] 25 | 26 | // eslint-disable-next-line no-inner-declarations 27 | function replacer(match: string, p1: string, _offset: number, _string: string): string { 28 | return match.replace(p1, '') 29 | } 30 | // remove events like onClick from DOM 31 | element.innerText = element.innerText.replace(/<\w+[^>]*(\s(on\w*)="[^"]*")/gm, replacer) 32 | 33 | return element 34 | } else { 35 | return null 36 | } 37 | } 38 | } 39 | 40 | const initGlobalVar = () => { 41 | const isSSR = detectSSR() === true ? true : undefined 42 | const location = { pathname: '/' } 43 | const document = isSSR ? documentSSR() : window.document 44 | globalThis._nano = { isSSR, location, document, customElements: new Map(), ssrTricks } 45 | } 46 | initGlobalVar() 47 | 48 | export const initSSR = (pathname: string = '/') => { 49 | _nano.location = { pathname } 50 | globalThis.document = _nano.document = isSSR() ? documentSSR() : window.document 51 | } 52 | 53 | export const renderSSR = (component: any, options: { pathname?: string; clearState?: boolean } = {}) => { 54 | const { pathname, clearState = true } = options 55 | 56 | initSSR(pathname) 57 | if (clearState) _state.clear() 58 | 59 | const tmp = render(component, null, true) as string | string[] 60 | if (Array.isArray(tmp)) return tmp.join('') 61 | else return Array.from(tmp).join('') 62 | } 63 | 64 | export const clearState = () => { 65 | _state.clear() 66 | } 67 | -------------------------------------------------------------------------------- /deno_lib/state.ts: -------------------------------------------------------------------------------- 1 | /** Holds the state of the whole application. */ 2 | export const _state: Map = new Map() 3 | 4 | /** Clears the state of the whole application. */ 5 | export const _clearState = () => { 6 | _state.clear() 7 | } 8 | -------------------------------------------------------------------------------- /deno_lib/store.ts: -------------------------------------------------------------------------------- 1 | import { isSSR } from './core.ts' 2 | 3 | export class Store { 4 | private _state: S 5 | private _prevState: S 6 | private _listeners: Map = new Map() 7 | private _storage: 'memory' | 'local' | 'session' 8 | private _id: string 9 | 10 | /** 11 | * Create your own Store. 12 | * @param defaultState Pass the initial State. 13 | * @param name The name of the Store (only required if you persist the state in localStorage or sessionStorage). 14 | * @param storage Pass 'memory', 'local' or 'session'. 15 | */ 16 | constructor(defaultState: Object, name: string = '', storage: 'memory' | 'local' | 'session' = 'memory') { 17 | if (isSSR()) storage = 'memory' 18 | 19 | this._id = name 20 | this._storage = storage 21 | 22 | this._state = this._prevState = defaultState as any 23 | 24 | if (storage === 'memory' || !storage) return 25 | 26 | const Storage = storage === 'local' ? localStorage : sessionStorage 27 | 28 | // get/set initial state of Storage 29 | const item = Storage.getItem(this._id) 30 | if (item) { 31 | this._state = this._prevState = JSON.parse(item) 32 | } else Storage.setItem(this._id, JSON.stringify(defaultState)) 33 | } 34 | 35 | private persist(newState: S) { 36 | if (this._storage === 'memory') return 37 | const Storage = this._storage === 'local' ? localStorage : sessionStorage 38 | Storage.setItem(this._id, JSON.stringify(newState)) 39 | } 40 | 41 | /** Clears the state of the whole store. */ 42 | public clear() { 43 | // @ts-ignore 44 | this._state = this._prevState = undefined 45 | 46 | if (this._storage === 'local') localStorage.removeItem(this._id) 47 | else if (this._storage === 'session') sessionStorage.removeItem(this._id) 48 | } 49 | 50 | public setState(newState: S) { 51 | this.state = newState 52 | } 53 | 54 | public set state(newState: S) { 55 | this._prevState = this._state 56 | this._state = newState 57 | 58 | this.persist(newState) 59 | 60 | this._listeners.forEach(fnc => { 61 | fnc(this._state, this._prevState) 62 | }) 63 | } 64 | 65 | public get state(): S { 66 | return this._state 67 | } 68 | 69 | public use() { 70 | const id = Math.random().toString(36).substring(2, 9) 71 | const _this = this 72 | return { 73 | get state(): S { 74 | return _this.state 75 | }, 76 | setState: (newState: S) => { 77 | this.state = newState 78 | }, 79 | subscribe: (fnc: (newState: S, prevState: S) => void) => { 80 | this._listeners.set(id, fnc) 81 | }, 82 | cancel: () => { 83 | if (this._listeners.has(id)) this._listeners.delete(id) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /deno_lib/types.ts: -------------------------------------------------------------------------------- 1 | // fixes an issue in std@0.80.0 (deno) 2 | // interface ReadableStream { 3 | // getIterator(): any 4 | // } 5 | 6 | declare global { 7 | export var _nano: { 8 | document: Document 9 | isSSR: true | undefined 10 | location: { pathname: string } 11 | customElements: Map 12 | ssrTricks: { 13 | isWebComponent: (tagNameOrComponent: any) => boolean 14 | renderWebComponent: (tagNameOrComponent: any, props: any, children: any, _render: any) => any 15 | } 16 | } 17 | 18 | export namespace Deno {} 19 | 20 | export namespace JSX { 21 | interface IntrinsicElements { 22 | [elemName: string]: any 23 | } 24 | interface ElementClass { 25 | render: any 26 | } 27 | interface ElementChildrenAttribute { 28 | children: any 29 | } 30 | } 31 | } 32 | // This export keeps the backward compatibility with the module resolution system in deno < 1.23 version. 33 | export {} 34 | -------------------------------------------------------------------------------- /deno_lib/ui/README.md: -------------------------------------------------------------------------------- 1 | # UI Elements 2 | 3 | These ui elements should look very similar to the [Material UI Components](https://material.io/components). 4 | 5 | Feel free to improve the elements or add new once. 6 | -------------------------------------------------------------------------------- /deno_lib/ui/_config.ts: -------------------------------------------------------------------------------- 1 | import { strToHash } from '../core.ts' 2 | 3 | export const boxShadow = ` 4 | -webkit-box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 5 | -moz-box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 6 | box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 7 | ` 8 | 9 | export const userSelect = ` 10 | -webkit-touch-callout:none; 11 | -webkit-user-select:none; 12 | -khtml-user-select:none; 13 | -moz-user-select:none; 14 | -ms-user-select:none; 15 | user-select:none; 16 | -webkit-tap-highlight-color:rgba(0,0,0,0); 17 | ` 18 | 19 | export const rippleEffect = (rippleClr: string, hoverClr: string) => { 20 | const rippleClass = `ripple-${strToHash(rippleClr + hoverClr)}` 21 | 22 | const styles = ` 23 | .${rippleClass} { 24 | background-position: center; 25 | transition: background 0.8s; 26 | } 27 | 28 | .${rippleClass}:hover { 29 | background: ${hoverClr} radial-gradient(circle, transparent 1%, ${hoverClr} 1%) center/15000%; 30 | } 31 | 32 | .${rippleClass}:focus { 33 | background: ${hoverClr} radial-gradient(circle, transparent 1%, ${hoverClr} 1%) center/15000%; 34 | } 35 | 36 | .${rippleClass}:active { 37 | background-color: ${rippleClr}; 38 | background-size: 100%; 39 | transition: background 0s; 40 | }` 41 | 42 | return { 43 | styles, 44 | class: rippleClass 45 | } 46 | } 47 | 48 | export const zIndex = { 49 | button: 'unset;', 50 | banner: '50;', 51 | bar: '100;', 52 | navigation: '100;', 53 | fab: '200;', 54 | sheet: '300;', 55 | menu: '400;', 56 | snackbar: '500;', 57 | dialog: '600;' 58 | } 59 | -------------------------------------------------------------------------------- /deno_lib/ui/_helpers.ts: -------------------------------------------------------------------------------- 1 | import { h } from '../core.ts' 2 | 3 | // https://gist.github.com/renancouto/4675192 4 | export const lightenColor = (color: string, percent: number) => { 5 | const num = parseInt(color.replace('#', ''), 16), 6 | amt = Math.round(2.55 * percent), 7 | R = (num >> 16) + amt, 8 | B = ((num >> 8) & 0x00ff) + amt, 9 | G = (num & 0x0000ff) + amt 10 | return `#${( 11 | 0x1000000 + 12 | (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + 13 | (B < 255 ? (B < 1 ? 0 : B) : 255) * 0x100 + 14 | (G < 255 ? (G < 1 ? 0 : G) : 255) 15 | ) 16 | .toString(16) 17 | .slice(1)}` 18 | } 19 | 20 | export const addStylesToHead = (styles: string, hash: string) => { 21 | const el = document.querySelector(`[data-css-hash*="${hash}"]`) 22 | if (!el) { 23 | const styleElement = h('style', { 'data-css-hash': hash }, styles) 24 | document.head.appendChild(styleElement) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /deno_lib/ui/banner.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { h } from '../core.ts' 3 | import { Button } from './button.ts' 4 | import { zIndex } from './_config.ts' 5 | 6 | interface BannerAction { 7 | name: string 8 | id?: string | number 9 | color?: string 10 | } 11 | 12 | interface BannerActionEvent { 13 | action: string 14 | } 15 | 16 | interface BannerProps { 17 | title?: string 18 | body?: string 19 | actions?: BannerAction[] 20 | onAction?: () => BannerActionEvent 21 | parentId?: string 22 | sticky?: number 23 | } 24 | 25 | export class Banner extends Component { 26 | defaultActionColor = '#6200EE' 27 | 28 | render() { 29 | const { 30 | sticky, 31 | body = 'There was a problem processing a transaction on your credit card.', 32 | actions = [ 33 | { name: 'fix it', color: this.defaultActionColor }, 34 | { name: 'learn more', color: this.defaultActionColor } 35 | ] 36 | } = this.props 37 | 38 | const stickyStyles = sticky 39 | ? ` 40 | position: -webkit-sticky; 41 | position: sticky; 42 | top: ${sticky}px;` 43 | : '' 44 | 45 | const styles = { 46 | container: ` 47 | margin: -16px -16px 16px -16px; 48 | ${stickyStyles} 49 | z-index: ${zIndex.banner} 50 | `, 51 | banner: ` 52 | 53 | display: flex; 54 | justify-content: space-between; 55 | flex-wrap: wrap; 56 | 57 | 58 | background: white; 59 | border-bottom: 1px rgb(0 0 0 / 0.12) solid; 60 | min-height: 54px;`, 61 | body: ` 62 | color: #000000; 63 | padding: 16px; 64 | padding-bottom: 8px; 65 | font-size: 16px; 66 | line-height: 1.5em;`, 67 | actions: ` 68 | margin: 0; 69 | padding: 8px; 70 | display: flex; 71 | flex-direction: row; 72 | align-items: flex-end; 73 | margin-left: auto; 74 | flex-wrap: wrap; 75 | justify-content: flex-end; 76 | `, 77 | action: ` 78 | margin-bottom: 0px; 79 | margin-left: 10px;` 80 | } 81 | 82 | const actionsArray = actions.map((action: any) => { 83 | return h( 84 | Button, 85 | { 86 | text: true, 87 | color: action.color || this.defaultActionColor, 88 | style: styles.action, 89 | // style: `color: ${action.color || this.defaultActionColor}`, 90 | onClick: () => { 91 | // callback({ name: action.name, id: action.id }) 92 | // this.remove() 93 | } 94 | }, 95 | action.name.toUpperCase() 96 | ) 97 | }) 98 | 99 | const bodyElement = h('div', { style: styles.body }, body) 100 | const actionsElement = h('div', { style: styles.actions }, actionsArray) 101 | const bannerElement = h('div', { style: styles.banner }, bodyElement, actionsElement) 102 | 103 | return h('div', { style: styles.container }, bannerElement) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /deno_lib/ui/button.ts: -------------------------------------------------------------------------------- 1 | import { h, strToHash } from '../core.ts' 2 | import { boxShadow, rippleEffect, userSelect, zIndex } from './_config.ts' 3 | import { addStylesToHead, lightenColor } from './_helpers.ts' 4 | import { Icon } from './icon.ts' 5 | 6 | export const Button = (props: { 7 | outlined?: boolean 8 | text?: boolean 9 | style?: string 10 | icon?: string 11 | [key: string]: any 12 | }) => { 13 | const { 14 | children, 15 | outlined = false, 16 | text = false, 17 | background = '#6200ee', 18 | color = '#ffffff', 19 | style = '', 20 | class: className = '', 21 | icon, 22 | ...rest 23 | } = props 24 | 25 | const normal = !(outlined || text) 26 | 27 | const bg = normal ? background : '#ffffff' 28 | const clr = normal ? color : background 29 | const hoverClr = normal ? lightenColor(bg, 10) : lightenColor(bg, -10) 30 | const rippleClr = normal ? lightenColor(bg, 50) : lightenColor(background, 50) 31 | const cssHash = strToHash(outlined.toString() + text.toString() + bg + clr + style) 32 | 33 | const ripple = rippleEffect(rippleClr, hoverClr) 34 | 35 | const styles = ` 36 | .nano_jsx_button-${cssHash} { 37 | color: ${clr}; 38 | background: ${bg}; 39 | border-radius: 4px; 40 | display: inline-flex; 41 | font-size: 14px; 42 | padding: 10px 16px; 43 | margin: 0px 0px 1em 0px; 44 | text-align: center; 45 | cursor: pointer; 46 | 47 | ${userSelect} 48 | 49 | 50 | z-index: ${zIndex.button} 51 | 52 | ${boxShadow} 53 | 54 | border: none; 55 | text-transform: uppercase; 56 | box-shadow: 0 0 4px #999; 57 | outline: none; 58 | } 59 | 60 | ${ripple.styles} 61 | ` 62 | 63 | addStylesToHead(styles, cssHash) 64 | 65 | let customStyles = '' 66 | if (outlined || text) { 67 | customStyles += 'padding-top: 9px; padding-bottom: 9px; ' 68 | customStyles += '-webkit-box-shadow: none; -moz-box-shadow: none; box-shadow none; ' 69 | if (outlined) customStyles += `border: 1px ${clr} solid; ` 70 | } 71 | customStyles += style 72 | 73 | return h( 74 | 'button', 75 | { class: `nano_jsx_button-${cssHash} ${ripple.class} ${className}`, style: customStyles, ...rest }, 76 | icon ? h(Icon, { style: 'margin-left: -4px; margin-right: 8px; width: 14px; height: 14px;' }, icon) : null, 77 | children 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /deno_lib/ui/fab.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { h, strToHash } from '../core.ts' 3 | import { boxShadow, userSelect, zIndex } from './_config.ts' 4 | import { addStylesToHead } from './_helpers.ts' 5 | 6 | interface FabProps { 7 | onClick?: (e: MouseEvent) => void 8 | offsetY?: number 9 | center?: boolean 10 | left?: boolean 11 | extended?: boolean 12 | mini?: boolean 13 | background?: string 14 | color?: string 15 | } 16 | 17 | export class Fab extends Component { 18 | render() { 19 | const { 20 | background = '#6200EE', 21 | color = 'white', 22 | extended = false, 23 | mini = false, 24 | center = false, 25 | left = false, 26 | onClick = () => {} 27 | } = this.props 28 | 29 | const height = mini ? 40 : extended ? 48 : 56 30 | const cssHash = strToHash(extended.toString() + mini.toString() + center.toString() + left.toString()) 31 | const className = `fab-container-${cssHash}` 32 | 33 | const styles = ` 34 | .${className} { 35 | ${mini ? 'width: 40px;' : extended ? 'padding: 0px 12px;' : 'width: 56px;'} 36 | height: ${height}px; 37 | position: fixed; 38 | background: ${background}; 39 | border-radius: 36px; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | color: ${color}; 44 | cursor: pointer; 45 | 46 | z-index: ${zIndex.fab} 47 | bottom: ${this.props.offsetY ? 16 + this.props.offsetY : 16}px; 48 | ${left ? 'left: 16px;' : 'right: 16px;'} 49 | ${center ? 'transform: translateX(50%); right: 50%;' : ''} 50 | ${boxShadow} 51 | ${userSelect} 52 | } 53 | ` 54 | 55 | addStylesToHead(styles, cssHash) 56 | 57 | const { children } = this.props as any 58 | 59 | return h('div', { class: className, onClick: (e: MouseEvent) => onClick(e) }, children) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /deno_lib/ui/icon.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { h, strToHash } from '../core.ts' 3 | import { addStylesToHead } from './_helpers.ts' 4 | 5 | interface IconProps { 6 | src: string 7 | active?: boolean 8 | color?: string 9 | style?: string 10 | size?: number 11 | onClick?: (e: MouseEvent) => void 12 | } 13 | 14 | export class Icon extends Component { 15 | cssHash: string 16 | 17 | didUnmount() { 18 | // not sure if I want to remove the css, since there might be another Icon with the same styles. 19 | // const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 20 | // if (el) el.remove() 21 | } 22 | 23 | render() { 24 | const { src, size = 16, active = true, color = '#6204EE', style = '', ...rest } = this.props 25 | 26 | // @ts-ignore 27 | const children = this.props.children 28 | 29 | this.cssHash = strToHash(active + color + size.toString()) 30 | 31 | const colors = { 32 | active: color, 33 | inactive: '#00000070' 34 | } 35 | 36 | const styles = ` 37 | i.icon-${this.cssHash} { 38 | width: ${size}px; 39 | height: ${size}px; 40 | display: inline-block; 41 | content: ''; 42 | 43 | /*-webkit-mask: url(YOUR_SVG_URL) no-repeat 50% 50%; 44 | mask: url(YOUR_SVG_URL) no-repeat 50% 50%;*/ 45 | 46 | -webkit-mask-size: cover; 47 | mask-size: cover; 48 | 49 | background-color: ${colors.active}; 50 | } 51 | 52 | i.icon-${this.cssHash}.icon_inactive-${this.cssHash} { 53 | background-color: ${colors.inactive}; 54 | } 55 | ` 56 | 57 | addStylesToHead(styles, this.cssHash) 58 | 59 | // const iconStyle = `-webkit-mask: url(/dev/font-awesome/ellipsis-v-solid.svg) no-repeat 50% 50%;mask: url(/dev/font-awesome/ellipsis-v-solid.svg) no-repeat 50% 50%;` 60 | const iconStyle = `-webkit-mask: url(${src || children}) no-repeat 50% 50%; mask: url(${ 61 | src || children 62 | }) no-repeat 50% 50%;` 63 | 64 | const classes = [`icon-${this.cssHash}`] 65 | if (!active) classes.push(`icon_inactive-${this.cssHash}`) 66 | 67 | const icon = h('i', { class: classes.join(' '), ...rest, style: iconStyle + style }) 68 | 69 | return icon 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /deno_lib/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { AppBar } from '../ui/appBar.ts' 2 | export { Toolbar } from '../ui/toolbar.ts' 3 | export { Navigation, NavigationAction } from '../ui/navigation.ts' 4 | export { Button } from '../ui/button.ts' 5 | export { Dialog } from '../ui/dialog.ts' 6 | export { Fab } from '../ui/fab.ts' 7 | export { Icon } from '../ui/icon.ts' 8 | export { List, ListItem } from '../ui/list.ts' 9 | export { Menu } from '../ui/menu.ts' 10 | export { Snackbar } from '../ui/snackbar.ts' 11 | export { Tabs, Tab } from '../ui/tabs.ts' 12 | export { Sheet } from '../ui/sheet.ts' 13 | -------------------------------------------------------------------------------- /deno_lib/ui/list.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.ts' 2 | import { h, strToHash } from '../core.ts' 3 | import { Icon } from './icon.ts' 4 | import { addStylesToHead } from './_helpers.ts' 5 | 6 | interface ListProps { 7 | small?: boolean 8 | children?: any 9 | } 10 | 11 | interface ListItemProps { 12 | onClick?: Function 13 | icon?: string 14 | avatar?: string 15 | square?: string 16 | image?: string 17 | children?: any 18 | } 19 | 20 | export class ListItem extends Component { 21 | render() { 22 | const { props: p } = this 23 | const { onClick = () => {} } = p 24 | 25 | const adjustedMargin = 'margin-right: 16px;' 26 | 27 | const icon = p.icon ? h(Icon, { size: 20, style: 'margin-right: 32px;', src: p.icon }) : null 28 | const avatar = p.avatar 29 | ? h('img', { src: p.avatar, width: 40, height: 40, style: `border-radius: 20px; ${adjustedMargin}` }) 30 | : null 31 | const square = p.square ? h('img', { src: p.square, width: 56, height: 56, style: adjustedMargin }) : null 32 | const image = p.image 33 | ? h('img', { src: p.image, width: 100, height: 56, style: 'margin-left: -16px; margin-right: 16px;' }) 34 | : null 35 | const text = h('span', null, p.children) 36 | 37 | // additional style for the list item 38 | let style = '' 39 | if (p.icon || p.avatar) style += 'min-height: 56px; ' 40 | if (p.square || p.image) style += 'min-height: 72px; ' 41 | 42 | return h('li', { style, onClick }, icon, avatar, square, image, text) 43 | } 44 | } 45 | 46 | export class List extends Component { 47 | cssHash: string 48 | 49 | render() { 50 | const { small = false } = this.props 51 | 52 | this.cssHash = strToHash(`List${small.toString()}`) 53 | 54 | const styles = ` 55 | .list-${this.cssHash} ul { 56 | margin: 0px; 57 | padding: 8px 16px; 58 | } 59 | 60 | .list-${this.cssHash} ul li { 61 | list-style: none; 62 | min-height: ${small ? 32 : 46}px; 63 | display: flex; 64 | align-items: center; 65 | margin: 0px -16px; 66 | padding: 0px 16px; 67 | cursor: pointer; 68 | } 69 | 70 | .list-${this.cssHash} ul li span { 71 | font-size: 16px; 72 | } 73 | 74 | .list-${this.cssHash} ul li:hover { 75 | background:#00000010 76 | } 77 | ` 78 | 79 | addStylesToHead(styles, this.cssHash) 80 | 81 | const ul = h('ul', null, this.props.children) 82 | return h('div', { class: `list-${this.cssHash}` }, ul) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /deno_lib/ui/menu.ts: -------------------------------------------------------------------------------- 1 | import { h, removeAllChildNodes, render } from '../core.ts' 2 | import { boxShadow, zIndex } from './_config.ts' 3 | import { addStylesToHead } from './_helpers.ts' 4 | 5 | interface MenuOptions { 6 | position: { x: number; y: number } 7 | list: any 8 | } 9 | 10 | export class Menu { 11 | defaultParentId = 'menu_items_container' 12 | cssHash = Math.random().toString(36).substring(2) 13 | 14 | // didUnmount() { 15 | // const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 16 | // if (el) el.remove() 17 | // } 18 | 19 | private getParentElement(id: string) { 20 | // delete all other 21 | const others = document.querySelectorAll(`[id^="${this.defaultParentId}"]`) 22 | others.forEach(e => { 23 | e.remove() 24 | }) 25 | 26 | let el = document.getElementById(`${this.defaultParentId}-${id}`) 27 | if (!el) { 28 | el = document.createElement('div') 29 | el.id = `${this.defaultParentId}-${id}` 30 | } 31 | 32 | removeAllChildNodes(el) 33 | document.body.appendChild(el) 34 | 35 | return el 36 | } 37 | 38 | close() { 39 | removeAllChildNodes(this.getParentElement(this.cssHash)) 40 | } 41 | 42 | open(menuOptions: MenuOptions) { 43 | const { position, list } = menuOptions 44 | 45 | // check in which corner the menu appears and adjust fixed position. 46 | const left = position.x < window.innerWidth / 2 ? 'left' : 'right' 47 | const top = position.y < window.innerHeight / 2 ? 'top' : 'bottom' 48 | 49 | const styles = ` 50 | 51 | #menu_items_background-${this.cssHash} { 52 | width: 100vw; 53 | height: 100vh; 54 | background: transparent; 55 | position: fixed; 56 | top: 0; 57 | left: 0; 58 | z-index: ${zIndex.menu} 59 | } 60 | 61 | #menu_items_list-${this.cssHash} { 62 | position: fixed; 63 | background: white; 64 | 65 | border-radius: 4px; 66 | min-width: 112px; 67 | 68 | ${top}: ${position.y > window.innerHeight / 2 ? window.innerHeight - position.y : position.y}px; 69 | ${left}: ${position.x > window.innerWidth / 2 ? window.innerWidth - position.x : position.x}px; 70 | 71 | z-index: ${zIndex.menu} 72 | 73 | ${boxShadow} 74 | } 75 | 76 | ` 77 | // remove old styles 78 | const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 79 | if (el) el.remove() 80 | 81 | // add new styles 82 | addStylesToHead(styles, this.cssHash) 83 | 84 | const itemsList = h('div', { id: `menu_items_list-${this.cssHash}` }, list) 85 | const itemsBg = h('div', { onClick: () => this.close(), id: `menu_items_background-${this.cssHash}` }, itemsList) 86 | 87 | itemsList.addEventListener('click', (e: Event) => e.stopPropagation()) 88 | 89 | this.getParentElement(this.cssHash).appendChild(render(itemsBg)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /deno_lib/version.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | export const VERSION = '0.1.0' 3 | -------------------------------------------------------------------------------- /deno_lib/withStyles.ts: -------------------------------------------------------------------------------- 1 | import { h } from './core.ts' 2 | import { Component } from './component.ts' 3 | import { Fragment } from './fragment.ts' 4 | import { Helmet } from './components/helmet.ts' 5 | 6 | interface ObjectHasToString { 7 | toString: () => string 8 | } 9 | type Styles = string 10 | type FunctionReturnsString = () => string 11 | type WithStylesType = Styles | ObjectHasToString | FunctionReturnsString 12 | 13 | export const withStyles: any = 14 | (...styles: WithStylesType[]) => 15 | (WrappedComponent: any) => { 16 | return class extends Component { 17 | render() { 18 | const { children, ...rest } = this.props 19 | 20 | const helmets: any[] = [] 21 | styles.forEach(style => { 22 | if (typeof style === 'string') { 23 | helmets.push(h(Helmet, null, h('style', null, style))) 24 | } else if (typeof style === 'function') { 25 | const _style = style() 26 | if (typeof _style === 'string') { 27 | helmets.push(h(Helmet, null, h('style', null, _style))) 28 | } 29 | } else if (typeof style === 'object') { 30 | const _style = style.toString?.() 31 | if (typeof _style === 'string') { 32 | helmets.push(h(Helmet, null, h('style', null, _style))) 33 | } 34 | } 35 | }) 36 | 37 | const component = 38 | children && children.length > 0 39 | ? h(WrappedComponent, { ...rest }, children) 40 | : h(WrappedComponent, { ...this.props }) 41 | 42 | return h(Fragment, null, ...helmets, component) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dev/bundled-core.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 30 |
31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /dev/bundled-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 21 |
22 | 23 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /dev/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dev/font-awesome/LICENSE: -------------------------------------------------------------------------------- 1 | https://fontawesome.com/license -------------------------------------------------------------------------------- /dev/font-awesome/ellipsis-v-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/font-awesome/heart-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/font-awesome/home-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/font-awesome/user-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/img/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/nano/b1abb6ad02274105ce003eeb821da11716988199/dev/img/placeholder.png -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverage": true, 3 | "collectCoverageFrom": [ 4 | "lib/**/*.js", 5 | "!lib/{bundles,dev,htm,ui}/**", 6 | "!lib/mod.js" 7 | ], 8 | "maxConcurrency": 2, 9 | "maxWorkers": 2, 10 | "testEnvironment": "jsdom" 11 | } -------------------------------------------------------------------------------- /readme/nano-jsx-logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/nano/b1abb6ad02274105ce003eeb821da11716988199/readme/nano-jsx-logo-512.png -------------------------------------------------------------------------------- /readme/nano-jsx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/nano/b1abb6ad02274105ce003eeb821da11716988199/readme/nano-jsx-logo.png -------------------------------------------------------------------------------- /readme/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/nano/b1abb6ad02274105ce003eeb821da11716988199/readme/thumbnail.png -------------------------------------------------------------------------------- /scripts/browserTest/README.md: -------------------------------------------------------------------------------- 1 | # browserTest 2 | 3 | This package is used for testing Nano JSX in the browser. 4 | -------------------------------------------------------------------------------- /scripts/browserTest/mime.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { extname } from 'path' 4 | 5 | export const mime = fileName => { 6 | switch (extname(fileName)) { 7 | case '.css': 8 | return 'text/css' 9 | case '.js': 10 | return 'application/javascript' 11 | case '.html': 12 | return 'text/html' 13 | case '.json': 14 | return 'application/json' 15 | case '.jpg': 16 | case '.jpeg': 17 | return 'image/jpeg' 18 | case '.png': 19 | return 'image/png' 20 | case '.svg': 21 | case '.svgz': 22 | return 'image/svg+xml' 23 | default: 24 | return 'text/plain' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/browserTest/nyc.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { spawn } from 'child_process' 4 | 5 | const isWin = process.platform === 'win32' 6 | 7 | export const NYC = { 8 | instrument: (filePath, res) => { 9 | if (isWin) spawn('powershell.exe', ['npx', 'nyc', 'instrument', filePath]).stdout.pipe(res) 10 | else spawn('npx', ['nyc', 'instrument', filePath]).stdout.pipe(res) 11 | }, 12 | report: () => { 13 | if (isWin) spawn('powershell.exe', 'npx nyc report --reporter=text'.split(' '), { stdio: 'inherit' }) 14 | else spawn('npx', 'nyc report --reporter=text'.split(' '), { stdio: 'inherit' }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/browserTest/requestListener.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import path from 'path' 4 | import { mime } from './mime.mjs' 5 | import { readFile } from 'fs/promises' 6 | import { dirname, join } from 'path' 7 | import { fileURLToPath } from 'url' 8 | import { NYC } from './nyc.mjs' 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = dirname(__filename) 12 | 13 | const testerJS = await readFile(join(__dirname, './tester.js'), { encoding: 'utf-8' }) 14 | 15 | export const totalPasses = [0, 0] 16 | 17 | /** @type {Array<{text:string, id: number, type?: string}>} */ 18 | let _messages = [] 19 | 20 | const sendMessages = () => { 21 | _messages 22 | .sort((a, b) => a.id - b.id) 23 | .forEach(m => { 24 | if (m.type === 'end') { 25 | const match = m.text.match(/(\d+)\/(\d+).passing/m) 26 | if (match) { 27 | totalPasses[0] += parseInt(match[1]) 28 | totalPasses[1] += parseInt(match[2]) 29 | } 30 | } 31 | 32 | // log the text (with 2 spaces more) 33 | console.log( 34 | m.text 35 | .split('\n') 36 | .map(l => ` ${l}`) 37 | .join('\n') 38 | ) 39 | }) 40 | 41 | _messages = [] 42 | } 43 | 44 | const queueMessages = message => { 45 | _messages.push(message) 46 | 47 | setTimeout(() => { 48 | sendMessages() 49 | }, 100) 50 | } 51 | 52 | export const requestListener = ({ serve, collectCoverage }) => { 53 | return async (req, res) => { 54 | if (req.method === 'POST') { 55 | let data = '' 56 | req.on('data', chunk => { 57 | data += chunk 58 | }) 59 | req.on('end', () => { 60 | const message = JSON.parse(data) 61 | queueMessages(message) 62 | res.end() 63 | }) 64 | return 65 | } 66 | 67 | if (req.method === 'GET') { 68 | try { 69 | const filePath = path.join(path.resolve(), req.url) 70 | const contentType = mime(req.url) 71 | 72 | // tester.js 73 | if (req.url === '/tester.js') { 74 | return res.writeHead(200, { 'Content-Type': contentType }).end(testerJS) 75 | } 76 | // Will instrument all javascript files not containing "instrumented" 77 | else if (collectCoverage && contentType === 'application/javascript' && !/instrumented/.test(filePath)) { 78 | const myOptions = '' 79 | res.writeHead(200, { 'Content-Type': contentType }) 80 | NYC.instrument(filePath, res) 81 | return 82 | } 83 | // else 84 | else { 85 | const file = await readFile(filePath, { encoding: 'utf-8' }) 86 | if (!file) return res.writeHead(404).end() 87 | return res.writeHead(200, { 'Content-Type': contentType }).end(file) 88 | } 89 | } catch (err) { 90 | if (!err.message.endsWith(".ico'")) { 91 | console.log('Error:', err.message) 92 | } 93 | return res.writeHead(500).end() 94 | } 95 | } 96 | 97 | return res.writeHead(400).end() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /scripts/browserTest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "UMD", 5 | 6 | "noImplicitAny": false, 7 | 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "typeRoots": [] 11 | }, 12 | "include": ["./**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /scripts/packageType.mjs: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises' 2 | 3 | const pkgModule = { 4 | type: 'module' 5 | } 6 | 7 | const pkgCommonjs = { 8 | type: 'commonjs' 9 | } 10 | 11 | await writeFile('esm/package.json', JSON.stringify(pkgModule, null, 2), { encoding: 'utf-8' }) 12 | await writeFile('lib/package.json', JSON.stringify(pkgCommonjs, null, 2), { encoding: 'utf-8' }) 13 | -------------------------------------------------------------------------------- /src/bundles/bundle.core.ts: -------------------------------------------------------------------------------- 1 | // core 2 | import { h, render } from '../core.js' 3 | 4 | export default { 5 | render, 6 | h 7 | } 8 | -------------------------------------------------------------------------------- /src/bundles/bundle.full.ts: -------------------------------------------------------------------------------- 1 | // core 2 | import { h, hydrate, removeAllChildNodes, render, tick } from '../core.js' 3 | import { hydrateLazy } from '../lazy.js' 4 | import { nodeToString, task } from '../helpers.js' 5 | 6 | // useful tools 7 | import { Component } from '../component.js' 8 | import { Fragment } from '../fragment.js' 9 | import { Store } from '../store.js' 10 | import { createContext } from '../context.js' 11 | import { withStyles } from '../withStyles.js' 12 | 13 | // built-in components 14 | import { Helmet } from '../components/helmet.js' 15 | import { Link } from '../components/link.js' 16 | import { Img } from '../components/img.js' 17 | import { Visible } from '../components/visible.js' 18 | import * as Router from '../components/router.js' 19 | 20 | // customElement 21 | import { defineAsCustomElements } from '../customElementsMode.js' 22 | 23 | // tagged templates 24 | import { jsx } from '../jsx.js' 25 | 26 | // ui 27 | import * as UI from '../ui/index.js' 28 | 29 | export default { 30 | Component, 31 | Fragment, 32 | Helmet, 33 | Img, 34 | Link, 35 | Router, 36 | Store, 37 | UI, 38 | Visible, 39 | createContext, 40 | h, 41 | hydrate, 42 | hydrateLazy, 43 | jsx, 44 | nodeToString, 45 | removeAllChildNodes, 46 | render, 47 | task, 48 | tick, 49 | withStyles, 50 | defineAsCustomElements 51 | } 52 | 53 | // version 54 | export { printVersion } from '../helpers.js' 55 | export { VERSION } from '../version.js' 56 | -------------------------------------------------------------------------------- /src/bundles/bundle.slim.ts: -------------------------------------------------------------------------------- 1 | // core 2 | import { h, render, tick } from '../core.js' 3 | import { task } from '../helpers.js' 4 | 5 | // useful tools 6 | import { Component } from '../component.js' 7 | import { Fragment } from '../fragment.js' 8 | import { Store } from '../store.js' 9 | import { createContext } from '../context.js' 10 | import { withStyles } from '../withStyles.js' 11 | import * as Router from '../components/router.js' 12 | 13 | // tagged templates 14 | import { jsx } from '../jsx.js' 15 | 16 | export default { 17 | Component, 18 | Fragment, 19 | Router, 20 | Store, 21 | createContext, 22 | h, 23 | jsx, 24 | render, 25 | task, 26 | tick, 27 | withStyles 28 | } 29 | 30 | // version 31 | export { printVersion } from '../helpers.js' 32 | export { VERSION } from '../version.js' 33 | -------------------------------------------------------------------------------- /src/bundles/bundle.ui.ts: -------------------------------------------------------------------------------- 1 | // UI Elements 2 | import { AppBar } from '../ui/appBar.js' 3 | import { Toolbar } from '../ui/toolbar.js' 4 | import { Navigation, NavigationAction } from '../ui/navigation.js' 5 | import { Button } from '../ui/button.js' 6 | import { Dialog } from '../ui/dialog.js' 7 | import { Fab } from '../ui/fab.js' 8 | import { Icon } from '../ui/icon.js' 9 | import { List, ListItem } from '../ui/list.js' 10 | import { Menu } from '../ui/menu.js' 11 | import { Snackbar } from '../ui/snackbar.js' 12 | 13 | export default { 14 | AppBar, 15 | Toolbar, 16 | Navigation, 17 | NavigationAction, 18 | Button, 19 | Dialog, 20 | Fab, 21 | Icon, 22 | List, 23 | ListItem, 24 | Menu, 25 | Snackbar 26 | } 27 | -------------------------------------------------------------------------------- /src/components/img.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { h, strToHash } from '../core.js' 3 | 4 | interface Props { 5 | [key: string]: any 6 | src: string 7 | height?: number | string 8 | width?: number | string 9 | lazy?: boolean 10 | placeholder?: any 11 | } 12 | 13 | /** 14 | * A useful Image component 15 | * Add { 20 | constructor(props: Props) { 21 | super(props) 22 | 23 | const { src, key } = props 24 | 25 | // id has to be unique 26 | this.id = `${strToHash(src)}-${strToHash(JSON.stringify(props))}` 27 | if (key) this.id += `key-${key}` 28 | 29 | // this could also be done in willMount() 30 | if (!this.state) this.setState({ isLoaded: false, image: undefined }) 31 | } 32 | 33 | didMount() { 34 | const { lazy = true, placeholder, children, key, ref, ...rest } = this.props 35 | 36 | if (typeof lazy === 'boolean' && lazy === false) return 37 | 38 | const observer = new IntersectionObserver( 39 | (entries, observer) => { 40 | entries.forEach(entry => { 41 | if (entry.isIntersecting) { 42 | observer.disconnect() 43 | this.state.image = h('img', { ...rest }) as HTMLImageElement 44 | if (this.state.image.complete) { 45 | this.state.isLoaded = true 46 | this.update() 47 | } else { 48 | this.state.image.onload = () => { 49 | this.state.isLoaded = true 50 | this.update() 51 | } 52 | } 53 | } 54 | }) 55 | }, 56 | { threshold: [0, 1] } 57 | ) 58 | observer.observe(this.elements[0]) 59 | } 60 | render() { 61 | const { src, placeholder, children, lazy = true, key, ref, ...rest } = this.props 62 | 63 | // return the img tag if not lazy loaded 64 | if (typeof lazy === 'boolean' && lazy === false) { 65 | this.state.image = h('img', { src, ...rest }) as HTMLImageElement 66 | return this.state.image 67 | } 68 | 69 | // if it is visible and loaded, show the image 70 | if (this.state.isLoaded) { 71 | return this.state.image 72 | // if the placeholder is an image src 73 | } else if (placeholder && typeof placeholder === 'string') { 74 | return h('img', { src: placeholder, ...rest }) 75 | // if the placeholder is an JSX element 76 | } else if (placeholder && typeof placeholder === 'function') { 77 | return placeholder() 78 | } else { 79 | // render a simple box 80 | const style: Record = {} 81 | if (rest.width) style.width = `${rest.width}px` 82 | if (rest.height) style.height = `${rest.height}px` 83 | const { width, height, ...others } = rest 84 | return h('div', { style, ...others }) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Helmet } from './helmet.js' 2 | export { Img } from './img.js' 3 | export { Link } from './link.js' 4 | export * as Router from './router.js' 5 | export { Suspense } from './suspense.js' 6 | export { Visible } from './visible.js' 7 | -------------------------------------------------------------------------------- /src/components/suspense.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { isSSR, strToHash } from '../core.js' 3 | 4 | interface Props { 5 | fallback: any 6 | cache?: boolean 7 | [key: string]: any 8 | } 9 | 10 | export class Suspense extends Component { 11 | ready = false 12 | 13 | constructor(props: Props) { 14 | super(props) 15 | 16 | // get props promises in ...rest 17 | const { children, fallback, cache = false, ...rest } = this.props 18 | 19 | // stringify ...rest 20 | const str = JSON.stringify(rest, function (_key, val) { 21 | if (typeof val === 'function') return `${val}` // implicitly `toString` it 22 | return val 23 | }) 24 | 25 | // create unique id based on ...rest 26 | this.id = strToHash(JSON.stringify(str)) 27 | } 28 | 29 | async didMount() { 30 | // get props promises in ...rest 31 | const { children, fallback, cache = false, ...rest } = this.props 32 | 33 | // set initial state to [] 34 | if (cache) this.initState = {} 35 | 36 | // check if we already cached the results in this.state 37 | if (this.loadFromCache(cache)) return 38 | 39 | // resolve the promises 40 | const promises = Object.values(rest).map(p => p()) 41 | const resolved = await Promise.all(promises) 42 | 43 | // prepare data 44 | const data = this.prepareData(rest, resolved, cache) 45 | 46 | // add data to children 47 | this.addDataToChildren(data) 48 | 49 | // update the component 50 | this.ready = true 51 | this.update() 52 | } 53 | 54 | ssr() { 55 | // get props promises in ...rest 56 | const { children, fallback, cache = false, ...rest } = this.props 57 | 58 | // execute the functions 59 | const functions = Object.values(rest).map(p => p()) 60 | 61 | // prepare data 62 | const data = this.prepareData(rest, functions, false) 63 | 64 | // add data to children 65 | this.addDataToChildren(data) 66 | } 67 | 68 | loadFromCache(cache: boolean) { 69 | const hasCachedProps = this.state && cache && Object.keys(this.state).length > 0 70 | 71 | if (hasCachedProps) { 72 | this.addDataToChildren(this.state) 73 | this.ready = true 74 | } 75 | 76 | return hasCachedProps 77 | } 78 | 79 | prepareData(rest: any, fnc: Function[], cache: boolean) { 80 | const data = Object.keys(rest).reduce((obj, item, index) => { 81 | if (cache) this.state = { ...this.state, [item]: fnc[index] } 82 | return { 83 | ...obj, 84 | [item]: fnc[index] 85 | } 86 | }, {}) 87 | return data 88 | } 89 | 90 | addDataToChildren(data: any) { 91 | // add data as props to children 92 | this.props.children.forEach((child: any) => { 93 | if (child.props) child.props = { ...child.props, ...data } 94 | }) 95 | } 96 | 97 | render() { 98 | if (!isSSR()) { 99 | const { cache = false } = this.props 100 | this.loadFromCache(cache) 101 | return !this.ready ? this.props.fallback : this.props.children 102 | } else { 103 | this.ssr() 104 | return this.props.children 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/visible.ts: -------------------------------------------------------------------------------- 1 | import { h, render } from '../core.js' 2 | import { Component } from '../component.js' 3 | 4 | export class Visible extends Component { 5 | isVisible = false 6 | 7 | didMount() { 8 | const observer = new IntersectionObserver( 9 | (entries, observer) => { 10 | entries.forEach(entry => { 11 | if (entry.isIntersecting) { 12 | observer.disconnect() 13 | this.isVisible = true 14 | this.update() 15 | } 16 | }) 17 | }, 18 | { threshold: [0, 1] } 19 | ) 20 | observer.observe(this.elements[0]) 21 | } 22 | 23 | render() { 24 | if (!this.isVisible) { 25 | return h('div', { 'data-visible': false, visibility: 'hidden' }) 26 | } else { 27 | if (this.props.onVisible) this.props.onVisible() 28 | return render(this.props.component || this.props.children[0]) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | export const createContext = (ctx: any) => { 2 | let _ctx = ctx 3 | return { 4 | Provider: (props: any) => { 5 | if (props.value) _ctx = props.value 6 | return props.children 7 | }, 8 | Consumer: (props: any) => { 9 | return { component: props.children[0](_ctx), props: { ...props, context: _ctx } } 10 | }, 11 | get: () => _ctx, 12 | set: (ctx: any) => (_ctx = ctx) 13 | } 14 | } 15 | 16 | export const useContext = (ctx: any) => { 17 | const _ctx = ctx 18 | if (_ctx && typeof _ctx.get === 'function') { 19 | return _ctx.get() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/dev/components/isomorphic.tsx: -------------------------------------------------------------------------------- 1 | import { h, FC } from '../../core.js' 2 | import { Component } from '../../component.js' 3 | 4 | /** 5 | * This Component work on the client and the server. 6 | * (Make your you prefetch the static getData() and pass it as "data" props in SSR) 7 | */ 8 | 9 | // simply mocks a server fetch and returns an array of comments 10 | const fetchMock = (): Promise => 11 | new Promise(resolve => setTimeout(() => resolve(['comment_one', 'comment_two']), 500)) 12 | 13 | // the comments component 14 | interface CommentsProps { 15 | comments: string[] 16 | } 17 | const Comments: FC = ({ comments }) => { 18 | console.log(comments) 19 | return ( 20 |
    21 | {comments.map(d => ( 22 |
  • {d}
  • 23 | ))} 24 |
25 | ) 26 | } 27 | 28 | // const Bla = () => { 29 | // console.log('BLA') 30 | // return
asdf
31 | // } 32 | 33 | // the app component 34 | class App extends Component { 35 | // this static method can be calles before the componend is rendered in SSR mode 36 | static async getData() { 37 | // get some data from your server 38 | return await fetchMock() 39 | } 40 | 41 | async didMount() { 42 | // will re-render the component and pass the result of getData() 43 | const data = await App.getData() 44 | this.update(data) 45 | } 46 | 47 | render(data: string[]) { 48 | // this.props.data will be defined if in SSR mode 49 | data = data || this.props.data 50 | 51 | // console.log(Nano.h(Bla, null)) 52 | if (data) return 53 | // 54 | // this will be shown while loading on the client side 55 | else return
loading...
56 | } 57 | } 58 | 59 | export { App } 60 | -------------------------------------------------------------------------------- /src/dev/dev.tsx: -------------------------------------------------------------------------------- 1 | import { h, render } from '../core.js' 2 | import { Component } from '../component.js' 3 | 4 | class App extends Component { 5 | render() { 6 | return
Nano JSX App
7 | } 8 | } 9 | 10 | render(, document.getElementById('root')) 11 | -------------------------------------------------------------------------------- /src/dev/devSSR.tsx: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | 3 | import { h } from '../core.js' 4 | import { renderSSR } from '../ssr.js' 5 | import { Component } from '../component.js' 6 | import { Helmet } from '../components/helmet.js' 7 | 8 | // @ts-ignore 9 | import fs from 'fs' 10 | // @ts-ignore 11 | import { join } from 'path' 12 | // @ts-ignore 13 | import http from 'http' 14 | 15 | class App extends Component { 16 | render() { 17 | return
Nano JSX App
18 | } 19 | } 20 | 21 | const app = renderSSR() 22 | const { body, head, footer } = Helmet.SSR(app) 23 | 24 | const html = ` 25 | 26 | 27 | 28 | 29 | 34 | ${head.join('\n')} 35 | 36 | 37 |
38 | ${body} 39 |
40 | 41 | ${footer.join('\n')} 42 | 43 | ` 44 | 45 | // minify 46 | // html = html.replace(/[\s]+/gm, ' ') 47 | 48 | http 49 | .createServer((req: any, res: any) => { 50 | const { url } = req 51 | 52 | if (/\.html$/.test(url)) return res.end(html) 53 | 54 | // @ts-ignore 55 | const path = join(__dirname, '../../', url) 56 | 57 | fs.readFile(path, (err: any, data: any) => { 58 | if (err) { 59 | res.writeHead(404) 60 | return res.end(data) 61 | } 62 | const type = /\.png$/.test(url) ? 'image/png' : 'image/svg+xml' 63 | res.setHeader('Content-Type', type) 64 | return res.end(data) 65 | }) 66 | }) 67 | .listen(8080, () => console.log('open http://localhost:8080/index.html in your browser')) 68 | -------------------------------------------------------------------------------- /src/fragment.ts: -------------------------------------------------------------------------------- 1 | export const Fragment = (props: any) => { 2 | return props.children 3 | } 4 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { VERSION } from './version.js' 2 | 3 | /** Creates a new Task using setTimeout() */ 4 | export const task = (task: () => void) => setTimeout(task, 0) 5 | 6 | export const nodeToString = (node: Node) => { 7 | const tmpNode = document.createElement('div') 8 | tmpNode.appendChild(node.cloneNode(true)) 9 | return tmpNode.innerHTML 10 | } 11 | 12 | export const detectSSR = (): boolean => { 13 | // @ts-ignore 14 | const isDeno = typeof Deno !== 'undefined' 15 | const hasWindow = typeof window !== 'undefined' ? true : false 16 | return (typeof _nano !== 'undefined' && _nano.isSSR) || isDeno || !hasWindow 17 | } 18 | 19 | function isDescendant(desc: ParentNode | null, root: Node): boolean { 20 | // @ts-ignore 21 | return !!desc && (desc === root || isDescendant(desc.parentNode, root)) 22 | } 23 | 24 | // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 25 | export const onNodeRemove = (element: HTMLElement, callback: () => void) => { 26 | let observer = new MutationObserver(mutationsList => { 27 | mutationsList.forEach(mutation => { 28 | mutation.removedNodes.forEach(removed => { 29 | if (isDescendant(element, removed)) { 30 | callback() 31 | if (observer) { 32 | // allow garbage collection 33 | observer.disconnect() 34 | // @ts-ignore 35 | observer = undefined 36 | } 37 | } 38 | }) 39 | }) 40 | }) 41 | observer.observe(document, { 42 | childList: true, 43 | subtree: true 44 | }) 45 | return observer 46 | } 47 | 48 | // https://stackoverflow.com/a/6234804 49 | export const escapeHtml = (unsafe: string) => { 50 | if (unsafe && typeof unsafe === 'string') 51 | return unsafe 52 | .replace(/&/g, '&') 53 | .replace(//g, '>') 55 | .replace(/"/g, '"') 56 | .replace(/'/g, ''') 57 | return unsafe 58 | } 59 | 60 | export const printVersion = () => { 61 | const info = `Powered by nano JSX v${VERSION}` 62 | console.log( 63 | `%c %c %c %c %c ${info} %c http://nanojsx.io`, 64 | 'background: #ff0000', 65 | 'background: #ffff00', 66 | 'background: #00ff00', 67 | 'background: #00ffff', 68 | 'color: #fff; background: #000000;', 69 | 'background: none' 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useState, getState, setState } from './useState.js' 2 | -------------------------------------------------------------------------------- /src/hooks/useState.ts: -------------------------------------------------------------------------------- 1 | import { _state } from '../state.js' 2 | 3 | export const useState = (state: T, id: string): readonly [T, (state: T) => void] => { 4 | const s = { 5 | setState(state: T) { 6 | if (state !== null) _state.set(id, state) 7 | }, 8 | get state(): T { 9 | return _state.get(id) 10 | } 11 | } 12 | 13 | if (!_state.has(id)) _state.set(id, state) 14 | 15 | return [s.state, s.setState] 16 | } 17 | 18 | export const getState = (id: string) => { 19 | return _state.get(id) 20 | } 21 | 22 | export const setState = (id: string, state: any) => { 23 | return _state.set(id, state) 24 | } 25 | -------------------------------------------------------------------------------- /src/htm.ts: -------------------------------------------------------------------------------- 1 | import htm from './htm/index.js' 2 | export default htm 3 | -------------------------------------------------------------------------------- /src/htm/README.md: -------------------------------------------------------------------------------- 1 | All files in this folder are copied from version 3.0.4 of htm from https://github.com/developit/htm and ported to TypeScript to make it compatible with Deno. 2 | 3 | Please take a look at the [LICENSE](./LICENSE) file. 4 | -------------------------------------------------------------------------------- /src/htm/constants.ts: -------------------------------------------------------------------------------- 1 | export const MINI = false 2 | -------------------------------------------------------------------------------- /src/htm/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { MINI } from './constants.js' 15 | import { build, evaluate } from './build.js' 16 | 17 | const CACHES = new Map() 18 | 19 | const regular = function (this: HTMLElement, statics: TemplateStringsArray) { 20 | let tmp = CACHES.get(this) 21 | if (!tmp) { 22 | tmp = new Map() 23 | CACHES.set(this, tmp) 24 | } 25 | tmp = evaluate(this, tmp.get(statics) || (tmp.set(statics, (tmp = build(statics))), tmp), arguments, []) 26 | return tmp.length > 1 ? tmp : tmp[0] 27 | } 28 | 29 | // export as htm 30 | export default MINI ? build : regular 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // core 2 | export { h, render, hydrate, tick } from './core.js' 3 | export type { FC } from './core.js' 4 | 5 | // component 6 | export { Component } from './component.js' 7 | 8 | // built-in Components 9 | export * from './components/index.js' 10 | 11 | // export some defaults (Nano) 12 | import { h, render, hydrate, isSSR } from './core.js' 13 | import { renderSSR } from './ssr.js' 14 | export default { h, render, hydrate, renderSSR, isSSR } 15 | 16 | // other 17 | export { isSSR } 18 | export { jsx } from './jsx.js' 19 | export { hydrateLazy } from './lazy.js' 20 | export { nodeToString, task } from './helpers.js' 21 | export { renderSSR } from './ssr.js' 22 | export { Fragment } from './fragment.js' 23 | export { Store } from './store.js' 24 | export { createContext, useContext } from './context.js' 25 | export { withStyles } from './withStyles.js' 26 | export { defineAsCustomElements } from './customElementsMode.js' 27 | 28 | // version 29 | export { printVersion } from './helpers.js' 30 | export { VERSION } from './version.js' 31 | -------------------------------------------------------------------------------- /src/jsx-runtime/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | export { Fragment } from '../fragment.js' 3 | import { h } from '../core.js' 4 | 5 | const createNode: (type: any, props: any, key: string, source?: string, self?: string) => any = function (type, props) { 6 | let { children = [], ..._props } = props 7 | if (!Array.isArray(children)) children = [children] 8 | return h(type, _props, ...children) 9 | } 10 | 11 | export { createNode as jsx } 12 | export { createNode as jsxs } 13 | export { createNode as jsxDev } 14 | -------------------------------------------------------------------------------- /src/jsx.ts: -------------------------------------------------------------------------------- 1 | import { h } from './core.js' 2 | import htm from './htm.js' 3 | const jsx = htm.bind(h) 4 | 5 | export { jsx } 6 | -------------------------------------------------------------------------------- /src/lazy.ts: -------------------------------------------------------------------------------- 1 | import { hydrate, h } from './core.js' 2 | import { Visible } from './components/visible.js' 3 | 4 | export const hydrateLazy = (component: any, parent: HTMLElement | null = null, removeChildNodes = true) => { 5 | const c = h(Visible, null, component) 6 | return hydrate(c, parent, removeChildNodes) 7 | } 8 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './index.js' 2 | -------------------------------------------------------------------------------- /src/ssr.ts: -------------------------------------------------------------------------------- 1 | import { isSSR, render } from './core.js' 2 | import { documentSSR } from './regexDom.js' 3 | import { _state } from './state.js' 4 | import { detectSSR } from './helpers.js' 5 | 6 | // functions that should only be available on the server-side 7 | const ssrTricks = { 8 | isWebComponent: (tagNameOrComponent: any) => { 9 | return ( 10 | typeof tagNameOrComponent === 'string' && 11 | tagNameOrComponent.includes('-') && 12 | _nano.customElements.has(tagNameOrComponent) 13 | ) 14 | }, 15 | renderWebComponent: (tagNameOrComponent: any, props: any, children: any, _render: any) => { 16 | const customElement = _nano.customElements.get(tagNameOrComponent) 17 | const component = _render({ component: customElement, props: { ...props, children: children } }) 18 | // get the html tag and the innerText from string 19 | // match[1]: HTMLTag 20 | // match[2]: innerText 21 | const match = component.toString().match(/^<(?[a-z]+)>(.*)<\/\k>$/) 22 | if (match) { 23 | const element = document.createElement(match[1]) 24 | element.innerText = match[2] 25 | 26 | // eslint-disable-next-line no-inner-declarations 27 | function replacer(match: string, p1: string, _offset: number, _string: string): string { 28 | return match.replace(p1, '') 29 | } 30 | // remove events like onClick from DOM 31 | element.innerText = element.innerText.replace(/<\w+[^>]*(\s(on\w*)="[^"]*")/gm, replacer) 32 | 33 | return element 34 | } else { 35 | return null 36 | } 37 | } 38 | } 39 | 40 | const initGlobalVar = () => { 41 | const isSSR = detectSSR() === true ? true : undefined 42 | const location = { pathname: '/' } 43 | const document = isSSR ? documentSSR() : window.document 44 | globalThis._nano = { isSSR, location, document, customElements: new Map(), ssrTricks } 45 | } 46 | initGlobalVar() 47 | 48 | export const initSSR = (pathname: string = '/') => { 49 | _nano.location = { pathname } 50 | globalThis.document = _nano.document = isSSR() ? documentSSR() : window.document 51 | } 52 | 53 | export const renderSSR = (component: any, options: { pathname?: string; clearState?: boolean } = {}) => { 54 | const { pathname, clearState = true } = options 55 | 56 | initSSR(pathname) 57 | if (clearState) _state.clear() 58 | 59 | const tmp = render(component, null, true) as string | string[] 60 | if (Array.isArray(tmp)) return tmp.join('') 61 | else return Array.from(tmp).join('') 62 | } 63 | 64 | export const clearState = () => { 65 | _state.clear() 66 | } 67 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | /** Holds the state of the whole application. */ 2 | export const _state: Map = new Map() 3 | 4 | /** Clears the state of the whole application. */ 5 | export const _clearState = () => { 6 | _state.clear() 7 | } 8 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { isSSR } from './core.js' 2 | 3 | export class Store { 4 | private _state: S 5 | private _prevState: S 6 | private _listeners: Map = new Map() 7 | private _storage: 'memory' | 'local' | 'session' 8 | private _id: string 9 | 10 | /** 11 | * Create your own Store. 12 | * @param defaultState Pass the initial State. 13 | * @param name The name of the Store (only required if you persist the state in localStorage or sessionStorage). 14 | * @param storage Pass 'memory', 'local' or 'session'. 15 | */ 16 | constructor(defaultState: Object, name: string = '', storage: 'memory' | 'local' | 'session' = 'memory') { 17 | if (isSSR()) storage = 'memory' 18 | 19 | this._id = name 20 | this._storage = storage 21 | 22 | this._state = this._prevState = defaultState as any 23 | 24 | if (storage === 'memory' || !storage) return 25 | 26 | const Storage = storage === 'local' ? localStorage : sessionStorage 27 | 28 | // get/set initial state of Storage 29 | const item = Storage.getItem(this._id) 30 | if (item) { 31 | this._state = this._prevState = JSON.parse(item) 32 | } else Storage.setItem(this._id, JSON.stringify(defaultState)) 33 | } 34 | 35 | private persist(newState: S) { 36 | if (this._storage === 'memory') return 37 | const Storage = this._storage === 'local' ? localStorage : sessionStorage 38 | Storage.setItem(this._id, JSON.stringify(newState)) 39 | } 40 | 41 | /** Clears the state of the whole store. */ 42 | public clear() { 43 | // @ts-ignore 44 | this._state = this._prevState = undefined 45 | 46 | if (this._storage === 'local') localStorage.removeItem(this._id) 47 | else if (this._storage === 'session') sessionStorage.removeItem(this._id) 48 | } 49 | 50 | public setState(newState: S) { 51 | this.state = newState 52 | } 53 | 54 | public set state(newState: S) { 55 | this._prevState = this._state 56 | this._state = newState 57 | 58 | this.persist(newState) 59 | 60 | this._listeners.forEach(fnc => { 61 | fnc(this._state, this._prevState) 62 | }) 63 | } 64 | 65 | public get state(): S { 66 | return this._state 67 | } 68 | 69 | public use() { 70 | const id = Math.random().toString(36).substring(2, 9) 71 | const _this = this 72 | return { 73 | get state(): S { 74 | return _this.state 75 | }, 76 | setState: (newState: S) => { 77 | this.state = newState 78 | }, 79 | subscribe: (fnc: (newState: S, prevState: S) => void) => { 80 | this._listeners.set(id, fnc) 81 | }, 82 | cancel: () => { 83 | if (this._listeners.has(id)) this._listeners.delete(id) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // fixes an issue in std@0.80.0 (deno) 2 | // interface ReadableStream { 3 | // getIterator(): any 4 | // } 5 | 6 | declare global { 7 | export var _nano: { 8 | document: Document 9 | isSSR: true | undefined 10 | location: { pathname: string } 11 | customElements: Map 12 | ssrTricks: { 13 | isWebComponent: (tagNameOrComponent: any) => boolean 14 | renderWebComponent: (tagNameOrComponent: any, props: any, children: any, _render: any) => any 15 | } 16 | } 17 | 18 | export namespace Deno {} 19 | 20 | export namespace JSX { 21 | interface IntrinsicElements { 22 | [elemName: string]: any 23 | } 24 | interface ElementClass { 25 | render: any 26 | } 27 | interface ElementChildrenAttribute { 28 | children: any 29 | } 30 | } 31 | } 32 | // This export keeps the backward compatibility with the module resolution system in deno < 1.23 version. 33 | export {} 34 | -------------------------------------------------------------------------------- /src/ui/README.md: -------------------------------------------------------------------------------- 1 | # UI Elements 2 | 3 | These ui elements should look very similar to the [Material UI Components](https://material.io/components). 4 | 5 | Feel free to improve the elements or add new once. 6 | -------------------------------------------------------------------------------- /src/ui/_config.ts: -------------------------------------------------------------------------------- 1 | import { strToHash } from '../core.js' 2 | 3 | export const boxShadow = ` 4 | -webkit-box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 5 | -moz-box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 6 | box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 6px 10px 0px rgba(0,0,0,0.14), 0px 1px 18px 0px rgba(0,0,0,0.12); 7 | ` 8 | 9 | export const userSelect = ` 10 | -webkit-touch-callout:none; 11 | -webkit-user-select:none; 12 | -khtml-user-select:none; 13 | -moz-user-select:none; 14 | -ms-user-select:none; 15 | user-select:none; 16 | -webkit-tap-highlight-color:rgba(0,0,0,0); 17 | ` 18 | 19 | export const rippleEffect = (rippleClr: string, hoverClr: string) => { 20 | const rippleClass = `ripple-${strToHash(rippleClr + hoverClr)}` 21 | 22 | const styles = ` 23 | .${rippleClass} { 24 | background-position: center; 25 | transition: background 0.8s; 26 | } 27 | 28 | .${rippleClass}:hover { 29 | background: ${hoverClr} radial-gradient(circle, transparent 1%, ${hoverClr} 1%) center/15000%; 30 | } 31 | 32 | .${rippleClass}:focus { 33 | background: ${hoverClr} radial-gradient(circle, transparent 1%, ${hoverClr} 1%) center/15000%; 34 | } 35 | 36 | .${rippleClass}:active { 37 | background-color: ${rippleClr}; 38 | background-size: 100%; 39 | transition: background 0s; 40 | }` 41 | 42 | return { 43 | styles, 44 | class: rippleClass 45 | } 46 | } 47 | 48 | export const zIndex = { 49 | button: 'unset;', 50 | banner: '50;', 51 | bar: '100;', 52 | navigation: '100;', 53 | fab: '200;', 54 | sheet: '300;', 55 | menu: '400;', 56 | snackbar: '500;', 57 | dialog: '600;' 58 | } 59 | -------------------------------------------------------------------------------- /src/ui/_helpers.ts: -------------------------------------------------------------------------------- 1 | import { h } from '../core.js' 2 | 3 | // https://gist.github.com/renancouto/4675192 4 | export const lightenColor = (color: string, percent: number) => { 5 | const num = parseInt(color.replace('#', ''), 16), 6 | amt = Math.round(2.55 * percent), 7 | R = (num >> 16) + amt, 8 | B = ((num >> 8) & 0x00ff) + amt, 9 | G = (num & 0x0000ff) + amt 10 | return `#${( 11 | 0x1000000 + 12 | (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + 13 | (B < 255 ? (B < 1 ? 0 : B) : 255) * 0x100 + 14 | (G < 255 ? (G < 1 ? 0 : G) : 255) 15 | ) 16 | .toString(16) 17 | .slice(1)}` 18 | } 19 | 20 | export const addStylesToHead = (styles: string, hash: string) => { 21 | const el = document.querySelector(`[data-css-hash*="${hash}"]`) 22 | if (!el) { 23 | const styleElement = h('style', { 'data-css-hash': hash }, styles) 24 | document.head.appendChild(styleElement) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/banner.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { h } from '../core.js' 3 | import { Button } from './button.js' 4 | import { zIndex } from './_config.js' 5 | 6 | interface BannerAction { 7 | name: string 8 | id?: string | number 9 | color?: string 10 | } 11 | 12 | interface BannerActionEvent { 13 | action: string 14 | } 15 | 16 | interface BannerProps { 17 | title?: string 18 | body?: string 19 | actions?: BannerAction[] 20 | onAction?: () => BannerActionEvent 21 | parentId?: string 22 | sticky?: number 23 | } 24 | 25 | export class Banner extends Component { 26 | defaultActionColor = '#6200EE' 27 | 28 | render() { 29 | const { 30 | sticky, 31 | body = 'There was a problem processing a transaction on your credit card.', 32 | actions = [ 33 | { name: 'fix it', color: this.defaultActionColor }, 34 | { name: 'learn more', color: this.defaultActionColor } 35 | ] 36 | } = this.props 37 | 38 | const stickyStyles = sticky 39 | ? ` 40 | position: -webkit-sticky; 41 | position: sticky; 42 | top: ${sticky}px;` 43 | : '' 44 | 45 | const styles = { 46 | container: ` 47 | margin: -16px -16px 16px -16px; 48 | ${stickyStyles} 49 | z-index: ${zIndex.banner} 50 | `, 51 | banner: ` 52 | 53 | display: flex; 54 | justify-content: space-between; 55 | flex-wrap: wrap; 56 | 57 | 58 | background: white; 59 | border-bottom: 1px rgb(0 0 0 / 0.12) solid; 60 | min-height: 54px;`, 61 | body: ` 62 | color: #000000; 63 | padding: 16px; 64 | padding-bottom: 8px; 65 | font-size: 16px; 66 | line-height: 1.5em;`, 67 | actions: ` 68 | margin: 0; 69 | padding: 8px; 70 | display: flex; 71 | flex-direction: row; 72 | align-items: flex-end; 73 | margin-left: auto; 74 | flex-wrap: wrap; 75 | justify-content: flex-end; 76 | `, 77 | action: ` 78 | margin-bottom: 0px; 79 | margin-left: 10px;` 80 | } 81 | 82 | const actionsArray = actions.map((action: any) => { 83 | return h( 84 | Button, 85 | { 86 | text: true, 87 | color: action.color || this.defaultActionColor, 88 | style: styles.action, 89 | // style: `color: ${action.color || this.defaultActionColor}`, 90 | onClick: () => { 91 | // callback({ name: action.name, id: action.id }) 92 | // this.remove() 93 | } 94 | }, 95 | action.name.toUpperCase() 96 | ) 97 | }) 98 | 99 | const bodyElement = h('div', { style: styles.body }, body) 100 | const actionsElement = h('div', { style: styles.actions }, actionsArray) 101 | const bannerElement = h('div', { style: styles.banner }, bodyElement, actionsElement) 102 | 103 | return h('div', { style: styles.container }, bannerElement) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ui/button.ts: -------------------------------------------------------------------------------- 1 | import { h, strToHash } from '../core.js' 2 | import { boxShadow, rippleEffect, userSelect, zIndex } from './_config.js' 3 | import { addStylesToHead, lightenColor } from './_helpers.js' 4 | import { Icon } from './icon.js' 5 | 6 | export const Button = (props: { 7 | outlined?: boolean 8 | text?: boolean 9 | style?: string 10 | icon?: string 11 | [key: string]: any 12 | }) => { 13 | const { 14 | children, 15 | outlined = false, 16 | text = false, 17 | background = '#6200ee', 18 | color = '#ffffff', 19 | style = '', 20 | class: className = '', 21 | icon, 22 | ...rest 23 | } = props 24 | 25 | const normal = !(outlined || text) 26 | 27 | const bg = normal ? background : '#ffffff' 28 | const clr = normal ? color : background 29 | const hoverClr = normal ? lightenColor(bg, 10) : lightenColor(bg, -10) 30 | const rippleClr = normal ? lightenColor(bg, 50) : lightenColor(background, 50) 31 | const cssHash = strToHash(outlined.toString() + text.toString() + bg + clr + style) 32 | 33 | const ripple = rippleEffect(rippleClr, hoverClr) 34 | 35 | const styles = ` 36 | .nano_jsx_button-${cssHash} { 37 | color: ${clr}; 38 | background: ${bg}; 39 | border-radius: 4px; 40 | display: inline-flex; 41 | font-size: 14px; 42 | padding: 10px 16px; 43 | margin: 0px 0px 1em 0px; 44 | text-align: center; 45 | cursor: pointer; 46 | 47 | ${userSelect} 48 | 49 | 50 | z-index: ${zIndex.button} 51 | 52 | ${boxShadow} 53 | 54 | border: none; 55 | text-transform: uppercase; 56 | box-shadow: 0 0 4px #999; 57 | outline: none; 58 | } 59 | 60 | ${ripple.styles} 61 | ` 62 | 63 | addStylesToHead(styles, cssHash) 64 | 65 | let customStyles = '' 66 | if (outlined || text) { 67 | customStyles += 'padding-top: 9px; padding-bottom: 9px; ' 68 | customStyles += '-webkit-box-shadow: none; -moz-box-shadow: none; box-shadow none; ' 69 | if (outlined) customStyles += `border: 1px ${clr} solid; ` 70 | } 71 | customStyles += style 72 | 73 | return h( 74 | 'button', 75 | { class: `nano_jsx_button-${cssHash} ${ripple.class} ${className}`, style: customStyles, ...rest }, 76 | icon ? h(Icon, { style: 'margin-left: -4px; margin-right: 8px; width: 14px; height: 14px;' }, icon) : null, 77 | children 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/ui/fab.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { h, strToHash } from '../core.js' 3 | import { boxShadow, userSelect, zIndex } from './_config.js' 4 | import { addStylesToHead } from './_helpers.js' 5 | 6 | interface FabProps { 7 | onClick?: (e: MouseEvent) => void 8 | offsetY?: number 9 | center?: boolean 10 | left?: boolean 11 | extended?: boolean 12 | mini?: boolean 13 | background?: string 14 | color?: string 15 | } 16 | 17 | export class Fab extends Component { 18 | render() { 19 | const { 20 | background = '#6200EE', 21 | color = 'white', 22 | extended = false, 23 | mini = false, 24 | center = false, 25 | left = false, 26 | onClick = () => {} 27 | } = this.props 28 | 29 | const height = mini ? 40 : extended ? 48 : 56 30 | const cssHash = strToHash(extended.toString() + mini.toString() + center.toString() + left.toString()) 31 | const className = `fab-container-${cssHash}` 32 | 33 | const styles = ` 34 | .${className} { 35 | ${mini ? 'width: 40px;' : extended ? 'padding: 0px 12px;' : 'width: 56px;'} 36 | height: ${height}px; 37 | position: fixed; 38 | background: ${background}; 39 | border-radius: 36px; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | color: ${color}; 44 | cursor: pointer; 45 | 46 | z-index: ${zIndex.fab} 47 | bottom: ${this.props.offsetY ? 16 + this.props.offsetY : 16}px; 48 | ${left ? 'left: 16px;' : 'right: 16px;'} 49 | ${center ? 'transform: translateX(50%); right: 50%;' : ''} 50 | ${boxShadow} 51 | ${userSelect} 52 | } 53 | ` 54 | 55 | addStylesToHead(styles, cssHash) 56 | 57 | const { children } = this.props as any 58 | 59 | return h('div', { class: className, onClick: (e: MouseEvent) => onClick(e) }, children) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/icon.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { h, strToHash } from '../core.js' 3 | import { addStylesToHead } from './_helpers.js' 4 | 5 | interface IconProps { 6 | src: string 7 | active?: boolean 8 | color?: string 9 | style?: string 10 | size?: number 11 | onClick?: (e: MouseEvent) => void 12 | } 13 | 14 | export class Icon extends Component { 15 | cssHash: string 16 | 17 | didUnmount() { 18 | // not sure if I want to remove the css, since there might be another Icon with the same styles. 19 | // const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 20 | // if (el) el.remove() 21 | } 22 | 23 | render() { 24 | const { src, size = 16, active = true, color = '#6204EE', style = '', ...rest } = this.props 25 | 26 | // @ts-ignore 27 | const children = this.props.children 28 | 29 | this.cssHash = strToHash(active + color + size.toString()) 30 | 31 | const colors = { 32 | active: color, 33 | inactive: '#00000070' 34 | } 35 | 36 | const styles = ` 37 | i.icon-${this.cssHash} { 38 | width: ${size}px; 39 | height: ${size}px; 40 | display: inline-block; 41 | content: ''; 42 | 43 | /*-webkit-mask: url(YOUR_SVG_URL) no-repeat 50% 50%; 44 | mask: url(YOUR_SVG_URL) no-repeat 50% 50%;*/ 45 | 46 | -webkit-mask-size: cover; 47 | mask-size: cover; 48 | 49 | background-color: ${colors.active}; 50 | } 51 | 52 | i.icon-${this.cssHash}.icon_inactive-${this.cssHash} { 53 | background-color: ${colors.inactive}; 54 | } 55 | ` 56 | 57 | addStylesToHead(styles, this.cssHash) 58 | 59 | // const iconStyle = `-webkit-mask: url(/dev/font-awesome/ellipsis-v-solid.svg) no-repeat 50% 50%;mask: url(/dev/font-awesome/ellipsis-v-solid.svg) no-repeat 50% 50%;` 60 | const iconStyle = `-webkit-mask: url(${src || children}) no-repeat 50% 50%; mask: url(${ 61 | src || children 62 | }) no-repeat 50% 50%;` 63 | 64 | const classes = [`icon-${this.cssHash}`] 65 | if (!active) classes.push(`icon_inactive-${this.cssHash}`) 66 | 67 | const icon = h('i', { class: classes.join(' '), ...rest, style: iconStyle + style }) 68 | 69 | return icon 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { AppBar } from '../ui/appBar.js' 2 | export { Toolbar } from '../ui/toolbar.js' 3 | export { Navigation, NavigationAction } from '../ui/navigation.js' 4 | export { Button } from '../ui/button.js' 5 | export { Dialog } from '../ui/dialog.js' 6 | export { Fab } from '../ui/fab.js' 7 | export { Icon } from '../ui/icon.js' 8 | export { List, ListItem } from '../ui/list.js' 9 | export { Menu } from '../ui/menu.js' 10 | export { Snackbar } from '../ui/snackbar.js' 11 | export { Tabs, Tab } from '../ui/tabs.js' 12 | export { Sheet } from '../ui/sheet.js' 13 | -------------------------------------------------------------------------------- /src/ui/list.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component.js' 2 | import { h, strToHash } from '../core.js' 3 | import { Icon } from './icon.js' 4 | import { addStylesToHead } from './_helpers.js' 5 | 6 | interface ListProps { 7 | small?: boolean 8 | children?: any 9 | } 10 | 11 | interface ListItemProps { 12 | onClick?: Function 13 | icon?: string 14 | avatar?: string 15 | square?: string 16 | image?: string 17 | children?: any 18 | } 19 | 20 | export class ListItem extends Component { 21 | render() { 22 | const { props: p } = this 23 | const { onClick = () => {} } = p 24 | 25 | const adjustedMargin = 'margin-right: 16px;' 26 | 27 | const icon = p.icon ? h(Icon, { size: 20, style: 'margin-right: 32px;', src: p.icon }) : null 28 | const avatar = p.avatar 29 | ? h('img', { src: p.avatar, width: 40, height: 40, style: `border-radius: 20px; ${adjustedMargin}` }) 30 | : null 31 | const square = p.square ? h('img', { src: p.square, width: 56, height: 56, style: adjustedMargin }) : null 32 | const image = p.image 33 | ? h('img', { src: p.image, width: 100, height: 56, style: 'margin-left: -16px; margin-right: 16px;' }) 34 | : null 35 | const text = h('span', null, p.children) 36 | 37 | // additional style for the list item 38 | let style = '' 39 | if (p.icon || p.avatar) style += 'min-height: 56px; ' 40 | if (p.square || p.image) style += 'min-height: 72px; ' 41 | 42 | return h('li', { style, onClick }, icon, avatar, square, image, text) 43 | } 44 | } 45 | 46 | export class List extends Component { 47 | cssHash: string 48 | 49 | render() { 50 | const { small = false } = this.props 51 | 52 | this.cssHash = strToHash(`List${small.toString()}`) 53 | 54 | const styles = ` 55 | .list-${this.cssHash} ul { 56 | margin: 0px; 57 | padding: 8px 16px; 58 | } 59 | 60 | .list-${this.cssHash} ul li { 61 | list-style: none; 62 | min-height: ${small ? 32 : 46}px; 63 | display: flex; 64 | align-items: center; 65 | margin: 0px -16px; 66 | padding: 0px 16px; 67 | cursor: pointer; 68 | } 69 | 70 | .list-${this.cssHash} ul li span { 71 | font-size: 16px; 72 | } 73 | 74 | .list-${this.cssHash} ul li:hover { 75 | background:#00000010 76 | } 77 | ` 78 | 79 | addStylesToHead(styles, this.cssHash) 80 | 81 | const ul = h('ul', null, this.props.children) 82 | return h('div', { class: `list-${this.cssHash}` }, ul) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/menu.ts: -------------------------------------------------------------------------------- 1 | import { h, removeAllChildNodes, render } from '../core.js' 2 | import { boxShadow, zIndex } from './_config.js' 3 | import { addStylesToHead } from './_helpers.js' 4 | 5 | interface MenuOptions { 6 | position: { x: number; y: number } 7 | list: any 8 | } 9 | 10 | export class Menu { 11 | defaultParentId = 'menu_items_container' 12 | cssHash = Math.random().toString(36).substring(2) 13 | 14 | // didUnmount() { 15 | // const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 16 | // if (el) el.remove() 17 | // } 18 | 19 | private getParentElement(id: string) { 20 | // delete all other 21 | const others = document.querySelectorAll(`[id^="${this.defaultParentId}"]`) 22 | others.forEach(e => { 23 | e.remove() 24 | }) 25 | 26 | let el = document.getElementById(`${this.defaultParentId}-${id}`) 27 | if (!el) { 28 | el = document.createElement('div') 29 | el.id = `${this.defaultParentId}-${id}` 30 | } 31 | 32 | removeAllChildNodes(el) 33 | document.body.appendChild(el) 34 | 35 | return el 36 | } 37 | 38 | close() { 39 | removeAllChildNodes(this.getParentElement(this.cssHash)) 40 | } 41 | 42 | open(menuOptions: MenuOptions) { 43 | const { position, list } = menuOptions 44 | 45 | // check in which corner the menu appears and adjust fixed position. 46 | const left = position.x < window.innerWidth / 2 ? 'left' : 'right' 47 | const top = position.y < window.innerHeight / 2 ? 'top' : 'bottom' 48 | 49 | const styles = ` 50 | 51 | #menu_items_background-${this.cssHash} { 52 | width: 100vw; 53 | height: 100vh; 54 | background: transparent; 55 | position: fixed; 56 | top: 0; 57 | left: 0; 58 | z-index: ${zIndex.menu} 59 | } 60 | 61 | #menu_items_list-${this.cssHash} { 62 | position: fixed; 63 | background: white; 64 | 65 | border-radius: 4px; 66 | min-width: 112px; 67 | 68 | ${top}: ${position.y > window.innerHeight / 2 ? window.innerHeight - position.y : position.y}px; 69 | ${left}: ${position.x > window.innerWidth / 2 ? window.innerWidth - position.x : position.x}px; 70 | 71 | z-index: ${zIndex.menu} 72 | 73 | ${boxShadow} 74 | } 75 | 76 | ` 77 | // remove old styles 78 | const el = document.querySelector(`[data-css-hash*="${this.cssHash}"]`) 79 | if (el) el.remove() 80 | 81 | // add new styles 82 | addStylesToHead(styles, this.cssHash) 83 | 84 | const itemsList = h('div', { id: `menu_items_list-${this.cssHash}` }, list) 85 | const itemsBg = h('div', { onClick: () => this.close(), id: `menu_items_background-${this.cssHash}` }, itemsList) 86 | 87 | itemsList.addEventListener('click', (e: Event) => e.stopPropagation()) 88 | 89 | this.getParentElement(this.cssHash).appendChild(render(itemsBg)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | export const VERSION = '0.1.0' 3 | -------------------------------------------------------------------------------- /src/withStyles.ts: -------------------------------------------------------------------------------- 1 | import { h } from './core.js' 2 | import { Component } from './component.js' 3 | import { Fragment } from './fragment.js' 4 | import { Helmet } from './components/helmet.js' 5 | 6 | interface ObjectHasToString { 7 | toString: () => string 8 | } 9 | type Styles = string 10 | type FunctionReturnsString = () => string 11 | type WithStylesType = Styles | ObjectHasToString | FunctionReturnsString 12 | 13 | export const withStyles: any = 14 | (...styles: WithStylesType[]) => 15 | (WrappedComponent: any) => { 16 | return class extends Component { 17 | render() { 18 | const { children, ...rest } = this.props 19 | 20 | const helmets: any[] = [] 21 | styles.forEach(style => { 22 | if (typeof style === 'string') { 23 | helmets.push(h(Helmet, null, h('style', null, style))) 24 | } else if (typeof style === 'function') { 25 | const _style = style() 26 | if (typeof _style === 'string') { 27 | helmets.push(h(Helmet, null, h('style', null, _style))) 28 | } 29 | } else if (typeof style === 'object') { 30 | const _style = style.toString?.() 31 | if (typeof _style === 'string') { 32 | helmets.push(h(Helmet, null, h('style', null, _style))) 33 | } 34 | } 35 | }) 36 | 37 | const component = 38 | children && children.length > 0 39 | ? h(WrappedComponent, { ...rest }, children) 40 | : h(WrappedComponent, { ...this.props }) 41 | 42 | return h(Fragment, null, ...helmets, component) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test.jsx-runtime/nodejs/client.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, Component, Helmet, isSSR } from '../../lib/index.js' 2 | import { initSSR } from '../../lib/ssr.js' 3 | import { wait, nodeToString } from '../../test/nodejs/helpers.js' 4 | 5 | const spy = jest.spyOn(global.console, 'error') 6 | 7 | describe('jsx-runtime (client-side)', () => { 8 | test('should render without errors', async () => { 9 | const Root = () => ( 10 |
11 |

Hello

12 |
13 | ) 14 | const html = render(Root) 15 | await wait() 16 | expect(nodeToString(html)).toBe('

Hello

') 17 | expect(spy).not.toHaveBeenCalled() 18 | }) 19 | 20 | test('should render without errors', async () => { 21 | const Root = () => ( 22 |
23 | 24 | TITLE 25 | 26 | 27 |

Hello

28 |
29 | ) 30 | render(Root, document.body) 31 | await wait() 32 | expect(nodeToString(document.head)).toBe( 33 | 'TITLE' 34 | ) 35 | expect(nodeToString(document.body)).toBe('

Hello

') 36 | expect(spy).not.toHaveBeenCalled() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test.jsx-runtime/nodejs/server.tsx: -------------------------------------------------------------------------------- 1 | import { renderSSR, Component, Helmet, isSSR } from '../../lib/index.js' 2 | import { detectSSR } from '../../lib/helpers.js' 3 | import { initSSR } from '../../lib/ssr.js' 4 | 5 | // I don't use jest here because it generated too many unexpected errors. 6 | 7 | let hasErrors = false 8 | const tests = [] 9 | 10 | const expect = one => { 11 | return { 12 | toBe: two => { 13 | const match = one === two 14 | if (match === false) { 15 | hasErrors = true 16 | console.log(`> ERROR: "${one} does NOT match "${two}"`) 17 | } else { 18 | console.log('> ok') 19 | } 20 | return match 21 | } 22 | } 23 | } 24 | 25 | const test = fnc => { 26 | tests.push(fnc) 27 | } 28 | 29 | test(() => { 30 | const Root = () => ( 31 |
32 |

Hello

33 |
34 | ) 35 | initSSR('/') 36 | const html = renderSSR(() => ) 37 | expect(html).toBe('

Hello

') 38 | }) 39 | 40 | test(() => { 41 | const Root = () => ( 42 |
43 | 44 | TITLE 45 | 46 | 47 |

Hello

48 |
49 | ) 50 | const html = renderSSR() 51 | const { body, head } = Helmet.SSR(html) 52 | expect(head[0]).toBe('TITLE') 53 | expect(body).toBe('

Hello

') 54 | }) 55 | 56 | // Children as Component 57 | test(() => { 58 | const Child = () =>

Hello

59 | const Root = (props: any) =>
{props.children}
60 | const html = renderSSR( 61 | 62 | 63 | 64 | ) 65 | expect(html).toBe('

Hello

') 66 | }) 67 | 68 | // Children as Props 69 | test(() => { 70 | const Child = () =>

Hello

71 | const Root = (props: any) =>
{props.children}
72 | const html = renderSSR() 73 | expect(html).toBe('

Hello

') 74 | }) 75 | 76 | // Children as Props (Array) 77 | test(() => { 78 | const Child = () =>

Hello

79 | const Root = (props: any) =>
{props.children}
80 | const html = renderSSR() 81 | expect(html).toBe('

Hello

') 82 | }) 83 | 84 | for (const test of tests) { 85 | test() 86 | } 87 | 88 | if (hasErrors) { 89 | process.exit(1) 90 | } else { 91 | process.exit(0) 92 | } 93 | -------------------------------------------------------------------------------- /test/browser/className.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 17 | 18 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/browser/component.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/browser/dangerous.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 17 | 18 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/browser/fragment.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /test/browser/simple.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/browser/svg.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/browser/webComponent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 34 | 35 | 36 |
37 | invisible 38 | John Doe 39 |
40 | 41 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /test/browser/withStyles.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/deno/deno.test.tsx: -------------------------------------------------------------------------------- 1 | import { h, Helmet, renderSSR, Component } from '../../deno_lib/mod.ts' 2 | import { assertEquals } from 'https://deno.land/std@0.115.1/testing/asserts.ts' 3 | 4 | Deno.test('should render without errors', () => { 5 | const comments = ['Comment One', 'Comment Two'] 6 | 7 | class Comments extends Component { 8 | render() { 9 | return ( 10 |
    11 | {this.props.comments.map((comment: any) => { 12 | return
  • {comment}
  • 13 | })} 14 |
15 | ) 16 | } 17 | } 18 | 19 | const App = () => ( 20 |
21 | 22 | Nano JSX SSR 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Comments

31 |
32 | 33 |
34 |
35 | ) 36 | 37 | const ssr = renderSSR() 38 | const { body, head, footer } = Helmet.SSR(ssr) 39 | 40 | assertEquals( 41 | body, 42 | '

Comments

  • Comment One
  • Comment Two
' 43 | ) 44 | assertEquals(head.join('\n'), 'Nano JSX SSR') 45 | assertEquals(footer.join('\n'), '') 46 | }) 47 | -------------------------------------------------------------------------------- /test/deno/nanossr.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | /// 3 | /// 4 | /// 5 | /// 6 | 7 | /** 8 | * test for https://crux.land/nanossr@0.0.1 and https://dash.deno.com/playground/example-nanossr 9 | */ 10 | 11 | import { assertStringIncludes } from 'https://deno.land/std@0.115.1/testing/asserts.ts' 12 | 13 | import { h, FC } from '../../deno_lib/mod.ts' 14 | import { tw } from 'https://cdn.skypack.dev/twind' 15 | 16 | import { Helmet, renderSSR as nanoRender } from '../../deno_lib/mod.ts' 17 | import { setup } from 'https://cdn.skypack.dev/twind' 18 | import { getStyleTag, virtualSheet } from 'https://cdn.skypack.dev/twind/sheets' 19 | import typography from 'https://cdn.skypack.dev/@twind/typography' 20 | 21 | let SHEET_SINGLETON: any = null 22 | function sheet(twOptions = {}) { 23 | return SHEET_SINGLETON ?? (SHEET_SINGLETON = setupSheet(twOptions)) 24 | } 25 | 26 | // Setup TW sheet singleton 27 | function setupSheet(twOptions: Record) { 28 | const sheet = virtualSheet() 29 | setup({ ...twOptions, sheet, plugins: { ...typography() } }) 30 | return sheet 31 | } 32 | 33 | interface MakeHtml { 34 | body: string 35 | head: string[] 36 | footer: string[] 37 | styleTag: string 38 | } 39 | const html = ({ body, head, footer, styleTag }: MakeHtml): string => ` 40 | 41 | 42 | 43 | 44 | 45 | ${head.join('\n')} 46 | ${styleTag} 47 | 48 | 49 | ${body} 50 | ${footer.join('\n')} 51 | 52 | 53 | ` 54 | 55 | export function ssr(render: CallableFunction, options?: any) { 56 | sheet(options?.tw ?? {}).reset() 57 | const app = nanoRender(render()) 58 | const { body, head, footer } = Helmet.SSR(app) 59 | const styleTag = getStyleTag(sheet()) 60 | return /*new Response*/ html({ body, head, footer, styleTag }) //, { headers: { 'content-type': 'text/html' } } 61 | } 62 | 63 | const Hello: FC = props => ( 64 |
65 |

Hello {props.name}!

66 |
67 | ) 68 | 69 | // console.log('Listening on http://localhost:8080') 70 | // await listenAndServe(':8080', req => { 71 | // const url = new URL(req.url) 72 | // const name = url.searchParams.get('name') ?? 'world' 73 | // return ssr(() => ) 74 | // }) 75 | 76 | Deno.test('should render without errors', () => { 77 | const res = ssr(() => ) 78 | 79 | // simply test some string outputs 80 | assertStringIncludes(res, '') 81 | assertStringIncludes(res, 'Hello deno!') 82 | assertStringIncludes(res, ' 58 | ) 59 | 60 | expect(head[0]).toContain( 61 | '@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto:100,300,400"' 62 | ) 63 | expect(head[0]).toContain('') 31 | expect(spy).not.toHaveBeenCalled() 32 | }) 33 | 34 | // fails in jest, but works in the browser :/ 35 | xtest('should render without errors', async () => { 36 | class AppA extends Component { 37 | render() { 38 | return ( 39 |
40 |

hello

41 |
42 | ) 43 | } 44 | } 45 | 46 | const App = withStyles('CSS1', () => 'CSS2', { toString: () => 'CSS3' })(AppA) 47 | 48 | Nano.render(, document.body) 49 | 50 | await wait() 51 | 52 | expect(document.head.innerHTML).toBe('') 53 | expect(spy).not.toHaveBeenCalled() 54 | }) 55 | 56 | test('should not escape webfonts and single quotes', async () => { 57 | class AppA extends Component { 58 | render() { 59 | return
{this.props.children}
60 | } 61 | } 62 | 63 | const App = withStyles(` 64 | @import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto:100,300,400"); 65 | 66 | body { 67 | font-family: 'Montserrat', 'Roboto', sans-serif; 68 | } 69 | `)(AppA) 70 | 71 | Nano.render( 72 | 73 |

with styles

74 |
, 75 | document.body 76 | ) 77 | 78 | await wait() 79 | expect(multiLineToSingleLine(document.head.innerHTML)).toBe( 80 | "" 81 | ) 82 | expect(spy).not.toHaveBeenCalled() 83 | }) 84 | -------------------------------------------------------------------------------- /test/scripts.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { readFile, readdir, writeFile } from 'fs/promises' 4 | import { resolve } from 'path' 5 | 6 | const args = process.argv.splice(2) 7 | const arg0 = args[0] 8 | 9 | async function* getFiles(dir) { 10 | const dirents = await readdir(dir, { withFileTypes: true }) 11 | for (const dirent of dirents) { 12 | const res = resolve(dir, dirent.name) 13 | if (dirent.isDirectory()) { 14 | yield* getFiles(res) 15 | } else { 16 | yield res 17 | } 18 | } 19 | } 20 | 21 | if (arg0 === 'add-jest-environment') { 22 | const str = '/**\n* @jest-environment node\n*/\n\n' 23 | 24 | for await (const f of getFiles('test')) { 25 | if (/ssr\.test\.js/.test(f) || /e2e\/core\.test\.js/.test(f)) { 26 | const file = await readFile(f, { encoding: 'utf-8' }) 27 | await writeFile(f, str + file, { encoding: 'utf-8' }) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"], 4 | "jsxFactory": "h", 5 | "jsxFragmentFactory": "Fragment" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./esm", 5 | "target": "ES2018", 6 | "module": "ES2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "rootDir": "./src", 5 | 6 | "target": "ES2018", 7 | "module": "commonjs", 8 | 9 | "jsx": "react", 10 | "jsxFactory": "h", 11 | 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | 16 | "esModuleInterop": true, 17 | "moduleResolution": "node", 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "strictPropertyInitialization": false 23 | }, 24 | "include": ["src/**/*"], 25 | "exclude": ["node_module"] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "", 5 | "rootDir": "", 6 | 7 | "jsx": "react", 8 | "jsxFactory": "Nano.h", 9 | 10 | "declaration": false, 11 | "declarationMap": false, 12 | "sourceMap": false, 13 | 14 | "alwaysStrict": false, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false 19 | }, 20 | "include": ["test/nodejs/**/*"], 21 | "exclude": ["node_modules", "**/*.spec.ts", "test/deno/**", "test/browser/**"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.test.jsx-runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "", 4 | "rootDir": "", 5 | 6 | "target": "ES2018", 7 | "module": "commonjs", 8 | 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "../../lib", 11 | 12 | "skipLibCheck": true 13 | }, 14 | "include": ["test.jsx-runtime/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /webpack/webpack.bundle.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | stats: 'errors-warnings', 6 | devtool: 'inline-source-map', 7 | entry: './lib/bundles/bundle.full.js', 8 | output: { 9 | filename: 'nano.dev.min.js', 10 | path: path.resolve(__dirname, '../bundles'), 11 | library: 'nanoJSX', 12 | libraryExport: 'default' 13 | }, 14 | resolve: { 15 | extensions: ['.js'] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack/webpack.bundle.instrumented.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | stats: 'errors-warnings', 6 | devtool: 'source-map', 7 | entry: './lib/bundles/bundle.full.js', 8 | output: { 9 | filename: 'nano.instrumented.min.js', 10 | path: path.resolve(__dirname, '../bundles'), 11 | library: 'nanoJSX', 12 | libraryExport: 'default' 13 | }, 14 | resolve: { 15 | extensions: ['.js'] 16 | }, 17 | module: { 18 | rules: [ 19 | { test: /\.jsx?$/, use: ['coverage-istanbul-loader'] } 20 | // { test: /\.tsx?$/, use: ['coverage-istanbul-loader', 'ts-loader'] } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webpack/webpack.bundle.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | stats: 'errors-warnings', 6 | entry: { 7 | core: './lib/bundles/bundle.core.js', 8 | slim: './lib/bundles/bundle.slim.js', 9 | full: './lib/bundles/bundle.full.js', 10 | ui: './lib/bundles/bundle.ui.js' 11 | }, 12 | output: { 13 | filename: 'nano.[name].min.js', 14 | path: path.resolve(__dirname, '../bundles'), 15 | library: 'nanoJSX', 16 | libraryExport: 'default' 17 | }, 18 | resolve: { 19 | extensions: ['.js'] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | stats: 'errors-warnings', 6 | devtool: 'inline-source-map', 7 | entry: './lib/dev/dev.js', 8 | output: { 9 | filename: 'dev.js', 10 | path: path.resolve(__dirname, '../dev') 11 | }, 12 | resolve: { 13 | extensions: ['.js'] 14 | } 15 | // module: { 16 | // rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] 17 | // } 18 | } 19 | --------------------------------------------------------------------------------