├── .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 |
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 |
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 |
71 | Click to increment: { count }
72 |
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 |
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 = `${this.name}>`
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 |
7 | Click to increment: { counterNumber }
8 |
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 |
27 | )
28 | }
29 |
30 |
31 |
32 |
33 | { children }
34 |
35 |
--------------------------------------------------------------------------------
/packages/create-bruh/vite/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 | import bruh from "vite-plugin-bruh"
3 |
4 | export default defineConfig({
5 | plugins: [
6 | bruh()
7 | ]
8 | })
9 |
--------------------------------------------------------------------------------
/packages/vite-plugin-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/vite-plugin-bruh/README.md:
--------------------------------------------------------------------------------
1 | # `vite-plugin-bruh` - A vite plugin to integrate with bruh
2 |
3 | ## Install
4 |
5 | `npm i -D vite-plugin-bruh` or use `npm init bruh` with the `vite` template to quickly get started.
6 |
7 | ## Use
8 |
9 | Example `vite.config.mjs` file:
10 | ```javascript
11 | import { defineConfig } from "vite"
12 | import bruh from "vite-plugin-bruh"
13 |
14 | export default defineConfig({
15 | plugins: [
16 | bruh({
17 | // Regex for the page render file extention
18 | // Defaults to /\.html\.(mjs|jsx?|tsx?)$/
19 | htmlRenderFileExtention,
20 | // Absolute path of the root pages directory
21 | // Defaults to vite's root
22 | root,
23 | // Options for the MDX compiler (xdm)
24 | // Documentation at https://github.com/wooorm/xdm#compilefile-options
25 | // e.g. for adding syntax highlighting by setting to { rehypePlugins: [ await import("@mapbox/rehype-prism") ] }
26 | xdmOptions
27 | })
28 | ]
29 | })
30 | ```
31 |
32 | This allows you to use the typical `vite` for development and `vite build` for production.
33 | `vite-plugin-bruh` will automatically allow you to prerender html files before vite sees them.
34 |
35 | Here is an example project structure:
36 | ```
37 | .
38 | ├── index.css
39 | ├── index.html.jsx
40 | ├── index.mjs
41 | ├── package-lock.json
42 | ├── package.json
43 | └── vite.config.mjs
44 | ```
45 |
46 | ## How it works
47 |
48 | Upon a page request for `/x` in dev:
49 | 1. The `x.html.mjs` (or `x/index.html.mjs`, `...js/jsx/ts/tsx`) file is imported
50 | 2. The default export is called and `await`ed
51 | 3. The returned string is exposed to vite as if it were from `x.html` (or `x/index.html`)
52 |
53 | At build time, all `x.html.mjs` files are automatically included as entry points (as if they were `x.html`)
54 |
55 | If this is `index.html.mjs`:
56 | ```javascript
57 | export default async () =>
58 | `
59 |
60 |
61 | ...
62 |
63 |
64 |
65 | ...
66 |
67 |
68 | `
69 | ```
70 |
71 | Vite sees this as if `index.html` existed and contained:
72 | ```html
73 |
74 |
75 |
76 | ...
77 |
78 |
79 |
80 | ...
81 |
82 |
83 | ```
84 |
85 | During dev, vite will automatically and quickly reload the page as `index.html.mjs` and its imports are edited.
86 |
87 | ## JSX/TSX
88 |
89 | This plugin automatically includes jsx support for bruh, meaning that you can freely write jsx content in both
90 | render files (`x.html.jsx`) and hydrate files (`x.jsx`, what vite typically handles).
91 |
92 | ## Current Caveats
93 |
94 | For MDX support, there is a workaround that replaces (in mdx files) any `className` strings with `class`.
95 | You probably won't run into that problem before it is fixed in a more correct way, but it can be solved
96 | by just writing `"class" + "Name"`, `"class\u004eame"`, `className` or something similar.
97 |
--------------------------------------------------------------------------------
/packages/vite-plugin-bruh/index.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises"
2 | import path from "path"
3 | import vite from "vite"
4 | import { compile } from "xdm"
5 |
6 | const mdx = ({ xdmOptions } = {}) => {
7 | return {
8 | name: "bruh:mdx",
9 | enforce: "pre",
10 |
11 | async transform(source, id) {
12 | if (!id.endsWith(".mdx"))
13 | return
14 |
15 | const result = await compile(source, {
16 | jsxRuntime: "classic",
17 | pragma: "h",
18 | pragmaFrag: "JSXFragment",
19 | ...xdmOptions
20 | })
21 |
22 | const code = result.value
23 | .replace(
24 | `import h from "react"`,
25 | `import { h, JSXFragment } from "bruh/dom"`
26 | )
27 | .replace(
28 | /classname/igm,
29 | "class"
30 | )
31 |
32 | return {
33 | code,
34 | map: { mappings: "" }
35 | }
36 | }
37 | }
38 | }
39 |
40 | const excludeEntry = (entry, directory) =>
41 | entry.isDirectory() && entry.name == "node_modules"
42 |
43 | const getHtmlRenderFiles = async (directory, htmlRenderFileExtention, maxDepth = Infinity) => {
44 | if (maxDepth < 1)
45 | return []
46 |
47 | try {
48 | const entries = await fs.readdir(directory, { withFileTypes: true })
49 | const unflattenedFiles = await Promise.all(
50 | entries
51 | .map(async entry => {
52 | if (excludeEntry(entry, directory))
53 | return []
54 |
55 | const entryPath = path.join(directory, entry.name)
56 |
57 | if (entry.isDirectory())
58 | return getHtmlRenderFiles(entryPath, htmlRenderFileExtention, maxDepth - 1)
59 | if (htmlRenderFileExtention.test(entry.name))
60 | return [entryPath]
61 |
62 | return []
63 | })
64 | )
65 | return unflattenedFiles.flat()
66 | }
67 | catch {
68 | return []
69 | }
70 | }
71 |
72 | export const bruhDev = ({ htmlRenderFileExtention, root } = {}) => {
73 | let config = {}
74 |
75 | const urlToHtmlRenderFile = async url => {
76 | const resolvedRoot = root || path.resolve(config.root || "")
77 | const pathname = path.join(resolvedRoot, path.normalize(url))
78 | const htmlRenderFiles = await getHtmlRenderFiles(path.dirname(pathname), htmlRenderFileExtention, 2)
79 | for (const htmlRenderFile of htmlRenderFiles) {
80 | const htmlRenderFileName = htmlRenderFile.replace(htmlRenderFileExtention, "")
81 | if (htmlRenderFileName == pathname)
82 | return htmlRenderFile
83 | if (htmlRenderFileName == path.join(pathname, "index"))
84 | return htmlRenderFile
85 | }
86 | }
87 |
88 | return {
89 | name: "bruh:dev",
90 | apply: "serve",
91 | enforce: "pre",
92 |
93 | configResolved(resolvedConfig) {
94 | config = resolvedConfig
95 | },
96 |
97 | configureServer(viteDevServer) {
98 | viteDevServer.middlewares.use(async (req, res, next) => {
99 | try {
100 | const htmlRenderFile = await urlToHtmlRenderFile(req.url)
101 | if (htmlRenderFile) {
102 | const { default: render } = await viteDevServer.ssrLoadModule(htmlRenderFile)
103 | const rendered = await render()
104 | const transformedHTML = await viteDevServer.transformIndexHtml(req.url, rendered.toString())
105 |
106 | res.setHeader("Content-Type", "text/html")
107 | return res.end(transformedHTML)
108 | }
109 | next()
110 | }
111 | catch (error) {
112 | viteDevServer.ssrFixStacktrace(error)
113 | console.error(error)
114 |
115 | res.statusCode = 500
116 | return res.end(error.stack)
117 | }
118 | })
119 | }
120 | }
121 | }
122 |
123 | export const bruhBuild = ({ htmlRenderFileExtention, root } = {}) => {
124 | let viteDevServer
125 |
126 | const idToHtmlRenderFile = {}
127 |
128 | return {
129 | name: "bruh:build",
130 | apply: "build",
131 | enforce: "pre",
132 |
133 | async buildStart() {
134 | viteDevServer = await vite.createServer()
135 | },
136 |
137 | async resolveId(source) {
138 | if (htmlRenderFileExtention.test(source)) {
139 | const id = source.replace(htmlRenderFileExtention, ".html")
140 | idToHtmlRenderFile[id] = source
141 | return id
142 | }
143 | },
144 |
145 | async load(id) {
146 | if (!idToHtmlRenderFile[id])
147 | return
148 |
149 | const { default: render } = await viteDevServer.ssrLoadModule(idToHtmlRenderFile[id])
150 | const rendered = await render()
151 | return {
152 | code: rendered,
153 | map: ""
154 | }
155 | },
156 |
157 | async closeBundle() {
158 | return viteDevServer.close()
159 | },
160 |
161 | // Add all page render files to the build inputs
162 | async config(config) {
163 | const resolvedRoot = root || path.resolve(config.root || "")
164 | const htmlRenderFiles = await getHtmlRenderFiles(resolvedRoot, htmlRenderFileExtention)
165 |
166 | const input = Object.fromEntries(
167 | htmlRenderFiles
168 | .map(pathname => {
169 | const name = path.relative(resolvedRoot, pathname).replace(htmlRenderFileExtention, "")
170 | return [name, pathname]
171 | })
172 | )
173 |
174 | return {
175 | build: {
176 | rollupOptions: {
177 | input
178 | }
179 | }
180 | }
181 | }
182 | }
183 | }
184 |
185 | export const bruhJSX = () => {
186 | return {
187 | name: "bruh:jsx",
188 |
189 | config() {
190 | return {
191 | esbuild: {
192 | jsxFactory: "h",
193 | jsxFragment: "JSXFragment",
194 | jsxInject: `import { h, JSXFragment } from "bruh/dom"`
195 | }
196 | }
197 | }
198 | }
199 | }
200 |
201 | export const bruh = ({
202 | htmlRenderFileExtention = /\.html\.(mjs|jsx?|tsx?)$/,
203 | root,
204 | xdmOptions = {}
205 | } = {}) =>
206 | [
207 | mdx({
208 | xdmOptions
209 | }),
210 | bruhDev({
211 | htmlRenderFileExtention,
212 | root
213 | }),
214 | bruhBuild({
215 | htmlRenderFileExtention,
216 | root
217 | }),
218 | bruhJSX()
219 | ]
220 |
221 | export default bruh
222 |
--------------------------------------------------------------------------------
/packages/vite-plugin-bruh/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-plugin-bruh",
3 | "version": "0.6.0",
4 | "description": "A vite plugin to integrate with bruh",
5 | "type": "module",
6 | "main": "./index.mjs",
7 | "scripts": {},
8 | "homepage": "https://github.com/Technical-Source/bruh/tree/main/packages/vite-plugin-bruh#readme",
9 | "bugs": "https://github.com/Technical-Source/bruh/issues",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/Technical-Source/bruh.git",
13 | "directory": "packages/vite-plugin-bruh"
14 | },
15 | "keywords": [
16 | "vite-plugin",
17 | "bruh",
18 | "prerender"
19 | ],
20 | "author": {
21 | "name": "Daniel Ethridge",
22 | "url": "https://git.io/de"
23 | },
24 | "license": "MIT",
25 | "peerDependencies": {
26 | "vite": "^2.6.10"
27 | },
28 | "dependencies": {
29 | "xdm": "^3.1.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------