├── .changeset ├── README.md ├── beige-garlics-kiss.md ├── calm-pans-accept.md ├── chilled-socks-hear.md ├── clean-ways-stare.md ├── clever-books-love.md ├── clever-mirrors-promise.md ├── config.json ├── fair-pigs-work.md ├── gentle-rivers-peel.md ├── grumpy-buses-love.md ├── hip-ads-drop.md ├── little-pumpkins-mix.md ├── neat-olives-melt.md ├── pre.json ├── rude-boxes-fetch.md ├── serious-yaks-burn.md └── seven-coins-relate.md ├── .github └── workflows │ ├── nodejs.yml │ ├── release.yml │ └── site.yml ├── .gitignore ├── README.md ├── astro-fritz ├── .gitignore ├── CHANGELOG.md ├── package.json ├── src │ ├── client-tailwind.mts │ ├── client.mts │ ├── index.mts │ ├── messages.mts │ ├── server.mts │ ├── vite-plugin.mts │ └── worker.mts └── tsconfig.json ├── core ├── .gitignore ├── CHANGELOG.md ├── jsx-runtime.mjs ├── package.json ├── scripts │ └── move-types.mjs ├── src │ ├── message-types.ts │ ├── node │ │ ├── index.ts │ │ └── worker.d.mts │ ├── types.ts │ ├── util.ts │ ├── window │ │ ├── cmd.ts │ │ ├── component.ts │ │ ├── idom-render.ts │ │ ├── index.ts │ │ ├── lifecycle.ts │ │ ├── mods.d.ts │ │ ├── styles │ │ │ ├── adopt.ts │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ └── tag.ts │ │ ├── types.ts │ │ ├── with-mount.ts │ │ ├── with-styles.ts │ │ ├── with-worker-connection.ts │ │ ├── with-worker-events.ts │ │ └── with-worker-render.ts │ └── worker │ │ ├── component-extras.d.ts │ │ ├── component.ts │ │ ├── env.ts │ │ ├── handle.ts │ │ ├── hyperscript.ts │ │ ├── index.ts │ │ ├── instance.ts │ │ ├── jsx.d.ts │ │ ├── lifecycle.ts │ │ ├── ns.d.ts │ │ ├── relay.ts │ │ ├── signal.ts │ │ └── tree.ts ├── test │ ├── browser │ │ ├── adopt-sheet.js │ │ ├── basics.js │ │ ├── basics │ │ │ ├── app.js │ │ │ └── index.html │ │ ├── events.js │ │ ├── events │ │ │ ├── app.js │ │ │ └── index.html │ │ ├── garbage.js │ │ ├── garbage │ │ │ ├── app.js │ │ │ └── index.html │ │ ├── helpers.js │ │ ├── props.js │ │ ├── props │ │ │ ├── app.js │ │ │ └── index.html │ │ ├── snapshot.js │ │ ├── state.js │ │ ├── state │ │ │ ├── app.js │ │ │ └── index.html │ │ ├── styles.js │ │ └── test.html │ ├── demo │ │ ├── circles │ │ │ ├── app.js │ │ │ ├── index.html │ │ │ └── styles.js │ │ ├── counter │ │ │ ├── app.js │ │ │ ├── index.html │ │ │ └── styles.css │ │ └── dbmonster │ │ │ ├── Makefile │ │ │ ├── app.js │ │ │ ├── index.html │ │ │ ├── memory-stats.js │ │ │ ├── monitor.js │ │ │ ├── rollup.config.js │ │ │ ├── src │ │ │ ├── app.css │ │ │ ├── app.js │ │ │ ├── bootstrap.css │ │ │ ├── data.js │ │ │ ├── row.css │ │ │ └── row.js │ │ │ └── style.css │ ├── node │ │ ├── functional.test.js │ │ └── styles.test.js │ └── typescript │ │ ├── core │ │ ├── basics.tsx │ │ ├── explicit-fragment.tsx │ │ ├── implicit-fragment.tsx │ │ └── tsconfig.json │ │ └── jsx-runtime │ │ ├── basics.tsx │ │ └── tsconfig.json └── tsconfig.json ├── package-lock.json ├── package.json ├── site ├── .gitignore ├── CHANGELOG.md ├── astro.config.mjs ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── images │ │ ├── logo-144.png │ │ ├── logo-192.png │ │ ├── logo-512.png │ │ └── logo-96.png │ ├── manifest.json │ ├── service-worker.js │ └── sw.js ├── src │ ├── components │ │ ├── About.astro │ │ ├── CodeFile.astro │ │ ├── CodeSnippet.astro │ │ ├── Collapsible.tsx │ │ ├── DocLayout.astro │ │ ├── DocSidebar.astro │ │ ├── Flame.astro │ │ ├── HeadCommon.astro │ │ ├── MadeWith.tsx │ │ ├── NotFound.astro │ │ ├── StylesCommon.astro │ │ ├── TitleCard.astro │ │ └── effects │ │ │ ├── Butter.astro │ │ │ ├── Slant.astro │ │ │ └── StrongA.astro │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── components-overview.md │ │ │ ├── installation.md │ │ │ └── what-is-fritz.md │ ├── env.d.ts │ ├── hlight.js │ ├── images │ │ ├── frankenstein-fritz-flame.png │ │ └── frankenstein-fritz-flame.webp │ ├── pages │ │ ├── docs │ │ │ ├── [slug].astro │ │ │ └── index.astro │ │ └── index.astro │ └── styles │ │ ├── agate.css │ │ ├── button.css │ │ ├── common.pcss │ │ └── global.css ├── tailwind.config.cjs └── tsconfig.json └── workbox-config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/beige-garlics-kiss.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": major 3 | "fritz": major 4 | --- 5 | 6 | Call the lifecycle callbacks during SSR 7 | -------------------------------------------------------------------------------- /.changeset/calm-pans-accept.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": patch 3 | --- 4 | 5 | Improved JSX for custom elements 6 | -------------------------------------------------------------------------------- /.changeset/chilled-socks-hear.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": patch 3 | --- 4 | 5 | Don't include children in island props 6 | -------------------------------------------------------------------------------- /.changeset/clean-ways-stare.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": major 3 | --- 4 | 5 | Add new lifecycle methods 6 | 7 | This change adds new lifecycle methods, `getSnapshotBeforeUpdate` and `componentDidUpdate`. 8 | 9 | It also removes `componentWillReceiveProps` and `componentWillUpdate`. 10 | 11 | The latter two are deprecated in both React and Preact. There is no reason for us to keep them. -------------------------------------------------------------------------------- /.changeset/clever-books-love.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": patch 3 | "fritz": patch 4 | --- 5 | 6 | Fixes types in JSX usage and adds tests 7 | -------------------------------------------------------------------------------- /.changeset/clever-mirrors-promise.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": minor 3 | --- 4 | 5 | Add the styles API 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/fair-pigs-work.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": patch 3 | --- 4 | 5 | Minor TS improvements 6 | -------------------------------------------------------------------------------- /.changeset/gentle-rivers-peel.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": minor 3 | "fritz": minor 4 | --- 5 | 6 | Fixes integration between fritz and astro-fritz 7 | -------------------------------------------------------------------------------- /.changeset/grumpy-buses-love.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": major 3 | "fritz": major 4 | --- 5 | 6 | Fritz 5 Beta 7 | -------------------------------------------------------------------------------- /.changeset/hip-ads-drop.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": patch 3 | --- 4 | 5 | Improve main types 6 | -------------------------------------------------------------------------------- /.changeset/little-pumpkins-mix.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": major 3 | "fritz": patch 4 | --- 5 | 6 | Bare minimum SSR support 7 | -------------------------------------------------------------------------------- /.changeset/neat-olives-melt.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": patch 3 | --- 4 | 5 | Add the new node entrypoint 6 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "beta", 4 | "initialVersions": { 5 | "astro-fritz": "2.0.0-alpha.2", 6 | "fritz": "5.0.0-alpha.8", 7 | "fritz-site": "1.0.1-alpha.0" 8 | }, 9 | "changesets": [ 10 | "beige-garlics-kiss", 11 | "calm-pans-accept", 12 | "chilled-socks-hear", 13 | "clean-ways-stare", 14 | "clever-books-love", 15 | "clever-mirrors-promise", 16 | "fair-pigs-work", 17 | "gentle-rivers-peel", 18 | "grumpy-buses-love", 19 | "hip-ads-drop", 20 | "little-pumpkins-mix", 21 | "neat-olives-melt", 22 | "rude-boxes-fetch", 23 | "serious-yaks-burn", 24 | "seven-coins-relate" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.changeset/rude-boxes-fetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": patch 3 | "fritz": patch 4 | --- 5 | 6 | Support props.children in server rendering 7 | -------------------------------------------------------------------------------- /.changeset/serious-yaks-burn.md: -------------------------------------------------------------------------------- 1 | --- 2 | "astro-fritz": minor 3 | "fritz": minor 4 | --- 5 | 6 | Partial Tailwind support 7 | -------------------------------------------------------------------------------- /.changeset/seven-coins-relate.md: -------------------------------------------------------------------------------- 1 | --- 2 | "fritz": patch 3 | --- 4 | 5 | New build 6 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - 'core/**' 9 | - 'astro-fritz/**' 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Type checking 30 | run: npm run check 31 | 32 | - name: Testing 33 | run: npm run --workspace=core test 34 | env: 35 | CI: true 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'core/**' 9 | - 'astro-fritz/**' 10 | 11 | jobs: 12 | changelog: 13 | name: Changelog PR or Release 14 | if: ${{ github.ref_name == 'main' && github.repository_owner == 'matthewp' }} 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | steps: 20 | - uses: actions/checkout@v2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install NPM Dependencies 30 | run: npm ci 31 | 32 | - name: Build 33 | run: npm run build 34 | 35 | - name: Create Release Pull Request or Publish to npm 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | publish: npm run changeset publish 40 | commit: '[ci] release' 41 | title: '[ci] release' 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'site/**' 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Build Site 25 | run: npm run --workspace=site build 26 | 27 | - name: Sync Bucket 28 | uses: jakejarvis/s3-sync-action@master 29 | with: 30 | args: --delete 31 | env: 32 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 33 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 34 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 35 | SOURCE_DIR: 'site/dist' # optional: defaults to entire repository 36 | 37 | - name: Invalidate CDN 38 | uses: chetan/invalidate-cloudfront-action@master 39 | env: 40 | DISTRIBUTION: ${{ secrets.DISTRIBUTION }} 41 | PATHS: '/*' 42 | AWS_REGION: 'us-east-1' 43 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 44 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .wireit/ 2 | node_modules/ 3 | types/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fritz 2 | 3 | A library for rendering custom elements in a web worker. 4 | 5 | **worker.js** 6 | 7 | ```js 8 | import { Component, h } from 'fritz'; 9 | 10 | class Hello extends Component { 11 | static get props() { 12 | return { 13 | name: { attribute: true } 14 | } 15 | } 16 | 17 | render({name}) { 18 | return ( 19 | Hello {name} 20 | ); 21 | } 22 | } 23 | 24 | fritz.define('x-hello', Hello); 25 | ``` 26 | 27 | **index.html** 28 | 29 | ```html 30 | 31 | 32 | My App 33 | 34 | 35 | 36 | 41 | ``` 42 | 43 | ## Install 44 | 45 | Using [Yarn](https://yarnpkg.com/en/): 46 | 47 | ```shell 48 | yarn add fritz 49 | ``` 50 | 51 | Using npm: 52 | 53 | ```shell 54 | npm install fritz 55 | ``` 56 | 57 | ## License 58 | 59 | BSD 2 Clause -------------------------------------------------------------------------------- /astro-fritz/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /astro-fritz/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # astro-fritz 2 | 3 | ## 2.0.0-beta.3 4 | 5 | ### Patch Changes 6 | 7 | - 049e40f: Don't include children in island props 8 | - 8296b45: Support props.children in server rendering 9 | - Updated dependencies [8296b45] 10 | - fritz@5.0.0-beta.5 11 | 12 | ## 2.0.0-beta.2 13 | 14 | ### Minor Changes 15 | 16 | - b6de1cf: Partial Tailwind support 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [5c0ce55] 21 | - Updated dependencies [b6de1cf] 22 | - fritz@5.0.0-beta.2 23 | 24 | ## 2.0.0-beta.1 25 | 26 | ### Major Changes 27 | 28 | - 4039953: Fritz 5 Beta 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [4039953] 33 | - fritz@5.0.0-beta.1 34 | 35 | ## 2.0.0-alpha.2 36 | 37 | ### Minor Changes 38 | 39 | - cee8c95: Fixes integration between fritz and astro-fritz 40 | 41 | ### Patch Changes 42 | 43 | - 2d76cb8: Fixes types in JSX usage and adds tests 44 | - Updated dependencies [2d76cb8] 45 | - Updated dependencies [cee8c95] 46 | - fritz@5.0.0-alpha.8 47 | 48 | ## 2.0.0-alpha.1 49 | 50 | ### Major Changes 51 | 52 | - e76dd50: Call the lifecycle callbacks during SSR 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [e76dd50] 57 | - fritz@5.0.0-alpha.7 58 | 59 | ## 2.0.0-alpha.0 60 | 61 | ### Major Changes 62 | 63 | - d1aa720: Bare minimum SSR support 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies [d1aa720] 68 | - fritz@5.0.0-alpha.6 69 | -------------------------------------------------------------------------------- /astro-fritz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-fritz", 3 | "version": "2.0.0-beta.3", 4 | "description": "An Astro integration for Fritz", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "default": "./dist/index.mjs" 9 | }, 10 | "./server": { 11 | "default": "./dist/server.mjs" 12 | }, 13 | "./client": { 14 | "default": "./dist/client.mjs" 15 | }, 16 | "./tailwind": { 17 | "default": "./dist/client-tailwind.mjs" 18 | }, 19 | "./worker": { 20 | "default": "./dist/worker.mjs" 21 | } 22 | }, 23 | "scripts": { 24 | "build": "wireit", 25 | "check": "wireit", 26 | "test": "echo \"Error: no test specified\" && exit 1" 27 | }, 28 | "wireit": { 29 | "build": { 30 | "command": "tsc -p tsconfig.json", 31 | "dependencies": [ 32 | "../core:build" 33 | ], 34 | "files": [ 35 | "src/*" 36 | ], 37 | "output": [ 38 | "dist" 39 | ] 40 | }, 41 | "check": { 42 | "command": "tsc -p tsconfig.json --noEmit", 43 | "dependencies": [ 44 | "../core:build" 45 | ], 46 | "files": [ 47 | "src/**/*" 48 | ], 49 | "output": [] 50 | } 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/matthewp/fritz.git" 55 | }, 56 | "keywords": [ 57 | "astro", 58 | "fritz" 59 | ], 60 | "author": "Matthew Phillips", 61 | "license": "BSD-2-Clause", 62 | "bugs": { 63 | "url": "https://github.com/matthewp/fritz/issues" 64 | }, 65 | "homepage": "https://github.com/matthewp/fritz#readme", 66 | "files": [ 67 | "dist" 68 | ], 69 | "dependencies": { 70 | "@babel/core": ">=7.0.0-0 <8.0.0", 71 | "@babel/plugin-transform-react-jsx": "^7.17.12", 72 | "fritz": "^5.0.0-beta.5" 73 | }, 74 | "devDependencies": { 75 | "typescript": "^4.9.4", 76 | "wireit": "^0.9.3" 77 | }, 78 | "peerDependencies": { 79 | "astro": "^2.0.0-beta.1" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /astro-fritz/src/client-tailwind.mts: -------------------------------------------------------------------------------- 1 | import fritz from 'fritz/window'; 2 | 3 | if((import.meta as any).env.DEV) { 4 | let exp = /\! tailwindcss/; 5 | for(let style of document.querySelectorAll('style[data-vite-dev-id]')) { 6 | if(exp.test(style.textContent ?? '')) { 7 | fritz.adopt(style as HTMLStyleElement); 8 | break; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /astro-fritz/src/client.mts: -------------------------------------------------------------------------------- 1 | import fritz from 'fritz/window'; 2 | import { IMPORT, type ImportMessage } from './messages.mjs'; 3 | 4 | const worker = new Worker(new URL('astro-fritz/worker', import.meta.url), { 5 | type: 'module' 6 | }); 7 | 8 | fritz.use(worker); 9 | 10 | export default () => (metadata: any) => { 11 | let msg: ImportMessage = { 12 | type: IMPORT, 13 | url: metadata.url 14 | } 15 | worker.postMessage(msg); 16 | }; 17 | 18 | class WorkerURL { 19 | public url; 20 | constructor(url: URL) { 21 | if((import.meta as any).env.PROD) { 22 | this.url = url.toString(); 23 | } else { 24 | this.url = url.toString() + '?fritz-worker'; 25 | } 26 | } 27 | } 28 | 29 | export { 30 | WorkerURL as Worker 31 | }; -------------------------------------------------------------------------------- /astro-fritz/src/index.mts: -------------------------------------------------------------------------------- 1 | import type { AstroIntegration } from 'astro'; 2 | import { pluginFritz } from './vite-plugin.mjs'; 3 | 4 | export type Options = { 5 | tailwind?: boolean; 6 | }; 7 | 8 | export default function(options?: Options): AstroIntegration { 9 | return { 10 | name: 'astro-fritz', 11 | hooks: { 12 | 'astro:config:setup'({ addRenderer, updateConfig }) { 13 | addRenderer({ 14 | name: 'astro-fritz', 15 | clientEntrypoint: 'astro-fritz/client', 16 | serverEntrypoint: 'astro-fritz/server', 17 | jsxImportSource: 'fritz', 18 | jsxTransformOptions: async () => { 19 | const { 20 | default: { default: jsx } 21 | // @ts-ignore 22 | } = await import('@babel/plugin-transform-react-jsx'); 23 | return { 24 | plugins: [jsx({}, { runtime: 'automatic', importSource: 'fritz' })] 25 | } 26 | } 27 | }); 28 | 29 | updateConfig({ 30 | vite: { 31 | plugins: [pluginFritz({ tailwind: options?.tailwind || false })], 32 | optimizeDeps: { 33 | include: ['astro-fritz/client', ...(options?.tailwind ? ['astro-fritz/tailwind'] : [])] 34 | }, 35 | worker: { 36 | format: 'es', 37 | rollupOptions: { 38 | output: { 39 | manualChunks: { 40 | fritz: ['fritz'] 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }); 47 | } 48 | } 49 | }; 50 | } -------------------------------------------------------------------------------- /astro-fritz/src/messages.mts: -------------------------------------------------------------------------------- 1 | export const IMPORT = 'astro-fritz:import'; 2 | 3 | export type ImportMessage = { 4 | type: typeof IMPORT, 5 | url: string; 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /astro-fritz/src/server.mts: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'fritz/node'; 2 | import { h, Component as FritzComponent } from 'fritz'; 3 | 4 | function check(Component: any) { 5 | return FritzComponent.isPrototypeOf(Component); 6 | } 7 | 8 | type AstroChildren = { default?: string; } 9 | 10 | function renderToStaticMarkup(Component: any, props: Record, children: AstroChildren) { 11 | let _props = Object.assign({}, props); 12 | if(children.default) { 13 | _props.children = children.default; 14 | } 15 | 16 | let tree = h(Component, _props); 17 | let html = renderToString(tree); 18 | return { 19 | html 20 | }; 21 | } 22 | 23 | export default { 24 | check, 25 | renderToStaticMarkup 26 | } -------------------------------------------------------------------------------- /astro-fritz/src/vite-plugin.mts: -------------------------------------------------------------------------------- 1 | import type { Plugin as VitePlugin } from 'vite'; 2 | 3 | export type Options = { 4 | tailwind: boolean; 5 | }; 6 | 7 | export function pluginFritz(options: Options): VitePlugin { 8 | return { 9 | name: 'fritz:build', 10 | transform(_code, id, opts) { 11 | if(/\.(t|j)sx$/.test(id)) { 12 | if(!opts?.ssr) { 13 | return ` 14 | import fritz from 'fritz/window'; 15 | import { Worker } from 'astro-fritz/client'; 16 | ${options.tailwind ? `import 'astro-fritz/tailwind';` : ''} 17 | 18 | let worker = new Worker(new URL('${id}', import.meta.url), { 19 | type: 'module' 20 | }); 21 | 22 | export default { 23 | url: worker.url 24 | }; 25 | `.trim(); 26 | } 27 | } 28 | } 29 | }; 30 | } -------------------------------------------------------------------------------- /astro-fritz/src/worker.mts: -------------------------------------------------------------------------------- 1 | import { IMPORT, type ImportMessage } from './messages.mjs'; 2 | 3 | addEventListener('message', ev => { 4 | if(ev.data?.type === IMPORT) { 5 | const data = ev.data as ImportMessage; 6 | import(/* @vite-ignore */ data.url); 7 | } 8 | }); -------------------------------------------------------------------------------- /astro-fritz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "target": "ESNext", 6 | "strict": true, 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules/**"] 13 | } 14 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | .wireit/ 2 | node_modules/ 3 | types/ 4 | 5 | /worker.mjs 6 | /worker.mjs.map 7 | /window.mjs 8 | /window.mjs.map 9 | /node.mjs 10 | /node.mjs.map -------------------------------------------------------------------------------- /core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fritz 2 | 3 | ## 5.0.0-beta.5 4 | 5 | ### Patch Changes 6 | 7 | - 8296b45: Support props.children in server rendering 8 | 9 | ## 5.0.0-beta.4 10 | 11 | ### Major Changes 12 | 13 | - 36ce3ba: Add new lifecycle methods 14 | 15 | This change adds new lifecycle methods, `getSnapshotBeforeUpdate` and `componentDidUpdate`. 16 | 17 | It also removes `componentWillReceiveProps` and `componentWillUpdate`. 18 | 19 | The latter two are deprecated in both React and Preact. There is no reason for us to keep them. 20 | 21 | ## 5.0.0-beta.3 22 | 23 | ### Patch Changes 24 | 25 | - 512d20b: Improved JSX for custom elements 26 | 27 | ## 5.0.0-beta.2 28 | 29 | ### Minor Changes 30 | 31 | - 5c0ce55: Add the styles API 32 | - b6de1cf: Partial Tailwind support 33 | 34 | ## 5.0.0-beta.1 35 | 36 | ### Major Changes 37 | 38 | - 4039953: Fritz 5 Beta 39 | 40 | ## 5.0.0-alpha.8 41 | 42 | ### Minor Changes 43 | 44 | - cee8c95: Fixes integration between fritz and astro-fritz 45 | 46 | ### Patch Changes 47 | 48 | - 2d76cb8: Fixes types in JSX usage and adds tests 49 | 50 | ## 5.0.0-alpha.7 51 | 52 | ### Major Changes 53 | 54 | - e76dd50: Call the lifecycle callbacks during SSR 55 | 56 | ## 5.0.0-alpha.6 57 | 58 | ### Patch Changes 59 | 60 | - d1aa720: Bare minimum SSR support 61 | 62 | ## 5.0.0-alpha.5 63 | 64 | ### Patch Changes 65 | 66 | - 9b999db: Minor TS improvements 67 | 68 | ## 5.0.0-alpha.4 69 | 70 | ### Patch Changes 71 | 72 | - 2938fbb: Improve main types 73 | 74 | ## 5.0.0-alpha.3 75 | 76 | ### Patch Changes 77 | 78 | - 9462c83: New build 79 | -------------------------------------------------------------------------------- /core/jsx-runtime.mjs: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from './worker.mjs'; 2 | 3 | function jsx(type, props, key, __self, __source) { 4 | let children = null; 5 | if(props.children) { 6 | children = props.children; 7 | delete props.children; 8 | } 9 | if(key) { 10 | props.key = key; 11 | } 12 | 13 | return h(type, props, children); 14 | } 15 | 16 | export { 17 | jsx, 18 | jsx as jsxs, 19 | jsx as jsxDEV, 20 | Fragment 21 | }; -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fritz", 3 | "type": "module", 4 | "version": "5.0.0-beta.5", 5 | "description": "Create web components that run in a Web Worker", 6 | "exports": { 7 | ".": { 8 | "types": "./types/worker/ns.d.ts", 9 | "default": "./worker.mjs" 10 | }, 11 | "./window": { 12 | "types": "./types/window/index.d.ts", 13 | "default": "./window.mjs" 14 | }, 15 | "./node": { 16 | "types": "./types/node/index.d.ts", 17 | "default": "./node.mjs" 18 | }, 19 | "./jsx-runtime": { 20 | "default": "./jsx-runtime.mjs" 21 | } 22 | }, 23 | "typesVersions": { 24 | "*": { 25 | "*": [ 26 | "types/worker/ns.d.ts" 27 | ], 28 | "window": [ 29 | "types/window/index.d.ts" 30 | ], 31 | "node": [ 32 | "types/node/index.d.ts" 33 | ] 34 | } 35 | }, 36 | "scripts": { 37 | "build:worker": "wireit", 38 | "build:window": "wireit", 39 | "build:node": "wireit", 40 | "build": "wireit", 41 | "server": "wireit", 42 | "test:core": "wireit", 43 | "test:node": "wireit", 44 | "test:types:core": "wireit", 45 | "test:types:jsx-runtime": "wireit", 46 | "test:types": "wireit", 47 | "test": "wireit", 48 | "check": "wireit", 49 | "build-and-server": "wireit" 50 | }, 51 | "wireit": { 52 | "build:worker": { 53 | "command": "microbundle -f modern --no-pkg-main -i src/worker/index.ts -o worker.mjs && node scripts/move-types.mjs && cp src/worker/*.d.ts types/worker", 54 | "files": [ 55 | "src/*.ts", 56 | "src/worker/**/*" 57 | ], 58 | "output": [ 59 | "worker.mjs", 60 | "worker.mjs.map", 61 | "types/*.d.ts", 62 | "types/worker" 63 | ] 64 | }, 65 | "build:window": { 66 | "command": "microbundle -f modern --no-pkg-main --define process.env.NODE_ENV=production -i src/window/index.ts -o window.mjs && node scripts/move-types.mjs", 67 | "files": [ 68 | "src/*.ts", 69 | "src/window/**/*" 70 | ], 71 | "output": [ 72 | "window.mjs", 73 | "window.mjs.map", 74 | "types/*.d.ts", 75 | "types/window" 76 | ] 77 | }, 78 | "build:node": { 79 | "command": "microbundle -f modern --no-pkg-main -i src/node/index.ts --external ./worker.mjs --no-compress -o node.mjs && node scripts/move-types.mjs", 80 | "files": [ 81 | "src/node/**/*" 82 | ], 83 | "output": [ 84 | "node.mjs", 85 | "node.mjs.map", 86 | "types/node" 87 | ] 88 | }, 89 | "check": { 90 | "command": "tsc -p tsconfig.json", 91 | "files": [ 92 | "src/**/*" 93 | ], 94 | "output": [] 95 | }, 96 | "build": { 97 | "dependencies": [ 98 | "build:worker", 99 | "build:window", 100 | "build:node" 101 | ] 102 | }, 103 | "test:types:core": { 104 | "command": "tsc -p test/typescript/core/tsconfig.json", 105 | "dependencies": [ 106 | "build:worker" 107 | ], 108 | "files": [ 109 | "src/**/*", 110 | "test/typescript/core/**/*" 111 | ], 112 | "output": [] 113 | }, 114 | "test:types:jsx-runtime": { 115 | "command": "tsc -p test/typescript/jsx-runtime/tsconfig.json", 116 | "dependencies": [ 117 | "build:worker" 118 | ], 119 | "files": [ 120 | "src/**/*", 121 | "test/typescript/jsx-runtime/**/*" 122 | ], 123 | "output": [] 124 | }, 125 | "test:types": { 126 | "dependencies": [ 127 | "test:types:core", 128 | "test:types:jsx-runtime" 129 | ] 130 | }, 131 | "test:core": { 132 | "command": "node ../node_modules/@matthewp/node-qunit-puppeteer/cli.js http://localhost:1931/test/browser/test.html", 133 | "dependencies": [ 134 | "build", 135 | "server" 136 | ] 137 | }, 138 | "test:node": { 139 | "command": "node --test test/node/", 140 | "dependencies": [ 141 | "build", 142 | "build:worker", 143 | "build:node" 144 | ], 145 | "files": [ 146 | "src/**/*", 147 | "test/node/**" 148 | ], 149 | "output": [] 150 | }, 151 | "test": { 152 | "dependencies": [ 153 | "test:types", 154 | "test:core", 155 | "test:node" 156 | ] 157 | }, 158 | "server": { 159 | "command": "serve -p 1931", 160 | "service": { 161 | "readyWhen": { 162 | "lineMatches": "Accepting connections" 163 | } 164 | } 165 | }, 166 | "build-and-server": { 167 | "dependencies": [ 168 | "build", 169 | "server" 170 | ] 171 | } 172 | }, 173 | "repository": { 174 | "type": "git", 175 | "url": "git+https://github.com/matthewp/fritz.git" 176 | }, 177 | "author": "Matthew Phillips", 178 | "license": "BSD-2-Clause", 179 | "bugs": { 180 | "url": "https://github.com/matthewp/fritz/issues" 181 | }, 182 | "files": [ 183 | "window.mjs", 184 | "window.mjs.map", 185 | "worker.mjs", 186 | "worker.mjs.map", 187 | "node.mjs", 188 | "node.mjs.map", 189 | "types", 190 | "jsx-runtime.mjs" 191 | ], 192 | "homepage": "https://fritz.work", 193 | "keywords": [ 194 | "web-components", 195 | "webcomponents", 196 | "react", 197 | "virtualdom", 198 | "web-worker" 199 | ], 200 | "devDependencies": { 201 | "@matthewp/node-qunit-puppeteer": "^3.0.0", 202 | "@matthewp/skatejs": "^5.1.1", 203 | "add": "^2.0.6", 204 | "highlight.js": "^10.4.1", 205 | "incremental-dom": "^0.6.0", 206 | "microbundle": "^0.15.1", 207 | "serve": "^14.1.2", 208 | "typescript": "^4.9.4", 209 | "wireit": "^0.9.3", 210 | "workbox-cli": "^4.3.1" 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /core/scripts/move-types.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | const root = new URL('../', import.meta.url); 4 | 5 | fs.mkdirSync(new URL('types', root), { recursive: true }); 6 | 7 | function moveIfExists(name) { 8 | try { 9 | fs.renameSync(new URL(name, root), new URL(`types/${name}`, root), { recursive: true }); 10 | } catch {} 11 | } 12 | 13 | [ 14 | 'message-types.d.ts', 15 | 'types.d.ts', 16 | 'util.d.ts', 17 | ].forEach(moveIfExists); 18 | 19 | moveIfExists('node'); 20 | moveIfExists('worker'); 21 | moveIfExists('window'); -------------------------------------------------------------------------------- /core/src/message-types.ts: -------------------------------------------------------------------------------- 1 | import type { PropDefinitions, RemoteEvent, Sheet, RemoteElement } from './types'; 2 | import type { Tree } from './worker/tree'; 3 | 4 | export const DEFINE = 'fritz:define'; 5 | export const TRIGGER = 'fritz:trigger'; 6 | export const RENDER = 'fritz:render'; 7 | export const EVENT = 'fritz:event'; 8 | export const STATE = 'fritz:state'; 9 | export const DESTROY = 'fritz:destroy'; 10 | export const RENDERED = 'fritz:rendered'; 11 | export const CLEANUP = 'fritz:cleanup'; 12 | 13 | 14 | // Window defined 15 | export type WindowRenderMessage = { 16 | type: typeof RENDER; 17 | tag: string; 18 | id: number; 19 | props: Record; 20 | }; 21 | 22 | export type EventMessage = { 23 | type: typeof EVENT; 24 | event: { 25 | type: string; 26 | detail: any; 27 | value: string; 28 | }; 29 | id: number; 30 | handle: number | undefined; 31 | }; 32 | 33 | export type StateMessage = { 34 | type: typeof STATE; 35 | state: any; 36 | }; 37 | 38 | export type DestroyMessage = { 39 | type: typeof DESTROY; 40 | id: number; 41 | }; 42 | 43 | export type RenderedMessage = { 44 | type: typeof RENDERED; 45 | id: number; 46 | }; 47 | 48 | export type CleanupMessage = { 49 | type: typeof CLEANUP; 50 | id: number; 51 | handles: Array; 52 | }; 53 | 54 | export type MessageSentFromWindow = WindowRenderMessage | EventMessage | StateMessage | DestroyMessage | RenderedMessage | CleanupMessage; 55 | 56 | // Worker Defined 57 | export type DefineMessage = { 58 | type: typeof DEFINE; 59 | tag: string; 60 | props: PropDefinitions | undefined; 61 | events: Array | undefined; 62 | styles: Array | undefined; 63 | features: { 64 | mount: boolean; 65 | }; 66 | }; 67 | 68 | export type TriggerMessage = { 69 | type: typeof TRIGGER; 70 | event: RemoteEvent; 71 | id: number; 72 | }; 73 | 74 | export type WorkerRenderMessage = { 75 | type: typeof RENDER; 76 | id: number; 77 | tree: Tree; 78 | }; 79 | 80 | export type MessageSentFromWorker = DefineMessage | TriggerMessage | WorkerRenderMessage; -------------------------------------------------------------------------------- /core/src/node/index.ts: -------------------------------------------------------------------------------- 1 | import type { Tree } from '../worker/tree'; 2 | import fritz from './worker.mjs'; 3 | 4 | const OPEN = 1; 5 | const CLOSE = 2; 6 | const TEXT = 4; 7 | 8 | const encodeEntities = (s: any) => String(s) 9 | .replace(/&/g, '&') 10 | .replace(//g, '>') 12 | .replace(/"/g, '"'); 13 | 14 | function renderStyle(text: string) { 15 | return ``; 16 | } 17 | 18 | type FritzTagMap = typeof fritz._tags; 19 | type ComponentConstructor = FritzTagMap extends Map ? I : never; 20 | type ComponentStyles = NonNullable; 21 | 22 | function * renderStyles(styles: ComponentStyles): Generator { 23 | if(typeof styles === 'string') { 24 | yield renderStyle(styles); 25 | } else { 26 | for(let defn of styles) { 27 | if(typeof defn === 'string') { 28 | yield renderStyle(defn); 29 | } else { 30 | // TODO 31 | } 32 | } 33 | } 34 | } 35 | 36 | function* render(vnode: Tree): Generator { 37 | let position = 0, len = vnode.length; 38 | while(position < len) { 39 | let instr = vnode[position]; 40 | let command = instr[0]; 41 | 42 | switch(command) { 43 | case OPEN: { 44 | let tagName = instr[1]; 45 | let Component = fritz._tags.get(tagName); 46 | let props: Record | null = Component ? {} : null; 47 | let pushProps = props ? (name: string, value: any) => props![name] = value : Function.prototype; 48 | 49 | yield '<' + tagName; 50 | let attrs = instr[3]; 51 | let i = 0, attrLen = attrs ? attrs.length : 0; 52 | while(i < attrLen) { 53 | if(i === 0) { 54 | yield ' '; 55 | } 56 | let attrName = attrs[i]; 57 | let attrValue = attrs[i + 1]; 58 | pushProps(attrName, attrValue); 59 | if(attrName !== 'children') { 60 | yield attrName + '="' + encodeEntities(attrValue) + '"'; 61 | } 62 | i += 2; 63 | } 64 | yield '>'; 65 | 66 | if(Component) { 67 | yield ''; 75 | 76 | if(props!.children) { 77 | yield props!.children.toString(); 78 | } 79 | } 80 | 81 | break; 82 | } 83 | 84 | case CLOSE: { 85 | yield ''; 86 | break; 87 | } 88 | 89 | case TEXT: { 90 | yield encodeEntities(instr[1]); 91 | break; 92 | } 93 | } 94 | 95 | position++; 96 | } 97 | } 98 | 99 | function renderToString(vnode: Tree) { 100 | let out = ''; 101 | for(let part of render(vnode)) { 102 | out += part; 103 | } 104 | return out; 105 | } 106 | 107 | export { 108 | render, 109 | renderToString 110 | }; 111 | -------------------------------------------------------------------------------- /core/src/node/worker.d.mts: -------------------------------------------------------------------------------- 1 | export * from '../worker'; 2 | export { default } from '../worker'; -------------------------------------------------------------------------------- /core/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { default as Component, ComponentConstructor } from './worker/component'; 2 | import type { MountBase } from './window/types'; 3 | import type { default as h } from './worker/hyperscript.js'; 4 | 5 | export type CustomElementTagName = `${string}-${string}`; 6 | 7 | export type Fritz | MountBase = Component> = { 8 | _instances: Map; 9 | _type: T; 10 | }; 11 | 12 | export type WindowFritz = Fritz & { 13 | _id: number; 14 | _workers: Worker[]; 15 | _sheets: CSSStyleSheet[]; 16 | state?: any; 17 | use(worker: Worker): void; 18 | adopt(element: HTMLStyleElement | HTMLLIElement): void; 19 | }; 20 | 21 | export type WorkerFritz = Fritz> & { 22 | state?: any; 23 | _define(tag: CustomElementTagName, constructor: ComponentConstructor): void; 24 | define(tag: CustomElementTagName, constructor: ComponentConstructor): void; 25 | h: typeof h; 26 | Component: typeof Component; 27 | fritz: WorkerFritz; 28 | _tags: Map; 29 | _port: MessagePort; 30 | _listening: boolean; 31 | }; 32 | 33 | export type PropDefinition = { 34 | attribute: boolean; 35 | }; 36 | 37 | export type PropDefinitions = { 38 | [P in K]: PropDefinition; 39 | }; 40 | 41 | export type RemoteEvent = { 42 | type: string; 43 | detail?: D; 44 | cancelable?: boolean; 45 | composed?: boolean; 46 | }; 47 | 48 | export type Sheet = { 49 | text: string; 50 | }; 51 | 52 | export type RemoteElement = { 53 | selector: string; 54 | }; -------------------------------------------------------------------------------- /core/src/util.ts: -------------------------------------------------------------------------------- 1 | import type { Fritz } from './types'; 2 | import type { default as Component } from './worker/component'; 3 | import type { MountBase } from './window/types'; 4 | 5 | type AcceptableFritz = Fritz | MountBase>; 6 | 7 | export function getInstance(fritz: F, id: number): F['_type'] | undefined { 8 | return fritz._instances.get(id); 9 | }; 10 | 11 | export function setInstance(fritz: F, id: number, instance: any) { 12 | fritz._instances.set(id, instance); 13 | }; 14 | 15 | export function delInstance(fritz: F, id: number) { 16 | fritz._instances.delete(id); 17 | }; 18 | 19 | export function isFunction(val: unknown): val is Function { 20 | return typeof val === 'function'; 21 | }; 22 | 23 | export const defer = Promise.resolve().then.bind(Promise.resolve()); -------------------------------------------------------------------------------- /core/src/window/cmd.ts: -------------------------------------------------------------------------------- 1 | import type { WindowFritz } from '../types'; 2 | import type { StateMessage } from '../message-types'; 3 | import { STATE } from '../message-types.js'; 4 | 5 | export function sendState(fritz: WindowFritz, worker?: Worker) { 6 | let workers = worker ? [worker] : fritz._workers; 7 | let state = fritz.state; 8 | workers.forEach(function(worker) { 9 | const msg: StateMessage = { 10 | type: STATE, 11 | state: state 12 | }; 13 | worker.postMessage(msg); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /core/src/window/component.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import type { WindowFritz } from '../types'; 3 | import type { DefineMessage } from '../message-types'; 4 | 5 | import { withUpdate } from '@matthewp/skatejs/dist/esnext/with-update'; 6 | import { withMount } from './with-mount.js'; 7 | import { withWorkerEvents } from './with-worker-events.js'; 8 | import { withWorkerRender } from './with-worker-render.js'; 9 | import { withWorkerConnection } from './with-worker-connection.js'; 10 | import { withStyles } from './with-styles.js'; 11 | 12 | const Base = withWorkerEvents( 13 | withWorkerRender( 14 | withUpdate(HTMLElement) 15 | ) 16 | ); 17 | 18 | export function withComponent( 19 | fritz: WindowFritz, 20 | worker: Worker, 21 | msg: DefineMessage 22 | ) { 23 | let { 24 | props = {}, 25 | events = [], 26 | features: { mount } 27 | } = msg; 28 | let ComponentElement = Base; 29 | if(mount) { 30 | ComponentElement = withMount(ComponentElement) as any; 31 | } 32 | ComponentElement = withStyles(fritz, msg.styles, ComponentElement) as any; 33 | return withWorkerConnection(fritz, events, props, worker, ComponentElement) as MountBase; 34 | }; 35 | -------------------------------------------------------------------------------- /core/src/window/idom-render.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import { 3 | attributes, 4 | elementOpen, 5 | elementClose, 6 | symbols, 7 | text, 8 | patch 9 | } from 'incremental-dom'; 10 | import { isFunction } from '../util.js'; 11 | 12 | var eventAttrExp = /^on[a-z]/; 13 | var orphanedHandles: any[] | null = null; 14 | var FN_HANDLE = Symbol('fritz.handle'); 15 | 16 | var attributesSet = attributes[symbols.default]; 17 | attributes[symbols.default] = preferProps; 18 | 19 | function preferProps(element: any, name: string, value: any){ 20 | if(name in element && !isSVG(element)) { 21 | if(isEventProperty(name, value)) { 22 | element[name] = setupEventHandler(element, name, value); 23 | } else { 24 | element[name] = value; 25 | } 26 | } 27 | 28 | else if(isEventProperty(name, value) && isFunction(element.addEventProperty)) { 29 | element.addEventProperty(name); 30 | element[name] = setupEventHandler(element, name, value); 31 | } 32 | else 33 | attributesSet(element, name, value); 34 | } 35 | 36 | function isEventProperty(name: string, value: any) { 37 | return eventAttrExp.test(name) && Array.isArray(value) && isFunction(value[1]); 38 | } 39 | 40 | function isSVG(element: Element) { 41 | return element.namespaceURI === 'http://www.w3.org/2000/svg'; 42 | } 43 | 44 | function setupEventHandler(element: any, name: string, value: any) { 45 | var currentValue = element[name]; 46 | var fn = value[1]; 47 | if(currentValue) { 48 | if(currentValue !== fn) { 49 | fn[FN_HANDLE] = value[0]; 50 | orphanedHandles!.push(currentValue[FN_HANDLE]); 51 | } 52 | } else { 53 | fn[FN_HANDLE] = value[0]; 54 | } 55 | return fn; 56 | } 57 | 58 | const TAG = 1; 59 | const ID = 2; 60 | const ATTRS = 3; 61 | const EVENTS = 4; 62 | 63 | function render(bc: any, component: MountBase){ 64 | var n; 65 | for(var i = 0, len = bc.length; i < len; i++) { 66 | n = bc[i]; 67 | switch(n[0]) { 68 | // Open 69 | case 1: 70 | if(n[EVENTS]) { 71 | var k; 72 | for(var j = 0, jlen = n[EVENTS].length; j < jlen; j++) { 73 | k = n[EVENTS][j]; 74 | let handler = component.addEventCallback(k[2], k[1]); 75 | n[ATTRS].push(k[1], [k[2], handler]); 76 | } 77 | } 78 | 79 | var openArgs = [n[TAG], n[ID], null].concat(n[ATTRS]); 80 | elementOpen.apply(null, openArgs as any); 81 | break; 82 | case 2: 83 | elementClose(n[1]); 84 | break; 85 | case 4: 86 | text(n[1]); 87 | break; 88 | } 89 | } 90 | } 91 | 92 | function idomRender(vdom: any, root: any, component: MountBase): number[] { 93 | orphanedHandles = []; 94 | patch(root, () => render(vdom, component)); 95 | let out = orphanedHandles; 96 | orphanedHandles = null; 97 | return out; 98 | } 99 | 100 | export { idomRender }; 101 | -------------------------------------------------------------------------------- /core/src/window/index.ts: -------------------------------------------------------------------------------- 1 | import type { MessageSentFromWorker } from '../message-types'; 2 | import type { WindowFritz } from '../types'; 3 | 4 | import { define, render, trigger } from './lifecycle.js'; 5 | import { DEFINE, RENDER, TRIGGER } from '../message-types.js'; 6 | import { sendState } from './cmd.js'; 7 | import './types'; 8 | 9 | const fritz = Object.create(null) as WindowFritz; 10 | fritz._id = 1; 11 | fritz._instances = new Map(); 12 | fritz._workers = []; 13 | fritz._sheets = [] 14 | 15 | function use(worker: Worker) { 16 | fritz._workers.push(worker); 17 | worker.addEventListener('message', handleMessage); 18 | if(fritz.state) { 19 | sendState(fritz, worker); 20 | } 21 | } 22 | 23 | function handleMessage(this: Worker, ev: MessageEvent) { 24 | let msg = ev.data; 25 | switch(msg.type) { 26 | case DEFINE: 27 | define.call(this, fritz, msg); 28 | break; 29 | case RENDER: 30 | render(fritz, msg); 31 | break; 32 | case TRIGGER: 33 | trigger(fritz, msg); 34 | } 35 | } 36 | 37 | fritz.use = use; 38 | 39 | Object.defineProperty(fritz, 'state', { 40 | set: function(val){ 41 | this._state = val; 42 | sendState(fritz); 43 | }, 44 | get: function(){ 45 | return this._state; 46 | } 47 | }); 48 | 49 | function adopt(element: HTMLStyleElement | HTMLLinkElement) { 50 | if(element.sheet) 51 | fritz._sheets.push(element.sheet); 52 | } 53 | 54 | fritz.adopt = adopt; 55 | 56 | export default fritz; 57 | -------------------------------------------------------------------------------- /core/src/window/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerRenderMessage, DefineMessage, TriggerMessage } from '../message-types'; 2 | import type { WindowFritz } from '../types'; 3 | 4 | import { withComponent } from './component.js'; 5 | import { getInstance } from '../util.js'; 6 | 7 | export function define(this: Worker, fritz: WindowFritz, msg: DefineMessage) { 8 | let worker = this; 9 | let tagName = msg.tag; 10 | 11 | let Element = withComponent(fritz, worker, msg); 12 | customElements.define(tagName, Element); 13 | }; 14 | 15 | export function render(fritz: WindowFritz, msg: WorkerRenderMessage) { 16 | let instance = getInstance(fritz, msg.id); 17 | if(instance !== undefined) { 18 | instance.doRenderCallback(msg.tree); 19 | } 20 | }; 21 | 22 | export function trigger(fritz: WindowFritz, msg: TriggerMessage) { 23 | let inst = getInstance(fritz, msg.id)!; 24 | let ev = msg.event; 25 | let event = new CustomEvent(ev.type, { 26 | bubbles: true,//ev.bubbles, 27 | cancelable: ev.cancelable, 28 | detail: ev.detail, 29 | composed: ev.composed 30 | }); 31 | 32 | inst.dispatchEvent(event); 33 | }; 34 | -------------------------------------------------------------------------------- /core/src/window/mods.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@matthewp/skatejs/dist/esnext/with-update' { 2 | export function withUpdate(a: any): any; 3 | } 4 | 5 | declare module '@matthewp/skatejs/dist/esnext/with-renderer' { 6 | export function withRenderer(a: any): typeof HTMLElement; 7 | } 8 | 9 | declare module 'incremental-dom' { 10 | export const symbols: { 11 | default: any; 12 | } 13 | export const attributes: { 14 | [a in any]: any; 15 | }; 16 | export function elementOpen(a: any, b: any, c: any): any; 17 | export function elementClose(a: any): any; 18 | 19 | export function text(a: any): any; 20 | export function patch(a: any, b: any): any; 21 | } -------------------------------------------------------------------------------- /core/src/window/styles/adopt.ts: -------------------------------------------------------------------------------- 1 | import { InjectorBase } from './base.js'; 2 | 3 | export class Adoptable extends InjectorBase { 4 | private adoptables: CSSStyleSheet[] | null = null; 5 | add(sheet: CSSStyleSheet | string) { 6 | if(this.adoptables === null) this.adoptables = []; 7 | let adoptable = new CSSStyleSheet(); 8 | if(typeof sheet === 'string') { 9 | adoptable.replaceSync(sheet); 10 | } else { 11 | for(let rule of sheet.cssRules) { 12 | adoptable.insertRule(rule.cssText); 13 | } 14 | } 15 | this.adoptables.push(adoptable); 16 | } 17 | f(shadowRoot: ShadowRoot) { 18 | if(this.adoptables) { 19 | shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, ...this.adoptables]; 20 | } 21 | return shadowRoot; 22 | } 23 | } -------------------------------------------------------------------------------- /core/src/window/styles/base.ts: -------------------------------------------------------------------------------- 1 | import type { WindowFritz, RemoteElement, Sheet } from "../../types"; 2 | import type { DefineMessage } from '../../message-types'; 3 | 4 | export abstract class InjectorBase { 5 | constructor(private fritz: WindowFritz, private styles: DefineMessage['styles']){} 6 | abstract add(sheet: CSSStyleSheet | string): void; 7 | abstract f(shadowRoot: ShadowRoot): DocumentFragment; 8 | inject(shadowRoot: ShadowRoot): DocumentFragment { 9 | let { fritz } = this; 10 | if(fritz._sheets.length) { 11 | for(let sheet of fritz._sheets) { 12 | this.add(sheet); 13 | } 14 | fritz._sheets.length = 0; 15 | } 16 | if(this.styles) { 17 | for(let defn of this.styles) { 18 | if(typeof (defn as any).text === 'string') { 19 | this.add((defn as Sheet).text); 20 | } else { 21 | for(let el of shadowRoot.ownerDocument.querySelectorAll((defn as RemoteElement).selector)) { 22 | if((el as any).sheet) { 23 | this.add((el as any).sheet); 24 | } 25 | } 26 | } 27 | } 28 | this.styles = undefined; 29 | } 30 | return this.f(shadowRoot); 31 | } 32 | } -------------------------------------------------------------------------------- /core/src/window/styles/index.ts: -------------------------------------------------------------------------------- 1 | export { Adoptable } from './adopt.js'; 2 | export { Taggable } from './tag.js'; -------------------------------------------------------------------------------- /core/src/window/styles/tag.ts: -------------------------------------------------------------------------------- 1 | import { InjectorBase } from './base.js'; 2 | 3 | // TODO remove when adoptedStylesheets has good browser support. 4 | export class Taggable extends InjectorBase { 5 | private adoptables: DocumentFragment | null = null; 6 | add(sheet: CSSStyleSheet | string) { 7 | if(this.adoptables == null) this.adoptables = document.createDocumentFragment(); 8 | let adoptable = document.createElement('style'); 9 | if(typeof sheet === 'string') { 10 | adoptable.textContent = sheet; 11 | } else { 12 | for(let rule of sheet.cssRules) { 13 | adoptable.textContent += rule.cssText; 14 | } 15 | } 16 | this.adoptables.append(adoptable); 17 | } 18 | f(shadowRoot: ShadowRoot) { 19 | if(this.adoptables) { 20 | let frag = this.adoptables.cloneNode(true); 21 | 22 | let root = document.createDocumentFragment(); 23 | root.append(...shadowRoot.childNodes); 24 | 25 | // Fake being a shadowroot 26 | Object.defineProperties(root, { 27 | insertBefore: { 28 | value: shadowRoot.insertBefore.bind(shadowRoot) 29 | }, 30 | removeChild: { 31 | value: shadowRoot.removeChild.bind(shadowRoot) 32 | } 33 | }); 34 | 35 | // Insert styles 36 | shadowRoot.prepend(frag); 37 | return root; 38 | } 39 | return shadowRoot; 40 | } 41 | } -------------------------------------------------------------------------------- /core/src/window/types.ts: -------------------------------------------------------------------------------- 1 | import type { Tree } from '../worker/tree'; 2 | 3 | export interface MountBase extends HTMLElement { 4 | new(): MountBase; 5 | connectedCallback(): void; 6 | disconnectedCallback(): void; 7 | shadowRoot: ShadowRoot; 8 | 9 | renderer?(): void; 10 | doRenderCallback(tree: Tree): void; 11 | addEventCallback(handle: number, b?: unknown): void; 12 | handleOrphanedHandles(handles: number[]): void; 13 | props: Record; 14 | _worker: Worker; 15 | _id: number; 16 | _handlers: Record; 17 | _root: DocumentFragment; 18 | } -------------------------------------------------------------------------------- /core/src/window/with-mount.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import { RENDERED } from '../message-types.js'; 3 | 4 | let currentComponent: HTMLElement; 5 | 6 | export function setComponent(component: HTMLElement) { 7 | let previousComponent = currentComponent; 8 | setComponentTo(component); 9 | return setComponentTo.bind(null, previousComponent); 10 | }; 11 | 12 | function setComponentTo(component: HTMLElement) { 13 | currentComponent = component; 14 | } 15 | 16 | /** 17 | * The algorithm to determine when mounted is: 18 | * 1. When a component is updated, it and parents are updating 19 | * 2. When children have rendered, parent is done. 20 | * 3. If no children, parent done after own render. 21 | */ 22 | 23 | export function withMount(Base: MountBase) { 24 | return class extends Base { 25 | public _resetComponent: any; 26 | public _parentComponent: any; 27 | public _renderCount: number; 28 | public _hasChildComponents: boolean; 29 | public _amMounted: boolean; 30 | constructor() { 31 | super(); 32 | this._resetComponent = Function.prototype; // placeholder 33 | this._parentComponent = currentComponent; 34 | this._renderCount = 0; 35 | this._hasChildComponents = false; 36 | this._amMounted = false; 37 | } 38 | 39 | connectedCallback() { 40 | // @ts-ignore 41 | if(super.connectedCallback) super.connectedCallback(); 42 | if(this._parentComponent) { 43 | this._parentComponent._hasChildComponents = true; 44 | } 45 | } 46 | 47 | disconnectedCallback() { 48 | // @ts-ignore 49 | if(super.disconnectedCallback) super.disconnectedCallback(); 50 | this._amMounted = false; 51 | } 52 | 53 | renderer() { 54 | if(super.renderer) super.renderer(); 55 | this._renderCount = 0; 56 | if(this._parentComponent) { 57 | this._parentComponent._incrementRender(); 58 | } 59 | } 60 | 61 | beforeRender() { 62 | this._resetComponent = setComponent(this); 63 | } 64 | 65 | afterRender() { 66 | this._resetComponent(); 67 | this._resetComponent = Function.prototype; 68 | 69 | if(!this._amMounted && !this._hasChildComponents) { 70 | this._checkIfRendered(); 71 | } 72 | } 73 | 74 | _incrementRender() { 75 | this._renderCount++; 76 | } 77 | 78 | _decrementRender() { 79 | this._renderCount--; 80 | this._checkIfRendered(); 81 | } 82 | 83 | _checkIfRendered() { 84 | if(this._amMounted) return; 85 | 86 | if(this._renderCount === 0) { 87 | this._amMounted = true; 88 | this._worker.postMessage({ 89 | type: RENDERED, 90 | id: this._id 91 | }); 92 | 93 | if(this._parentComponent) { 94 | this._parentComponent._decrementRender(); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/window/with-styles.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import type { WindowFritz } from '../types'; 3 | import type { DefineMessage } from '../message-types'; 4 | import { Adoptable, Taggable } from './styles/index.js'; 5 | 6 | export function withStyles(fritz: WindowFritz, styles: DefineMessage['styles'], Base: MountBase) { 7 | let Injector = ('adoptedStyleSheets' in document) ? Adoptable : Taggable; 8 | let injector = new Injector(fritz, styles); 9 | 10 | return class extends Base { 11 | constructor() { 12 | super(); 13 | this._root = injector.inject(this.shadowRoot); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /core/src/window/with-worker-connection.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import type { WindowFritz, PropDefinitions } from '../types'; 3 | import type { DefineMessage, DestroyMessage } from '../message-types'; 4 | 5 | import { DESTROY } from '../message-types.js'; 6 | import { setInstance, delInstance } from '../util.js'; 7 | 8 | export function withWorkerConnection( 9 | fritz: WindowFritz, 10 | events: Array, 11 | props: PropDefinitions, 12 | worker: Worker, 13 | Base: MountBase 14 | ) { 15 | return class extends Base { 16 | static get props() { 17 | return props; 18 | } 19 | 20 | constructor() { 21 | super(); 22 | this._id = ++fritz._id; 23 | this._worker = worker; 24 | } 25 | 26 | connectedCallback() { 27 | super.connectedCallback(); 28 | setInstance(fritz, this._id, this); 29 | events.forEach(eventName => { 30 | (this.shadowRoot as any).addEventListener(eventName, this); 31 | }); 32 | } 33 | 34 | disconnectedCallback() { 35 | // @ts-ignore 36 | if(super.disconnectedCallback) super.disconnectedCallback(); 37 | delInstance(fritz, this._id); 38 | events.forEach(eventName => { 39 | (this.shadowRoot as any).removeEventListener(eventName, this); 40 | }); 41 | let msg: DestroyMessage = { 42 | type: DESTROY, 43 | id: this._id 44 | }; 45 | this._worker.postMessage(msg); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/window/with-worker-events.ts: -------------------------------------------------------------------------------- 1 | import type { MountBase } from './types'; 2 | import type { EventMessage } from '../message-types'; 3 | 4 | import { CLEANUP, EVENT } from '../message-types.js'; 5 | 6 | function postEvent(event: Event, inst: MountBase, handle?: number) { 7 | let worker = inst._worker; 8 | let id = inst._id; 9 | let msg: EventMessage = { 10 | type: EVENT, 11 | event: { 12 | type: event.type, 13 | detail: (event as any).detail, 14 | value: (event as any).target.value 15 | }, 16 | id: id, 17 | handle: handle 18 | }; 19 | worker.postMessage(msg); 20 | } 21 | 22 | export function withWorkerEvents(Base: MountBase) { 23 | return class extends Base { 24 | constructor() { 25 | super(); 26 | this._handlers = Object.create(null); 27 | } 28 | 29 | addEventCallback(handleId: number) { 30 | let key = handleId; 31 | let fn; 32 | if(fn = this._handlers[key]) { 33 | return fn; 34 | } 35 | 36 | // TODO optimize this so functions are reused if possible. 37 | fn = (ev: Event) => { 38 | ev.preventDefault(); 39 | postEvent(ev, this, handleId); 40 | }; 41 | this._handlers[key] = fn; 42 | return fn; 43 | } 44 | 45 | addEventProperty(name: string) { 46 | let evName = name.slice(2); 47 | let priv = '_' + name; 48 | let proto = Object.getPrototypeOf(this); 49 | Object.defineProperty(proto, name, { 50 | get: function(){ return this[priv]; }, 51 | set: function(val) { 52 | let cur; 53 | if(cur = this[priv]) { 54 | this.removeEventListener(evName, cur); 55 | } 56 | this[priv] = val; 57 | this.addEventListener(evName, val); 58 | } 59 | }); 60 | } 61 | 62 | handleEvent(ev: Event) { 63 | ev.preventDefault(); 64 | postEvent(ev, this); 65 | } 66 | 67 | handleOrphanedHandles(handles: number[]) { 68 | if(handles.length) { 69 | let worker = this._worker; 70 | worker.postMessage({ 71 | type: CLEANUP, 72 | id: this._id, 73 | handles: handles 74 | }); 75 | let handlers = this._handlers; 76 | handles.forEach(function(id){ 77 | delete handlers[id]; 78 | }); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /core/src/window/with-worker-render.ts: -------------------------------------------------------------------------------- 1 | import type { WindowRenderMessage, WorkerRenderMessage } from '../message-types'; 2 | import type { MountBase } from './types'; 3 | import type { Tree } from '../worker/tree'; 4 | 5 | import { idomRender as render } from './idom-render.js'; 6 | import { withRenderer } from '@matthewp/skatejs/dist/esnext/with-renderer'; 7 | import { RENDER } from '../message-types.js'; 8 | 9 | export function withWorkerRender(Base: MountBase): any { 10 | return class extends withRenderer(Base) { 11 | constructor() { 12 | super(); 13 | if(!this.shadowRoot) { 14 | this.attachShadow({ mode: 'open' }); 15 | } 16 | } 17 | 18 | renderer(this: MountBase) { 19 | // Only send a render when connected 20 | if(!this.isConnected) return; 21 | let msg: WindowRenderMessage = { 22 | type: RENDER, 23 | tag: this.localName, 24 | id: this._id, 25 | props: this.props 26 | } 27 | this._worker.postMessage(msg); 28 | } 29 | 30 | beforeRender() {} 31 | afterRender() {} 32 | 33 | doRenderCallback(this: MountBase, tree: Tree) { 34 | (this as any).beforeRender(); 35 | let root = this._root; 36 | let out = render(tree, root, this); 37 | (this as any).afterRender(); 38 | this.handleOrphanedHandles(out); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/worker/component-extras.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace fritz; 2 | 3 | import { default as Component, ComponentConstructor } from './component'; 4 | 5 | export type { 6 | Component 7 | }; 8 | 9 | export type Key = string | number | any; 10 | 11 | export interface VNode

{ 12 | type: ComponentType

| string; 13 | props: P & { children: ComponentChildren }; 14 | key: Key; 15 | } 16 | 17 | export interface Attributes { 18 | key?: Key; 19 | jsx?: boolean; 20 | } 21 | 22 | export type ComponentChild = 23 | | VNode 24 | | object 25 | | string 26 | | number 27 | | bigint 28 | | boolean 29 | | null 30 | | undefined; 31 | 32 | export type ComponentChildren = ComponentChild[] | ComponentChild; 33 | 34 | export type ComponentType

= ComponentConstructor

; 35 | 36 | export interface FritzDOMAttributes { 37 | children?: ComponentChildren; 38 | } -------------------------------------------------------------------------------- /core/src/worker/component.ts: -------------------------------------------------------------------------------- 1 | import type { PropDefinitions, RemoteEvent, RemoteElement } from '../types'; 2 | import type { default as Handle } from './handle'; 3 | import type { ComponentChild } from './component-extras'; 4 | 5 | import { isFunction } from '../util.js'; 6 | import { TRIGGER } from '../message-types.js'; 7 | import { enqueueRender } from './instance.js'; 8 | 9 | interface Component

{ 10 | _fritzId: number; 11 | _fritzHandles: Map; 12 | _fritzPort: MessagePort; 13 | _dirty: boolean | undefined; 14 | localName: string; 15 | 16 | shouldComponentUpdate(props: P): boolean; 17 | componentDidMount(): void; 18 | getSnapshotBeforeUpdate(prevProps: P, prevState: S): SS | null; 19 | componentDidUpdate(prevProps: P, prevState: S, snapshot: SS | null): void; 20 | } 21 | 22 | abstract class Component { 23 | public state: S; 24 | public props: P; 25 | constructor() { 26 | this.state = {} as S; 27 | this.props = {} as P; 28 | } 29 | 30 | dispatch(ev: RemoteEvent) { 31 | let id = this._fritzId; 32 | this._fritzPort.postMessage({ 33 | type: TRIGGER, 34 | event: ev, 35 | id: id 36 | }); 37 | } 38 | 39 | setState(state: Record | ((state: S, props: P) => S)) { 40 | let s = this.state; 41 | Object.assign(s as S, isFunction(state) ? state(s, this.props) : state); 42 | enqueueRender(this, undefined, false); 43 | } 44 | 45 | shouldComponentUpdate() { return true; } 46 | componentWillUnmount(){} 47 | getSnapshotBeforeUpdate(){ return null; } 48 | componentDidUpdate(){} 49 | 50 | abstract render(props: P, state: S): ComponentChild; 51 | } 52 | 53 | export interface ComponentConstructor

{ 54 | props?: PropDefinitions; 55 | events?: Array; 56 | styles?: string | Array; 57 | new (...params: any[]): Component; 58 | } 59 | 60 | export default Component; 61 | -------------------------------------------------------------------------------- /core/src/worker/env.ts: -------------------------------------------------------------------------------- 1 | const noop = Function.prototype; 2 | 3 | // @ts-ignore 4 | export const isWorker = typeof WorkerGlobalScope !== 'undefined'; -------------------------------------------------------------------------------- /core/src/worker/handle.ts: -------------------------------------------------------------------------------- 1 | class Store { 2 | public handleMap: WeakMap; 3 | public idMap: Map; 4 | public id: number; 5 | 6 | constructor() { 7 | this.handleMap = new WeakMap(); 8 | this.idMap = new Map(); 9 | this.id = 0; 10 | } 11 | 12 | from(fn: Function) { 13 | let handle; 14 | let id = this.handleMap.get(fn); 15 | if(id == null) { 16 | id = this.id++; 17 | handle = new Handle(id, fn); 18 | this.handleMap.set(fn, id); 19 | this.idMap.set(id, handle); 20 | } else { 21 | handle = this.idMap.get(id); 22 | } 23 | return handle as Handle; 24 | } 25 | 26 | get(id: number) { 27 | return this.idMap.get(id); 28 | } 29 | } 30 | 31 | class Handle { 32 | public static _store: Store; 33 | public id: number; 34 | public fn: Function; 35 | public inUse: boolean; 36 | static get store() { 37 | if(!this._store) { 38 | this._store = new Store(); 39 | } 40 | return this._store; 41 | } 42 | 43 | static from(fn: Function) { 44 | return this.store.from(fn); 45 | } 46 | 47 | static get(id: number) { 48 | return this.store.get(id); 49 | } 50 | 51 | constructor(id: number, fn: Function) { 52 | this.id = id; 53 | this.fn = fn; 54 | this.inUse = true; 55 | } 56 | 57 | del() { 58 | let store = Handle.store; 59 | store.handleMap.delete(this.fn); 60 | store.idMap.delete(this.id); 61 | } 62 | } 63 | 64 | export default Handle; 65 | -------------------------------------------------------------------------------- /core/src/worker/hyperscript.ts: -------------------------------------------------------------------------------- 1 | import type { default as Component } from './component'; 2 | import type { Tree } from './tree'; 3 | import { JSXInternal } from './jsx'; 4 | 5 | import { isFunction } from '../util.js'; 6 | import signal from './signal.js'; 7 | import { createTree, isTree } from './tree.js'; 8 | 9 | type Attrs = Record | any[] | null; 10 | type FunctionComponent = (a: Attrs, children?: any) => Tree; 11 | type Children = Array; 12 | 13 | function Fragment(_attrs: Attrs, children: Children) { 14 | let child; 15 | let tree = createTree(); 16 | for(let i = 0; i < children.length; i++) { 17 | child = children[i]; 18 | tree.push.apply(tree, child); 19 | } 20 | return tree; 21 | } 22 | 23 | function h(tag: string | typeof Component | FunctionComponent, attrs: Attrs, children?: Children): Tree { 24 | let argsLen = arguments.length; 25 | let childrenType = typeof children; 26 | if(argsLen === 2) { 27 | if(typeof attrs !== 'object' || Array.isArray(attrs)) { 28 | children = attrs; 29 | attrs = null; 30 | } 31 | } else if(argsLen > 3 || isTree(children) || isPrimitive(childrenType)) { 32 | children = Array.prototype.slice.call(arguments, 2); 33 | } 34 | 35 | let isFn = isFunction(tag); 36 | 37 | if(isFn) { 38 | let localName = (tag as typeof Component).prototype.localName; 39 | if(localName) { 40 | return h(localName, attrs, children); 41 | } 42 | 43 | return (tag as FunctionComponent)(attrs || {}, children); 44 | } 45 | 46 | let tree = createTree(); 47 | let uniq: any, evs: Array> | undefined; 48 | if(attrs) { 49 | attrs = Object.keys(attrs).reduce(function(acc, key){ 50 | let value = (attrs as any)[key]; 51 | 52 | let eventInfo = signal(tag as any, key, value, attrs as any); 53 | if(eventInfo) { 54 | if(!evs) evs = []; 55 | evs.push(eventInfo); 56 | } else if(key === 'key') { 57 | uniq = value; 58 | } else { 59 | // @ts-ignore 60 | acc.push(key); 61 | // @ts-ignore 62 | acc.push(value); 63 | } 64 | 65 | return acc; 66 | }, []); 67 | } 68 | 69 | let open = [1, tag, uniq]; 70 | if(attrs) { 71 | open.push(attrs); 72 | } 73 | if(evs) { 74 | open.push(evs); 75 | } 76 | tree.push(open); 77 | 78 | if(children) { 79 | children.forEach(function(child: any){ 80 | if(typeof child !== 'undefined' && !Array.isArray(child)) { 81 | tree.push([4, child + '']); 82 | return; 83 | } 84 | 85 | while(child && child.length) { 86 | tree.push(child.shift()); 87 | } 88 | }); 89 | } 90 | 91 | tree.push([2, tag]); 92 | 93 | return tree; 94 | }; 95 | 96 | h.frag = Fragment; 97 | 98 | declare namespace h { 99 | export import JSX = JSXInternal; 100 | } 101 | 102 | function isPrimitive(type: string) { 103 | return type === 'string' || type === 'number' || type === 'boolean'; 104 | } 105 | 106 | export { 107 | Fragment, 108 | h as default 109 | }; -------------------------------------------------------------------------------- /core/src/worker/index.ts: -------------------------------------------------------------------------------- 1 | import type { DefineMessage } from '../message-types'; 2 | import type { CustomElementTagName, WorkerFritz } from '../types'; 3 | import type { ComponentConstructor } from './component'; 4 | 5 | import Component from './component.js'; 6 | import h, { Fragment } from './hyperscript.js'; 7 | import relay from './relay.js'; 8 | import { DEFINE } from '../message-types.js'; 9 | import '../types'; // This is needed to get the types to be built. 10 | 11 | const fritz = Object.create(null) as WorkerFritz; 12 | fritz.Component = Component; 13 | fritz._define = define; 14 | fritz.define = define.bind(fritz) 15 | fritz.h = h; 16 | fritz._tags = new Map(); 17 | fritz._instances = new Map(); 18 | fritz._port = globalThis as any; 19 | fritz._listening = false; 20 | fritz.fritz = fritz; 21 | 22 | function define(this: WorkerFritz, tag: CustomElementTagName, constructor: ComponentConstructor) { 23 | if(constructor === undefined) { 24 | throw new Error('fritz.define expects 2 arguments'); 25 | } 26 | if(constructor.prototype === undefined || 27 | constructor.prototype.render === undefined) { 28 | let render = constructor; 29 | // @ts-ignore 30 | constructor = class extends Component{}; 31 | // @ts-ignore 32 | constructor.prototype.render = render; 33 | } 34 | 35 | this._tags.set(tag, constructor); 36 | 37 | Object.defineProperties(constructor.prototype, { 38 | _fritzPort: { 39 | value: this._port 40 | }, 41 | localName: { 42 | enumerable: false, 43 | value: tag 44 | } 45 | }); 46 | 47 | relay(this); 48 | 49 | let styles = undefined; 50 | if(constructor.styles) { 51 | styles = []; 52 | if(typeof constructor.styles === 'string') { 53 | styles.push({ text: constructor.styles }); 54 | } else { 55 | for(let def of constructor.styles) { 56 | if(typeof def === 'string') { 57 | styles.push({ text: def }); 58 | } else { 59 | styles.push(def); 60 | } 61 | } 62 | } 63 | } 64 | 65 | const msg: DefineMessage = { 66 | type: DEFINE, 67 | tag: tag, 68 | props: constructor.props, 69 | events: constructor.events, 70 | styles, 71 | features: { 72 | mount: !!constructor.prototype.componentDidMount 73 | } 74 | } 75 | 76 | this._port.postMessage?.(msg); 77 | } 78 | 79 | let state: any; 80 | Object.defineProperty(fritz, 'state', { 81 | set: function(val) { state = val; }, 82 | get: function() { return state; } 83 | }); 84 | 85 | const adopt = (selector: string) => ({ selector }); 86 | 87 | export const css = String.raw; 88 | export default fritz; 89 | export { Component, h, Fragment, adopt, state }; -------------------------------------------------------------------------------- /core/src/worker/instance.ts: -------------------------------------------------------------------------------- 1 | import type { default as Component } from './component'; 2 | import type { WorkerRenderMessage } from '../message-types'; 3 | import type { Tree } from './tree'; 4 | 5 | import { RENDER } from '../message-types.js'; 6 | import { defer } from '../util.js'; 7 | 8 | export let currentInstance: Component | null = null; 9 | 10 | export function renderInstance(instance: Component) { 11 | currentInstance = instance; 12 | let tree = instance.render(instance.props, instance.state); 13 | currentInstance = null; 14 | return tree; 15 | }; 16 | 17 | let queue: Array<[Component, Record | undefined, boolean]> = []; 18 | 19 | export function enqueueRender(instance: Component, sentProps: Record | undefined, isNew: boolean) { 20 | if(!instance._dirty && (instance._dirty = true) && queue.push([instance, sentProps, isNew])==1) { 21 | defer(rerender); 22 | } 23 | } 24 | 25 | function rerender() { 26 | let p, list = queue; 27 | queue = []; 28 | while ( (p = list.pop()) ) { 29 | if (p[0]._dirty) render(p[0], p[1], p[2]); 30 | } 31 | } 32 | 33 | function render(instance: Component, sentProps: Record | undefined, isNew: boolean) { 34 | let prevProps = instance.props; 35 | let prevState = instance.state; 36 | if(sentProps) { 37 | var nextProps = Object.assign({}, instance.props, sentProps); 38 | instance.props = nextProps; 39 | } 40 | 41 | if(isNew) { 42 | callRender(instance); 43 | } else if(instance.shouldComponentUpdate(nextProps) !== false) { 44 | let snapshot = instance.getSnapshotBeforeUpdate(prevProps, prevState); 45 | callRender(instance); 46 | instance.componentDidUpdate(prevProps, prevState, snapshot); 47 | } 48 | } 49 | 50 | function callRender(instance: Component) { 51 | instance._dirty = false; 52 | const msg: WorkerRenderMessage = { 53 | type: RENDER, 54 | id: instance._fritzId, 55 | tree: renderInstance(instance) as Tree 56 | }; 57 | 58 | instance._fritzPort.postMessage(msg); 59 | } -------------------------------------------------------------------------------- /core/src/worker/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerFritz } from '../types'; 2 | import type { 3 | WindowRenderMessage, 4 | CleanupMessage, 5 | DestroyMessage, 6 | EventMessage, 7 | RenderedMessage 8 | } from '../message-types'; 9 | 10 | import { getInstance, setInstance, delInstance } from '../util.js'; 11 | import Handle from './handle.js'; 12 | import { enqueueRender } from './instance.js'; 13 | 14 | export function render(fritz: WorkerFritz, msg: WindowRenderMessage) { 15 | let id = msg.id; 16 | let props = msg.props || {}; 17 | 18 | let instance = getInstance(fritz, id); 19 | let isNew = !instance; 20 | if(isNew) { 21 | let constructor = fritz._tags.get(msg.tag)!; 22 | instance = new constructor(); 23 | Object.defineProperties(instance, { 24 | _fritzId: { 25 | enumerable: false, 26 | value: id 27 | }, 28 | _fritzHandles: { 29 | enumerable: false, 30 | writable: true, 31 | value: new Map() 32 | } 33 | }); 34 | setInstance(fritz, id, instance); 35 | } 36 | 37 | enqueueRender(instance as any, props, isNew); 38 | }; 39 | 40 | export function trigger(fritz: WorkerFritz, msg: EventMessage) { 41 | let inst = getInstance(fritz, msg.id)!; 42 | 43 | let method; 44 | if(msg.handle != null) { 45 | method = Handle.get(msg.handle)!.fn; 46 | } else { 47 | let name = msg.event.type; 48 | let methodName = 'on' + name[0].toUpperCase() + name.substr(1); 49 | method = (inst as any)[methodName]; 50 | } 51 | 52 | if(method) { 53 | let event = msg.event; 54 | method.call(inst, event); 55 | 56 | enqueueRender(inst, undefined, false); 57 | } else { 58 | // TODO warn? 59 | } 60 | }; 61 | 62 | export function destroy(fritz: WorkerFritz, msg: DestroyMessage) { 63 | let instance = getInstance(fritz, msg.id); 64 | if(instance) { 65 | instance.componentWillUnmount(); 66 | 67 | let handles = instance._fritzHandles; 68 | handles.forEach(function(handle){ 69 | handle.del(); 70 | }); 71 | handles.clear(); 72 | 73 | delInstance(fritz, msg.id); 74 | } 75 | }; 76 | 77 | export function rendered(fritz: WorkerFritz, msg: RenderedMessage) { 78 | let instance = getInstance(fritz, msg.id)!; 79 | instance.componentDidMount(); 80 | }; 81 | 82 | export function cleanup(fritz: WorkerFritz, msg: CleanupMessage) { 83 | let instance = getInstance(fritz, msg.id)!; 84 | let handles = instance._fritzHandles; 85 | msg.handles.forEach(function(id){ 86 | // A handle might have been previously deleted by a destroy 87 | // and then the same element re-connected. Those old handles 88 | // can be ignored. 89 | if(handles.has(id)) { 90 | let handle = handles.get(id)!; 91 | handle.del(); 92 | handles.delete(id); 93 | } 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /core/src/worker/ns.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace fritz; 2 | 3 | import { JSXInternal } from './jsx'; 4 | export * from './index'; 5 | export { default } from './index'; 6 | 7 | export import JSX = JSXInternal; -------------------------------------------------------------------------------- /core/src/worker/relay.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerFritz } from '../types'; 2 | import type { MessageSentFromWindow } from '../message-types'; 3 | 4 | import { render, trigger, destroy, rendered, cleanup } from './lifecycle.js'; 5 | import { RENDER, EVENT, STATE, DESTROY, RENDERED, CLEANUP } from '../message-types.js'; 6 | 7 | export default function relay(fritz: WorkerFritz) { 8 | if(!fritz._listening) { 9 | fritz._listening = true; 10 | 11 | fritz._port.addEventListener?.('message', function(ev: MessageEvent){ 12 | let msg = ev.data; 13 | switch(msg.type) { 14 | case RENDER: 15 | render(fritz, msg); 16 | break; 17 | case EVENT: 18 | trigger(fritz, msg); 19 | break; 20 | case STATE: 21 | fritz.state = msg.state; 22 | break; 23 | case DESTROY: 24 | destroy(fritz, msg); 25 | break; 26 | case RENDERED: 27 | rendered(fritz, msg); 28 | break; 29 | case CLEANUP: 30 | cleanup(fritz, msg); 31 | break; 32 | } 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /core/src/worker/signal.ts: -------------------------------------------------------------------------------- 1 | import { currentInstance } from './instance.js'; 2 | import { isWorker } from './env.js'; 3 | import Handle from './handle.js'; 4 | 5 | const eventAttrExp = /^on[A-Z]/; 6 | 7 | function signal(_tagName: string, attrName: string, attrValue: any, attrs: Record) { 8 | if(eventAttrExp.test(attrName)) { 9 | if(!isWorker) { 10 | return []; 11 | } 12 | 13 | let eventName = attrName.toLowerCase(); 14 | let handle = Handle.from(attrValue); 15 | handle.inUse = true; 16 | currentInstance!._fritzHandles.set(handle.id, handle); 17 | return [1, eventName, handle.id]; 18 | } 19 | } 20 | 21 | export default signal; 22 | -------------------------------------------------------------------------------- /core/src/worker/tree.ts: -------------------------------------------------------------------------------- 1 | const _tree = Symbol('ftree'); 2 | 3 | export function isTree(obj: any): obj is Tree { 4 | return !!(obj && obj[_tree]); 5 | }; 6 | 7 | export function createTree(): Tree { 8 | let out: any = []; 9 | out[_tree] = true; 10 | return out; 11 | } 12 | 13 | export type Tree = Array & { 14 | [_tree]: boolean; 15 | } -------------------------------------------------------------------------------- /core/test/browser/adopt-sheet.js: -------------------------------------------------------------------------------- 1 | import fritzWindow from "../../window.mjs"; 2 | import { Component, h, adopt } from "../../worker.mjs"; 3 | import { waitForRender, hooks } from "./helpers.js"; 4 | 5 | QUnit.module('Adoptable sheets', hooks); 6 | 7 | QUnit.test('can use a

Hello world
`); 16 | }); -------------------------------------------------------------------------------- /core/test/typescript/core/basics.tsx: -------------------------------------------------------------------------------- 1 | import fritz, { Component, h } from '../../../'; 2 | 3 | // https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4Aoc4AOxiSk3STgGFdJqlaAeABTgC8cAN4BfAHwjycGXDTsInWgHVgAGzXI0SYADckvHGADOACjBHjALji8AlDd0RgAEwqy4xgBYQArmpc2cEUuGABVMBcUOnNLG3sbACMICDVUandZeWClGAARVwBZP1pTBzgnVwpRchRE4xgodHg0NRRjY1YFXL5JYWksxQaoXwxoMpEajzrh5rgiahd6WIgTePKUagBPasoYLbBmQ1XOoX6PahQQJBthmgBzaopyVvbOwq2AFS8HuCQADzoi06QQ4oT4lj6AxkCyWUBWJjsUg8HiIMF8UGocG4Lj04joDQe3AA9LjdOJMjIaqIgA 4 | // I this this is a bug: https://github.com/microsoft/TypeScript/issues/23911ain 5 | 6 | type Props = { 7 | name: string; 8 | } 9 | 10 | class MyThing extends Component { 11 | render(props: Props) { 12 | props.name; 13 | return ( 14 |
testing
15 | ); 16 | } 17 | } 18 | 19 | fritz.define('some-thing', MyThing); -------------------------------------------------------------------------------- /core/test/typescript/core/explicit-fragment.tsx: -------------------------------------------------------------------------------- 1 | import fritz, { Component, h, Fragment } from '../../../'; 2 | 3 | type Props = { 4 | name: string; 5 | } 6 | 7 | class MyThing extends Component { 8 | render(props: Props) { 9 | props.name; 10 | return ( 11 | 12 |
Fragment content
13 |
14 | ); 15 | } 16 | } 17 | 18 | fritz.define('some-thing', MyThing); -------------------------------------------------------------------------------- /core/test/typescript/core/implicit-fragment.tsx: -------------------------------------------------------------------------------- 1 | import fritz, { Component, h, Fragment } from '../../../'; 2 | 3 | type Props = { 4 | name: string; 5 | } 6 | 7 | class MyThing extends Component { 8 | render(props: Props) { 9 | props.name; 10 | return ( 11 | <> 12 |
Fragment content
13 | 14 | ); 15 | } 16 | } 17 | 18 | fritz.define('some-thing', MyThing); -------------------------------------------------------------------------------- /core/test/typescript/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "jsxFactory": "h", 5 | "jsxFragmentFactory": "Fragment" 6 | }, 7 | "extends": "../../../tsconfig.json", 8 | "include": ["."] 9 | } -------------------------------------------------------------------------------- /core/test/typescript/jsx-runtime/basics.tsx: -------------------------------------------------------------------------------- 1 | import fritz, { Component } from '../../../'; 2 | 3 | // https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4Aoc4AOxiSk3STgGFdJqlaAeABTgC8cAN4BfAHwjycGXDTsInWgHVgAGzXI0SYADckvHGADOACjBHjALji8AlDd0RgAEwqy4xgBYQArmpc2cEUuGABVMBcUOnNLG3sbACMICDVUandZeWClGAARVwBZP1pTBzgnVwpRchRE4xgodHg0NRRjY1YFXL5JYWksxQaoXwxoMpEajzrh5rgiahd6WIgTePKUagBPasoYLbBmQ1XOoX6PahQQJBthmgBzaopyVvbOwq2AFS8HuCQADzoi06QQ4oT4lj6AxkCyWUBWJjsUg8HiIMF8UGocG4Lj04joDQe3AA9LjdOJMjIaqIgA 4 | // I this this is a bug: https://github.com/microsoft/TypeScript/issues/23911ain 5 | 6 | type Props = { 7 | name: string; 8 | } 9 | 10 | class MyThing extends Component { 11 | render(props: Props) { 12 | return ( 13 |
testing
14 | ); 15 | } 16 | } 17 | 18 | fritz.define('some-thing', MyThing); -------------------------------------------------------------------------------- /core/test/typescript/jsx-runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "jsxImportSource": "fritz" 5 | }, 6 | "extends": "../../../tsconfig.json", 7 | "include": ["."] 8 | } -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "target": "ESNext", 6 | "strict": true, 7 | "declaration": true, 8 | "noEmit": true, 9 | "skipLibCheck": true 10 | }, 11 | "files": ["src/window/mods.d.ts"], 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fritz-repo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "A library for rendering custom elements in a web worker.", 6 | "scripts": { 7 | "build": "wireit", 8 | "build:site": "wireit", 9 | "check": "wireit", 10 | "changeset": "changeset" 11 | }, 12 | "wireit": { 13 | "build": { 14 | "dependencies": [ 15 | "./astro-fritz:build", 16 | "./core:build" 17 | ] 18 | }, 19 | "check": { 20 | "dependencies": [ 21 | "./astro-fritz:check", 22 | "./core:check" 23 | ] 24 | }, 25 | "build:site": { 26 | "dependencies": [ 27 | "./site:build" 28 | ] 29 | } 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/matthewp/fritz.git" 34 | }, 35 | "keywords": [], 36 | "author": "", 37 | "license": "Unlicensed", 38 | "bugs": { 39 | "url": "https://github.com/matthewp/fritz/issues" 40 | }, 41 | "homepage": "https://github.com/matthewp/fritz#readme", 42 | "workspaces": [ 43 | "core", 44 | "astro-fritz", 45 | "site" 46 | ], 47 | "devDependencies": { 48 | "@changesets/cli": "^2.26.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .astro/ -------------------------------------------------------------------------------- /site/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fritz-site 2 | 3 | ## 1.0.1-alpha.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [d1aa720] 8 | - astro-fritz@2.0.0-alpha.0 9 | -------------------------------------------------------------------------------- /site/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import fritz from 'astro-fritz'; 3 | import rehypeHighlight from 'rehype-highlight'; 4 | 5 | export default defineConfig({ 6 | integrations: [ 7 | fritz() 8 | ], 9 | markdown: { 10 | syntaxHighlight: false, 11 | rehypePlugins: [rehypeHighlight] 12 | } 13 | }); -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fritz-site", 3 | "version": "1.0.1-alpha.0", 4 | "description": "Fritz site and documentation", 5 | "main": "dist/index.html", 6 | "private": true, 7 | "scripts": { 8 | "dev": "wireit", 9 | "build": "wireit", 10 | "preview": "wireit", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "wireit": { 14 | "dev": { 15 | "command": "astro dev", 16 | "dependencies": [ 17 | "../astro-fritz:build", 18 | "../core:build" 19 | ], 20 | "service": true, 21 | "readyWhen": { 22 | "lineMatches": "started in" 23 | } 24 | }, 25 | "build": { 26 | "command": "astro build", 27 | "dependencies": [ 28 | "../astro-fritz:build", 29 | "../core:build" 30 | ], 31 | "files": [ 32 | "astro.config.mjs", 33 | "src/**/*" 34 | ], 35 | "output": [ 36 | "dist/**/*" 37 | ] 38 | }, 39 | "preview": { 40 | "command": "astro preview", 41 | "dependencies": [ 42 | "build" 43 | ], 44 | "service": true, 45 | "readyWhen": { 46 | "lineMatches": "started in" 47 | } 48 | } 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/matthewp/fritz-template.git" 53 | }, 54 | "keywords": [], 55 | "author": "", 56 | "license": "BSD-2-Clause", 57 | "bugs": { 58 | "url": "https://github.com/matthewp/fritz-template/issues" 59 | }, 60 | "homepage": "https://github.com/matthewp/fritz-template#readme", 61 | "devDependencies": { 62 | "astro": "^2.0.7", 63 | "astro-fritz": "^2.0.0-alpha.0", 64 | "fritz": "^5.0.0-alpha.6", 65 | "highlight.js": "^9.15.8", 66 | "lowlight": "^1.12.1", 67 | "postcss-preset-env": "^8.0.1", 68 | "rehype-highlight": "^6.0.0", 69 | "rollup": "^1.16.6" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /site/postcss.config.js: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | postcssPresetEnv(/* pluginOptions */) 6 | ] 7 | }; -------------------------------------------------------------------------------- /site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/public/favicon.ico -------------------------------------------------------------------------------- /site/public/images/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/public/images/logo-144.png -------------------------------------------------------------------------------- /site/public/images/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/public/images/logo-192.png -------------------------------------------------------------------------------- /site/public/images/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/public/images/logo-512.png -------------------------------------------------------------------------------- /site/public/images/logo-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/public/images/logo-96.png -------------------------------------------------------------------------------- /site/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Fritz", 3 | "name": "Fritz", 4 | "icons": [ 5 | { 6 | "src": "images/logo-96.png", 7 | "sizes": "96x96", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "images/logo-144.png", 12 | "sizes": "144x144", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "images/logo-192.png", 17 | "sizes": "192x192", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "images/logo-512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/", 27 | "display": "standalone", 28 | "orientation": "portrait", 29 | "theme_color": "#58A4B0", 30 | "background_color": "#BAC1B8" 31 | } 32 | -------------------------------------------------------------------------------- /site/public/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // DO NOT EDIT THIS GENERATED OUTPUT DIRECTLY! 18 | // This file should be overwritten as part of your build process. 19 | // If you need to extend the behavior of the generated service worker, the best approach is to write 20 | // additional code and include it using the importScripts option: 21 | // https://github.com/GoogleChrome/sw-precache#importscripts-arraystring 22 | // 23 | // Alternatively, it's possible to make changes to the underlying template file and then use that as the 24 | // new base for generating output, via the templateFilePath option: 25 | // https://github.com/GoogleChrome/sw-precache#templatefilepath-string 26 | // 27 | // If you go that route, make sure that whenever you update your sw-precache dependency, you reconcile any 28 | // changes made to this original template file with your modified copy. 29 | 30 | // This generated service worker JavaScript will precache your site's resources. 31 | // The code needs to be saved in a .js file at the top-level of your site, and registered 32 | // from your pages in order to be used. See 33 | // https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js 34 | // for an example of how you can register this script and handle various service worker events. 35 | 36 | /* eslint-env worker, serviceworker */ 37 | /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ 38 | 'use strict'; 39 | 40 | var precacheConfig = [["app.js","db111f5e8f770efffa1214c473b34549"],["frankenstein-fritz-flame.png","d382c041fa5e0564cf780e5ec57dfb7f"],["frankenstein-fritz-flame.webp","64a0d9e360f13b66a5b9c7fe82802cd1"],["index.html","8ea58980dcff6895e0c91d5e385b2d29"],["main.js","728721710b63f80eadafc6f027fdf6fd"],["service-worker-registration.js","854cb02fab00d5e4b382a00771ba8774"]]; 41 | var cacheName = 'sw-precache-v3-sw-precache-' + (self.registration ? self.registration.scope : ''); 42 | 43 | 44 | var ignoreUrlParametersMatching = [/^utm_/]; 45 | 46 | 47 | 48 | var addDirectoryIndex = function(originalUrl, index) { 49 | var url = new URL(originalUrl); 50 | if (url.pathname.slice(-1) === '/') { 51 | url.pathname += index; 52 | } 53 | return url.toString(); 54 | }; 55 | 56 | var cleanResponse = function(originalResponse) { 57 | // If this is not a redirected response, then we don't have to do anything. 58 | if (!originalResponse.redirected) { 59 | return Promise.resolve(originalResponse); 60 | } 61 | 62 | // Firefox 50 and below doesn't support the Response.body stream, so we may 63 | // need to read the entire body to memory as a Blob. 64 | var bodyPromise = 'body' in originalResponse ? 65 | Promise.resolve(originalResponse.body) : 66 | originalResponse.blob(); 67 | 68 | return bodyPromise.then(function(body) { 69 | // new Response() is happy when passed either a stream or a Blob. 70 | return new Response(body, { 71 | headers: originalResponse.headers, 72 | status: originalResponse.status, 73 | statusText: originalResponse.statusText 74 | }); 75 | }); 76 | }; 77 | 78 | var createCacheKey = function(originalUrl, paramName, paramValue, 79 | dontCacheBustUrlsMatching) { 80 | // Create a new URL object to avoid modifying originalUrl. 81 | var url = new URL(originalUrl); 82 | 83 | // If dontCacheBustUrlsMatching is not set, or if we don't have a match, 84 | // then add in the extra cache-busting URL parameter. 85 | if (!dontCacheBustUrlsMatching || 86 | !(url.pathname.match(dontCacheBustUrlsMatching))) { 87 | url.search += (url.search ? '&' : '') + 88 | encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); 89 | } 90 | 91 | return url.toString(); 92 | }; 93 | 94 | var isPathWhitelisted = function(whitelist, absoluteUrlString) { 95 | // If the whitelist is empty, then consider all URLs to be whitelisted. 96 | if (whitelist.length === 0) { 97 | return true; 98 | } 99 | 100 | // Otherwise compare each path regex to the path of the URL passed in. 101 | var path = (new URL(absoluteUrlString)).pathname; 102 | return whitelist.some(function(whitelistedPathRegex) { 103 | return path.match(whitelistedPathRegex); 104 | }); 105 | }; 106 | 107 | var stripIgnoredUrlParameters = function(originalUrl, 108 | ignoreUrlParametersMatching) { 109 | var url = new URL(originalUrl); 110 | // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290 111 | url.hash = ''; 112 | 113 | url.search = url.search.slice(1) // Exclude initial '?' 114 | .split('&') // Split into an array of 'key=value' strings 115 | .map(function(kv) { 116 | return kv.split('='); // Split each 'key=value' string into a [key, value] array 117 | }) 118 | .filter(function(kv) { 119 | return ignoreUrlParametersMatching.every(function(ignoredRegex) { 120 | return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. 121 | }); 122 | }) 123 | .map(function(kv) { 124 | return kv.join('='); // Join each [key, value] array into a 'key=value' string 125 | }) 126 | .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each 127 | 128 | return url.toString(); 129 | }; 130 | 131 | 132 | var hashParamName = '_sw-precache'; 133 | var urlsToCacheKeys = new Map( 134 | precacheConfig.map(function(item) { 135 | var relativeUrl = item[0]; 136 | var hash = item[1]; 137 | var absoluteUrl = new URL(relativeUrl, self.location); 138 | var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false); 139 | return [absoluteUrl.toString(), cacheKey]; 140 | }) 141 | ); 142 | 143 | function setOfCachedUrls(cache) { 144 | return cache.keys().then(function(requests) { 145 | return requests.map(function(request) { 146 | return request.url; 147 | }); 148 | }).then(function(urls) { 149 | return new Set(urls); 150 | }); 151 | } 152 | 153 | self.addEventListener('install', function(event) { 154 | event.waitUntil( 155 | caches.open(cacheName).then(function(cache) { 156 | return setOfCachedUrls(cache).then(function(cachedUrls) { 157 | return Promise.all( 158 | Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { 159 | // If we don't have a key matching url in the cache already, add it. 160 | if (!cachedUrls.has(cacheKey)) { 161 | var request = new Request(cacheKey, {credentials: 'same-origin'}); 162 | return fetch(request).then(function(response) { 163 | // Bail out of installation unless we get back a 200 OK for 164 | // every request. 165 | if (!response.ok) { 166 | throw new Error('Request for ' + cacheKey + ' returned a ' + 167 | 'response with status ' + response.status); 168 | } 169 | 170 | return cleanResponse(response).then(function(responseToCache) { 171 | return cache.put(cacheKey, responseToCache); 172 | }); 173 | }); 174 | } 175 | }) 176 | ); 177 | }); 178 | }).then(function() { 179 | 180 | // Force the SW to transition from installing -> active state 181 | return self.skipWaiting(); 182 | 183 | }) 184 | ); 185 | }); 186 | 187 | self.addEventListener('activate', function(event) { 188 | var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); 189 | 190 | event.waitUntil( 191 | caches.open(cacheName).then(function(cache) { 192 | return cache.keys().then(function(existingRequests) { 193 | return Promise.all( 194 | existingRequests.map(function(existingRequest) { 195 | if (!setOfExpectedUrls.has(existingRequest.url)) { 196 | return cache.delete(existingRequest); 197 | } 198 | }) 199 | ); 200 | }); 201 | }).then(function() { 202 | 203 | return self.clients.claim(); 204 | 205 | }) 206 | ); 207 | }); 208 | 209 | 210 | self.addEventListener('fetch', function(event) { 211 | if (event.request.method === 'GET') { 212 | // Should we call event.respondWith() inside this fetch event handler? 213 | // This needs to be determined synchronously, which will give other fetch 214 | // handlers a chance to handle the request if need be. 215 | var shouldRespond; 216 | 217 | // First, remove all the ignored parameters and hash fragment, and see if we 218 | // have that URL in our cache. If so, great! shouldRespond will be true. 219 | var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); 220 | shouldRespond = urlsToCacheKeys.has(url); 221 | 222 | // If shouldRespond is false, check again, this time with 'index.html' 223 | // (or whatever the directoryIndex option is set to) at the end. 224 | var directoryIndex = 'index.html'; 225 | if (!shouldRespond && directoryIndex) { 226 | url = addDirectoryIndex(url, directoryIndex); 227 | shouldRespond = urlsToCacheKeys.has(url); 228 | } 229 | 230 | // If shouldRespond is still false, check to see if this is a navigation 231 | // request, and if so, whether the URL matches navigateFallbackWhitelist. 232 | var navigateFallback = ''; 233 | if (!shouldRespond && 234 | navigateFallback && 235 | (event.request.mode === 'navigate') && 236 | isPathWhitelisted([], event.request.url)) { 237 | url = new URL(navigateFallback, self.location).toString(); 238 | shouldRespond = urlsToCacheKeys.has(url); 239 | } 240 | 241 | // If shouldRespond was set to true at any point, then call 242 | // event.respondWith(), using the appropriate cache key. 243 | if (shouldRespond) { 244 | event.respondWith( 245 | caches.open(cacheName).then(function(cache) { 246 | return cache.match(urlsToCacheKeys.get(url)).then(function(response) { 247 | if (response) { 248 | return response; 249 | } 250 | throw Error('The cached response that was expected is missing.'); 251 | }); 252 | }).catch(function(e) { 253 | // Fall back to just fetch()ing the request if some unexpected error 254 | // prevented the cached response from being valid. 255 | console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); 256 | return fetch(event.request); 257 | }) 258 | ); 259 | } 260 | } 261 | }); 262 | 263 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /site/public/sw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.2.0/workbox-sw.js"); 15 | 16 | self.addEventListener('message', (event) => { 17 | if (event.data && event.data.type === 'SKIP_WAITING') { 18 | self.skipWaiting(); 19 | } 20 | }); 21 | 22 | /** 23 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 24 | * requests for URLs in the manifest. 25 | * See https://goo.gl/S9QRab 26 | */ 27 | self.__precacheManifest = [ 28 | { 29 | "url": "app.js", 30 | "revision": "2983a4b1bf8763e5c3018d73bd9385f4" 31 | }, 32 | { 33 | "url": "favicon.ico", 34 | "revision": "7522e123750db337a8fbb15ecb24bb77" 35 | }, 36 | { 37 | "url": "frankenstein-fritz-flame.png", 38 | "revision": "d382c041fa5e0564cf780e5ec57dfb7f" 39 | }, 40 | { 41 | "url": "frankenstein-fritz-flame.webp", 42 | "revision": "64a0d9e360f13b66a5b9c7fe82802cd1" 43 | }, 44 | { 45 | "url": "index.html", 46 | "revision": "432275db1d0c0afa19380fb2aa5596ca" 47 | }, 48 | { 49 | "url": "main.js", 50 | "revision": "2c45122305f0c7391f9cad97dfafeabf" 51 | }, 52 | { 53 | "url": "manifest.json", 54 | "revision": "4c5a863777c53de7143a363767de0ad8" 55 | }, 56 | { 57 | "url": "service-worker.js", 58 | "revision": "1a74471f1f9522c25f49eb818b44853c" 59 | }, 60 | { 61 | "url": "images/logo-144.png", 62 | "revision": "df8c7d604b90c1e2e4fd5dfa4fc1239d" 63 | }, 64 | { 65 | "url": "images/logo-192.png", 66 | "revision": "db6fcdf854262a09bf49066cfd28db6f" 67 | }, 68 | { 69 | "url": "images/logo-512.png", 70 | "revision": "15fda8d80d083e8c46c0226f7bffafcf" 71 | }, 72 | { 73 | "url": "images/logo-96.png", 74 | "revision": "14e97738e405176ea97e08fa5d4bbd91" 75 | } 76 | ].concat(self.__precacheManifest || []); 77 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 78 | -------------------------------------------------------------------------------- /site/src/components/About.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import CodeFile from "./CodeFile.astro"; 3 | import CodeSnippet from "./CodeSnippet.astro"; 4 | const { class: className } = Astro.props; 5 | 6 | const npmInstall = ` 7 | npm install fritz@next --save 8 | `; 9 | 10 | const yarnAdd = ` 11 | yarn add fritz@next 12 | `; 13 | --- 14 | 53 |
54 |

What is Fritz?

55 |

Fritz is a UI library that allows you to define components that run inside of a Web Worker. By running your application logic inside of a Worker, you can ensure that the main thread and scrolling are never blocked by expensive work you are doing. Fritz makes jank-free apps possible.

56 | 57 |

Fritz plays nicely with frameworks. Since it is built on web components you can use Fritz just by adding a tag. Use Fritz within your React, Vue.js, Angular, or any other framework. If you have an expensive component that operates on a large dataset, this is a good candidate to turn into a Fritz component. Although you can create your entire app using Fritz (this page is), you don't have to.

58 | 59 |

If you've heard of React's new version, Fiber, Fritz is in some ways an alternative. Fiber enables React to smartly schedule updates. Fritz allows for parallel updates. You're app can launch as many workers as you want and Fritz will use them all. The main thread only ever needs to apply changes. Due to this design, Fritz's scheduler is dead simple; it only needs to ensure that it applies only 16ms of work per frame. It can completely ignore the cost of user-code; that's free with Fritz.

60 | 61 |

Getting Started

62 |

Installation

63 | 64 |

Install Fritz with npm:

65 | 66 | 67 |

Or with Yarn:

68 | 69 | 70 |

Using Fritz

71 |

Fritz lets you define web components inside of a Web Worker. So, the first step to using Fritz is to create a Worker. Use new Worker to do so:

72 | 73 | 74 | 75 |

And then define a component inside of that worker. We'll assume you know how to configure your bundler tool and skip that part. But we should point out that you want to change your Babel config so that it renders JSX to Fritz h() calls.

76 | 77 | 82 | 83 |

Then import all of the needed things and create a basic component:

84 | 85 | Hello {name}! 96 | ) 97 | } 98 | } 99 | 100 | fritz.define('hello-message', Hello); 101 | `} /> 102 | 103 |

