From 0d1a02e5ea881fb79f4e7b3bec70fa2ee61c1b02 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 10 Jun 2026 15:19:17 +0530 Subject: [PATCH 1/3] chore(release): bump react-ui to 0.12.0, lang-core to 0.2.6, cli to 0.0.8 Version bumps for the first publish since 2026-05-20 (4b663b90): - @openuidev/react-ui 0.11.8 -> 0.12.0 (minor): component CSS now ships in `@layer openui` (#589/#621) and react-syntax-highlighter moved to ^16.1.1 (#577, fixes prismjs CVE-2024-53382) - @openuidev/lang-core 0.2.5 -> 0.2.6 (patch): parser preserves markdown fences and comments inside string props (#605) - @openuidev/cli 0.0.7 -> 0.0.8 (patch): cross-platform template build (#601, #627); no functional changes to the published CLI Remaining packages have no consumer-visible changes since the last publish and are not republished; the lang wrappers pick up lang-core 0.2.6 transitively via their ^0.2.5 ranges. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/lang-core/package.json | 2 +- packages/openui-cli/package.json | 2 +- packages/react-ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lang-core/package.json b/packages/lang-core/package.json index ee2c87ed0..3e96ed168 100644 --- a/packages/lang-core/package.json +++ b/packages/lang-core/package.json @@ -1,6 +1,6 @@ { "name": "@openuidev/lang-core", - "version": "0.2.5", + "version": "0.2.6", "description": "Framework-agnostic core for OpenUI Lang: parser, prompt generation, validation, and type definitions", "license": "MIT", "type": "module", diff --git a/packages/openui-cli/package.json b/packages/openui-cli/package.json index 892181b08..f4adb8666 100644 --- a/packages/openui-cli/package.json +++ b/packages/openui-cli/package.json @@ -1,6 +1,6 @@ { "name": "@openuidev/cli", - "version": "0.0.7", + "version": "0.0.8", "description": "CLI for OpenUI — scaffold generative UI chat apps and generate LLM system prompts from component libraries", "bin": { "openui": "dist/index.js" diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index 9c3889a47..2b6c96253 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@openuidev/react-ui", "license": "MIT", - "version": "0.11.8", + "version": "0.12.0", "description": "Component library for Generative UI SDK", "main": "dist/index.cjs", "module": "dist/index.mjs", From 263df21ebfc8f635ba3b1dd772bb14d95e073759 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 10 Jun 2026 15:56:04 +0530 Subject: [PATCH 2/3] fix(react-ui): strip leading BOM before wrapping CSS in @layer openui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sass emits a UTF-8 BOM for compressed output containing non-ASCII characters. At byte 0 the CSS decoder strips it, but wrapInLayer() concatenated the layer prelude in front of it, pushing the BOM inside the block where U+FEFF parses as an identifier: `:root` becomes a type selector that matches nothing, silently killing the first rule. In the packed 0.12.0 tarball this dropped the entire :root theme-token block of dist/components/index.css (and the first rule of dist/styles/index.css and markDownRenderer.css). Verified in Chrome: the BOM-poisoned rule does not apply; subsequent rules are unaffected. Strip the BOM before wrapping. Repacked tarball now has 0 BOMs across all 171 shipped CSS files and the :root block parses correctly. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/react-ui/cp-css.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react-ui/cp-css.js b/packages/react-ui/cp-css.js index dc8f9b78a..bcf0fb785 100644 --- a/packages/react-ui/cp-css.js +++ b/packages/react-ui/cp-css.js @@ -15,6 +15,11 @@ function ensureDirectoryExists(dirPath) { // Wrap a CSS file's contents in @layer openui { ... } if not already wrapped. // Idempotency check protects watch-mode and back-to-back builds. function wrapInLayer(content) { + // Sass emits a UTF-8 BOM for files with non-ASCII output. At byte 0 the + // decoder strips it, but wrapping would push it inside the layer block, + // where U+FEFF parses as an identifier and kills the first rule + // (e.g. the :root theme tokens). Strip it before wrapping. + content = content.replace(/^\uFEFF/, ""); if (content.trim() === "") return content; if (/^\s*@layer\s+openui\b/.test(content)) return content; return `@layer openui{${content}}`; From db825efda4ed81f4cdeb76913c12b68b292bdc65 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Tue, 16 Jun 2026 02:41:03 +0530 Subject: [PATCH 3/3] fix(react-ui): strip leading BOM from unlayered default CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The opt-in re-land (#631) only strips the BOM when wrapping the layered mirror, so Sass's compressed-mode BOM leaked into the unlayered defaults (components.css, styles/index.css, markDownRenderer.css). A leading BOM is harmless to browsers but breaks the package's zero-BOM contract and would become fatal if those files were ever wrapped. cp-css.js now strips it from the unlayered output, and check-css-artifacts.js asserts the unlayered exports are BOM-free too (the guard previously only checked the layered mirror). Root cause is on main (#631's cp-css.js) — flagged for a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/react-ui/check-css-artifacts.js | 8 +++++++ packages/react-ui/cp-css.js | 29 +++++++++++++++++++++++- packages/react-ui/css-layer-utils.mjs | 17 ++++++++++---- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/check-css-artifacts.js b/packages/react-ui/check-css-artifacts.js index 1f972fb88..dfa0001db 100644 --- a/packages/react-ui/check-css-artifacts.js +++ b/packages/react-ui/check-css-artifacts.js @@ -55,6 +55,14 @@ for (const name of unlayered) { assert(!/^\s*@layer/.test(read(path.join("styles", name))), `styles/${name} must stay unlayered`); } +// Unlayered default exports must also be BOM-free. A leading BOM is harmless in +// an unlayered file (the decoder drops it at byte 0), but cp-css.js strips it, +// and a regression re-arms the 2026-06 incident the moment these files are ever +// wrapped. Covers ./components.css and every ./styles/* (incl. openui-defaults). +for (const rel of ["components/index.css", ...unlayered.map((n) => path.join("styles", n))]) { + assert(!read(rel).includes(""), `${rel} contains a BOM (unlayered export must be BOM-free)`); +} + for (const f of [ ...layered.map((n) => path.join("layered", "styles", n)), "layered/components/index.css", diff --git a/packages/react-ui/cp-css.js b/packages/react-ui/cp-css.js index 2c645f860..321559c75 100644 --- a/packages/react-ui/cp-css.js +++ b/packages/react-ui/cp-css.js @@ -2,7 +2,7 @@ import fs from "fs"; import { camelCase } from "lodash-es"; import path from "path"; import { fileURLToPath } from "url"; -import { mirrorStylesWithLayer, writeLayeredCopy } from "./css-layer-utils.mjs"; +import { mirrorStylesWithLayer, stripBom, writeLayeredCopy } from "./css-layer-utils.mjs"; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -33,6 +33,23 @@ function fixScssImportsInJs(dir) { }); } +// Strip Sass's leading UTF-8 BOM (compressed-mode output for non-ASCII) from +// every *.css under dir, in place — so the unlayered default exports +// (./components.css, ./styles/*) ship BOM-free. The layered mirror strips it +// separately via wrapInLayer. +function stripBomFromCssInDir(dir) { + for (const entry of fs.readdirSync(dir)) { + const full = path.join(dir, entry); + if (fs.statSync(full).isDirectory()) { + stripBomFromCssInDir(full); + } else if (entry.endsWith(".css")) { + const content = fs.readFileSync(full, "utf8"); + const stripped = stripBom(content); + if (stripped !== content) fs.writeFileSync(full, stripped, "utf8"); + } + } +} + // Copy CSS files from src to dist function copyCssFiles() { const srcDir = path.join(dirname, "dist", "components"); @@ -41,6 +58,16 @@ function copyCssFiles() { // Ensure the dist/styles directory exists ensureDirectoryExists(distDir); + // Strip Sass's leading BOM from the unlayered sass output before copying, so + // the default exports (./components.css, ./styles/*) ship BOM-free. + stripBomFromCssInDir(srcDir); + const defaultsCssSrc = path.join(dirname, "dist", "openui-defaults.css"); + if (fs.existsSync(defaultsCssSrc)) { + const content = fs.readFileSync(defaultsCssSrc, "utf8"); + const stripped = stripBom(content); + if (stripped !== content) fs.writeFileSync(defaultsCssSrc, stripped, "utf8"); + } + // Read all component directories const components = fs.readdirSync(srcDir); diff --git a/packages/react-ui/css-layer-utils.mjs b/packages/react-ui/css-layer-utils.mjs index a4799c68d..fa4acd912 100644 --- a/packages/react-ui/css-layer-utils.mjs +++ b/packages/react-ui/css-layer-utils.mjs @@ -1,14 +1,21 @@ import fs from "fs"; import path from "path"; +// Strip a leading UTF-8 BOM (U+FEFF). Sass emits one in compressed mode for +// files with non-ASCII output. A browser drops it at byte 0, but it is noise in +// shipped artifacts and becomes fatal if the content is ever wrapped \u2014 the BOM +// would land mid-stylesheet inside a layer block, where U+FEFF parses as an +// identifier and kills the first rule (e.g. the :root theme tokens; the 2026-06 +// incident). Used for both the unlayered defaults (cp-css.js) and the layered +// mirror (wrapInLayer below). +export function stripBom(content) { + return content.replace(/^\uFEFF/, ""); +} + // Wrap a CSS file's contents in @layer openui { ... } if not already wrapped. // Idempotency check protects watch-mode and back-to-back builds. export function wrapInLayer(content) { - // Sass emits a UTF-8 BOM for files with non-ASCII output. At byte 0 the - // decoder strips it, but wrapping would push it inside the layer block, - // where U+FEFF parses as an identifier and kills the first rule - // (e.g. the :root theme tokens). Strip it before wrapping. - content = content.replace(/^\uFEFF/, ""); + content = stripBom(content); if (content.trim() === "") return content; if (/^\s*@layer\s+openui\b/.test(content)) return content; return `@layer openui{${content}}`;