From 7a08efb568b0e9c8d10d14f3be3c9225eb2a794e Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Wed, 16 Jul 2025 22:53:53 -0700 Subject: [PATCH] Introducing... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ░▒▓██████▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒▒▓███▓▒░▒▓█▓▒░ ░▒▓██████▓▒░░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░░▒▓████████▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ --- .../use-bun-instead-of-node-vite-npm-pnpm.mdc | 117 +++++++++++++ packages/glyph/.cursor/rules/workshop.mdc | 23 +++ packages/glyph/.gitignore | 34 ++++ packages/glyph/README.md | 15 ++ packages/glyph/example.tsx | 26 +++ packages/glyph/index.ts | 162 ++++++++++++++++++ packages/glyph/package.json | 14 ++ packages/glyph/tsconfig.json | 29 ++++ 8 files changed, 420 insertions(+) create mode 100644 packages/glyph/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc create mode 100644 packages/glyph/.cursor/rules/workshop.mdc create mode 100644 packages/glyph/.gitignore create mode 100644 packages/glyph/README.md create mode 100644 packages/glyph/example.tsx create mode 100644 packages/glyph/index.ts create mode 100644 packages/glyph/package.json create mode 100644 packages/glyph/tsconfig.json diff --git a/packages/glyph/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/packages/glyph/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 100644 index 0000000..97efeb5 --- /dev/null +++ b/packages/glyph/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1,117 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/packages/glyph/.cursor/rules/workshop.mdc b/packages/glyph/.cursor/rules/workshop.mdc new file mode 100644 index 0000000..7392f5e --- /dev/null +++ b/packages/glyph/.cursor/rules/workshop.mdc @@ -0,0 +1,23 @@ +--- +description: The Workshop's JS/TS style. +alwaysApply: false +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx" +--- + +- No semicolons — ever. +- No comments — ever. +- 2‑space indentation. +- Double quotes for strings. +- Trailing commas where ES5 allows (objects, arrays, imports). +- Keep lines <= 100 characters. +- End every file with a single newline. + +- This project runs on Bun. +- Assume `strict` mode is on (no implicit `any`). +- Prefer `const`; use `let` only when reassignment is required. +- Avoid the `any` type unless unavoidable. +- Use `import type { … }` when importing only types. +- In TypeScript files put npm imports first, then std imports, then library imports. + +- Respond in a concise, direct tone. +- Do not ask follow‑up questions unless clarification is essential. \ No newline at end of file diff --git a/packages/glyph/.gitignore b/packages/glyph/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/packages/glyph/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/glyph/README.md b/packages/glyph/README.md new file mode 100644 index 0000000..cbc2450 --- /dev/null +++ b/packages/glyph/README.md @@ -0,0 +1,15 @@ +# @workshop/glyph + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/glyph/example.tsx b/packages/glyph/example.tsx new file mode 100644 index 0000000..b2f1d02 --- /dev/null +++ b/packages/glyph/example.tsx @@ -0,0 +1,26 @@ + + card: VStack max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md + name: h3 text-2xl font-bold mb-4 text-gray-800 + description: p text-gray-600 mb-6 leading-relaxed + socials: div inline-flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-200 + + + + Bobby Tables + + Bobby is a software engineer and the creator of the popular web development framework, Treeact. He is a true master of all things front-end, and his work has been praised by developers and non-developers alike. + + + + + + + + Roger Tables + + Roger is a software engineer and the creator of no popular web development frameworks. He is a true lover of all things front-end, but his work has not been praised by developers and non-developers alike. + + + + + \ No newline at end of file diff --git a/packages/glyph/index.ts b/packages/glyph/index.ts new file mode 100644 index 0000000..c8210cf --- /dev/null +++ b/packages/glyph/index.ts @@ -0,0 +1,162 @@ +import { parseFragment as parse, serialize } from "parse5" + +type TagDefinitions = Record + +type Node = { + nodeName: string + tagName: string + parentNode: Node | null + childNodes: Node[] + value: string + attrs: { name: string, value: string }[] +} + +// search for node +function findTagsNode(node: Node): Node | null { + if (node.nodeName === "tags" && node.childNodes) { + return node + } + + if (node.childNodes) { + for (const child of node.childNodes) { + const result = findTagsNode(child) + if (result) return result + } + } + + return null +} + +// pull out 's inner text +function tagsNodeText(node: Node): string | null { + if (node && node.childNodes) { + const textNode = node.childNodes.find((n: Node) => n.nodeName === "#text") + if (textNode) + return textNode.value + } + return null +} + +// turn what's inside into a map of tag definitions +// ignores //comment lines +// format is `tag: parent class1 class2` +function parseTagDefinitions(text: string): TagDefinitions { + const tags: TagDefinitions = {} + text.split("\n") + .forEach(line => { + line = line.trim() + if (line === "") return + if (line.startsWith("//")) return + + const [name, definition] = line.split(/:(.+)/) + if (!name || !definition) { + console.error(`Invalid tag definition: ${line}`) + return + } + + const [parent, ...classes] = definition.trim().split(" ") + if (!parent) { + console.error(`Invalid tag definition: ${line}`) + return + } + + tags[name.trim()] = [parent.trim(), classes] + }) + + return tags +} + +// using our tag definitions, replace the tags in the AST with the real tags w/ classes +function replaceTags(ast: Node, tagDefinitions: TagDefinitions): string { + const tags = Object.keys(tagDefinitions) + + for (const tag of tags) { + const [parent, classes] = tagDefinitions[tag] as [string, string[]] + const parentNodes = findTagsByName(ast, tag) + for (const parentNode of parentNodes) { + if (parentNode) { + parentNode.tagName = parent + parentNode.nodeName = parent + if (parentNode.attrs.length === 0) { + parentNode.attrs = [{ name: "class", value: classes.join(" ") }] + } else { + const classAttr = parentNode.attrs.find((a: any) => a.name === "class") + if (classAttr) { + classAttr.value += " " + classes.join(" ") + } else { + parentNode.attrs.push({ name: "class", value: classes.join(" ") }) + } + } + } + } + } + + // delete node + const tagsIndex = ast.childNodes.findIndex((n: Node) => n.nodeName === "tags") + if (tagsIndex !== -1) { + ast.childNodes.splice(tagsIndex, 1) + } + + return serialize(ast as any) +} + +// find tag by name in the AST +function findTagByName(ast: Node, tag: string): Node | null { + if (ast.nodeName === tag) { + return ast + } + + if (ast.childNodes) { + for (const child of ast.childNodes) { + const result = findTagByName(child, tag) + if (result) return result + } + } + + return null +} + +// find tags by name in the AST +function findTagsByName(ast: Node, tag: string): Node[] { + const tags: Node[] = [] + if (ast.nodeName === tag) { + tags.push(ast) + } + if (ast.childNodes) { + for (const child of ast.childNodes) { + tags.push(...findTagsByName(child, tag)) + } + } + return tags +} + +// transform string HTML w/ into new HTML w/ + friends replaced by new HTML +function transform(content: string): string { + const ast = parse(content) as Node + + const tagsNode = findTagsNode(ast) + if (!tagsNode) { + console.error("No element found") + return content + } + + const tagsText = tagsNodeText(tagsNode) + if (!tagsText) { + console.error("No text found") + return content + } + + const tagDefinitions = parseTagDefinitions(tagsText) + + return replaceTags(ast, tagDefinitions) +} + +async function main() { + const content = await Bun.file("example.tsx").text() + const transformed = transform(content) + console.log(transformed.trim()) +} + +if (import.meta.main) { + main() +} diff --git a/packages/glyph/package.json b/packages/glyph/package.json new file mode 100644 index 0000000..a5eb264 --- /dev/null +++ b/packages/glyph/package.json @@ -0,0 +1,14 @@ +{ + "name": "@workshop/glyph", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "parse5": "^8.0.0" + } +} diff --git a/packages/glyph/tsconfig.json b/packages/glyph/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/packages/glyph/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}