Cool, now that we have created a component we need to actually use it. Create another bundle named main.js, this will be a script we add to our page which will sync up the DOM to our component:

104 | 105 | 111 | 112 |

Now we just need to add this script to our page and use the component.

113 | 114 | 116 | 117 | Our app 118 | 119 | 120 | 121 | 122 | `} /> 123 | 124 |

And that's it!

125 | 126 |

In a React app

127 |

Using Fritz components within a React application is simple. First step is to update your .babelrc to use h as the pragma:

128 | 135 | 136 |

This will allow you to transform JSX both for the React and Fritz sides of your application. As before, we won't explain how to configure your bundler, but know that you will need to create a worker bundle (that contains Fritz code) and a bundle for your React code.

137 | 138 |

React doesn't properly handle passing data to web components, but luckily there is a helper library that fixes the issue for us. Install skatejs/val like so:

139 | 140 | 141 |

Then create the module that will act as our wrapper:

142 | 143 | 149 | 150 |

And within your React code, use it:

151 | 152 | 153 | 165 | Hello world 166 | 167 | 168 | ); 169 | } 170 | } 171 | 172 | const main = document.querySelector('main'); 173 | ReactDOM.render(, main); 174 | `} /> 175 |

Note that this imports our implementation of h, which is just a small wrapper around React.createElement. Since we are using h in both the React app and the worker, our babel config remains the same.

