diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ba7d550
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+This repository contains multiple packages. Changelogs are maintained per package:
+
+- [`@explodingcamera/css`](packages/css/CHANGELOG.md)
+- [`minify-literals`](packages/minify-literals/CHANGELOG.md)
+- [`rollup-plugin-minify-template-literals`](packages/rollup-plugin-minify-template-literals/CHANGELOG.md)
+- [`simplejsx`](packages/simplejsx/CHANGELOG.md)
+- [`spaify`](packages/spaify/CHANGELOG.md)
diff --git a/README.md b/README.md
index 38f3d4e..bcf4082 100644
--- a/README.md
+++ b/README.md
@@ -10,20 +10,21 @@
## Projects
-| Package | | Description |
-| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
-| [`@explodingcamera/css`](./packages/css) | [](https://paka.dev/npm/@explodingcamera/css) [](https://www.npmjs.com/package/@explodingcamera/css) | Small CSS reset/base files. |
-| [`minify-literals`](./packages/minify-literals) | [](https://paka.dev/npm/minify-literals) [](https://www.npmjs.com/package/minify-literals) | Minify CSS and HTML literals. |
-| [`rollup-plugin-minify-template-literals`](./packages/rollup-plugin-minify-template-literals) | [](https://paka.dev/npm/rollup-plugin-minify-template-literals) [](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) | Vite/Rollup plugin that minifies template literals. |
-| [`spaify`](./packages/spaify) | [](https://paka.dev/npm/spaify) [](https://www.npmjs.com/package/spaify) | Seamless page transitions for static sites. |
+| Package | | Description |
+| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
+| [`@explodingcamera/css`](./packages/css) | [](https://www.jsdocs.io/package/@explodingcamera/css) [](https://www.npmjs.com/package/@explodingcamera/css) | Small CSS reset/base files. |
+| [`minify-literals`](./packages/minify-literals) | [](https://www.jsdocs.io/package/minify-literals) [](https://www.npmjs.com/package/minify-literals) | Minify CSS and HTML literals. |
+| [`rollup-plugin-minify-template-literals`](./packages/rollup-plugin-minify-template-literals) | [](https://www.jsdocs.io/package/rollup-plugin-minify-template-literals) [](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) | Vite/Rollup plugin that minifies template literals. |
+| [`simplejsx`](./packages/simplejsx) | [](https://www.jsdocs.io/package/simplejsx) [](https://www.npmjs.com/package/simplejsx) | Minimal JSX templating for safe HTML strings. |
+| [`spaify`](./packages/spaify) | [](https://www.jsdocs.io/package/spaify) [](https://www.npmjs.com/package/spaify) | Seamless page transitions for static sites. |
## Packages Contained In Other Repositories
Please open issues and pull requests for these packages in their respective repositories.
-| Package | | Description |
-| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
-| [`subsonic-api`](https://github.com/explodingcamera/subsonic-api) | [](https://paka.dev/npm/subsonic-api) [](https://www.npmjs.com/package/subsonic-api) | API library for Subsonic-compatible servers. |
+| Package | | Description |
+| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
+| [`subsonic-api`](https://github.com/explodingcamera/subsonic-api) | [](https://www.jsdocs.io/package/subsonic-api) [](https://www.npmjs.com/package/subsonic-api) | API library for Subsonic-compatible servers. |
## Deprecated Packages
diff --git a/biome.json b/biome.json
index d45f3cd..8b44ad6 100644
--- a/biome.json
+++ b/biome.json
@@ -30,7 +30,8 @@
},
"complexity": {
"noBannedTypes": "off",
- "noForEach": "off"
+ "noForEach": "off",
+ "useLiteralKeys": "off"
}
}
}
diff --git a/bun.lock b/bun.lock
index d67cbe5..5bf87e9 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,13 +10,24 @@
"typescript": "^6.0.3",
},
},
+ "examples": {
+ "name": "examples",
+ "version": "0.0.0",
+ "dependencies": {
+ "simplejsx": "workspace:*",
+ },
+ "devDependencies": {
+ "bun-types": "^1.3.14",
+ "typescript": "^6.0.3",
+ },
+ },
"packages/css": {
"name": "@explodingcamera/css",
"version": "0.0.4",
},
"packages/minify-literals": {
"name": "minify-literals",
- "version": "2.0.0",
+ "version": "2.0.2",
"dependencies": {
"@sveltejs/acorn-typescript": "^1.0.10",
"acorn": "^8.16.0",
@@ -36,6 +47,13 @@
"rollup": "^4.61.0",
},
},
+ "packages/simplejsx": {
+ "name": "simplejsx",
+ "version": "0.0.1",
+ "dependencies": {
+ "csstype": "^3.2.3",
+ },
+ },
"packages/spaify": {
"name": "spaify",
"version": "0.0.8",
@@ -217,6 +235,8 @@
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -237,6 +257,8 @@
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "examples": ["examples@workspace:examples"],
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -307,6 +329,8 @@
"semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
+ "simplejsx": ["simplejsx@workspace:packages/simplejsx"],
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
diff --git a/examples/package.json b/examples/package.json
new file mode 100644
index 0000000..6d47d82
--- /dev/null
+++ b/examples/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "examples",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "simplejsx": "workspace:*"
+ },
+ "devDependencies": {
+ "bun-types": "^1.3.14",
+ "typescript": "^6.0.3"
+ }
+}
diff --git a/examples/simplejsx/README.md b/examples/simplejsx/README.md
new file mode 100644
index 0000000..e692ca1
--- /dev/null
+++ b/examples/simplejsx/README.md
@@ -0,0 +1,9 @@
+# simplejsx example
+
+Run this example from the repo root:
+
+```bash
+bun run build
+bun examples/simplejsx/index.tsx
+bun examples/simplejsx/middleware.tsx
+```
diff --git a/examples/simplejsx/index.tsx b/examples/simplejsx/index.tsx
new file mode 100644
index 0000000..4abee7a
--- /dev/null
+++ b/examples/simplejsx/index.tsx
@@ -0,0 +1,40 @@
+/** @jsxImportSource simplejsx */
+import { renderAsync, unsafeHTML, type PropsWithChildren } from "simplejsx";
+
+function Document({ children }: PropsWithChildren) {
+ return (
+
+
+
+
+
+ {children}
+
+ );
+}
+
+async function Page() {
+ const name = await Promise.resolve("");
+
+ return (
+ <>
+
+ simplejsx example
+
+
+
+ Hello {name}
+ Text is escaped by default.
+ {unsafeHTML(" ")}
+
+ >
+ );
+}
+
+const html = await renderAsync(
+
+
+ ,
+);
+
+console.log(`${html}`);
diff --git a/examples/simplejsx/middleware.tsx b/examples/simplejsx/middleware.tsx
new file mode 100644
index 0000000..5cbedaf
--- /dev/null
+++ b/examples/simplejsx/middleware.tsx
@@ -0,0 +1,52 @@
+/** @jsxImportSource simplejsx */
+import { renderAsync, type ElementMiddleware, type PropsWithChildren } from "simplejsx";
+
+function Document({ children }: PropsWithChildren) {
+ return (
+
+
+
+ simplejsx middleware
+
+ {children}
+
+ );
+}
+
+const externalLinks: ElementMiddleware = ({ tag, props }) => {
+ if (tag !== "a" || typeof props["href"] !== "string") return;
+ if (!props["href"].startsWith("https://")) return;
+
+ return { ...props, target: "_blank", rel: "noreferrer" };
+};
+
+const imageMetadata: ElementMiddleware = async ({ tag, props }) => {
+ if (tag !== "img" || typeof props["src"] !== "string") return;
+
+ const size = await getImageSize(props["src"]);
+ return {
+ ...props,
+ loading: props["loading"] ?? "lazy",
+ width: size.width,
+ height: size.height,
+ };
+};
+
+async function getImageSize(_src: string) {
+ return { width: 1200, height: 800 };
+}
+
+const html = await renderAsync(
+
+
+ Element middleware
+ External link
+
+
+ ,
+ {
+ element: [externalLinks, imageMetadata],
+ },
+);
+
+console.log(`${html}`);
diff --git a/examples/tsconfig.json b/examples/tsconfig.json
new file mode 100644
index 0000000..6ab5480
--- /dev/null
+++ b/examples/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "jsxImportSource": "simplejsx",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["bun-types"],
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "noUncheckedIndexedAccess": true,
+ "verbatimModuleSyntax": true
+ },
+ "include": ["**/*.tsx"]
+}
diff --git a/package.json b/package.json
index 3aa10af..e076fc1 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,11 @@
{
"workspaces": [
+ "examples",
"packages/*"
],
"scripts": {
"build": "bun run --bun tsdown",
- "check": "biome check packages",
+ "check": "biome check packages examples",
"typecheck": "tsc --noEmit",
"test": "bun test"
},
diff --git a/packages/css/CHANGELOG.md b/packages/css/CHANGELOG.md
index 9f242cc..880635d 100644
--- a/packages/css/CHANGELOG.md
+++ b/packages/css/CHANGELOG.md
@@ -1,19 +1,14 @@
-# @explodingcamera/css
+# Changelog
-## 0.0.4
+All notable changes to this project will be documented in this file.
-### Patch Changes
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- fix: update build scripts
+## [0.0.4]
-## 0.0.3
+- Update build scripts.
-### Patch Changes
+## [0.0.2]
-- [`7adf233`](https://github.com/explodingcamera/esm/commit/7adf23335c7ae82b5237e3bb8e5288fdaf78b701) - chore: update dependencies
-
-## 0.0.2
-
-### Patch Changes
-
-- [`ca412ad`](https://github.com/explodingcamera/esm/commit/ca412ad9bfbac3860f8ea579f9a9282e5cd60bbe) - initial release
+- Initial release.
diff --git a/packages/css/LICENSE.md b/packages/css/LICENSE.md
new file mode 100644
index 0000000..bd0e8bf
--- /dev/null
+++ b/packages/css/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2023-2026 Henry Gressmann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/css/package.json b/packages/css/package.json
index 69f6841..05bd5e8 100644
--- a/packages/css/package.json
+++ b/packages/css/package.json
@@ -4,10 +4,26 @@
"description": "",
"keywords": [],
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/css",
- "repository": "https://github.com/explodingcamera/esm",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/explodingcamera/esm.git",
+ "directory": "packages/css"
+ },
"license": "MIT",
"author": "Henry Gressmann ",
"type": "module",
+ "sideEffects": [
+ "*.css",
+ "*.js"
+ ],
+ "files": [
+ "*.css",
+ "*.js",
+ "CHANGELOG.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
"exports": {
"./reset": "./reset.js",
"./reset.css": "./reset.css",
diff --git a/packages/minify-literals/CHANGELOG.md b/packages/minify-literals/CHANGELOG.md
index 8feb303..518b659 100644
--- a/packages/minify-literals/CHANGELOG.md
+++ b/packages/minify-literals/CHANGELOG.md
@@ -1,138 +1,61 @@
-# minify-literals
+# Changelog
-## 2.0.2
+All notable changes to this project will be documented in this file.
-### Patch Changes
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- Fixed stale build artifacts in the npm package
+## [2.0.2]
-## 2.0.1
+- Fix stale build artifacts in the npm package.
-### Patch Changes
+## [2.0.1]
-- Replace `parse-literals` with a local implementation
+- Replace `parse-literals` with a local implementation.
-## 2.0.0
+## [2.0.0]
-### Major Changes
+- Replace html-minifier-terser and clean-css with html-minifier-next and Lightning CSS.
+- Simplify the options API.
+- Remove custom strategy, parser, and MagicString hooks in favor of `html`, `css`, `htmlTags`, `cssTags`, and `sourceMap` options.
-- Replace html-minifier-terser/clean-css with html-minifier-next/Lightning CSS and simplify the options API.
-- Remove custom strategy/parser/MagicString hooks: use `html`, `css`, `htmlTags`, `cssTags`, and `sourceMap` options instead.
+## [1.0.10]
-## 1.0.10
+- Update build scripts.
-### Patch Changes
+## [1.0.6]
-- fix: update build scripts
+- Fix linting issues.
-## 1.0.9
+## [1.0.0]
-### Patch Changes
+- Mark `minify-literals` and `rollup-plugin-minify-template-literals` as stable.
-- [`7adf233`](https://github.com/explodingcamera/esm/commit/7adf23335c7ae82b5237e3bb8e5288fdaf78b701) - chore: update dependencies
+## [0.2.3]
-## 1.0.8
+- Fix issues with key-value pairs.
-### Patch Changes
+## [0.2.2]
-- [`2b328ea`](https://github.com/explodingcamera/esm/commit/2b328eacef3fe12dcce1587d4f7f3dce14f26764) - update dependencies
+- Add API docs link to README.
-## 1.0.7
+## [0.2.1]
-### Patch Changes
+- Fix HTML and CSS comments.
+- Fix template literals used as property names.
-- update dependencies
+## [0.2.0]
-- [`3169f4f`](https://github.com/explodingcamera/esm/commit/3169f4f5924f4e870bf25910ab2e9c79fd718057) - chore: update dependencies
+- Clean up `package.json`.
-## 1.0.6
+## [0.1.0]
-### Patch Changes
+- Fix various open bugs.
-- [`7f1660c`](https://github.com/explodingcamera/esm/commit/7f1660c88677547d9eafb8ab30ff9032a75b42be) - fix linting issues
+## [0.0.2]
-- chore: update dependencies
+- Fix npm `package.json`.
-## 1.0.5
+## [0.0.1]
-### Patch Changes
-
-- [`ac9f992`](https://github.com/explodingcamera/esm/commit/ac9f992ea5b540beddaea7feb002a76ccaccb29d) - chore: update dependencies
-
-- [`28b2765`](https://github.com/explodingcamera/esm/commit/28b276523ad007ab9ae0402a7d5d5b7360f1d7ed) - chore: update dependencies
-
-## 1.0.4
-
-### Patch Changes
-
-- [`29a9ef1`](https://github.com/explodingcamera/esm/commit/29a9ef118db1f1184f4f599b2806e25b1c41187b) - chore: update dependencies
-
-## 1.0.3
-
-### Patch Changes
-
-- chore: update dependencies
-
-## 1.0.2
-
-### Patch Changes
-
-- chore: update dependencies
-
-- [`575efe3`](https://github.com/explodingcamera/esm/commit/575efe385756abd44408d83535c27c99ff7efea2) - chore: update dependencies
-
-## 1.0.1
-
-### Patch Changes
-
-- [`2ed7279`](https://github.com/explodingcamera/esm/commit/2ed72792bf13fa4b712fb477208ebb7d061a1e8f) - update dependencies
-
-- [`de8e91f`](https://github.com/explodingcamera/esm/commit/de8e91f4f1052fbd6bf4f82b3c8010195b98a7b1) - update dependencies
-
-## 1.0.0
-
-### Major Changes
-
-- [`eb2bd74`](https://github.com/explodingcamera/esm/commit/eb2bd74d7150d4bda09779d9e47bb230c34056fc) - First major release of `minify-literals` and `rollup-plugin-minify-template-literals` to mark these as stable.
-
-## 0.2.3
-
-### Patch Changes
-
-- [`7d31d3a`](https://github.com/explodingcamera/esm/commit/7d31d3aa301e519209c8019d9675434a1a011f03) - fix issues with kv-pairs
-
-## 0.2.2
-
-### Patch Changes
-
-- [`1a2bed9`](https://github.com/explodingcamera/esm/commit/1a2bed92806690fe6bd2eba714c81d05d4d725c8) - add api docs link to readme
-
-## 0.2.1
-
-### Patch Changes
-
-- [`135ceba`](https://github.com/explodingcamera/esm/commit/135cebae5020bb17694600792785eddb4e5e3bab) - fix html/css comments & template literals as property names
-
-## 0.2.0
-
-### Minor Changes
-
-- [`cb24d87`](https://github.com/explodingcamera/esm/commit/cb24d87d3027b6da3477a2ab8eb7e9fe79ba5656) - cleanup package.json
-
-## 0.1.0
-
-### Minor Changes
-
-- [`a4d659d`](https://github.com/explodingcamera/esm/commit/a4d659da28294c2ed787f39c0062183a38677eb1) - fix various open bugs
-
-## 0.0.2
-
-### Patch Changes
-
-- [`d14a9ac`](https://github.com/explodingcamera/esm/commit/d14a9accdcd602695ca97ad3189247a5c19545b5) - fix npm package.json
-
-## 0.0.1
-
-### Patch Changes
-
-- first release
+- Initial release.
diff --git a/packages/minify-literals/README.md b/packages/minify-literals/README.md
index f14f38b..bb7b94b 100644
--- a/packages/minify-literals/README.md
+++ b/packages/minify-literals/README.md
@@ -1,4 +1,4 @@
-# minify-literals [](https://paka.dev/npm/minify-literals) [](https://www.npmjs.com/package/minify-literals)
+# minify-literals [](https://www.jsdocs.io/package/minify-literals) [](https://www.npmjs.com/package/minify-literals)
> Minify HTML & CSS markup inside JavaScript/TypeScript template literal strings.
diff --git a/packages/minify-literals/package.json b/packages/minify-literals/package.json
index 93a1722..8bef41a 100644
--- a/packages/minify-literals/package.json
+++ b/packages/minify-literals/package.json
@@ -11,10 +11,15 @@
"template-literals"
],
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/minify-literals",
- "repository": "https://github.com/explodingcamera/esm",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/explodingcamera/esm.git",
+ "directory": "packages/minify-literals"
+ },
"license": "MIT",
"author": "Henry Gressmann ",
"type": "module",
+ "sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -28,6 +33,9 @@
"files": [
"dist"
],
+ "scripts": {
+ "prepublishOnly": "bun run --cwd ../.. build"
+ },
"tsdown": {
"entry": "lib/index.ts"
},
diff --git a/packages/rollup-plugin-minify-template-literals/CHANGELOG.md b/packages/rollup-plugin-minify-template-literals/CHANGELOG.md
index 2333790..20fa1aa 100644
--- a/packages/rollup-plugin-minify-template-literals/CHANGELOG.md
+++ b/packages/rollup-plugin-minify-template-literals/CHANGELOG.md
@@ -1,190 +1,96 @@
-# rollup-plugin-minify-template-literals
+# Changelog
-## 2.0.0
+All notable changes to this project will be documented in this file.
-### Major Changes
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- Update for minify-literals v2 and simplify plugin options.
-- Rename nested `options` to `minify` and remove deprecated/custom override hooks and `failOnError`.
+## [2.0.0]
-## 1.1.7
+- Update minify-literals to 2.0.0 and simplify plugin options.
+- Rename nested `options` to `minify`.
+- Remove custom override hooks and `failOnError` in favor of minify-literals options.
-### Patch Changes
+## [1.1.7]
-- [`2a64803`](https://github.com/explodingcamera/esm/commit/2a648036beb85c641e56bf56653ef87be5760531) - fix: broken dependency version
+- Fix broken dependency version.
-## 1.1.6
+## [1.1.6]
-### Patch Changes
+- Update build scripts.
+- Update minify-literals to 1.0.10.
-- fix: update build scripts
+## [1.1.5]
-- Updated dependencies []:
- - minify-literals@1.0.10
+- Update minify-literals to 1.0.9.
-## 1.1.5
+## [1.1.4]
-### Patch Changes
+- Update minify-literals to 1.0.8.
-- [`7adf233`](https://github.com/explodingcamera/esm/commit/7adf23335c7ae82b5237e3bb8e5288fdaf78b701) - chore: update dependencies
+## [1.1.3]
-- Updated dependencies [[`7adf233`](https://github.com/explodingcamera/esm/commit/7adf23335c7ae82b5237e3bb8e5288fdaf78b701)]:
- - minify-literals@1.0.9
+- Update minify-literals to 1.0.7.
-## 1.1.4
+## [1.1.2]
-### Patch Changes
+- Update minify-literals to 1.0.6.
-- [`2b328ea`](https://github.com/explodingcamera/esm/commit/2b328eacef3fe12dcce1587d4f7f3dce14f26764) - update dependencies
+## [1.1.1]
-- Updated dependencies [[`2b328ea`](https://github.com/explodingcamera/esm/commit/2b328eacef3fe12dcce1587d4f7f3dce14f26764)]:
- - minify-literals@1.0.8
+- Update minify-literals to 1.0.5.
-## 1.1.3
+## [1.1.0]
-### Patch Changes
+- Update minify-literals to 1.0.4.
+- Deprecate the old package name.
+- Fix package exports.
-- update dependencies
+## [1.0.3]
-- [`3169f4f`](https://github.com/explodingcamera/esm/commit/3169f4f5924f4e870bf25910ab2e9c79fd718057) - chore: update dependencies
+- Update minify-literals to 1.0.3.
-- Updated dependencies [[`3169f4f`](https://github.com/explodingcamera/esm/commit/3169f4f5924f4e870bf25910ab2e9c79fd718057)]:
- - minify-literals@1.0.7
+## [1.0.2]
-## 1.1.2
+- Update minify-literals to 1.0.2.
-### Patch Changes
+## [1.0.1]
-- chore: update dependencies
+- Update minify-literals to 1.0.1.
-- [`b8b2d3d`](https://github.com/explodingcamera/esm/commit/b8b2d3dbb0c99159f5d643f476e623cd56278017) - update dependencies
+## [1.0.0]
-- Updated dependencies [[`7f1660c`](https://github.com/explodingcamera/esm/commit/7f1660c88677547d9eafb8ab30ff9032a75b42be)]:
- - minify-literals@1.0.6
+- Mark `minify-literals` and `rollup-plugin-minify-template-literals` as stable.
+- Update minify-literals to 1.0.0.
-## 1.1.1
+## [0.2.3]
-### Patch Changes
+- Update minify-literals to 0.2.3.
-- [`ac9f992`](https://github.com/explodingcamera/esm/commit/ac9f992ea5b540beddaea7feb002a76ccaccb29d) - chore: update dependencies
+## [0.2.2]
-- [`28b2765`](https://github.com/explodingcamera/esm/commit/28b276523ad007ab9ae0402a7d5d5b7360f1d7ed) - chore: update dependencies
+- Add API docs link to README.
+- Update minify-literals to 0.2.2.
-- Updated dependencies [[`ac9f992`](https://github.com/explodingcamera/esm/commit/ac9f992ea5b540beddaea7feb002a76ccaccb29d), [`28b2765`](https://github.com/explodingcamera/esm/commit/28b276523ad007ab9ae0402a7d5d5b7360f1d7ed)]:
- - minify-literals@1.0.5
+## [0.2.1]
-## 1.1.0
+- Update minify-literals to 0.2.1.
-### Minor Changes
+## [0.2.0]
-- [`29a9ef1`](https://github.com/explodingcamera/esm/commit/29a9ef118db1f1184f4f599b2806e25b1c41187b) - fix exports, deprecate old package name
+- Clean up `package.json`.
+- Update minify-literals to 0.2.0.
-### Patch Changes
+## [0.1.0]
-- Updated dependencies [[`29a9ef1`](https://github.com/explodingcamera/esm/commit/29a9ef118db1f1184f4f599b2806e25b1c41187b)]:
- - minify-literals@1.0.4
+- Update minify-literals to 0.1.0.
+- Fix various open bugs.
-## 1.0.3
+## [0.0.2]
-### Patch Changes
+- Update minify-literals to 0.0.2.
+- Fix npm `package.json`.
-- chore: update dependencies
+## [0.0.1]
-- Updated dependencies []:
- - minify-literals@1.0.3
-
-## 1.0.2
-
-### Patch Changes
-
-- chore: update dependencies
-
-- [`575efe3`](https://github.com/explodingcamera/esm/commit/575efe385756abd44408d83535c27c99ff7efea2) - chore: update dependencies
-
-- Updated dependencies [[`575efe3`](https://github.com/explodingcamera/esm/commit/575efe385756abd44408d83535c27c99ff7efea2)]:
- - minify-literals@1.0.2
-
-## 1.0.1
-
-### Patch Changes
-
-- [`2ed7279`](https://github.com/explodingcamera/esm/commit/2ed72792bf13fa4b712fb477208ebb7d061a1e8f) - update dependencies
-
-- [`de8e91f`](https://github.com/explodingcamera/esm/commit/de8e91f4f1052fbd6bf4f82b3c8010195b98a7b1) - update depdendencies
-
-- Updated dependencies [[`2ed7279`](https://github.com/explodingcamera/esm/commit/2ed72792bf13fa4b712fb477208ebb7d061a1e8f), [`de8e91f`](https://github.com/explodingcamera/esm/commit/de8e91f4f1052fbd6bf4f82b3c8010195b98a7b1)]:
- - minify-literals@1.0.1
-
-## 1.0.0
-
-### Major Changes
-
-- [`eb2bd74`](https://github.com/explodingcamera/esm/commit/eb2bd74d7150d4bda09779d9e47bb230c34056fc) - First major release of `minify-literals` and `rollup-plugin-minify-template-literals` to mark these as stable.
-
-### Patch Changes
-
-- Updated dependencies [[`eb2bd74`](https://github.com/explodingcamera/esm/commit/eb2bd74d7150d4bda09779d9e47bb230c34056fc)]:
- - minify-literals@1.0.0
-
-## 0.2.3
-
-### Patch Changes
-
-- Updated dependencies [[`7d31d3a`](https://github.com/explodingcamera/esm/commit/7d31d3aa301e519209c8019d9675434a1a011f03)]:
- - minify-literals@0.2.3
-
-## 0.2.2
-
-### Patch Changes
-
-- [`1a2bed9`](https://github.com/explodingcamera/esm/commit/1a2bed92806690fe6bd2eba714c81d05d4d725c8) - add api docs link to readme
-
-- Updated dependencies [[`1a2bed9`](https://github.com/explodingcamera/esm/commit/1a2bed92806690fe6bd2eba714c81d05d4d725c8)]:
- - minify-literals@0.2.2
-
-## 0.2.1
-
-### Patch Changes
-
-- Updated dependencies [[`135ceba`](https://github.com/explodingcamera/esm/commit/135cebae5020bb17694600792785eddb4e5e3bab)]:
- - minify-literals@0.2.1
-
-## 0.2.0
-
-### Minor Changes
-
-- [`cb24d87`](https://github.com/explodingcamera/esm/commit/cb24d87d3027b6da3477a2ab8eb7e9fe79ba5656) - cleanup package.json
-
-### Patch Changes
-
-- Updated dependencies [[`cb24d87`](https://github.com/explodingcamera/esm/commit/cb24d87d3027b6da3477a2ab8eb7e9fe79ba5656)]:
- - minify-literals@0.2.0
-
-## 0.1.0
-
-### Minor Changes
-
-- [`a4d659d`](https://github.com/explodingcamera/esm/commit/a4d659da28294c2ed787f39c0062183a38677eb1) - fix various open bugs
-
-### Patch Changes
-
-- Updated dependencies [[`a4d659d`](https://github.com/explodingcamera/esm/commit/a4d659da28294c2ed787f39c0062183a38677eb1)]:
- - minify-literals@0.1.0
-
-## 0.0.2
-
-### Patch Changes
-
-- [`d14a9ac`](https://github.com/explodingcamera/esm/commit/d14a9accdcd602695ca97ad3189247a5c19545b5) - fix npm package.json
-
-- Updated dependencies [[`d14a9ac`](https://github.com/explodingcamera/esm/commit/d14a9accdcd602695ca97ad3189247a5c19545b5)]:
- - minify-literals@0.0.2
-
-## 0.0.1
-
-### Patch Changes
-
-- first release
-
-- Updated dependencies []:
- - minify-literals@0.0.1
+- Initial release.
diff --git a/packages/rollup-plugin-minify-template-literals/README.md b/packages/rollup-plugin-minify-template-literals/README.md
index 4eac1bb..c6fddcd 100644
--- a/packages/rollup-plugin-minify-template-literals/README.md
+++ b/packages/rollup-plugin-minify-template-literals/README.md
@@ -1,4 +1,4 @@
-# rollup-plugin-minify-template-literals [](https://paka.dev/npm/rollup-plugin-minify-template-literals) [](https://www.npmjs.com/package/rollup-plugin-minify-template-literals)
+# rollup-plugin-minify-template-literals [](https://www.jsdocs.io/package/rollup-plugin-minify-template-literals) [](https://www.npmjs.com/package/rollup-plugin-minify-template-literals)
> Minify HTML & CSS markup inside JavaScript/TypeScript template literal strings - for vite and rollup.
diff --git a/packages/rollup-plugin-minify-template-literals/package.json b/packages/rollup-plugin-minify-template-literals/package.json
index 1cbc880..c08669e 100644
--- a/packages/rollup-plugin-minify-template-literals/package.json
+++ b/packages/rollup-plugin-minify-template-literals/package.json
@@ -16,10 +16,15 @@
"template-literals"
],
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/rollup-plugin-minify-template-literals",
- "repository": "https://github.com/explodingcamera/esm",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/explodingcamera/esm.git",
+ "directory": "packages/rollup-plugin-minify-template-literals"
+ },
"license": "MIT",
"author": "Henry Gressmann ",
"type": "module",
+ "sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -33,6 +38,9 @@
"files": [
"dist"
],
+ "scripts": {
+ "prepublishOnly": "bun run --cwd ../.. build"
+ },
"tsdown": {
"entry": "lib/index.ts",
"deps": {
@@ -43,7 +51,7 @@
},
"dependencies": {
"@rollup/pluginutils": "^5.4.0",
- "minify-literals": "^2.0.0"
+ "minify-literals": "^2.0.2"
},
"devDependencies": {
"rollup": "^4.61.0"
diff --git a/packages/simplejsx/CHANGELOG.md b/packages/simplejsx/CHANGELOG.md
new file mode 100644
index 0000000..e684186
--- /dev/null
+++ b/packages/simplejsx/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.1.0] - 2026-06-17
+
+- Initial release.
diff --git a/packages/simplejsx/LICENSE.md b/packages/simplejsx/LICENSE.md
new file mode 100644
index 0000000..749c67f
--- /dev/null
+++ b/packages/simplejsx/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2026 Henry Gressmann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/simplejsx/README.md b/packages/simplejsx/README.md
new file mode 100644
index 0000000..c2e49de
--- /dev/null
+++ b/packages/simplejsx/README.md
@@ -0,0 +1,67 @@
+# simplejsx
+
+Minimal JSX templating for safe HTML strings.
+
+`simplejsx` renders JSX to HTML strings. It works well for server-side templates, static sites, emails,
+and other places where a string is the whole output. This is not a React replacement and does not handle
+browser interactivity.
+
+Full API docs: [jsdocs.io/package/simplejsx](https://www.jsdocs.io/package/simplejsx)
+Examples: [`examples/simplejsx`](../../examples/simplejsx)
+
+## Install
+
+```bash
+npm install simplejsx
+```
+
+## Setup
+
+Use the automatic JSX runtime with `jsxImportSource`. In `tsconfig.json`:
+
+```json
+{
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "simplejsx"
+ }
+}
+```
+
+For Deno, use `"jsxImportSource": "npm:simplejsx"` in `deno.json`.
+
+## Example
+
+```tsx
+import { render, unsafeHTML, type PropsWithChildren } from "simplejsx";
+
+function Layout({ title, children }: PropsWithChildren<{ title: string }>) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
+
+const html = render(
+
+ Hello {""}
+ {unsafeHTML(" ")}
+ ,
+);
+```
+
+## Behavior
+
+- Text and attributes are escaped by default. Use `unsafeHTML()` only for trusted markup.
+- `style` supports strings and objects, e.g. `style="color:red"` or `style={{ marginTop: 8 }}`.
+- `renderAsync()` awaits async components, promise children, and promise attributes.
+- Nested `` elements are merged into the top-level ``.
+
+## See Also
+
+- [Hono JSX](https://hono.dev/docs/guides/jsx) for Hono apps. It has a much broader runtime and DOM API.
+- [Preact](https://preactjs.com/) for React-compatible UI components, DOM rendering, and hydration.
diff --git a/packages/simplejsx/lib/elements.ts b/packages/simplejsx/lib/elements.ts
new file mode 100644
index 0000000..278bbc5
--- /dev/null
+++ b/packages/simplejsx/lib/elements.ts
@@ -0,0 +1,129 @@
+export const voidTags: Set = new Set([
+ "area",
+ "base",
+ "br",
+ "col",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr",
+]);
+
+export const booleanAttributes: Set = new Set([
+ "allowfullscreen",
+ "async",
+ "autofocus",
+ "autoplay",
+ "checked",
+ "controls",
+ "default",
+ "defer",
+ "disabled",
+ "download",
+ "formnovalidate",
+ "hidden",
+ "inert",
+ "ismap",
+ "itemscope",
+ "loop",
+ "multiple",
+ "muted",
+ "nomodule",
+ "novalidate",
+ "open",
+ "playsinline",
+ "readonly",
+ "required",
+ "reversed",
+ "selected",
+]);
+
+/** Maps DOM property names to HTML attribute names for rendering. */
+export const attributeAliases: Map = new Map([
+ ["acceptCharset", "accept-charset"],
+ ["autoFocus", "autofocus"],
+ ["autoPlay", "autoplay"],
+ ["charSet", "charset"],
+ ["className", "class"],
+ ["colSpan", "colspan"],
+ ["crossOrigin", "crossorigin"],
+ ["dateTime", "datetime"],
+ ["fetchPriority", "fetchpriority"],
+ ["formAction", "formaction"],
+ ["formEncType", "formenctype"],
+ ["formMethod", "formmethod"],
+ ["formNoValidate", "formnovalidate"],
+ ["formTarget", "formtarget"],
+ ["htmlFor", "for"],
+ ["httpEquiv", "http-equiv"],
+ ["itemProp", "itemprop"],
+ ["maxLength", "maxlength"],
+ ["minLength", "minlength"],
+ ["noModule", "nomodule"],
+ ["playsInline", "playsinline"],
+ ["readOnly", "readonly"],
+ ["rowSpan", "rowspan"],
+ ["srcDoc", "srcdoc"],
+ ["srcSet", "srcset"],
+ ["tabIndex", "tabindex"],
+]);
+
+export const unitlessStyleProperties: Set = new Set([
+ "animation-iteration-count",
+ "aspect-ratio",
+ "border-image-outset",
+ "border-image-slice",
+ "border-image-width",
+ "box-flex",
+ "box-flex-group",
+ "box-ordinal-group",
+ "column-count",
+ "columns",
+ "flex",
+ "flex-grow",
+ "flex-negative",
+ "flex-order",
+ "flex-positive",
+ "flex-shrink",
+ "flood-opacity",
+ "font-weight",
+ "grid-area",
+ "grid-column",
+ "grid-column-end",
+ "grid-column-span",
+ "grid-column-start",
+ "grid-row",
+ "grid-row-end",
+ "grid-row-span",
+ "grid-row-start",
+ "line-clamp",
+ "line-height",
+ "opacity",
+ "order",
+ "orphans",
+ "scale",
+ "stop-opacity",
+ "stroke-dasharray",
+ "stroke-dashoffset",
+ "stroke-miterlimit",
+ "stroke-opacity",
+ "stroke-width",
+ "tab-size",
+ "widows",
+ "z-index",
+ "zoom",
+]);
+
+export const invalidAttributeNameCodes: Set = new Set([
+ 0x20, 0x22, 0x27, 0x2f, 0x3c, 0x3d, 0x3e, 0x5c, 0x60,
+]);
+
+export const invalidStylePropertyNameCodes: Set = new Set([
+ 0x20, 0x22, 0x27, 0x28, 0x29, 0x2f, 0x3a, 0x3b, 0x5b, 0x5c, 0x5d, 0x7b, 0x7d,
+]);
diff --git a/packages/simplejsx/lib/index.test.tsx b/packages/simplejsx/lib/index.test.tsx
new file mode 100644
index 0000000..f9badbd
--- /dev/null
+++ b/packages/simplejsx/lib/index.test.tsx
@@ -0,0 +1,235 @@
+/** @jsxImportSource . */
+import { expect, test } from "bun:test";
+import { jsx, render, renderAsync, unsafeHTML, type PropsWithChildren } from "./index.js";
+
+test("escapes", () => {
+ expect(render(Hello {"<&'\""}
)).toBe(
+ 'Hello <&'"
',
+ );
+});
+
+test("components", () => {
+ function Layout({ title, children }: PropsWithChildren<{ title: string }>) {
+ return (
+
+ );
+ }
+
+ const items = ["one", "two"];
+ expect(
+ render(
+
+
+ {items.map((item) => (
+ {item}
+ ))}
+
+ {false}
+ {null}
+ {undefined}
+ ,
+ ),
+ ).toBe("");
+});
+
+test("fragments", () => {
+ expect(
+ render(
+ <>
+ one
+ two
+ >,
+ ),
+ ).toBe("one
two
");
+});
+
+test("raw html", () => {
+ expect(render({"escaped "}
)).toBe(
+ "<strong>escaped</strong>
",
+ );
+ expect(render({unsafeHTML("trusted ")}
)).toBe(
+ "trusted
",
+ );
+});
+
+test("attributes", () => {
+ expect(
+ render(
+
+ Name
+ ,
+ ),
+ ).toBe('Name ');
+ expect(render( )).toBe(' ');
+});
+
+test("element middleware", () => {
+ expect(
+ render(Docs , {
+ element: [
+ ({ tag, props }) => (tag === "a" ? { ...props, target: "_blank" } : undefined),
+ ({ tag, props }) => {
+ if (tag === "a") props["rel"] = "noreferrer";
+ },
+ ],
+ }),
+ ).toBe('Docs ');
+ expect(
+ render(Hi
, {
+ element: [[({ props }) => ({ ...props, class: "message" })]],
+ }),
+ ).toBe('Hi
');
+});
+
+test("style", () => {
+ expect(
+ render(
+
,
+ ),
+ ).toBe('
');
+ expect(render(
)).toBe(
+ '
',
+ );
+});
+
+test("children arrays and null", () => {
+ function Box({ children }: PropsWithChildren) {
+ return {children}
;
+ }
+
+ expect(render({[one , null, "two"]} )).toBe("one two
");
+ expect(render(jsx("div", { children: null }))).toBe("
");
+});
+
+test("head hoisting", () => {
+ function Page() {
+ return (
+ <>
+
+ Page
+
+
+ Page
+ >
+ );
+ }
+
+ expect(
+ render(
+
+
+
+
+
+
+
+ ,
+ ),
+ ).toBe(
+ 'Page Page ',
+ );
+});
+
+test("head creation", () => {
+ expect(
+ render(
+
+
+
+ Page
+
+ Page
+
+ ,
+ ),
+ ).toBe('Page Page ');
+});
+
+test("async", async () => {
+ async function Message() {
+ return Async ;
+ }
+
+ await expect(
+ renderAsync(
+
+
+ {Promise.resolve("x
,
+ ),
+ ).resolves.toBe('Async x<y
');
+ await expect(
+ renderAsync(
),
+ ).resolves.toBe("
");
+ await expect(
+ renderAsync(jsx("img", { src: "/image.png" }), {
+ element: async ({ tag, props }) =>
+ tag === "img" ? { ...props, alt: Promise.resolve("A&B") } : undefined,
+ }),
+ ).resolves.toBe(' ');
+ await expect(
+ renderAsync( , {
+ element: async ({ tag, props }) => {
+ if (tag !== "img") return;
+
+ await Promise.resolve();
+ props["width"] = 1200;
+ props["height"] = 800;
+ },
+ }),
+ ).resolves.toBe(' ');
+});
+
+test("async head", async () => {
+ await expect(
+ renderAsync(
+
+
+ Base
+
+
+
+
+
+
+ ,
+ ),
+ ).resolves.toBe(
+ 'Base ',
+ );
+});
+
+test("sync rejects async", () => {
+ async function Message() {
+ return Async ;
+ }
+
+ expect(() => render( )).toThrow("renderAsync");
+ expect(() => render({Promise.resolve("Async")}
)).toThrow("renderAsync");
+ expect(() => render(
, { element: async () => {} })).toThrow("renderAsync");
+});
+
+test("function attrs", () => {
+ expect(
+ render(
+ {}}>
+ Click
+ ,
+ ),
+ ).toBe('Click ');
+ expect(() => render(jsx("div", { value: () => {} }))).toThrow("function prop");
+});
+
+test("void children", () => {
+ expect(() => render(jsx("input", { children: "bad" }))).toThrow("void element");
+});
diff --git a/packages/simplejsx/lib/index.ts b/packages/simplejsx/lib/index.ts
new file mode 100644
index 0000000..a6368f4
--- /dev/null
+++ b/packages/simplejsx/lib/index.ts
@@ -0,0 +1,508 @@
+/**
+ * Minimal JSX templating for safe HTML strings.
+ *
+ * `simplejsx` renders JSX to escaped HTML strings. It works well for
+ * server-side templates, static sites, emails, and other places where a string
+ * is the whole output. This is not a React replacement and does not handle
+ * browser interactivity.
+ *
+ * @remarks
+ * ## Setup
+ *
+ * Use the automatic JSX runtime. In TypeScript projects, put this in `tsconfig.json`:
+ *
+ * ```json
+ * {
+ * "compilerOptions": {
+ * "jsx": "react-jsx",
+ * "jsxImportSource": "simplejsx"
+ * }
+ * }
+ * ```
+ *
+ * In Deno, use `"jsxImportSource": "npm:simplejsx"` unless an import map points
+ * `simplejsx` at the package.
+ *
+ * ## Render HTML
+ *
+ * ```tsx
+ * import { render, type PropsWithChildren } from "simplejsx";
+ *
+ * function Layout({ children }: PropsWithChildren) {
+ * return {children} ;
+ * }
+ *
+ * render(Hello {""} );
+ * // "Hello <Henry> "
+ * ```
+ *
+ * ## Rendering Rules
+ *
+ * ### Escaping
+ *
+ * Text and attributes are escaped by default. `null`, `undefined`, and booleans
+ * render nothing. Arrays and other iterables render their children in order.
+ * Use {@link unsafeHTML} only for trusted markup that should be inserted as-is.
+ *
+ * ### Styles
+ *
+ * `style` accepts a CSS string or an object. Object styles accept camelCase,
+ * hyphenated names, and custom properties. Finite numeric values get `px`.
+ * The exceptions are `0`, custom properties, and known unitless properties.
+ *
+ * ### Element middleware
+ *
+ * Pass `element` to {@link render} or {@link renderAsync} to inspect or change
+ * intrinsic element props before they are written. Mutate `props` or return a
+ * replacement props object. Async middleware is supported by {@link renderAsync}.
+ *
+ * ```tsx
+ * render(Docs , {
+ * element({ tag, props }) {
+ * if (tag === "a") props["rel"] = "noreferrer";
+ * },
+ * });
+ * ```
+ *
+ * ### Head hoisting
+ *
+ * All `` elements are merged into one top-level ``. Page components
+ * can set titles and metadata. A layout can still own the document shell.
+ *
+ * ```tsx
+ * render( Page );
+ * // "
Page "
+ * ```
+ *
+ * ### Async rendering
+ *
+ * JSX can contain async components, Promise children, and Promise attributes.
+ * {@link render} stays synchronous and throws when it sees a Promise, so async
+ * work is not accidentally omitted. Use {@link renderAsync} to await the whole tree.
+ *
+ * ```tsx
+ * import { renderAsync } from "simplejsx";
+ *
+ * const title = Promise.resolve("a&b");
+ * const name = Promise.resolve("
");
+ *
+ * await renderAsync(Hello {name}
);
+ * // "Hello <Henry>
"
+ * ```
+ *
+ * @packageDocumentation
+ */
+import {
+ attributeAliases,
+ booleanAttributes,
+ invalidAttributeNameCodes,
+ invalidStylePropertyNameCodes,
+ unitlessStyleProperties,
+ voidTags,
+} from "./elements.js";
+import { HTML, JSXNode } from "./node.js";
+import type { Child, ElementType, MaybePromise, Props, PropsWithChildren } from "./types.js";
+
+export { HTML, JSXNode } from "./node.js";
+export type {
+ AttributeValue,
+ Child,
+ Component,
+ CSSProperties,
+ ElementType,
+ HTMLAttributes,
+ JSX,
+ MaybePromise,
+ Props,
+ PropsWithChildren,
+} from "./types.js";
+
+const asyncRenderError = "simplejsx: render() encountered async content. Use renderAsync() instead.";
+
+export type ElementMiddlewareArgs = {
+ tag: string;
+ props: Props;
+};
+
+type ElementMiddlewareReplacement = (
+ element: ElementMiddlewareArgs,
+) => MaybePromise;
+type ElementMiddlewareMutation = (element: ElementMiddlewareArgs) => MaybePromise;
+
+/** Runs before an intrinsic element is serialized. */
+export type ElementMiddleware = ElementMiddlewareReplacement | ElementMiddlewareMutation;
+
+export type RenderOptions = {
+ /** Runs before each intrinsic element is serialized. */
+ element?: ElementMiddleware | readonly (ElementMiddleware | readonly ElementMiddleware[])[];
+};
+
+type HeadRoot = {
+ headAttributes?: string;
+ headChildren: string[];
+};
+
+type RenderContext = {
+ root: HeadRoot;
+ elementMiddleware: readonly ElementMiddleware[];
+ parentTag?: string;
+};
+
+/** JSX runtime entry used by TypeScript. Prefer JSX syntax over calling this directly. */
+export function jsx(type: ElementType, props: Props | null, _key?: unknown): JSXNode {
+ return new JSXNode(type, props ? { ...props } : {});
+}
+
+/** JSX runtime entry used by TypeScript for elements with multiple children. */
+export const jsxs: typeof jsx = jsx;
+
+/** Development JSX runtime entry used by TypeScript-compatible compilers. */
+export function jsxDEV(
+ type: ElementType,
+ props: Props | null,
+ key?: unknown,
+ _isStaticChildren?: boolean,
+ _source?: unknown,
+ _self?: unknown,
+): JSXNode {
+ return jsx(type, props, key);
+}
+
+/**
+ * Groups JSX children without adding a wrapper element.
+ *
+ * @example
+ * ```tsx
+ * render(<>one
two
>);
+ * // "one
two
"
+ * ```
+ */
+export function Fragment({ children }: PropsWithChildren): Child {
+ return children;
+}
+
+/**
+ * Marks trusted markup as already-safe HTML.
+ *
+ * Use this only for HTML you control. Plain strings are escaped by default.
+ *
+ * @example
+ * ```tsx
+ * render({unsafeHTML("trusted ")}
);
+ * // "trusted
"
+ * ```
+ */
+export function unsafeHTML(value: string): HTML {
+ return HTML.from(value);
+}
+
+/**
+ * Renders JSX to an HTML string synchronously.
+ *
+ * Throws if it encounters async content so Promises are never silently skipped.
+ *
+ * @example
+ * ```tsx
+ * render(Hello {""}
);
+ * // "Hello <Henry>
"
+ * ```
+ */
+export function render(value: Child, options?: RenderOptions): string {
+ const root: HeadRoot = { headChildren: [] };
+ const elementMiddleware: readonly ElementMiddleware[] = options?.element
+ ? Array.isArray(options.element)
+ ? options.element.flat()
+ : [options.element]
+ : [];
+ return renderHead(root, renderChildSync(value, { root, elementMiddleware }));
+}
+
+/**
+ * Renders JSX to an HTML string and awaits async content.
+ *
+ * Use this for async components, promise children, promise attributes, or async
+ * element middleware.
+ *
+ * @example
+ * ```tsx
+ * const title = Promise.resolve("a&b");
+ * const message = Promise.resolve("Hello ");
+ *
+ * await renderAsync({message}
);
+ * // "Hello <Henry>
"
+ * ```
+ */
+export async function renderAsync(value: Child, options?: RenderOptions): Promise {
+ const root: HeadRoot = { headChildren: [] };
+ const elementMiddleware: readonly ElementMiddleware[] = options?.element
+ ? Array.isArray(options.element)
+ ? options.element.flat()
+ : [options.element]
+ : [];
+ return renderHead(root, await renderChildAsync(value, { root, elementMiddleware }));
+}
+
+/** Escapes HTML-sensitive characters: `&`, `<`, `>`, `"`, and `'`. */
+export function escapeHTML(value: string): string {
+ return value.replace(/[&<>"']/g, (character) => {
+ switch (character) {
+ case "&":
+ return "&";
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case '"':
+ return """;
+ case "'":
+ return "'";
+ default:
+ return character;
+ }
+ });
+}
+
+function renderChildSync(value: Child, context: RenderContext): string {
+ if (value === null || value === undefined || typeof value === "boolean") return "";
+ if (typeof value === "string") return escapeHTML(value);
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
+ if (value instanceof HTML) return value.toString();
+ if (isPromiseLike(value)) throw new Error(asyncRenderError);
+
+ if (value instanceof JSXNode) {
+ if (typeof value.type === "function") {
+ const result = value.type(value.props);
+ if (isPromiseLike(result)) throw new Error(asyncRenderError);
+ return renderChildSync(result, context);
+ }
+ return renderElementSync(value.type, value.props, context);
+ }
+
+ if (isIterable(value)) {
+ let html = "";
+ for (const child of value) html += renderChildSync(child, context);
+ return html;
+ }
+
+ throw new TypeError(`simplejsx: cannot render ${typeof value} as a child.`);
+}
+
+async function renderChildAsync(value: Child, context: RenderContext): Promise {
+ if (isPromiseLike(value)) return renderChildAsync((await value) as Child, context);
+ if (value === null || value === undefined || typeof value === "boolean") return "";
+ if (typeof value === "string") return escapeHTML(value);
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
+ if (value instanceof HTML) return value.toString();
+
+ if (value instanceof JSXNode) {
+ if (typeof value.type === "function") return renderChildAsync(value.type(value.props), context);
+ return renderElementAsync(value.type, value.props, context);
+ }
+
+ if (isIterable(value)) {
+ let html = "";
+ for (const child of value) html += await renderChildAsync(child, context);
+ return html;
+ }
+
+ throw new TypeError(`simplejsx: cannot render ${typeof value} as a child.`);
+}
+
+function renderElementSync(tag: string, props: Props, context: RenderContext): string {
+ if (!tag || tag.startsWith("!") || tag.startsWith("?") || hasInvalidAttributeNameCharacter(tag)) {
+ throw new Error(`simplejsx: invalid JSX tag name \`${tag}\`.`);
+ }
+
+ props = applyElementMiddlewareSync(tag, props, context);
+ const lowerTag = tag.toLowerCase();
+ const attributes = renderAttributesSync(props);
+
+ const children = props.children;
+ const nextContext = { ...context, parentTag: lowerTag };
+ if (lowerTag === "head") {
+ return collectHead(context, attributes, renderChildSync(children, nextContext));
+ }
+
+ const html = `<${tag}${attributes}`;
+ if (!voidTags.has(lowerTag)) return `${html}>${renderChildSync(children, nextContext)}${tag}>`;
+
+ if (renderChildSync(children, nextContext))
+ throw new Error(`simplejsx: void element <${tag}> cannot have children.`);
+ return `${html}>`;
+}
+
+async function renderElementAsync(tag: string, props: Props, context: RenderContext): Promise {
+ if (!tag || tag.startsWith("!") || tag.startsWith("?") || hasInvalidAttributeNameCharacter(tag)) {
+ throw new Error(`simplejsx: invalid JSX tag name \`${tag}\`.`);
+ }
+
+ props = await applyElementMiddlewareAsync(tag, props, context);
+ const lowerTag = tag.toLowerCase();
+ const attributes = await renderAttributesAsync(props);
+
+ const children = props.children;
+ const nextContext = { ...context, parentTag: lowerTag };
+ if (lowerTag === "head") {
+ return collectHead(context, attributes, await renderChildAsync(children, nextContext));
+ }
+
+ const html = `<${tag}${attributes}`;
+ if (!voidTags.has(lowerTag)) return `${html}>${await renderChildAsync(children, nextContext)}${tag}>`;
+
+ if (await renderChildAsync(children, nextContext))
+ throw new Error(`simplejsx: void element <${tag}> cannot have children.`);
+ return `${html}>`;
+}
+
+function applyElementMiddlewareSync(tag: string, props: Props, context: RenderContext): Props {
+ if (!context.elementMiddleware.length) return props;
+
+ let nextProps = { ...props };
+ for (const middleware of context.elementMiddleware) {
+ const result = middleware({ tag, props: nextProps });
+ if (isPromiseLike(result)) throw new Error(asyncRenderError);
+ if (result !== null && result !== undefined) nextProps = result;
+ }
+ return nextProps;
+}
+
+async function applyElementMiddlewareAsync(
+ tag: string,
+ props: Props,
+ context: RenderContext,
+): Promise {
+ if (!context.elementMiddleware.length) return props;
+
+ let nextProps = { ...props };
+ for (const middleware of context.elementMiddleware) {
+ const result = middleware({ tag, props: nextProps });
+ const resolved = isPromiseLike(result) ? await result : result;
+ if (resolved !== null && resolved !== undefined) nextProps = resolved;
+ }
+ return nextProps;
+}
+
+function renderAttributesSync(props: Props): string {
+ let attributes = "";
+ for (const [key, value] of Object.entries(props)) {
+ const name = normalizeAttributeName(key);
+ if (!name || value === null || value === undefined) continue;
+ if (isPromiseLike(value)) throw new Error(asyncRenderError);
+ attributes += renderAttribute(name, value);
+ }
+ return attributes;
+}
+
+async function renderAttributesAsync(props: Props): Promise {
+ let attributes = "";
+ for (const [key, value] of Object.entries(props)) {
+ const name = normalizeAttributeName(key);
+ if (!name || value === null || value === undefined) continue;
+ const attributeValue = isPromiseLike(value) ? await value : value;
+ if (attributeValue === null || attributeValue === undefined) continue;
+ attributes += renderAttribute(name, attributeValue);
+ }
+ return attributes;
+}
+
+function collectHead(context: RenderContext, attributes: string, children: string): string {
+ if (context.root.headAttributes === undefined && (!context.parentTag || context.parentTag === "html")) {
+ context.root.headAttributes = attributes;
+ }
+ context.root.headChildren.push(children);
+ return "";
+}
+
+function renderHead(root: HeadRoot, html: string): string {
+ if (!root.headChildren.length) return html;
+
+ // Head elements render out-of-band so page components can add metadata from anywhere in the tree.
+ const head = `${root.headChildren.join("")}`;
+ const bodyIndex = html.search(/)/i);
+ if (bodyIndex !== -1) return `${html.slice(0, bodyIndex)}${head}${html.slice(bodyIndex)}`;
+
+ const htmlTag = html.match(/^]*)?>/i)?.[0];
+ return htmlTag ? `${htmlTag}${head}${html.slice(htmlTag.length)}` : `${head}${html}`;
+}
+
+function renderAttribute(name: string, value: unknown): string {
+ if (name === "style" && typeof value === "object" && value !== null && !(value instanceof HTML)) {
+ return ` style="${escapeHTML(serializeStyle(value as Record))}"`;
+ }
+
+ if (typeof value === "boolean") {
+ if (booleanAttributes.has(name)) return value ? ` ${name}` : "";
+ return ` ${name}="${value}"`;
+ }
+
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
+ return ` ${name}="${escapeHTML(String(value))}"`;
+ }
+
+ if (value instanceof HTML) return ` ${name}="${escapeHTML(value.toString())}"`;
+
+ if (typeof value === "function") {
+ if (name.startsWith("on") || name === "ref") return "";
+ throw new TypeError(`simplejsx: cannot render function prop \`${name}\`.`);
+ }
+
+ throw new TypeError(`simplejsx: cannot render ${typeof value} prop \`${name}\`.`);
+}
+
+function normalizeAttributeName(key: string): string | null {
+ if (key === "children" || key === "key") return null;
+ const name = attributeAliases.get(key) ?? key;
+ return name && !hasInvalidAttributeNameCharacter(name) ? name : null;
+}
+
+function serializeStyle(style: Record): string {
+ let css = "";
+ for (const [rawName, rawValue] of Object.entries(style)) {
+ if (rawValue === null || rawValue === undefined) continue;
+
+ const name = rawName.startsWith("--")
+ ? rawName
+ : rawName.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`);
+ let validName = !!name;
+ for (let index = 0; index < name.length; index++) {
+ const code = name.charCodeAt(index);
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || invalidStylePropertyNameCodes.has(code)) {
+ validName = false;
+ break;
+ }
+ }
+ if (!validName) continue;
+
+ let value: string | null = null;
+ if (typeof rawValue === "string") value = rawValue;
+ if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
+ value =
+ rawValue === 0 || name.startsWith("--") || unitlessStyleProperties.has(name)
+ ? `${rawValue}`
+ : `${rawValue}px`;
+ }
+ if (value !== null) css += `${name}:${value};`;
+ }
+ return css;
+}
+
+function hasInvalidAttributeNameCharacter(name: string): boolean {
+ for (let index = 0; index < name.length; index++) {
+ const code = name.charCodeAt(index);
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || invalidAttributeNameCodes.has(code)) return true;
+ }
+ return false;
+}
+
+function isPromiseLike(value: unknown): value is PromiseLike {
+ return (
+ (typeof value === "object" || typeof value === "function") &&
+ value !== null &&
+ "then" in value &&
+ typeof value.then === "function"
+ );
+}
+
+function isIterable(value: unknown): value is Iterable {
+ return typeof value === "object" && value !== null && Symbol.iterator in value;
+}
diff --git a/packages/simplejsx/lib/jsx-dev-runtime.ts b/packages/simplejsx/lib/jsx-dev-runtime.ts
new file mode 100644
index 0000000..be11799
--- /dev/null
+++ b/packages/simplejsx/lib/jsx-dev-runtime.ts
@@ -0,0 +1,2 @@
+export { Fragment, jsx, jsxDEV, jsxs } from "./index.js";
+export type { JSX } from "./index.js";
diff --git a/packages/simplejsx/lib/jsx-runtime.ts b/packages/simplejsx/lib/jsx-runtime.ts
new file mode 100644
index 0000000..250283c
--- /dev/null
+++ b/packages/simplejsx/lib/jsx-runtime.ts
@@ -0,0 +1,2 @@
+export { Fragment, jsx, jsxs } from "./index.js";
+export type { JSX } from "./index.js";
diff --git a/packages/simplejsx/lib/node.ts b/packages/simplejsx/lib/node.ts
new file mode 100644
index 0000000..3cb135a
--- /dev/null
+++ b/packages/simplejsx/lib/node.ts
@@ -0,0 +1,29 @@
+import type { ElementType, Props } from "./types.js";
+
+/** Branded wrapper so raw HTML has to be opted into with `unsafeHTML()`. */
+export class HTML {
+ readonly #value: string;
+
+ private constructor(value: string) {
+ this.#value = value;
+ }
+
+ static from(value: string): HTML {
+ return new HTML(value);
+ }
+
+ toString(): string {
+ return this.#value;
+ }
+}
+
+/** Keeps JSX lazy so components and promises resolve during rendering. */
+export class JSXNode {
+ readonly type: ElementType;
+ readonly props: Props;
+
+ constructor(type: ElementType, props: Props) {
+ this.type = type;
+ this.props = props;
+ }
+}
diff --git a/packages/simplejsx/lib/types.ts b/packages/simplejsx/lib/types.ts
new file mode 100644
index 0000000..a1b07a0
--- /dev/null
+++ b/packages/simplejsx/lib/types.ts
@@ -0,0 +1,80 @@
+import type { Properties, PropertiesHyphen } from "csstype";
+import type { HTML, JSXNode } from "./node.js";
+
+export type MaybePromise = T | Promise;
+
+export type CSSProperties = Properties &
+ PropertiesHyphen & {
+ [property: `--${string}`]: string | number | null | undefined;
+ };
+
+type ChildValue = HTML | JSXNode | string | number | bigint | boolean | null | undefined | Iterable;
+
+export type Child = ChildValue | Promise;
+export type Props = Record & {
+ children?: Child;
+};
+export type Component> = (props: ComponentProps) => Child;
+export type PropsWithChildren> = ComponentProps & {
+ children?: Child;
+};
+export type ElementType> = string | Component;
+export type AttributeValue = string | number | bigint | boolean | null | undefined;
+
+/**
+ * Global attributes available on all elements.
+ *
+ * Common camelCase aliases (className, htmlFor, etc.) are included for
+ * React/Preact familiarity. The renderer maps them to their HTML equivalents.
+ */
+export type HTMLAttributes = {
+ children?: Child;
+ class?: MaybePromise;
+ id?: MaybePromise;
+ style?: MaybePromise;
+ [attribute: `aria-${string}`]: MaybePromise;
+ [attribute: `data-${string}`]: MaybePromise;
+ [attribute: string]: unknown;
+};
+
+/**
+ * Derives element-specific attributes from a DOM element interface.
+ *
+ * Filters out inherited base properties and methods, keeping DOM property
+ * names as-is (camelCase). The renderer handles conversion to HTML attributes.
+ */
+type ElementSpecific = {
+ [K in keyof T as K extends keyof Base
+ ? never
+ : T[K] extends (...args: any[]) => any
+ ? never
+ : K extends string
+ ? K
+ : never]?: MaybePromise;
+};
+
+export namespace JSX {
+ export type Element = JSXNode;
+ export type ElementType = keyof IntrinsicElements | Component;
+
+ export interface ElementChildrenAttribute {
+ children: Child;
+ }
+
+ export type IntrinsicAttributes = {};
+
+ type HTMLIntrinsicElements = {
+ [Tag in keyof HTMLElementTagNameMap]: HTMLAttributes &
+ ElementSpecific;
+ };
+
+ type SVGIntrinsicElements = {
+ [Tag in keyof SVGElementTagNameMap as Tag extends keyof HTMLElementTagNameMap
+ ? never
+ : Tag]: HTMLAttributes & ElementSpecific;
+ };
+
+ export interface IntrinsicElements extends HTMLIntrinsicElements, SVGIntrinsicElements {
+ [tagName: string]: HTMLAttributes;
+ }
+}
diff --git a/packages/simplejsx/package.json b/packages/simplejsx/package.json
new file mode 100644
index 0000000..687894a
--- /dev/null
+++ b/packages/simplejsx/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "simplejsx",
+ "version": "0.1.0",
+ "support": "unstable",
+ "description": "Minimal JSX templating for safe HTML strings",
+ "keywords": [
+ "jsx",
+ "html",
+ "template",
+ "templating",
+ "ssg",
+ "ssr"
+ ],
+ "homepage": "https://github.com/explodingcamera/esm/tree/main/packages/simplejsx",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/explodingcamera/esm.git",
+ "directory": "packages/simplejsx"
+ },
+ "license": "MIT",
+ "author": "Henry Gressmann ",
+ "type": "module",
+ "sideEffects": false,
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.mts",
+ "import": "./dist/index.mjs",
+ "default": "./dist/index.mjs"
+ },
+ "./jsx-runtime": {
+ "types": "./dist/jsx-runtime.d.mts",
+ "import": "./dist/jsx-runtime.mjs",
+ "default": "./dist/jsx-runtime.mjs"
+ },
+ "./jsx-dev-runtime": {
+ "types": "./dist/jsx-dev-runtime.d.mts",
+ "import": "./dist/jsx-dev-runtime.mjs",
+ "default": "./dist/jsx-dev-runtime.mjs"
+ }
+ },
+ "main": "dist/index.mjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.mts",
+ "source": "lib/index.ts",
+ "files": [
+ "dist",
+ "lib/elements.ts",
+ "lib/index.ts",
+ "lib/jsx-dev-runtime.ts",
+ "lib/jsx-runtime.ts",
+ "lib/node.ts",
+ "lib/types.ts"
+ ],
+ "scripts": {
+ "prepublishOnly": "bun run --cwd ../.. build"
+ },
+ "tsdown": {
+ "entry": [
+ "lib/index.ts",
+ "lib/jsx-runtime.ts",
+ "lib/jsx-dev-runtime.ts"
+ ]
+ },
+ "dependencies": {
+ "csstype": "^3.2.3"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+}
diff --git a/packages/spaify/CHANGELOG.md b/packages/spaify/CHANGELOG.md
index 98e60a3..1d279a3 100644
--- a/packages/spaify/CHANGELOG.md
+++ b/packages/spaify/CHANGELOG.md
@@ -1,54 +1,22 @@
-# spaify [](https://paka.dev/npm/spaify) [](https://www.npmjs.com/package/spaify)
+# Changelog
-## 0.0.8
+All notable changes to this project will be documented in this file.
-### Patch Changes
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- fix: update build scripts
+## [0.0.8]
-## 0.0.7
+- Update build scripts.
-### Patch Changes
+## [0.0.3]
-- [`7adf233`](https://github.com/explodingcamera/esm/commit/7adf23335c7ae82b5237e3bb8e5288fdaf78b701) - chore: update dependencies
+- Support nested anchor tags.
-## 0.0.6
+## [0.0.2]
-### Patch Changes
+- Add script handling.
-- [`2b328ea`](https://github.com/explodingcamera/esm/commit/2b328eacef3fe12dcce1587d4f7f3dce14f26764) - update dependencies
+## [0.0.1]
-## 0.0.5
-
-### Patch Changes
-
-- update dependencies
-
-- [`3169f4f`](https://github.com/explodingcamera/esm/commit/3169f4f5924f4e870bf25910ab2e9c79fd718057) - chore: update dependencies
-
-## 0.0.4
-
-### Patch Changes
-
-- chore: update dependencies
-
-- [`b8b2d3d`](https://github.com/explodingcamera/esm/commit/b8b2d3dbb0c99159f5d643f476e623cd56278017) - update dependencies
-
-## 0.0.3
-
-### Patch Changes
-
-- feat: nested a tags
-
-## 0.0.2
-
-### Patch Changes
-
-- [`67ec722`](https://github.com/explodingcamera/esm/commit/67ec722d0193008b3ff5487b5685a71d4b2e7999) - feat: scripts
-
-## 0.0.1
-
-### Patch Changes
-
-- [`28b2765`](https://github.com/explodingcamera/esm/commit/28b276523ad007ab9ae0402a7d5d5b7360f1d7ed) - initial release
-- [`28b2765`](https://github.com/explodingcamera/esm/commit/28b276523ad007ab9ae0402a7d5d5b7360f1d7ed) - chore: update dependencies
+- Initial release.
diff --git a/packages/spaify/LICENSE.md b/packages/spaify/LICENSE.md
new file mode 100644
index 0000000..bd0e8bf
--- /dev/null
+++ b/packages/spaify/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2023-2026 Henry Gressmann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/spaify/package.json b/packages/spaify/package.json
index 4be34ab..78be30f 100644
--- a/packages/spaify/package.json
+++ b/packages/spaify/package.json
@@ -8,10 +8,17 @@
"turbolinks"
],
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/spaify",
- "repository": "https://github.com/explodingcamera/esm",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/explodingcamera/esm.git",
+ "directory": "packages/spaify"
+ },
"license": "MIT",
"author": "Henry Gressmann ",
"type": "module",
+ "sideEffects": [
+ "./dist/default.mjs"
+ ],
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -30,6 +37,9 @@
"files": [
"dist"
],
+ "scripts": {
+ "prepublishOnly": "bun run --cwd ../.. build"
+ },
"tsdown": {
"entry": [
"lib/index.ts",