├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── packages ├── bruh ├── LICENSE ├── README.md ├── mod.mjs ├── package.json ├── src │ ├── cli │ │ └── index.mjs │ ├── components │ │ └── optimized-picture │ │ │ ├── hydrate.mjs │ │ │ └── render.mjs │ ├── dom │ │ ├── index.browser.mjs │ │ └── index.server.mjs │ ├── index.browser.mjs │ ├── media │ │ └── images.node.mjs │ ├── reactive │ │ └── index.mjs │ └── util │ │ └── index.mjs └── vite.config.mjs ├── create-bruh ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.mjs ├── minimal │ ├── gitignore │ ├── index.css │ ├── index.html │ ├── index.mjs │ └── package.json ├── package.json ├── utils.mjs └── vite │ ├── components │ └── counter │ │ ├── hydrate.mjs │ │ └── render.jsx │ ├── gitignore │ ├── index.css │ ├── index.html.jsx │ ├── index.mjs │ ├── package.json │ ├── postcss.config.js │ ├── shell.jsx │ └── vite.config.mjs └── vite-plugin-bruh ├── LICENSE ├── README.md ├── index.mjs └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/bruh/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bruh-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "./packages/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/bruh/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Ethridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/bruh/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Bruh 4 |

5 |

The thinnest possible layer between development and production for the modern web.

6 |

7 | 8 | MIT License 12 | 13 | 14 | NPM Version 18 | 19 | 20 | Distributed download size for the entire library 24 | 25 | 26 | Github Discussions 30 | 31 |

32 |
33 | 34 |
35 | 36 | # What's This? 37 | 38 | A js library for the web that places your control on a pedestal. 39 | It packs flexible SSR (Server-Side HTML Rendering), 40 | an awesome DOM interface, 41 | and elegant functional reactivity in a tiny code size. 42 | 43 | Along with modern build tooling integration ([vite](https://vitejs.dev)), you're one step away from: 44 | - JSX and MDX (markdown with JSX instead of HTML) for both HTML rendering and DOM element creation 45 | - Instant HMR (Hot Module Reloading) for both server rendered HTML and client CSS/JS/TS 46 | - [Everything else vite provides](https://vitejs.dev/guide/features.html) - CSS modules, PostCSS, production builds, nearly 0 config, _&c_. 47 | 48 |
49 | 50 |

51 | It looks like this, which is pretty epic: 52 | 53 | 54 | Open in CodePen 59 | 60 |


61 | 62 | ```jsx 63 | const Counter = () => { 64 | // A reactive value 65 | const count = r(0) 66 | const increment = () => count.value++ 67 | 68 | // Declarative UI without vdom! (and build tools are completely optional) 69 | const counter = 70 | 73 | 74 | return counter 75 | } 76 | 77 | // Yes, all of these are vanilla DOM nodes! 78 | document.body.append( 79 |
80 |

Bruh

81 | 82 |
83 | ) 84 | ``` 85 | 86 | # How do I Get It? 87 | 88 | `npm init bruh` and pick [the "vite" template](https://github.com/Technical-Source/bruh/tree/main/packages/create-bruh/vite) 89 | 90 |

91 | Think that's too hard? 👉 92 | 93 | 94 | Open in CodeSandbox 99 | 100 |