176 | 177 |

Now we just need to implement ${'<'}worker-component${'>'}.

178 | 179 | 199 |
Hi {name}. This has been clicked {count} times.
200 | Add 201 |
202 | ); 203 | } 204 | } 205 | 206 | fritz.define('worker-component', MyWorkerComponent); 207 | `} /> 208 | 209 |

A few things worth noting here:

210 |
    211 |
  • We define props that we expect to receive with a static props getter (of course you can use class properties here if using the Babel plugin).
  • 212 |
  • render receives props and state as its arguments, so you can destruct.
  • 213 |
  • Unlike in React and Preact, you can directly pass your class methods as event handlers. Fritz will know to call your component as the this value when calling that function.
  • 214 |
  • As always, we finish out component by calling fritz.define to define the custom element tag name.
  • 215 |
216 | 217 |

And that's it! Now you can seemlessly use any Fritz components within your React application.

218 | -------------------------------------------------------------------------------- /site/src/components/CodeFile.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import CodeSnippet from './CodeSnippet.astro'; 3 | 4 | const {code, lang, name, class: className} = Astro.props; 5 | --- 6 | 24 |
25 |
{name}
26 | 27 |
-------------------------------------------------------------------------------- /site/src/components/CodeSnippet.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import highlight from '../hlight.js'; 3 | import '../styles/agate.css'; 4 | 5 | let { code, lang = 'js', class: className } = Astro.props; 6 | let html = highlight(code.trim(), lang); 7 | --- 8 | 23 |
24 |
25 |     
26 |   
27 |
-------------------------------------------------------------------------------- /site/src/components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | // @jsxPragma h 2 | import fritz, {Component} from 'fritz'; 3 | import ButtonCSS from '../styles/button.css?raw'; 4 | 5 | type State = { 6 | open: boolean | undefined; 7 | } 8 | 9 | const SSR = import.meta.env.SSR; 10 | 11 | export default class Collapsible extends Component { 12 | static props = { 13 | queryMatches: { attribute: false } 14 | } 15 | static styles = [ButtonCSS, /* css */` 16 | .area:not(.open) { 17 | display: none; 18 | } 19 | .root:not(.matches) .selector { 20 | display: none; 21 | } 22 | 23 | .selector { 24 | display: flex; 25 | margin-bottom: 1rem; 26 | } 27 | 28 | button { 29 | --button-font-size: 110%; 30 | flex: 1; 31 | } 32 | 33 | @media (max-width: 420px) { 34 | .root:not(.client) .area { 35 | display: none; 36 | } 37 | } 38 | `]; 39 | state = { 40 | open: undefined 41 | }; 42 | onButtonClick() { 43 | this.setState({ open: !this.state.open }); 44 | } 45 | render() { 46 | let client = !SSR; 47 | let matches = client && (this.props.queryMatches ?? true); 48 | let open = this.state.open ?? !matches; 49 | let rootClasses = ["root"]; 50 | if(matches) rootClasses.push("matches"); 51 | if(client) rootClasses.push("client"); 52 | 53 | return ( 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | fritz.define('x-collapse', Collapsible); -------------------------------------------------------------------------------- /site/src/components/DocLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/global.css'; 3 | import '../styles/agate.css'; 4 | import StylesCommon from './StylesCommon.astro'; 5 | import MadeWith from '../components/MadeWith'; 6 | import TitleCard from '../components/TitleCard.astro'; 7 | import HeadCommon from './HeadCommon.astro'; 8 | import DocSidebar from './DocSidebar.astro'; 9 | 10 | const { docId } = Astro.props; 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | 53 | 54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 | 68 |
69 | 73 | 74 | -------------------------------------------------------------------------------- /site/src/components/DocSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import Collapsible from './Collapsible.jsx'; 4 | 5 | const { docId } = Astro.props; 6 | const collection = await getCollection('docs'); 7 | 8 | const sortPreferences = ['Introduction', 'Components']; 9 | 10 | 11 | type CollectionItem = typeof collection[number]; 12 | const byCategory = Array.from(collection.reduce(((acc, item) => { 13 | if(!acc.has(item.data.category)) { 14 | acc.set(item.data.category, []); 15 | } 16 | let items = acc.get(item.data.category)!; 17 | if(item.data.sort != null) { 18 | items.splice(item.data.sort, 0, item); 19 | } else { 20 | items.push(item); 21 | } 22 | return acc; 23 | }), new Map())).sort((a, b) => { 24 | if(sortPreferences.indexOf(a[0]) > sortPreferences.indexOf(b[0])) return 1; 25 | else return -1; 26 | }); 27 | --- 28 | 75 | 83 | 84 | 85 | 99 | 100 | -------------------------------------------------------------------------------- /site/src/components/Flame.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import fPng from '../images/frankenstein-fritz-flame.png'; 3 | import fWebp from '../images/frankenstein-fritz-flame.webp'; 4 | 5 | const { size = 'small' } = Astro.props; 6 | --- 7 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /site/src/components/HeadCommon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export type Props = { 3 | title?: string; 4 | }; 5 | 6 | const { 7 | title = 'Fritz - Take your UI off the main thread' 8 | } = Astro.props as Props; 9 | --- 10 | 11 | {title} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/src/components/MadeWith.tsx: -------------------------------------------------------------------------------- 1 | import fritz, { Component } from 'fritz'; 2 | 3 | export default class MadeWith extends Component { 4 | static styles = ` 5 | p { 6 | margin: 0; 7 | } 8 | 9 | a, 10 | a:visited { 11 | color: #fff; 12 | font-weight: 600; 13 | } 14 | `; 15 | render() { 16 | return ( 17 | <> 18 |

Made with 🎃 by @matthewcp

19 | 20 | ); 21 | } 22 | } 23 | 24 | fritz.define('fritz-made-with', MadeWith); -------------------------------------------------------------------------------- /site/src/components/NotFound.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { slug } = Astro.props; 3 | --- 4 | 5 | 6 | Not found 7 | 8 | 9 |

