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) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/@explodingcamera/css) [![NPM Version](https://img.shields.io/npm/v/@explodingcamera/css.svg)](https://www.npmjs.com/package/@explodingcamera/css) | Small CSS reset/base files. | -| [`minify-literals`](./packages/minify-literals) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/minify-literals) [![NPM Version](https://img.shields.io/npm/v/minify-literals.svg)](https://www.npmjs.com/package/minify-literals) | Minify CSS and HTML literals. | -| [`rollup-plugin-minify-template-literals`](./packages/rollup-plugin-minify-template-literals) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/rollup-plugin-minify-template-literals) [![NPM Version](https://img.shields.io/npm/v/rollup-plugin-minify-template-literals.svg)](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) | Vite/Rollup plugin that minifies template literals. | -| [`spaify`](./packages/spaify) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/spaify) [![NPM Version](https://img.shields.io/npm/v/spaify.svg)](https://www.npmjs.com/package/spaify) | Seamless page transitions for static sites. | +| Package | | Description | +| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| [`@explodingcamera/css`](./packages/css) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/@explodingcamera/css) [![NPM Version](https://img.shields.io/npm/v/@explodingcamera/css.svg)](https://www.npmjs.com/package/@explodingcamera/css) | Small CSS reset/base files. | +| [`minify-literals`](./packages/minify-literals) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/minify-literals) [![NPM Version](https://img.shields.io/npm/v/minify-literals.svg)](https://www.npmjs.com/package/minify-literals) | Minify CSS and HTML literals. | +| [`rollup-plugin-minify-template-literals`](./packages/rollup-plugin-minify-template-literals) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/rollup-plugin-minify-template-literals) [![NPM Version](https://img.shields.io/npm/v/rollup-plugin-minify-template-literals.svg)](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) | Vite/Rollup plugin that minifies template literals. | +| [`simplejsx`](./packages/simplejsx) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/simplejsx) [![NPM Version](https://img.shields.io/npm/v/simplejsx.svg)](https://www.npmjs.com/package/simplejsx) | Minimal JSX templating for safe HTML strings. | +| [`spaify`](./packages/spaify) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/spaify) [![NPM Version](https://img.shields.io/npm/v/spaify.svg)](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) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/subsonic-api) [![NPM Version](https://img.shields.io/npm/v/subsonic-api.svg)](https://www.npmjs.com/package/subsonic-api) | API library for Subsonic-compatible servers. | +| Package | | Description | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| [`subsonic-api`](https://github.com/explodingcamera/subsonic-api) | [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/subsonic-api) [![NPM Version](https://img.shields.io/npm/v/subsonic-api.svg)](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 + A mountain lake +
+
, + { + 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 [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/minify-literals) [![NPM Version](https://img.shields.io/npm/v/minify-literals.svg)](https://www.npmjs.com/package/minify-literals) +# minify-literals [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/minify-literals) [![NPM Version](https://img.shields.io/npm/v/minify-literals.svg)](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 [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/rollup-plugin-minify-template-literals) [![NPM Version](https://img.shields.io/npm/v/rollup-plugin-minify-template-literals.svg)](https://www.npmjs.com/package/rollup-plugin-minify-template-literals) +# rollup-plugin-minify-template-literals [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://www.jsdocs.io/package/rollup-plugin-minify-template-literals) [![NPM Version](https://img.shields.io/npm/v/rollup-plugin-minify-template-literals.svg)](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 ( +
+

{title}

+ {children} +
+ ); + } + + const items = ["one", "two"]; + expect( + render( + +
    + {items.map((item) => ( +
  • {item}
  • + ))} +
+ {false} + {null} + {undefined} +
, + ), + ).toBe("

Items

  • one
  • two
"); +}); + +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( + , + ), + ).toBe(''); + 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("
onetwo
"); + 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('
Asyncx<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('A&B'); + await expect( + renderAsync(Mountain lake, { + element: async ({ tag, props }) => { + if (tag !== "img") return; + + await Promise.resolve(); + props["width"] = 1200; + props["height"] = 800; + }, + }), + ).resolves.toBe('Mountain lake'); +}); + +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( + , + ), + ).toBe(''); + 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)}`; + + 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)}`; + + 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 [![API Docs](https://img.shields.io/badge/API%20Docs-blue.svg)](https://paka.dev/npm/spaify) [![NPM Version](https://img.shields.io/npm/v/spaify.svg)](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",