101 | 102 | # Where is the documentation? 103 | 104 | [Right here](https://technicalsource.dev/bruh) - but it's not really complete. 105 | The best way to use this project is to just read the code, it's pretty short. 106 | If you have any questions, even without reading the code first, feel free to [ask all of them in the discussions](https://github.com/Technical-Source/bruh/discussions). 107 | -------------------------------------------------------------------------------- /packages/bruh/mod.mjs: -------------------------------------------------------------------------------- 1 | export * as dom from "./src/dom/index.server.mjs" 2 | export * as reactive from "./src/reactive/index.mjs" 3 | export * as util from "./src/util/index.mjs" 4 | -------------------------------------------------------------------------------- /packages/bruh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bruh", 3 | "description": "The thinnest possible layer between development and production for the modern web.", 4 | "keywords": [ 5 | "web", 6 | "frontend", 7 | "ui", 8 | "backend", 9 | "utilities", 10 | "library", 11 | "modern" 12 | ], 13 | "version": "1.13.1", 14 | "license": "MIT", 15 | "author": { 16 | "name": "Daniel Ethridge", 17 | "url": "https://git.io/de" 18 | }, 19 | "homepage": "https://technicalsource.dev/bruh", 20 | "bugs": "https://github.com/Technical-Source/bruh/issues", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Technical-Source/bruh.git", 24 | "directory": "packages/bruh" 25 | }, 26 | "type": "module", 27 | "main": "./dist/bruh.umd.js", 28 | "sideEffects": false, 29 | "exports": { 30 | ".": { 31 | "browser": "./dist/bruh.es.js", 32 | "default": "./dist/bruh.umd.js" 33 | }, 34 | "./dom": { 35 | "node": "./src/dom/index.server.mjs", 36 | "browser": "./src/dom/index.browser.mjs" 37 | }, 38 | "./reactive": "./src/reactive/index.mjs", 39 | "./util": "./src/util/index.mjs", 40 | "./components/*": "./src/components/*.mjs", 41 | "./media/images": { 42 | "node": "./src/media/images.node.mjs" 43 | } 44 | }, 45 | "bin": { 46 | "bruh": "./src/cli/index.mjs" 47 | }, 48 | "files": [ 49 | "./src/", 50 | "./dist/" 51 | ], 52 | "scripts": { 53 | "build": "vite build", 54 | "prepare": "npm run build" 55 | }, 56 | "optionalDependencies": { 57 | "cac": "^6.7.14", 58 | "sharp": "^0.31.0" 59 | }, 60 | "devDependencies": { 61 | "vite": "^3.1.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/bruh/src/cli/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { processImages } from "../media/images.mjs" 3 | import { join } from "path" 4 | 5 | import { cac } from "cac" 6 | const cli = cac("bruh", "Command-line interfaces for bruh") 7 | 8 | cli 9 | .command( 10 | "process-images ", 11 | "Processes the images in the given directory for the optimized-picture component" 12 | ) 13 | .action((directory, options) => { 14 | const imagesDirectory = join(process.cwd(), directory) 15 | processImages(imagesDirectory) 16 | }) 17 | 18 | cli.help() 19 | cli.parse() 20 | -------------------------------------------------------------------------------- /packages/bruh/src/components/optimized-picture/hydrate.mjs: -------------------------------------------------------------------------------- 1 | export default (className = "bruh-optimized-picture") => 2 | document.querySelectorAll(`.${className} > img`) 3 | .forEach(img => { 4 | const removeLQIP = () => img.removeAttribute("style") 5 | 6 | if (img.complete) 7 | removeLQIP() 8 | else 9 | img.addEventListener("load", removeLQIP, { once: true }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/bruh/src/components/optimized-picture/render.mjs: -------------------------------------------------------------------------------- 1 | import { e } from "bruh/dom" 2 | import { functionAsObject } from "bruh/util" 3 | const { picture, source, img } = functionAsObject(e) 4 | 5 | import { readFile } from "fs/promises" 6 | 7 | export default async options => { 8 | const imagePath = options.src 9 | 10 | const { width, height, lqip } = JSON.parse( 11 | await readFile(`${imagePath}.json`) 12 | ) 13 | 14 | return picture({ class: "bruh-optimized-picture" }, 15 | source({ type: "image/avif", srcset: `${imagePath}.avif` }), 16 | source({ type: "image/webp", srcset: `${imagePath}.webp` }), 17 | img({ 18 | src: imagePath, 19 | alt: options.alt || "", 20 | width: options.width || width, 21 | height: options.height || height, 22 | loading: options.loading || "lazy", 23 | style: `background-image: url(${lqip})` 24 | }) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/bruh/src/dom/index.browser.mjs: -------------------------------------------------------------------------------- 1 | import { isReactive, reactiveDo } from "../reactive/index.mjs" 2 | 3 | //#region Bruh child functions e.g. bruhChildrenToNodes() 4 | 5 | // A basic check for if a value is allowed as a child in bruh 6 | // It's responsible for quickly checking the type, not deep validation 7 | const isBruhChild = x => 8 | // Reactives and DOM nodes 9 | x?.[isReactive] || 10 | x instanceof Node || 11 | // Any array, just assume it contains valid children 12 | Array.isArray(x) || 13 | // Allow nullish 14 | x == null || 15 | // Disallow functions and objects 16 | !(typeof x === "function" || typeof x === "object") 17 | // Everything else can be a child when stringified 18 | 19 | // Coerces input into a DOM node, if it isn't already one 20 | const unreactiveChildToNode = x => { 21 | // Existing DOM nodes are untouched 22 | if (x instanceof Node) 23 | return x 24 | // booleans and nullish are ignored 25 | else if (typeof x === "boolean" || x === undefined || x === null) 26 | return document.createComment(x) 27 | // Anything else is treated as text 28 | else 29 | return document.createTextNode(x) 30 | } 31 | 32 | // Auto-swapping single reactive node 33 | const reactiveChildToNode = child => { 34 | let node = unreactiveChildToNode(child.value) 35 | 36 | const stopReacting = child.addReaction(() => { 37 | // Stop swapping if no longer possible 38 | if (!node.parentNode) { 39 | stopReacting() 40 | return 41 | } 42 | 43 | // Normal swap 44 | if (!Array.isArray(child.value)) { 45 | const oldNode = node 46 | node = unreactiveChildToNode(child.value) 47 | oldNode.replaceWith(node) 48 | } 49 | // If an array now, stop swapping, then switch to reactive array swapping 50 | else { 51 | stopReacting() 52 | node.replaceWith(...reactiveArrayChildToNodes(child)) 53 | } 54 | }) 55 | 56 | return node 57 | } 58 | 59 | // Auto-swapping reactive array of nodes 60 | const reactiveArrayChildToNodes = child => { 61 | // Markers owned by the swapper here itself, so that 62 | // the values in the array can be swapped separately 63 | const first = document.createComment("[") 64 | const last = document.createComment("]") 65 | 66 | const stopReacting = child.addReaction(() => { 67 | // Stop swapping if there is no parent to swap within 68 | if (!first.parentNode) { 69 | stopReacting() 70 | return 71 | } 72 | 73 | // Make a range starting after the first marker 74 | const range = document.createRange() 75 | range.setStartAfter(first) 76 | 77 | // Normal swap, replacing content between the first and last markers 78 | if (Array.isArray(child.value)) { 79 | range.setEndBefore(last) 80 | range.deleteContents() 81 | first.after(...bruhChildrenToNodes(child.value)) 82 | } 83 | // Switch to single swapping node by replacing everything 84 | else { 85 | stopReacting() 86 | range.setEndAfter(last) 87 | range.deleteContents() 88 | first.replaceWith(reactiveChildToNode(child)) 89 | } 90 | }) 91 | 92 | return [ 93 | first, 94 | ...bruhChildrenToNodes(child.value), 95 | last 96 | ] 97 | } 98 | 99 | // Processes bruh children into an array of DOM nodes 100 | // Reactive values are automatically replaced, so the output must be placed into a parent node 101 | // before any top level (after flattening arrays) reactions run 102 | export const bruhChildrenToNodes = children => 103 | children 104 | .flat(Infinity) 105 | .flatMap(child => { 106 | // Non-reactive child 107 | if (!child?.[isReactive]) 108 | return [unreactiveChildToNode(child)] 109 | 110 | // Single reactive value 111 | if (!Array.isArray(child.value)) 112 | return [reactiveChildToNode(child)] 113 | 114 | // Reactive array 115 | return reactiveArrayChildToNodes(child) 116 | }) 117 | 118 | //#endregion 119 | 120 | //#region Reactive-aware element helper functions e.g. applyAttributes() 121 | 122 | // Style attribute rules from an object with 123 | // potentially reactive and/or undefined values 124 | export const applyStyles = (element, styles) => { 125 | for (const property in styles) 126 | reactiveDo(styles[property], value => { 127 | if (value !== undefined) 128 | element.style.setProperty (property, value) 129 | else 130 | element.style.removeProperty(property) 131 | }) 132 | } 133 | 134 | // Class list from an object mapping from 135 | // class names to potentially reactive booleans 136 | export const applyClasses = (element, classes) => { 137 | for (const name in classes) 138 | reactiveDo(classes[name], value => { 139 | element.classList.toggle(name, value) 140 | }) 141 | } 142 | 143 | // Attributes from an object with 144 | // potentially reactive and/or undefined values 145 | export const applyAttributes = (element, attributes) => { 146 | for (const name in attributes) 147 | reactiveDo(attributes[name], value => { 148 | if (value !== undefined) 149 | element.setAttribute (name, value) 150 | else 151 | element.removeAttribute(name) 152 | }) 153 | } 154 | 155 | //#endregion 156 | 157 | //#region t() for text nodes and e() for element nodes 158 | 159 | // Text nodes 160 | export const t = textContent => { 161 | // Non-reactive values are just text nodes 162 | if (!textContent[isReactive]) 163 | return document.createTextNode(textContent) 164 | 165 | // Reactive values auto-update the node's text content 166 | const node = document.createTextNode(textContent.value) 167 | textContent.addReaction(() => { 168 | node.textContent = textContent.value 169 | }) 170 | return node 171 | } 172 | 173 | // Elements 174 | export const e = name => (...variadic) => { 175 | if (variadic.length === 0) 176 | return document.createElement(name) 177 | 178 | // If there are no props 179 | if (isBruhChild(variadic[0])) { 180 | const element = document.createElement(name) 181 | element.append(...bruhChildrenToNodes(variadic)) 182 | return element 183 | } 184 | 185 | // If props exist as the first variadic argument 186 | const [props, ...children] = variadic 187 | 188 | // Extract explicit options from the bruh prop 189 | const { namespace } = props.bruh ?? {} 190 | delete props.bruh 191 | 192 | // Make an element with optional namespace 193 | const element = 194 | namespace 195 | ? document.createElementNS(namespace, name) 196 | : document.createElement ( name) 197 | 198 | // Apply overloaded props, if possible 199 | 200 | // Inline style object 201 | if (typeof props.style === "object" && !props.style[isReactive]) { 202 | applyStyles(element, props.style) 203 | delete props.style 204 | } 205 | // Classes object 206 | if (typeof props.class === "object" && !props.class[isReactive]) { 207 | applyClasses(element, props.class) 208 | delete props.class 209 | } 210 | for (const name in props) { 211 | // Event listener functions 212 | if (name.startsWith("on") && typeof props[name] === "function") { 213 | element.addEventListener(name.slice(2), props[name]) 214 | delete props[name] 215 | } 216 | } 217 | 218 | // The rest of the props are attributes 219 | applyAttributes(element, props) 220 | 221 | // Add the children to the element 222 | element.append(...bruhChildrenToNodes(children)) 223 | return element 224 | } 225 | 226 | //#endregion 227 | 228 | //#region JSX integration 229 | 230 | // The function that jsx tags (except fragments) compile to 231 | export const h = (nameOrComponent, props, ...children) => { 232 | // If we are making an element, this is just a wrapper of e() 233 | // This is likely when the JSX tag name begins with a lowercase character 234 | if (typeof nameOrComponent === "string") { 235 | const makeElement = e(nameOrComponent) 236 | return props 237 | ? makeElement(props, ...children) 238 | : makeElement(...children) 239 | } 240 | 241 | // It must be a component, then, as bruh components are just functions 242 | // Due to JSX, this would mean a function with only one parameter - props 243 | // This object includes the all of the normal props and a "children" key 244 | return nameOrComponent({ ...props, children }) 245 | } 246 | 247 | // The JSX fragment is made into a bruh fragment (just an array) 248 | export const JSXFragment = ({ children }) => children 249 | 250 | //#endregion 251 | 252 | 253 | 254 | // Hydration of all bruh-textnode's from prerendered html 255 | export const hydrateTextNodes = () => { 256 | const tagged = {} 257 | const bruhTextNodes = document.getElementsByTagName("bruh-textnode") 258 | 259 | for (const bruhTextNode of bruhTextNodes) { 260 | const textNode = document.createTextNode(bruhTextNode.textContent) 261 | 262 | const tag = bruhTextNode.getAttribute("tag") 263 | if (tag) 264 | tagged[tag] = textNode 265 | 266 | bruhTextNode.replaceWith(textNode) 267 | } 268 | 269 | return tagged 270 | } 271 | -------------------------------------------------------------------------------- /packages/bruh/src/dom/index.server.mjs: -------------------------------------------------------------------------------- 1 | const isMetaNode = Symbol.for("bruh meta node") 2 | const isMetaTextNode = Symbol.for("bruh meta text node") 3 | const isMetaElement = Symbol.for("bruh meta element") 4 | const isMetaRawString = Symbol.for("bruh meta raw string") 5 | 6 | //#region HTML syntax functions 7 | 8 | // https://html.spec.whatwg.org/multipage/syntax.html#void-elements 9 | const voidElements = [ 10 | "base", 11 | "link", 12 | "meta", 13 | 14 | "hr", 15 | "br", 16 | "wbr", 17 | 18 | "area", 19 | "img", 20 | "track", 21 | 22 | "embed", 23 | "param", 24 | "source", 25 | 26 | "col", 27 | 28 | "input" 29 | ] 30 | 31 | const isVoidElement = element => 32 | voidElements.includes(element) 33 | 34 | // https://html.spec.whatwg.org/multipage/syntax.html#elements-2 35 | // https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions 36 | // Does not work for https://html.spec.whatwg.org/multipage/syntax.html#raw-text-elements (script and style) 37 | const escapeForElement = x => 38 | (x + "") 39 | .replace(/&/g, "&") 40 | .replace(/ 44 | (x + "") 45 | .replace(/&/g, "&") 46 | .replace(/"/g, """) 47 | 48 | // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 49 | const attributesToString = attributes => 50 | Object.entries(attributes) 51 | .map(([name, value]) => 52 | value === "" 53 | ? ` ${name}` 54 | : ` ${name}="${escapeForDoubleQuotedAttribute(value)}"` 55 | ).join("") 56 | 57 | //#endregion 58 | 59 | // A basic check for if a value is allowed as a meta node's child 60 | // It's responsible for quickly checking the type, not deep validation 61 | const isMetaChild = x => 62 | // meta nodes, reactives, and DOM nodes 63 | x?.[isMetaNode] || 64 | x?.[isMetaRawString] || 65 | // Any array, just assume it contains valid children 66 | Array.isArray(x) || 67 | // Allow nullish 68 | x == null || 69 | // Disallow functions and objects 70 | !(typeof x === "function" || typeof x === "object") 71 | // Everything else can be a child when stringified 72 | 73 | 74 | //#region Meta Nodes that act like lightweight rendering-oriented DOM nodes 75 | 76 | // Text nodes have no individual HTML representation 77 | // We emulate this with a custom element with an inline style reset 78 | // These elements can be hydrated very quickly and even be marked with a tag 79 | export class MetaTextNode { 80 | [isMetaNode] = true; 81 | [isMetaTextNode] = true 82 | 83 | textContent 84 | tag 85 | 86 | constructor(textContent) { 87 | this.textContent = textContent 88 | } 89 | 90 | toString() { 91 | const tag = this.tag 92 | ? ` tag="${escapeForDoubleQuotedAttribute(this.tag)}"` 93 | : "" 94 | return `${ 95 | escapeForElement(this.textContent) 96 | }` 97 | } 98 | 99 | setTag(tag) { 100 | this.tag = tag 101 | 102 | return this 103 | } 104 | } 105 | 106 | // A light model of an element 107 | export class MetaElement { 108 | [isMetaNode] = true; 109 | [isMetaElement] = true 110 | 111 | name 112 | attributes = {} 113 | children = [] 114 | 115 | constructor(name) { 116 | this.name = name 117 | } 118 | 119 | toString() { 120 | const attributes = attributesToString(this.attributes) 121 | // https://html.spec.whatwg.org/multipage/syntax.html#syntax-start-tag 122 | const startTag = `<${this.name}${attributes}>` 123 | if (isVoidElement(this.name)) 124 | return startTag 125 | 126 | const contents = this.children 127 | .flat(Infinity) 128 | .filter(x => typeof x !== "boolean" && x !== undefined && x !== null) 129 | .map(child => 130 | (child[isMetaNode] || child[isMetaRawString]) 131 | ? child.toString() 132 | : escapeForElement(child) 133 | ) 134 | .join("") 135 | // https://html.spec.whatwg.org/multipage/syntax.html#end-tags 136 | const endTag = `` 137 | return startTag + contents + endTag 138 | } 139 | } 140 | 141 | // Raw strings can be meta element children, where they bypass string escaping 142 | // This should be avoided in general, but is needed for unsupported HTML features 143 | export class MetaRawString extends String { 144 | [isMetaRawString] = true 145 | 146 | constructor(string) { 147 | super(string) 148 | } 149 | } 150 | 151 | //#endregion 152 | 153 | //#region Meta element helper functions e.g. applyAttributes() 154 | 155 | // Merge style rules with an object 156 | export const applyStyles = (element, styles) => { 157 | // Doesn't support proper escaping 158 | // https://www.w3.org/TR/css-syntax-3/#ref-for-parse-a-list-of-declarations%E2%91%A0 159 | // https://www.w3.org/TR/css-syntax-3/#typedef-ident-token 160 | const currentStyles = Object.fromEntries( 161 | (element.attributes.style || "") 162 | .split(";").filter(s => s.length) 163 | .map(declaration => declaration.split(":").map(s => s.trim())) 164 | ) 165 | 166 | Object.entries(styles) 167 | .forEach(([property, value]) => { 168 | if (value !== undefined) 169 | currentStyles[property] = value 170 | else 171 | delete currentStyles[property] 172 | }) 173 | 174 | element.attributes.style = 175 | Object.entries(currentStyles) 176 | .map(([property, value]) => `${property}:${value}`) 177 | .join(";") 178 | } 179 | 180 | // Merge classes with an object mapping from class names to booleans 181 | export const applyClasses = (element, classes) => { 182 | // Doesn't support proper escaping 183 | // https://html.spec.whatwg.org/multipage/dom.html#global-attributes:classes-2 184 | const currentClasses = new Set( 185 | (element.attributes.class || "") 186 | .split(/\s+/).filter(s => s.length) 187 | ) 188 | 189 | Object.entries(classes) 190 | .forEach(([name, value]) => { 191 | if (value) 192 | currentClasses.add(name) 193 | else 194 | currentClasses.delete(name) 195 | }) 196 | 197 | element.attributes.class = [...currentClasses].join(" ") 198 | } 199 | 200 | // Merge attributes with an object 201 | export const applyAttributes = (element, attributes) => { 202 | Object.entries(attributes) 203 | .forEach(([name, value]) => { 204 | if (value !== undefined) 205 | element.attributes[name] = value 206 | else 207 | delete element.attributes[name] 208 | }) 209 | } 210 | 211 | //#endregion 212 | 213 | //#region rawString(), t(), and e() 214 | 215 | export const rawString = string => 216 | new MetaRawString(string) 217 | 218 | export const t = textContent => 219 | new MetaTextNode(textContent) 220 | 221 | export const e = name => (...variadic) => { 222 | const element = new MetaElement(name) 223 | 224 | if (variadic.length === 0) 225 | return element 226 | 227 | // If there are no props 228 | if (isMetaChild(variadic[0])) { 229 | element.children.push(...variadic) 230 | return element 231 | } 232 | 233 | // If props exist as the first variadic argument 234 | const [props, ...children] = variadic 235 | 236 | // The bruh prop is reserved for future use 237 | delete props.bruh 238 | 239 | // Apply overloaded props, if possible 240 | if (typeof props.style === "object") { 241 | applyStyles(element, props.style) 242 | delete props.style 243 | } 244 | if (typeof props.class === "object") { 245 | applyClasses(element, props.class) 246 | delete props.class 247 | } 248 | // The rest of the props are attributes 249 | applyAttributes(element, props) 250 | 251 | // Add the children to the element 252 | element.children.push(...children) 253 | return element 254 | } 255 | 256 | //#endregion 257 | 258 | //#region JSX integration 259 | 260 | // The function that jsx tags (except fragments) compile to 261 | export const h = (nameOrComponent, props, ...children) => { 262 | // If we are making an element, this is just a wrapper of e() 263 | // This is likely when the JSX tag name begins with a lowercase character 264 | if (typeof nameOrComponent === "string") { 265 | const makeElement = e(nameOrComponent) 266 | return props 267 | ? makeElement(props, ...children) 268 | : makeElement(...children) 269 | } 270 | 271 | // It must be a component, then, as bruh components are just functions 272 | // Due to JSX, this would mean a function with only one parameter - props 273 | // This object includes the all of the normal props and a "children" key 274 | return nameOrComponent({ ...props, children }) 275 | } 276 | 277 | // The JSX fragment is made into a bruh fragment (just an array) 278 | export const JSXFragment = ({ children }) => children 279 | 280 | //#endregion 281 | -------------------------------------------------------------------------------- /packages/bruh/src/index.browser.mjs: -------------------------------------------------------------------------------- 1 | export * as dom from "./dom/index.browser.mjs" 2 | export * as reactive from "./reactive/index.mjs" 3 | export * as util from "./util/index.mjs" 4 | -------------------------------------------------------------------------------- /packages/bruh/src/media/images.node.mjs: -------------------------------------------------------------------------------- 1 | import sharp from "sharp" 2 | import { readdir, writeFile } from "fs/promises" 3 | import { extname, join } from "path" 4 | 5 | const avif = async (filePath, sharpInstance) => 6 | sharpInstance 7 | .avif({ }) 8 | .toFile(`${filePath}.avif`) 9 | 10 | const webp = async (filePath, sharpInstance) => 11 | sharpInstance 12 | .webp({ }) 13 | .toFile(`${filePath}.webp`) 14 | 15 | // Low Quality Image Placeholder inline css for the style attribute 16 | const json = async (filePath, sharpInstance) => { 17 | const imageManifest = {} 18 | 19 | const metadata = await sharpInstance.metadata() 20 | imageManifest.format = metadata.format 21 | imageManifest.width = metadata.width 22 | imageManifest.height = metadata.height 23 | 24 | const buffer = await sharpInstance 25 | .resize({ fit: "inside", width: 16, height: 16 }) 26 | .blur() 27 | .webp({ reductionEffort: 6 }) 28 | .toBuffer() 29 | 30 | imageManifest.lqip = `data:image/webp;base64,${buffer.toString("base64")}` 31 | return writeFile(`${filePath}.json`, JSON.stringify(imageManifest)) 32 | } 33 | 34 | const getUnprocessedImages = async directory => { 35 | const directoryEntries = await readdir(directory, { withFileTypes: true }) 36 | 37 | const promisedUnproccessedImages = directoryEntries 38 | .map(async entry => { 39 | const entryPath = join(directory, entry.name) 40 | 41 | if (entry.isDirectory()) 42 | return await getUnprocessedImages(entryPath) 43 | 44 | if ( 45 | entry.name[0] == "." || 46 | [".avif", ".webp", ".json"] 47 | .includes(extname(entry.name)) || 48 | directoryEntries.some(siblingEntry => 49 | [".avif", ".webp", ".json"] 50 | .map(processedExtention => `${entry.name}${processedExtention}`) 51 | .includes(siblingEntry.name) 52 | ) 53 | ) 54 | return [] 55 | 56 | return [entryPath] 57 | }) 58 | 59 | return (await Promise.all(promisedUnproccessedImages)).flat() 60 | } 61 | 62 | export const processImages = async directory => { 63 | const unprocessedImages = await getUnprocessedImages(directory) 64 | for (const filePath of unprocessedImages) { 65 | await Promise.all( 66 | [avif, webp, json] 67 | .map(process => process(filePath, sharp(filePath))) 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/bruh/src/reactive/index.mjs: -------------------------------------------------------------------------------- 1 | export const isReactive = Symbol.for("bruh reactive") 2 | 3 | // A super simple and performant reactive value implementation 4 | export class SimpleReactive { 5 | [isReactive] = true 6 | 7 | #value 8 | #reactions = new Set() 9 | 10 | constructor(value) { 11 | this.#value = value 12 | } 13 | 14 | get value() { 15 | return this.#value 16 | } 17 | 18 | set value(newValue) { 19 | if (newValue === this.#value) 20 | return 21 | 22 | this.#value = newValue 23 | for (const reaction of this.#reactions) 24 | reaction() 25 | } 26 | 27 | addReaction(reaction) { 28 | this.#reactions.add(reaction) 29 | 30 | return () => 31 | this.#reactions.delete(reaction) 32 | } 33 | } 34 | 35 | // A reactive implementation for building functional reactive graphs 36 | // Ensures state consistency, minimal node updates, and transparent update batching 37 | export class FunctionalReactive { 38 | [isReactive] = true 39 | 40 | #value 41 | #reactions = new Set() 42 | 43 | // For derived nodes, f is the derivation function 44 | #f 45 | // Source nodes are 0 deep in the derivation graph 46 | // This is for topological sort 47 | #depth = 0 48 | // All nodes have a set of derivatives that update when the node changes 49 | #derivatives = new Set() 50 | 51 | // Keep track of all the pending changes from the value setter 52 | static #settersQueue = new Map() 53 | // A queue of derivatives to potentially update, sorted into sets by depth 54 | // This starts with depth 1 and can potentially have holes 55 | static #derivativesQueue = [] 56 | // A queue of reactions to run after the graph is fully updated 57 | static #reactionsQueue = [] 58 | 59 | constructor(x, f) { 60 | if (!f) { 61 | this.#value = x 62 | return 63 | } 64 | 65 | this.#value = f() 66 | this.#f = f 67 | this.#depth = Math.max(...x.map(d => d.#depth)) + 1 68 | 69 | x.forEach(d => d.#derivatives.add(this)) 70 | } 71 | 72 | get value() { 73 | // If there are any pending updates 74 | if (FunctionalReactive.#settersQueue.size) { 75 | // Heuristic quick invalidation for derived nodes 76 | // Apply updates now, it's ok that there's already a microtask queued for this 77 | if (this.#depth !== 0) 78 | FunctionalReactive.applyUpdates() 79 | // If this is a source node that was updated, just return that 80 | // new value without actually updating any derived nodes yet 81 | else if (FunctionalReactive.#settersQueue.has(this)) 82 | return FunctionalReactive.#settersQueue.get(this) 83 | } 84 | 85 | return this.#value 86 | } 87 | 88 | set value(newValue) { 89 | // Only allow source nodes to be directly updated 90 | if (this.#depth !== 0) 91 | return 92 | 93 | // Unless asked for earlier, these updates are just queued up until the microtasks run 94 | if (!FunctionalReactive.#settersQueue.size) 95 | queueMicrotask(FunctionalReactive.applyUpdates) 96 | 97 | FunctionalReactive.#settersQueue.set(this, newValue) 98 | } 99 | 100 | addReaction(reaction) { 101 | this.#reactions.add(reaction) 102 | 103 | return () => 104 | this.#reactions.delete(reaction) 105 | } 106 | 107 | // Apply an update for a node and queue its derivatives if it actually changed 108 | #applyUpdate(newValue) { 109 | if (newValue === this.#value) 110 | return 111 | 112 | this.#value = newValue 113 | FunctionalReactive.#reactionsQueue.push(...this.#reactions) 114 | 115 | const queue = FunctionalReactive.#derivativesQueue 116 | for (const derivative of this.#derivatives) { 117 | const depth = derivative.#depth 118 | if (!queue[depth]) 119 | queue[depth] = new Set() 120 | 121 | queue[depth].add(derivative) 122 | } 123 | } 124 | 125 | // Apply pending updates from actually changed source nodes 126 | static applyUpdates() { 127 | if (!FunctionalReactive.#settersQueue.size) 128 | return 129 | 130 | // Bootstrap by applying the updates from the pending setters 131 | for (const [sourceNode, newValue] of FunctionalReactive.#settersQueue.entries()) 132 | sourceNode.#applyUpdate(newValue) 133 | FunctionalReactive.#settersQueue.clear() 134 | 135 | // Iterate down the depths, ignoring holes 136 | // Note that both the queue (Array) and each depth Set iterators update as items are added 137 | for (const depthSet of FunctionalReactive.#derivativesQueue) if (depthSet) 138 | for (const derivative of depthSet) 139 | derivative.#applyUpdate(derivative.#f()) 140 | FunctionalReactive.#derivativesQueue.length = 0 141 | 142 | // Call all reactions now that the graph has a fully consistent state 143 | for (const reaction of FunctionalReactive.#reactionsQueue) 144 | reaction() 145 | FunctionalReactive.#reactionsQueue.length = 0 146 | } 147 | } 148 | 149 | // A little convenience function 150 | export const r = (x, f) => new FunctionalReactive(x, f) 151 | 152 | // Do something with a value, updating if it is reactive 153 | export const reactiveDo = (x, f) => { 154 | if (x?.[isReactive]) { 155 | f(x.value) 156 | return x.addReaction(() => f(x.value)) 157 | } 158 | 159 | f(x) 160 | } 161 | -------------------------------------------------------------------------------- /packages/bruh/src/util/index.mjs: -------------------------------------------------------------------------------- 1 | // Create a pipeline with an initial value and a series of functions 2 | export const pipe = (x, ...fs) => 3 | fs.reduce((y, f) => f(y), x) 4 | 5 | // Dispatch a custom event to (capturing) and from (bubbling) a target (usually a DOM node) 6 | // Returns false if the event was cancelled (preventDefault()) and true otherwise 7 | // Note that this is synchronous 8 | export const dispatch = (target, type, options) => 9 | target.dispatchEvent( 10 | // Default to behave like most DOM events 11 | new CustomEvent(type, { 12 | bubbles: true, 13 | cancelable: true, 14 | composed: true, 15 | ...options 16 | }) 17 | ) 18 | 19 | // Inspired by https://antfu.me/posts/destructuring-with-object-or-array#take-away 20 | // Creates an object that is both destructable with {...} and [...] 21 | // Useful for writing library functions à la react-use & vueuse 22 | export const createDestructable = (object, iterable) => { 23 | const destructable = { 24 | ...object, 25 | [Symbol.iterator]: () => iterable[Symbol.iterator]() 26 | } 27 | 28 | Object.defineProperty(destructable, Symbol.iterator, { 29 | enumerable: false 30 | }) 31 | 32 | return destructable 33 | } 34 | 35 | // Creates an object (as a Proxy) that acts as a function 36 | // So functionAsObject(f).property is equivalent to f("property") 37 | // This is can be useful when combined with destructuring syntax, e.g.: 38 | // const { html, head, title, body, main, h1, p } = functionAsObject(e) 39 | export const functionAsObject = f => 40 | new Proxy({}, { 41 | get: (_, property) => f(property) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/bruh/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | 3 | export default defineConfig({ 4 | build: { 5 | lib: { 6 | name: "bruh", 7 | entry: new URL("./src/index.browser.mjs", import.meta.url).pathname, 8 | fileName: format => `bruh.${format}.js` 9 | }, 10 | sourcemap: true 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /packages/create-bruh/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /packages/create-bruh/.npmignore: -------------------------------------------------------------------------------- 1 | */dist/ 2 | */node_modules/ 3 | -------------------------------------------------------------------------------- /packages/create-bruh/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Ethridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/create-bruh/README.md: -------------------------------------------------------------------------------- 1 | # `create-bruh` - Quickly start working with bruh 2 | 3 | Simply run: 4 | ```sh 5 | # Using npm 6 | npm init bruh 7 | 8 | # or npx... 9 | npx create-bruh 10 | 11 | # or yarn... 12 | yarn create bruh 13 | 14 | # or pnpm... 15 | pnpx create-bruh 16 | ``` 17 | 18 | At the moment, the starter choices are: 19 | 20 | - `vite` 21 | - `minimal` 22 | 23 | `vite` is the recommended option because it enables extremely fast prerendering within vite, along with JSX. 24 | `minimal` is an example of how bruh can be used entirely without build tools (it doesn't even use npm) 25 | 26 | If you don't see a template you like, keep in mind that bruh can simply be installed with `npm i bruh` and used in any project anyways. 27 | -------------------------------------------------------------------------------- /packages/create-bruh/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { resolve } from "path" 4 | import { readFile, writeFile, rename } from "fs/promises" 5 | 6 | import prompts from "prompts" 7 | import kleur from "kleur" 8 | 9 | import { assertVersion, copy, commentedCommands } from "./utils.mjs" 10 | 11 | await assertVersion().catch(({ actual: current, expected: latest }) => { 12 | console.error(`You are running create-bruh@${current}, when the latest version is ${latest}`) 13 | console.log("This can be corrected by running:") 14 | console.log(kleur.bold().green("npm init bruh@latest")) 15 | process.exit(1) 16 | }) 17 | 18 | const questions = [ 19 | { 20 | type: "select", 21 | name: "template", 22 | message: "Choose a template (vite is recommended)", 23 | choices: [ 24 | { 25 | title: "vite", 26 | value: "./vite/", 27 | description: "The fastest and most feature-rich choice" 28 | }, 29 | { 30 | title: "minimal", 31 | value: "./minimal/", 32 | description: "The absolute simplest possible (no build tool)" 33 | } 34 | ], 35 | initial: 0 36 | }, 37 | { 38 | type: "text", 39 | name: "name", 40 | message: "What will you name this package?" 41 | }, 42 | { 43 | type: "text", 44 | name: "directory", 45 | message: "Which directory to scaffold the package? (the project's root)", 46 | initial: "./" 47 | } 48 | ] 49 | 50 | const answers = await prompts(questions, { 51 | onCancel() { 52 | console.error("You cancelled early, so nothing happened.") 53 | process.exit(1) 54 | } 55 | }) 56 | const template = answers.template 57 | const packageDirectory = resolve(process.cwd(), answers.directory) 58 | 59 | await copy( 60 | new URL(template, import.meta.url).pathname, 61 | packageDirectory 62 | ) 63 | 64 | await rename( 65 | resolve(packageDirectory, "gitignore"), 66 | resolve(packageDirectory, ".gitignore") 67 | ) 68 | 69 | const packageDotJson = resolve(packageDirectory, "package.json") 70 | const packageObject = JSON.parse( await readFile(packageDotJson) ) 71 | packageObject.name = answers.name 72 | await writeFile(packageDotJson, JSON.stringify(packageObject, null, 2)) 73 | 74 | console.log(kleur.bold().green("Done!\n")) 75 | 76 | console.log(kleur.bold("Now just:")) 77 | 78 | commentedCommands( 79 | [`cd ${answers.directory}`, "Go to your new package directory"], 80 | ["npm i", "Install dependencies"], 81 | ["npm run dev", "Start coding!"] 82 | ) 83 | -------------------------------------------------------------------------------- /packages/create-bruh/minimal/gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /packages/create-bruh/minimal/index.css: -------------------------------------------------------------------------------- 1 | /* A little style reset */ 2 | *, ::after, ::before { 3 | box-sizing: border-box; 4 | margin: 0; 5 | padding: 0; 6 | border: 0 solid; 7 | color: inherit; 8 | font-family: inherit; 9 | overflow-wrap: break-word; 10 | } 11 | 12 | :root { 13 | font-family: system-ui; 14 | font-size: clamp(1em, 1em + 1vmin, 2em); 15 | } 16 | 17 | main { 18 | width: 100vw; 19 | height: 100vh; 20 | 21 | display: grid; 22 | gap: 1em; 23 | place-items: center; 24 | padding: 1em; 25 | } 26 | 27 | main > h1 { 28 | font-size: 3em; 29 | } 30 | 31 | main > .counter { 32 | font-size: 1em; 33 | padding: 0.25em; 34 | border-radius: 0.25em; 35 | } 36 | 37 | main > .counter:hover { 38 | cursor: pointer; 39 | } 40 | -------------------------------------------------------------------------------- /packages/create-bruh/minimal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bruh... 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/create-bruh/minimal/index.mjs: -------------------------------------------------------------------------------- 1 | import "./node_modules/bruh/dist/bruh.umd.js" 2 | const { r } = bruh.reactive 3 | const { main, h1, button } = bruh.util.functionAsObject(bruh.dom.e) 4 | 5 | const Counter = () => { 6 | const count = r(0) 7 | const increment = () => count.value++ 8 | 9 | const counter = 10 | button({ class: "counter", onclick: increment }, 11 | "Click to increment: ", count 12 | ) 13 | 14 | return counter 15 | } 16 | 17 | document.body.append( 18 | main( 19 | h1("Bruh"), 20 | Counter() 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /packages/create-bruh/minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-bruh-template-minimal", 3 | "scripts": { 4 | "dev": "serve" 5 | }, 6 | "devDependencies": { 7 | "serve": "^14.0.1" 8 | }, 9 | "dependencies": { 10 | "bruh": "^1.13.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/create-bruh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-bruh", 3 | "version": "0.8.1", 4 | "description": "Quickly start working with bruh", 5 | "main": "./index.mjs", 6 | "bin": { 7 | "create-bruh": "./index.mjs" 8 | }, 9 | "dependencies": { 10 | "kleur": "^4.1.4", 11 | "node-fetch": "^3.0.0", 12 | "prompts": "^2.4.2", 13 | "semver": "^7.3.5" 14 | }, 15 | "scripts": {}, 16 | "homepage": "https://github.com/Technical-Source/bruh/tree/main/packages/create-bruh#readme", 17 | "bugs": "https://github.com/Technical-Source/bruh/issues", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Technical-Source/bruh.git", 21 | "directory": "packages/create-bruh" 22 | }, 23 | "keywords": [ 24 | "init", 25 | "create", 26 | "bruh", 27 | "app" 28 | ], 29 | "author": { 30 | "name": "Daniel Ethridge", 31 | "url": "https://git.io/de" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /packages/create-bruh/utils.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { resolve } from "path" 4 | import { mkdir, readdir, stat, copyFile, readFile } from "fs/promises" 5 | import { execSync } from "child_process" 6 | import fetch from "node-fetch" 7 | 8 | import kleur from "kleur" 9 | import semver from "semver" 10 | import { AssertionError } from "assert/strict" 11 | 12 | export const assertVersion = async () => { 13 | const { version } = JSON.parse( 14 | await readFile( 15 | new URL("./package.json", import.meta.url).pathname 16 | ) 17 | ) 18 | const latestVersion = await fetch("https://registry.npmjs.org/-/package/create-bruh/dist-tags") 19 | .then(res => res.json()) 20 | .then(data => data.latest) 21 | .catch(() => { 22 | try { 23 | return execSync("npm view create-bruh version").toString().trim() 24 | } catch {} 25 | }) 26 | 27 | if (latestVersion && semver.lt(version, latestVersion)) 28 | throw new AssertionError({ 29 | actual: version, 30 | expected: latestVersion 31 | }) 32 | } 33 | 34 | const copyDirectory = async (from, to) => { 35 | await mkdir(to, { recursive: true }) 36 | 37 | return Promise.all( 38 | (await readdir(from)) 39 | .map(entry => 40 | copy( 41 | resolve(from, entry), 42 | resolve(to, entry) 43 | ) 44 | ) 45 | ) 46 | } 47 | 48 | export const copy = async (from, to) => { 49 | const statResult = await stat(from) 50 | 51 | if (statResult.isDirectory()) 52 | return copyDirectory(from, to) 53 | else 54 | return copyFile(from, to) 55 | } 56 | 57 | export const commentedCommands = (...lines) => { 58 | const commandLength = Math.max( 59 | ...lines.map(([command]) => command.length) 60 | ) 61 | lines 62 | .map(([command, comment]) => [command.padEnd(commandLength), comment]) 63 | .map(([command, comment]) => [command, kleur.gray(`# ${comment}`)]) 64 | .map(([command, comment]) => `${command} ${comment}`) 65 | .forEach(line => console.log(line)) 66 | } 67 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/components/counter/hydrate.mjs: -------------------------------------------------------------------------------- 1 | import { hydrateTextNodes, t } from "bruh/dom" 2 | import { r } from "bruh/reactive" 3 | 4 | const { counterNumber } = hydrateTextNodes() 5 | const count = r(0) 6 | counterNumber.replaceWith(t(count)) 7 | 8 | document.querySelector(".counter") 9 | .addEventListener("click", () => count.value++) 10 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/components/counter/render.jsx: -------------------------------------------------------------------------------- 1 | import { t } from "bruh/dom" 2 | 3 | const counterNumber = t(0).setTag("counterNumber") 4 | 5 | export default () => 6 | 9 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | .DS_Store 3 | dist/ 4 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/index.css: -------------------------------------------------------------------------------- 1 | /* A little style reset */ 2 | *, ::after, ::before { 3 | box-sizing: border-box; 4 | margin: 0; 5 | padding: 0; 6 | border: 0 solid; 7 | color: inherit; 8 | font-family: inherit; 9 | overflow-wrap: break-word; 10 | } 11 | 12 | :root { 13 | font-family: system-ui; 14 | font-size: clamp(1em, 1em + 1vmin, 2em); 15 | } 16 | 17 | main { 18 | width: 100vw; 19 | height: 100vh; 20 | 21 | display: grid; 22 | gap: 1em; 23 | place-items: center; 24 | padding: 1em; 25 | 26 | & > h1 { 27 | font-size: 3em; 28 | } 29 | 30 | & > .counter { 31 | font-size: 1em; 32 | padding: 0.25em; 33 | border-radius: 0.25em; 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/index.html.jsx: -------------------------------------------------------------------------------- 1 | import Shell from "./shell" 2 | import Counter from "./components/counter/render" 3 | 4 | export default () => 5 | 10 |
11 |

Bruh

12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/index.mjs: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | import "./components/counter/hydrate.mjs" 3 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-bruh-template-vite", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "serve": "vite preview" 7 | }, 8 | "devDependencies": { 9 | "postcss-preset-env": "^7.8.1", 10 | "vite": "^3.1.0", 11 | "vite-plugin-bruh": "^0.6.0" 12 | }, 13 | "dependencies": { 14 | "bruh": "^1.13.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/postcss.config.js: -------------------------------------------------------------------------------- 1 | const presetEnv = require("postcss-preset-env") 2 | 3 | module.exports = { 4 | plugins: [ 5 | presetEnv({ 6 | features: { 7 | "nesting-rules": true, 8 | "custom-selectors": true, 9 | "custom-media-queries": true 10 | } 11 | }) 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/create-bruh/vite/shell.jsx: -------------------------------------------------------------------------------- 1 | export default async ({ 2 | title = "", 3 | description = "", 4 | InHead = () => [], 5 | css = [], 6 | js = [], 7 | children 8 | }) => 9 | "" + 10 | 11 | 12 | { title } 13 | 14 | 15 | 16 | 17 | 18 | { 19 | css.map(href => 20 | 21 | ) 22 | } 23 | 24 | { 25 | js.map(src => 26 |