Oops, this is not what you wanted to happen.

10 |

The page {slug} doesn't seem to exist.

11 | 12 | -------------------------------------------------------------------------------- /site/src/components/StylesCommon.astro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/src/components/TitleCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Flame from './Flame.astro'; 3 | import Slant from './effects/Slant.astro'; 4 | 5 | export type Props = { 6 | size?: 'small' | 'large'; 7 | } 8 | 9 | const { size = 'small' } = Astro.props as Props; 10 | --- 11 | 26 |
27 | 28 | 29 |
-------------------------------------------------------------------------------- /site/src/components/effects/Butter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export type Props = { 3 | text: string; 4 | }; 5 | const { text } = Astro.props; 6 | --- 7 | 34 |
{text}
-------------------------------------------------------------------------------- /site/src/components/effects/Slant.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { text, tag: Tag, size = 'small' } = Astro.props; 3 | --- 4 | 93 |
94 |
95 | 96 | {text} 97 | 98 | 99 | 100 |
101 |
102 | -------------------------------------------------------------------------------- /site/src/components/effects/StrongA.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export type Props = { 3 | text: string; 4 | }; 5 | const { text } = Astro.props as Props; 6 | --- 7 | 56 |
57 |

{text}

58 | 59 |
-------------------------------------------------------------------------------- /site/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from 'astro:content'; 2 | 3 | const docs = defineCollection({ 4 | schema: z.object({ 5 | title: z.string(), 6 | category: z.string(), 7 | sort: z.number().optional(), 8 | }) 9 | }); 10 | 11 | export const collections = { 12 | docs 13 | }; -------------------------------------------------------------------------------- /site/src/content/docs/components-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | category: Components 4 | --- 5 | 6 | This is about components -------------------------------------------------------------------------------- /site/src/content/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | category: Introduction 4 | --- 5 | 6 | Fritz is available on npm with the `fritz` package. 7 | 8 | ```shell 9 | npm install fritz 10 | ``` -------------------------------------------------------------------------------- /site/src/content/docs/what-is-fritz.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is Fritz 3 | category: Introduction 4 | sort: 0 5 | --- 6 | 7 | Fritz is a library for building applications with components that ensures your application stays responsive. Fritz is able to achieve this by running most of your application in [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). -------------------------------------------------------------------------------- /site/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /site/src/hlight.js: -------------------------------------------------------------------------------- 1 | //import html from './html.js'; 2 | import hljs from 'highlight.js'; 3 | 4 | hljs.configure({ useBR: true }); 5 | 6 | // const entities = /(<|>|\n)/g; 7 | // const LESS_THAN = '<'; 8 | // const GREATER_THAN = '>'; 9 | // const NEW_LINE = '\n'; 10 | // const entityMap = { 11 | // [LESS_THAN]: '<', 12 | // [GREATER_THAN]: '>', 13 | // [NEW_LINE]: '
' 14 | // }; 15 | 16 | // function clean(value) { 17 | // let strings = [], values = []; 18 | // let res; 19 | 20 | // let start = 0; 21 | 22 | // while(res = (entities.exec(value))) { 23 | // let [ match ] = res; 24 | // let { index } = res; 25 | // let entityValue = entityMap[match]; 26 | // let matchLength = match.length; 27 | 28 | // let indexLength = index - start; 29 | // let string = value.substr(start, indexLength); 30 | 31 | // if(match === NEW_LINE) { 32 | // string = string + entityValue; 33 | // entityValue = ''; 34 | // } 35 | 36 | // strings.push(string); 37 | // values.push(entityValue); 38 | 39 | // start = index + matchLength; 40 | // } 41 | 42 | // // Push the final string. 43 | // strings.push(value.substr(start)); 44 | 45 | // return [strings, values]; 46 | // } 47 | 48 | export default function(code, lang) { 49 | const result = hljs.highlightAuto(code.trim(), [lang]); 50 | let { value } = result; 51 | 52 | return value; 53 | 54 | /* 55 | 56 | let [strings, values] = clean(value); 57 | 58 | return html(strings, ...values); 59 | */ 60 | } -------------------------------------------------------------------------------- /site/src/images/frankenstein-fritz-flame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/src/images/frankenstein-fritz-flame.png -------------------------------------------------------------------------------- /site/src/images/frankenstein-fritz-flame.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fritz/d2128919ba984735df26ee4cd473ad8cf0200b73/site/src/images/frankenstein-fritz-flame.webp -------------------------------------------------------------------------------- /site/src/pages/docs/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import DocLayout from '../../components/DocLayout.astro'; 3 | import { getCollection, CollectionEntry } from 'astro:content'; 4 | 5 | export async function getStaticPaths() { 6 | const docs = await getCollection('docs'); 7 | return docs.map(entry => ({ 8 | // Pass blog entry via props 9 | params: { slug: entry.slug }, props: { entry }, 10 | })); 11 | } 12 | 13 | interface Props { 14 | // Optionally use `CollectionEntry` for type safety 15 | entry: CollectionEntry<'docs'>; 16 | } 17 | 18 | const { entry } = Astro.props; 19 | const { Content } = await entry.render(); 20 | --- 21 | 47 | 48 |

{entry.data.title}

49 | 50 |
-------------------------------------------------------------------------------- /site/src/pages/docs/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const redirectTo = new URL('./what-is-fritz/', Astro.url); 3 | --- 4 | 5 | 6 | 7 | Docs 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /site/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import StylesCommon from '../components/StylesCommon.astro'; 3 | import HeadCommon from '../components/HeadCommon.astro'; 4 | import CodeFile from '../components/CodeFile.astro'; 5 | import MadeWith from '../components/MadeWith'; 6 | import TitleCard from '../components/TitleCard.astro'; 7 | import '../styles/global.css'; 8 | 9 | const workerJsCode = ` 10 | class HelloMessage extends Component { 11 | static props = { 12 | name: { attribute: true } 13 | } 14 | 15 | render() { 16 | return ( 17 |
Hello {this.props.name}!
18 | ); 19 | } 20 | } 21 | 22 | fritz.define('hello-message', HelloMessage); 23 | `; 24 | 25 | const mainJsCode = ` 26 | import fritz from 'fritz/window'; 27 | 28 | fritz.use(new Worker('./worker.js')); 29 | `; 30 | 31 | const htmlCode = ` 32 | 33 | 34 | 35 | `; 36 | --- 37 | 38 | 39 | 40 | 41 | 42 | 160 |
161 |
162 |
163 | 164 |

Take your UI off the main thread.

165 | 175 |
176 | 177 |
178 | 179 | 180 | 181 |
182 |
183 | 184 |
185 | 186 |
187 |
188 | 189 | 193 | -------------------------------------------------------------------------------- /site/src/styles/agate.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Agate by Taufik Nurrohman 3 | * ---------------------------------------------------- 4 | * 5 | * #ade5fc 6 | * #a2fca2 7 | * #c6b4f0 8 | * #d36363 9 | * #fcc28c 10 | * #fc9b9b 11 | * #ffa 12 | * #fff 13 | * #333 14 | * #62c8f3 15 | * #888 16 | * 17 | */ 18 | 19 | .hljs { 20 | display: block; 21 | overflow-x: auto; 22 | padding: 0.5em; 23 | background: #333; 24 | color: white; 25 | } 26 | 27 | .hljs-name, 28 | .hljs-strong { 29 | font-weight: bold; 30 | } 31 | 32 | .hljs-code, 33 | .hljs-emphasis { 34 | font-style: italic; 35 | } 36 | 37 | .hljs-tag { 38 | color: #62c8f3; 39 | } 40 | 41 | .hljs-variable, 42 | .hljs-template-variable, 43 | .hljs-selector-id, 44 | .hljs-selector-class { 45 | color: #ade5fc; 46 | } 47 | 48 | .hljs-string, 49 | .hljs-bullet { 50 | color: #a2fca2; 51 | } 52 | 53 | .hljs-type, 54 | .hljs-title, 55 | .hljs-section, 56 | .hljs-attribute, 57 | .hljs-quote, 58 | .hljs-built_in, 59 | .hljs-builtin-name { 60 | color: #ffa; 61 | } 62 | 63 | .hljs-number, 64 | .hljs-symbol, 65 | .hljs-bullet { 66 | color: #d36363; 67 | } 68 | 69 | .hljs-keyword, 70 | .hljs-selector-tag, 71 | .hljs-literal { 72 | color: #fcc28c; 73 | } 74 | 75 | .hljs-comment, 76 | .hljs-deletion, 77 | .hljs-code { 78 | color: #888; 79 | } 80 | 81 | .hljs-regexp, 82 | .hljs-link { 83 | color: #c6b4f0; 84 | } 85 | 86 | .hljs-meta { 87 | color: #fc9b9b; 88 | } 89 | 90 | .hljs-deletion { 91 | background-color: #fc9b9b; 92 | color: #333; 93 | } 94 | 95 | .hljs-addition { 96 | background-color: #a2fca2; 97 | color: #333; 98 | } 99 | 100 | .hljs a { 101 | color: inherit; 102 | } 103 | 104 | .hljs a:focus, 105 | .hljs a:hover { 106 | color: inherit; 107 | text-decoration: underline; 108 | } 109 | -------------------------------------------------------------------------------- /site/src/styles/button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | border: none; 4 | text-align: center; 5 | width: 11rem; 6 | padding: 0.5rem 0; 7 | margin: 0.5rem 1rem; 8 | font-size: var(--button-font-size, 150%); 9 | font-weight: 100; 10 | } 11 | 12 | .button, .button:visited { 13 | color: #fff; 14 | background-color: var(--bittersweet-shimmer); 15 | text-decoration: none; 16 | } -------------------------------------------------------------------------------- /site/src/styles/common.pcss: -------------------------------------------------------------------------------- 1 | @custom-media --small-viewport (max-width: 420px); -------------------------------------------------------------------------------- /site/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import "./button.css"; 2 | 3 | footer { 4 | background-color: var(--main-bg); 5 | height: 7rem; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | align-self: end; 10 | color: #fff; 11 | font-size: 120%; 12 | } -------------------------------------------------------------------------------- /site/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "target": "ESNext", 6 | "strict": true, 7 | "declaration": true, 8 | "noEmit": true, 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "fritz" 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "globDirectory": "site/public/", 3 | "globPatterns": [ 4 | "*.{js,html,css,ico,png,webp,json}", 5 | "**/images/*.png" 6 | ], 7 | "swDest": "site/public/sw.js" 8 | }; --------------------------------------------------------------------------------