From 6160636c7e0e5677bda17862b37f829d64f79052 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 10:26:21 +0000 Subject: [PATCH 1/9] docs: add design/implementation/feature simplification plan Reviews the whole monorepo and lays out a phased simplification plan: unify the triplicated JSX runtime (fixes Fragment ReferenceError in CLI builds), fix uno.config.js never being loaded, preserve nested page paths, drop the Node-side bundler in favor of native module resolution, remove h3/ws dependencies, migrate to Node's built-in snapshot testing, and consolidate examples/scope. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- SIMPLIFICATION_PLAN.md | 210 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 SIMPLIFICATION_PLAN.md diff --git a/SIMPLIFICATION_PLAN.md b/SIMPLIFICATION_PLAN.md new file mode 100644 index 0000000..c746e9d --- /dev/null +++ b/SIMPLIFICATION_PLAN.md @@ -0,0 +1,210 @@ +# Ono シンプル化計画 + +全体設計をレビューした結果に基づく、設計・実装・機能のシンプル化計画。 +「最小限の依存関係・高い移植性・シンプルなアーキテクチャ」というコア哲学に照らして、 +**現状がその哲学からずれている箇所**を特定し、フェーズごとに解消する。 + +## 現状サマリー + +| パッケージ | 役割 | 規模 | +|---|---|---| +| `packages/ono` | コアSSG + CLI | src 約1,600行 / テスト約1,600行 | +| `packages/create-ono` | スキャフォールディング | 約40行 | +| `packages/repl` | ブラウザREPL | 約400行 | +| `packages/ono-lp` | LLM LPジェネレーター | TS約300行 + 重い依存 | +| `packages/example` | サンプルサイト | - | + +コアの依存: `typescript`, `unocss`, `@unocss/preset-uno`, `h3`, `ws` + +## レビューで見つかった問題点 + +シンプル化の動機となる具体的な事実。**(A)〜(C) は実害のあるバグ**。 + +### (A) JSXランタイムが3重に存在し、すでに乖離してバグ化している + +1. `src/jsx-runtime.js` — 正規版。`Fragment` 対応済み +2. `src/constants.js` の `INLINE_JSX_RUNTIME` — ビルド時に注入される文字列版。**`Fragment` が未定義** +3. `src/barrels.js` の `loadMeta()` 内のインライン `h` — `flattenChildren` すらない3つ目のコピー + +結果: `transformer.js` は `jsxFragmentFactory: 'Fragment'` でコンパイルするため、 +**`<>...` を使ったページを `ono build` すると `ReferenceError: Fragment is not defined` で失敗する** +(PR #18 で Fragment 対応したのは REPL 経路のみで、CLI 経路は壊れたまま)。 + +### (B) `uno.config.js` は CLI から一切読まれていない + +`commands/dev.js:39` と `commands/build.js:110` は `loadUnoConfig()` を**引数なし**で呼ぶ。 +`loadUnoConfig(configPath)` は `path.resolve(undefined)` で即例外 → catch → 常に `{}` を返す。 +つまり **ユーザーの `uno.config.js`(テーマ、ショートカット等)はサイレントに無視される**。 +それにもかかわらず `unocssConfig` オプションが builder / watcher / commands の全域に配線されている +(常に `{}` しか流れない配管)。 + +### (C) ネストしたページがフラット化され、衝突する + +`builder.js` の `buildFile()` は `basename` しか見ないため: + +- `pages/blog/first-post.jsx` → `dist/first-post.html`(ディレクトリ構造消失) +- `pages/index.jsx` と `pages/blog/index.jsx` → 両方 `dist/index.html`(**後勝ちで上書き**) + +### (D) バンドラが2実装ある + +- `src/bundler.js` + `src/resolver.js`(計218行): 連結方式。import文を正規表現で除去して結合 +- `src/browser/compiler.js`(253行): 正規表現ベースの簡易モジュールシステム + +同じ「JSXの依存解決」を別々の意味論で2回実装している。どちらも正規表現パースで壊れやすい。 + +### (E) 細かい実装の重複・死にコード + +- `debounce` が2実装(`utils.browser.js` 版は誰も使っておらず、`watcher.js` が独自版を持つ) +- `transformer.js` の `transformJSXWithImports()` は本体から未使用(ヒューリスティックなimport自動付与) +- `utils.js` ⇄ `utils.browser.js` の再エクスポート層 +- `parseDevArgs` / `parseBuildArgs` がほぼ同一のargv手書きパース +- `constants.js` の過剰な定数化(`HTTP_STATUS.NOT_FOUND` や `ERROR_CODES.ENOENT` など、 + リテラルより読みにくくなる「定数のための定数」) + +### (F) 依存関係の過剰・不整合 + +- `h3`: 静的ファイル配信1ルートのためだけに使用 → `node:http` で40行相当 +- `ws`: 「reload」と一言送るためだけに使用 + 専用ポート(35729)を1つ消費 +- `unocss`(メタパッケージ)と `@unocss/preset-uno` を二重宣言 +- `browser/unocss.js` は `@unocss/core` を import しているが **package.json に未宣言**(幽霊依存) +- `unocss.js` の reset CSS 読み込みが `../node_modules/...` の相対パスハック(pnpm配下で壊れやすい) + +### (G) テスト体系の二重化 + +- unit テストと snapshot テストが transformer / renderer で同じ対象を二重にカバー +- 自作スナップショットフレームワーク `snapshot-utils.js`(139行)+ 専用ドキュメント + `SNAPSHOT_TESTING.md` — **Node 22 標準の `t.assert.snapshot` で置き換え可能**(engines は要更新) + +### (H) サンプル・スコープの散らばり + +- サンプルが4箇所: `packages/example`、`packages/ono/example`、`packages/ono/pages`、 + REPL の `main.js` 内ハードコード(さらに `create-ono/template`) +- `packages/ono/example/output.html` などビルド成果物がコミットされている +- `ono-lp` は SSG と無関係の別プロダクト(AI SDK×3 + commander + dotenv + zod という最重量の依存群)で、 + 「ミニマリストSSG」というリポジトリの焦点をぼかしている + +--- + +## シンプル化計画 + +フェーズごとに独立した PR にできる粒度で分割。上から順に価値が高い。 + +### Phase 1: バグを直す方向への統合((A)(B)(C)) + +**1-1. JSXランタイムの一本化** + +`INLINE_JSX_RUNTIME` 文字列と barrels 内のインライン `h` を削除し、 +ビルド時の一時ファイルには実体の import を注入する: + +```js +import { h, Fragment } from "@hashrock/ono/jsx-runtime"; +``` + +一時ファイルは Node の import で評価されるため node_modules 解決が効く。 +ランタイムの定義箇所が `jsx-runtime.js` の1箇所になり、Fragment バグ (A) が構造的に再発しなくなる。 + +**1-2. UnoCSS 設定読み込みの修正と配管の集約** + +- `loadUnoConfig()` をデフォルトで `cwd/uno.config.js` を探すよう修正((B) の修正) +- 「設定ファイルが無い」以外のエラー(設定ファイル内の構文エラー等)は握りつぶさず表示する +- `unocssConfig` の全域配線をやめ、`generateUnoCSS()` が自分で設定を読む形に集約 + (builder / watcher / commands のシグネチャから `unocssConfig` が消える) + +**1-3. 出力パスのディレクトリ構造保持** + +`buildFile()` の出力先を「入力ルートからの相対パス」で計算し、 +`pages/blog/first-post.jsx → dist/blog/first-post.html` とする((C) の修正)。 +※ 挙動変更だが、現状は衝突・上書きなので実質バグ修正。 + +### Phase 2: Node側バンドラの廃止((D) の半分と (E)) + +**2-1. bundler.js / resolver.js を削除し、Node のモジュール解決に任せる** + +現在の Node 側パイプラインは「自前で依存収集 → トポソート → 連結 → 一時ファイル1個 → import」。 +これを「**各 .jsx を変換して一時ディレクトリに書き出し → import specifier の `.jsx`→`.js` 書き換え → +エントリを import**」に変更する。 + +- 依存解決・トポソート・循環検出・npmパッケージ import をすべて Node 本体に任せられる +- `bundler.js`(81行) + `resolver.js`(137行) と正規表現 import パースが丸ごと消える +- 一時ファイルを `dist/` に書く現状(ビルド失敗時に `_temp_*.js` が成果物に残る)も解消され、 + 一時ディレクトリ(`node:os` の tmpdir か `.ono/` )に隔離される + +`browser/compiler.js` はブラウザに Node の解決系がない以上、自前実装が必要なので残す。 +ただし現状すでに独立しているので「Node 側と共有している振り」の構造だけ整理する。 + +**2-2. 死にコード・重複の削除** + +- `transformJSXWithImports()` 削除 +- `debounce` を1実装に統一(`watcher.js` のエラーログ付き版を採用) +- `utils.js` / `utils.browser.js` の再エクスポート層を解消: + ブラウザ互換関数は `utils.js` に一本化し、fs 依存の2関数(`cleanupTempFile`, + `getFilesRecursively`)は使用箇所へ移す +- `parseDevArgs` / `parseBuildArgs` を `node:util` の `parseArgs` による共通パーサ1つに統一 +- `constants.js` は実際に共有価値のあるもの(`SELF_CLOSING_TAGS`, `MIME_TYPES`, `DIRS`, + `PORTS`, `TIMING`)だけに縮小し、`HTTP_STATUS` / `ERROR_CODES` はリテラルへ戻す + +### Phase 3: 依存の削減((F)) + +**3-1. `h3` を削除** — `node:http` + 既存の `MIME_TYPES` で静的サーバーを実装(約40行)。 + +**3-2. `ws` を削除、SSE に移行** — ライブリロードは `EventSource` で十分: + +- dev サーバー自身が `/__reload` エンドポイントで SSE を配信(**専用ポート 35729 が不要になる**) +- クライアント注入スクリプトは `new EventSource('/__reload').onmessage = () => location.reload()` の1行相当 +- `createWebSocketServer` / ポート衝突フォールバック処理が消える + +**3-3. UnoCSS 依存の整理** + +- メタパッケージ `unocss` をやめ、実際に使う `@unocss/core` + `@unocss/preset-uno` + + `@unocss/reset` のみ宣言(幽霊依存の解消も兼ねる) +- reset CSS は `createRequire(import.meta.url).resolve("@unocss/reset/tailwind.css")` で + 正しく解決し、相対パスハックを削除 + +**達成後のコア依存: `typescript` + `@unocss/*` のみ**(README の哲学と一致する)。 + +### Phase 4: テストのシンプル化((G)) + +- engines を `>=22` に上げ、自作 `snapshot-utils.js`(139行)を Node 標準の + `t.assert.snapshot` に置き換え。`SNAPSHOT_TESTING.md` は数行の説明に縮小 +- transformer / renderer で unit と snapshot が二重カバーしている部分を統合し、 + 「挙動の分岐はunitテスト、出力形式はsnapshot」に役割分担 +- Phase 1〜3 の各変更はこのテスト群が回帰防止になるため、実施順は柔軟でよい + (先にやればフェーズ全体の安全網になる) + +### Phase 5: スコープ・サンプルの整理((H))— 要オーナー判断 + +- **`ono-lp` はモノレポから分離を推奨**(別リポジトリへ)。SSG コアと開発サイクルも + 依存も共有しておらず、lockfile とワークスペースを最も重くしている +- サンプルを `packages/example` に一本化し、`packages/ono/example`・`packages/ono/pages`・ + コミット済みビルド成果物(`output.html` 等)を削除 +- REPL 内ハードコードのサンプル3種は、効果が小さいので現状維持でよい(任意で外部ファイル化) + +### 維持するもの(判断と理由) + +- **barrels 機能**: PR #16 で Content Collections の代替として意図的に導入した直後であり、 + 唯一の「コンテンツ列挙」手段なので維持。ただし Phase 1-1 のランタイム統一で + `loadMeta` のインライン `h` は共通化する +- **`browser/compiler.js`**: REPL の中核であり Node の解決系が使えない環境なので自前実装を維持 +- **renderer / jsx-runtime / transformer**: すでに十分小さい(計約200行) + +--- + +## 期待効果 + +| 指標 | 現状 | 計画後 | +|---|---|---| +| コア src 行数 | 約1,600行 | 約1,000行 | +| コア依存 | typescript, unocss, @unocss/preset-uno, h3, ws | typescript, @unocss/core, @unocss/preset-uno, @unocss/reset | +| 使用ポート | HTTP + WebSocket(35729) | HTTP のみ | +| 既知バグ | Fragment 未定義 / uno.config 無視 / ネストページ衝突 | 0 | +| JSXランタイム定義 | 3箇所(乖離あり) | 1箇所 | +| バンドラ実装 | 2つ | 1つ(ブラウザのみ) | + +## 実施順の推奨 + +1. **Phase 1**(バグ修正を兼ねる。ユーザー影響が最も大きい) +2. **Phase 4**(テスト基盤を整え、以降の安全網にする) +3. **Phase 2** → **Phase 3**(内部リファクタリング。テストで回帰を検知) +4. **Phase 5**(オーナー判断が必要な項目。特に ono-lp 分離) + +各フェーズは独立した PR にでき、Phase 3-2(WS→SSE)以外に外部挙動の変更はない。 From 652c53ec15b14a1cdaae17a6b12e78c831c645f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 11:34:31 +0000 Subject: [PATCH 2/9] fix: unify JSX runtime, load uno.config.js, preserve nested page paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the simplification plan — three bugs fixed by removing duplication: - jsx-runtime.js is now self-contained and the single source of truth; the builder injects its real source instead of the drifted INLINE_JSX_RUNTIME copy (which lacked Fragment, so CLI builds of pages using <>... crashed). barrels' third inline runtime copy is gone too. - loadUnoConfig() now defaults to uno.config.js in the project root; it previously threw on its undefined path argument and silently returned {}, so user configs were never applied. The always-empty unocssConfig plumbing through builder/watcher/commands is removed; generateUnoCSS loads the config itself. - buildFile() preserves directory structure relative to the input root: pages/blog/first-post.jsx now outputs dist/blog/first-post.html instead of flattening (and colliding) at dist/. This also fixes .tsx pages being emitted as *.tsx.html. The example's uno.config.js is now a plain object (no unocss import, which wasn't resolvable from the example package anyway). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- packages/example/pages/about.jsx | 2 +- packages/example/uno.config.js | 15 ++++---- packages/ono/src/barrels.js | 9 ++--- packages/ono/src/builder.js | 62 +++++++++++++++++++----------- packages/ono/src/commands/build.js | 9 ++--- packages/ono/src/commands/dev.js | 10 ++--- packages/ono/src/constants.js | 21 ---------- packages/ono/src/jsx-runtime.js | 31 ++++++++++++++- packages/ono/src/unocss.js | 20 +++++----- packages/ono/src/utils.browser.js | 23 ----------- packages/ono/src/utils.js | 1 - packages/ono/src/watcher.js | 18 ++++----- 12 files changed, 105 insertions(+), 116 deletions(-) diff --git a/packages/example/pages/about.jsx b/packages/example/pages/about.jsx index 5ac9698..f0ceac8 100644 --- a/packages/example/pages/about.jsx +++ b/packages/example/pages/about.jsx @@ -6,7 +6,7 @@ function About() {

About This Example

-
+

This example site demonstrates how to build a modern static site using Ono with UnoCSS, reusable layouts, and component-based architecture. diff --git a/packages/example/uno.config.js b/packages/example/uno.config.js index 459691f..e331065 100644 --- a/packages/example/uno.config.js +++ b/packages/example/uno.config.js @@ -1,10 +1,9 @@ -import { defineConfig, presetUno, presetTypography } from "unocss"; - -export default defineConfig({ - presets: [ - presetUno(), - presetTypography(), - ], +/** + * UnoCSS Configuration + * A plain object is enough — Ono applies its default preset (presetUno) + * and merges this config on top. + */ +export default { theme: { colors: { primary: { @@ -37,4 +36,4 @@ export default defineConfig({ "nav-link": "text-gray-600 hover:text-primary-600 transition-colors", "section": "py-12", }, -}); +}; diff --git a/packages/ono/src/barrels.js b/packages/ono/src/barrels.js index 0243874..c291c3e 100644 --- a/packages/ono/src/barrels.js +++ b/packages/ono/src/barrels.js @@ -4,6 +4,7 @@ import { readdir, writeFile } from "node:fs/promises"; import { join, basename, dirname, relative } from "node:path"; import { bundle } from "./bundler.js"; +import { getInlineRuntime } from "./builder.js"; import { toCamelCase, cleanupTempFile, isJSXFile } from "./utils.js"; /** @@ -100,12 +101,8 @@ async function loadMeta(entryPath) { // Bundle the file to resolve imports const bundledCode = await bundle(entryPath); - // Add JSX runtime and extract meta - const code = `function h(tag, props, ...children) { - return { tag, props: props || {}, children }; -} -${bundledCode} -`; + // Add the shared JSX runtime and extract meta + const code = `${await getInlineRuntime()}\n${bundledCode}\n`; await writeFile(tempFile, code); diff --git a/packages/ono/src/builder.js b/packages/ono/src/builder.js index 281d25e..fd444e3 100644 --- a/packages/ono/src/builder.js +++ b/packages/ono/src/builder.js @@ -1,25 +1,42 @@ /** * Build utilities for Ono SSG */ -import { writeFile, mkdir } from "node:fs/promises"; +import { readFile, writeFile, mkdir } from "node:fs/promises"; import { resolve, join, dirname, basename, relative } from "node:path"; import { renderToString } from "./renderer.js"; -import { generateCSSFromFiles } from "./unocss.js"; +import { generateCSSFromFiles, loadUnoConfig } from "./unocss.js"; import { bundle } from "./bundler.js"; import { cleanupTempFile, getFilesRecursively, isJSXFile, isHTMLFile } from "./utils.js"; -import { INLINE_JSX_RUNTIME, DIRS } from "./constants.js"; +import { DIRS } from "./constants.js"; + +const JSX_RUNTIME_URL = new URL("./jsx-runtime.js", import.meta.url); +let runtimeSourceCache; +let tempFileCounter = 0; + +/** + * Get the JSX runtime source with export keywords stripped, ready to be + * injected into compiled pages. jsx-runtime.js is the single source of truth. + * @returns {Promise} Runtime source code + */ +export async function getInlineRuntime() { + if (!runtimeSourceCache) { + const source = await readFile(JSX_RUNTIME_URL, "utf-8"); + runtimeSourceCache = source.replace(/^export /gm, ""); + } + return runtimeSourceCache; +} /** * Build a single JSX file * @param {string} inputFile - Path to the JSX file * @param {Object} options - Build options * @param {string} [options.outputDir] - Output directory - * @param {Object} [options.unocssConfig] - UnoCSS configuration + * @param {string} [options.inputRoot] - Root directory the page paths are relative to * @param {boolean} [options.silent] - Suppress console output * @returns {Promise<{outputPath: string, html: string}>} */ export async function buildFile(inputFile, options = {}) { - const { outputDir = DIRS.OUTPUT, unocssConfig, silent = false } = options; + const { outputDir = DIRS.OUTPUT, inputRoot, silent = false } = options; const outDir = resolve(process.cwd(), outputDir); const resolvedInput = resolve(process.cwd(), inputFile); @@ -27,11 +44,11 @@ export async function buildFile(inputFile, options = {}) { // Bundle the file with all its dependencies const bundledCode = await bundle(resolvedInput); - // Add inline JSX runtime - const codeWithRuntime = INLINE_JSX_RUNTIME + "\n" + bundledCode; + // Inject the JSX runtime + const codeWithRuntime = (await getInlineRuntime()) + "\n" + bundledCode; // Write transformed JS temporarily - const tempFile = join(outDir, `_temp_${Date.now()}.js`); + const tempFile = join(outDir, `_temp_${Date.now()}_${tempFileCounter++}.js`); await mkdir(dirname(tempFile), { recursive: true }); await writeFile(tempFile, codeWithRuntime); @@ -51,10 +68,11 @@ export async function buildFile(inputFile, options = {}) { const html = renderToString(vnode); - // Determine output path - const baseName = basename(inputFile, ".jsx"); - const outputFileName = baseName === "index" ? "index.html" : `${baseName}.html`; - const outputPath = join(outDir, outputFileName); + // Determine output path, preserving directory structure relative to inputRoot + const relPath = inputRoot + ? relative(resolve(process.cwd(), inputRoot), resolvedInput) + : basename(resolvedInput); + const outputPath = join(outDir, relPath.replace(/\.(jsx|tsx)$/, ".html")); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, html); @@ -74,41 +92,41 @@ export async function buildFile(inputFile, options = {}) { * @param {string} inputPattern - Input directory path * @param {Object} options - Build options * @param {string} [options.outputDir] - Output directory - * @param {Object} [options.unocssConfig] - UnoCSS configuration * @param {boolean} [options.silent] - Suppress console output * @returns {Promise>} */ export async function buildFiles(inputPattern, options = {}) { - const { outputDir = DIRS.OUTPUT, unocssConfig, silent = false } = options; + const { outputDir = DIRS.OUTPUT, silent = false } = options; const pagesDir = resolve(process.cwd(), inputPattern); const files = await getFilesRecursively(pagesDir, isJSXFile); if (!silent) { console.log(`Found ${files.length} page(s) in ${inputPattern}/\n`); - } - - if (!silent) { for (const file of files) { console.log(`Building ${relative(process.cwd(), file)}...`); } } return Promise.all( - files.map((file) => buildFile(file, { outputDir, unocssConfig, silent: true })), + files.map((file) => + buildFile(file, { outputDir, inputRoot: pagesDir, silent: true }), + ), ); } /** - * Generate UnoCSS file + * Generate UnoCSS file from the built HTML. + * Loads uno.config.js from the project root unless a config is passed in. * @param {Object} options - Generation options * @param {string} [options.outputDir] - Output directory - * @param {Object} [options.unocssConfig] - UnoCSS configuration + * @param {Object} [options.config] - UnoCSS configuration override * @param {boolean} [options.silent] - Suppress console output * @returns {Promise} CSS file path or null */ export async function generateUnoCSS(options = {}) { - const { outputDir = DIRS.OUTPUT, unocssConfig, silent = false } = options; + const { outputDir = DIRS.OUTPUT, silent = false } = options; + const config = options.config ?? (await loadUnoConfig()); const outDir = resolve(process.cwd(), outputDir); @@ -120,7 +138,7 @@ export async function generateUnoCSS(options = {}) { } // Generate CSS from HTML files - const css = await generateCSSFromFiles(htmlFiles, unocssConfig); + const css = await generateCSSFromFiles(htmlFiles, config); if (!css) { return null; diff --git a/packages/ono/src/commands/build.js b/packages/ono/src/commands/build.js index db098c4..c201774 100644 --- a/packages/ono/src/commands/build.js +++ b/packages/ono/src/commands/build.js @@ -5,7 +5,6 @@ import { resolve } from "node:path"; import { copyFile, mkdir, readdir, stat } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join } from "node:path"; -import { loadUnoConfig } from "../unocss.js"; import { buildFile, buildFiles, generateUnoCSS } from "../builder.js"; import { generateBarrels } from "../barrels.js"; import { DIRS } from "../constants.js"; @@ -107,8 +106,6 @@ export async function runBuildCommand(args) { process.exit(0); } - const unocssConfig = await loadUnoConfig(); - // Generate barrel files if barrels directory exists await initializeBarrels(); @@ -117,16 +114,16 @@ export async function runBuildCommand(args) { const inputStat = await stat(inputPath); if (inputStat.isDirectory()) { - await buildFiles(input, { outputDir, unocssConfig }); + await buildFiles(input, { outputDir }); } else { - await buildFile(input, { outputDir, unocssConfig }); + await buildFile(input, { outputDir }); } // Copy public files await copyPublicFiles(outputDir); // Generate UnoCSS - await generateUnoCSS({ outputDir, unocssConfig }); + await generateUnoCSS({ outputDir }); console.log("\n✨ Build complete!"); } diff --git a/packages/ono/src/commands/dev.js b/packages/ono/src/commands/dev.js index 439fd8e..489501c 100644 --- a/packages/ono/src/commands/dev.js +++ b/packages/ono/src/commands/dev.js @@ -3,7 +3,6 @@ */ import { resolve, relative } from "node:path"; import { stat } from "node:fs/promises"; -import { loadUnoConfig } from "../unocss.js"; import { createDevServer } from "../server.js"; import { buildFile, buildFiles, generateUnoCSS } from "../builder.js"; import { watchFile, watchFiles, createWebSocketServer } from "../watcher.js"; @@ -36,8 +35,6 @@ export function parseDevArgs(args) { export async function runDevCommand(args) { const { input, port, outputDir } = parseDevArgs(args); - const unocssConfig = await loadUnoConfig(); - // Generate barrel files if barrels directory exists await initializeBarrels(); @@ -46,11 +43,11 @@ export async function runDevCommand(args) { const isDirectory = inputStat.isDirectory(); const initialBuild = isDirectory - ? await buildFiles(input, { outputDir, unocssConfig }) - : [await buildFile(input, { outputDir, unocssConfig })]; + ? await buildFiles(input, { outputDir }) + : [await buildFile(input, { outputDir })]; await copyPublicFiles(outputDir); - await generateUnoCSS({ outputDir, unocssConfig }); + await generateUnoCSS({ outputDir }); const { wss } = createWebSocketServer(); @@ -68,7 +65,6 @@ export async function runDevCommand(args) { const watchOpts = { outputDir, - unocssConfig, wss, onRebuild: () => copyPublicFiles(outputDir), }; diff --git a/packages/ono/src/constants.js b/packages/ono/src/constants.js index e6e57c4..56a5c42 100644 --- a/packages/ono/src/constants.js +++ b/packages/ono/src/constants.js @@ -91,24 +91,3 @@ export const HTTP_STATUS = { NOT_FOUND: 404, SERVER_ERROR: 500, }; - -/** - * Inline JSX runtime for bundled output - * This is injected into bundled files to provide JSX support without external dependencies - */ -export const INLINE_JSX_RUNTIME = `function flattenChildren(children) { - const result = []; - for (const child of children) { - if (child === null || child === undefined || typeof child === 'boolean') continue; - if (Array.isArray(child)) { - result.push(...flattenChildren(child)); - } else { - result.push(child); - } - } - return result; -} -function h(tag, props, ...children) { - return { tag, props: props || {}, children: flattenChildren(children) }; -} -`; diff --git a/packages/ono/src/jsx-runtime.js b/packages/ono/src/jsx-runtime.js index 617b998..e10db0f 100644 --- a/packages/ono/src/jsx-runtime.js +++ b/packages/ono/src/jsx-runtime.js @@ -1,14 +1,41 @@ /** * JSX Runtime - createElement function * Creates a VNode (Virtual Node) from JSX + * + * This module is self-contained (no imports) so the builder can inject it + * verbatim into compiled pages as the single source of truth for the runtime. + * It must stay browser-compatible for the REPL. */ -import { flattenChildren } from "./utils.browser.js"; /** - * Fragment symbol for grouping elements without a wrapper + * Fragment symbol for grouping elements without a wrapper. + * Symbol.for keeps identity stable even if the runtime is evaluated twice. */ export const Fragment = Symbol.for("ono.fragment"); +/** + * Flatten array recursively and filter out null/undefined/boolean children + * @param {any[]} children - Array of children to flatten + * @returns {any[]} Flattened array + */ +function flattenChildren(children) { + const result = []; + + for (const child of children) { + if (child === null || child === undefined || typeof child === "boolean") { + continue; + } + + if (Array.isArray(child)) { + result.push(...flattenChildren(child)); + } else { + result.push(child); + } + } + + return result; +} + /** * Create a VNode * @param {string|Function} tag - HTML tag name or component function diff --git a/packages/ono/src/unocss.js b/packages/ono/src/unocss.js index 430ac0f..e2514bb 100644 --- a/packages/ono/src/unocss.js +++ b/packages/ono/src/unocss.js @@ -4,8 +4,9 @@ import { createGenerator, presetUno } from "unocss"; import fs from "node:fs/promises"; +import { existsSync } from "node:fs"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -42,19 +43,20 @@ export async function createUnoGenerator(userConfig = {}) { } /** - * Load UnoCSS config from file - * @param {string} configPath - Path to config file + * Load UnoCSS config from file (defaults to uno.config.js in the project root). + * Returns an empty config when the file doesn't exist; errors inside an + * existing config file are NOT swallowed. + * @param {string} [configPath] - Path to config file * @returns {Promise} Configuration object */ export async function loadUnoConfig(configPath) { - try { - const configUrl = `file://${path.resolve(configPath)}?t=${Date.now()}`; - const module = await import(configUrl); - return module.default || module; - } catch (error) { - // Config file doesn't exist, return empty config + const resolved = path.resolve(process.cwd(), configPath || "uno.config.js"); + if (!existsSync(resolved)) { return {}; } + const configUrl = `${pathToFileURL(resolved).href}?t=${Date.now()}`; + const module = await import(configUrl); + return module.default || module; } /** diff --git a/packages/ono/src/utils.browser.js b/packages/ono/src/utils.browser.js index 2057b90..f605514 100644 --- a/packages/ono/src/utils.browser.js +++ b/packages/ono/src/utils.browser.js @@ -3,29 +3,6 @@ * These functions can be used in both Node.js and browser environments. */ -/** - * Flatten array recursively and filter out falsy values (null, undefined, boolean) - * @param {any[]} children - Array of children to flatten - * @returns {any[]} Flattened array with falsy values removed - */ -export function flattenChildren(children) { - const result = []; - - for (const child of children) { - if (child === null || child === undefined || typeof child === "boolean") { - continue; - } - - if (Array.isArray(child)) { - result.push(...flattenChildren(child)); - } else { - result.push(child); - } - } - - return result; -} - /** * Check if a filename has a JSX extension (.jsx or .tsx) * @param {string} filename - The filename to check diff --git a/packages/ono/src/utils.js b/packages/ono/src/utils.js index 4e381f9..a4172ea 100644 --- a/packages/ono/src/utils.js +++ b/packages/ono/src/utils.js @@ -9,7 +9,6 @@ import { join } from "node:path"; // Re-export browser-compatible utilities for backward compatibility export { - flattenChildren, isJSXFile, isHTMLFile, toCamelCase, diff --git a/packages/ono/src/watcher.js b/packages/ono/src/watcher.js index 8f5d579..64b3179 100644 --- a/packages/ono/src/watcher.js +++ b/packages/ono/src/watcher.js @@ -67,16 +67,15 @@ async function afterRebuild({ onRebuild, wss }) { * @param {string} inputPattern - Input directory to watch * @param {Object} options - Watch options * @param {string} [options.outputDir] - Output directory - * @param {Object} [options.unocssConfig] - UnoCSS configuration * @param {Function} [options.onRebuild] - Callback after rebuild * @param {WebSocketServer} [options.wss] - WebSocket server for live reload * @returns {Promise<{watcher: any, publicWatcher?: any, barrelsWatcher?: any}>} */ export async function watchFiles(inputPattern, options = {}) { - const { outputDir = DIRS.OUTPUT, unocssConfig, onRebuild, wss } = options; - const buildOpts = { outputDir, unocssConfig, silent: false }; + const { outputDir = DIRS.OUTPUT, onRebuild, wss } = options; const pagesDir = resolve(process.cwd(), inputPattern); + const buildOpts = { outputDir, inputRoot: pagesDir, silent: false }; const publicDir = resolve(process.cwd(), DIRS.PUBLIC); const barrelsDir = resolve(process.cwd(), DIRS.BARRELS); @@ -86,7 +85,7 @@ export async function watchFiles(inputPattern, options = {}) { console.log(`\n📝 File changed: ${relative(process.cwd(), file)}`); console.log("🔄 Rebuilding...\n"); await buildFile(file, buildOpts); - await generateUnoCSS(buildOpts); + await generateUnoCSS({ outputDir }); await afterRebuild({ onRebuild, wss }); }); @@ -94,7 +93,7 @@ export async function watchFiles(inputPattern, options = {}) { console.log(`\n📝 ${reason}`); console.log("🔄 Rebuilding...\n"); await buildFiles(inputPattern, buildOpts); - await generateUnoCSS(buildOpts); + await generateUnoCSS({ outputDir }); await afterRebuild({ onRebuild, wss }); }); @@ -118,7 +117,7 @@ export async function watchFiles(inputPattern, options = {}) { console.log("🔄 Regenerating barrel...\n"); await generateBarrel(barrelDir); await buildFiles(inputPattern, buildOpts); - await generateUnoCSS(buildOpts); + await generateUnoCSS({ outputDir }); await afterRebuild({ onRebuild, wss }); }); @@ -142,14 +141,13 @@ export async function watchFiles(inputPattern, options = {}) { * @param {string} inputFile - Input file to watch * @param {Object} options - Watch options * @param {string} [options.outputDir] - Output directory - * @param {Object} [options.unocssConfig] - UnoCSS configuration * @param {Function} [options.onRebuild] - Callback after rebuild * @param {WebSocketServer} [options.wss] - WebSocket server for live reload * @returns {Promise<{watcher: any}>} */ export async function watchFile(inputFile, options = {}) { - const { outputDir = DIRS.OUTPUT, unocssConfig, onRebuild, wss } = options; - const buildOpts = { outputDir, unocssConfig, silent: false }; + const { outputDir = DIRS.OUTPUT, onRebuild, wss } = options; + const buildOpts = { outputDir, silent: false }; const resolvedInput = resolve(process.cwd(), inputFile); console.log(`👀 Watching for changes in ${inputFile}...`); @@ -158,7 +156,7 @@ export async function watchFile(inputFile, options = {}) { console.log(`\n📝 File changed: ${inputFile}`); console.log("🔄 Rebuilding...\n"); await buildFile(resolvedInput, buildOpts); - await generateUnoCSS(buildOpts); + await generateUnoCSS({ outputDir }); await afterRebuild({ onRebuild, wss }); }); From b0cb79e524a4164206336f5248bb8f52b6109eaa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 11:40:39 +0000 Subject: [PATCH 3/9] refactor: replace Node-side bundler with native module resolution Phase 2 of the simplification plan: - Pages are now compiled file-by-file into a per-build temp directory (.ono/) that mirrors the project layout, relative .jsx/.tsx/.ts specifiers are rewritten to .js, and the entry is imported normally. Evaluation order, circular imports, and package imports are handled by Node itself instead of a hand-rolled bundler, and failed builds no longer leave _temp_*.js files inside dist/. - bundler.js is deleted; resolver.js shrinks to import scanning only (no topological sort, no import/export text surgery). - barrels' meta loading reuses the same importJSXModule() helper. - Dead code removed: transformJSXWithImports(), the unused plain debounce(), and the utils.js/utils.browser.js re-export split (flattenChildren lives in jsx-runtime.js, toCamelCase in barrels.js). - parseBuildArgs/parseDevArgs are unified into one parseCommandArgs() built on node:util's parseArgs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- .gitignore | 4 +- packages/ono/package.json | 1 - packages/ono/src/barrels.js | 35 ++++----- packages/ono/src/builder.js | 114 +++++++++++++++++++++-------- packages/ono/src/bundler.js | 81 -------------------- packages/ono/src/commands/build.js | 38 ++++++---- packages/ono/src/commands/dev.js | 23 +----- packages/ono/src/resolver.js | 96 +++++------------------- packages/ono/src/transformer.js | 20 ----- packages/ono/src/utils.browser.js | 45 ------------ packages/ono/src/utils.js | 36 ++++----- packages/ono/test/builder.test.js | 107 +++++++++++++++++++++++++++ packages/ono/test/bundler.test.js | 78 -------------------- packages/ono/test/resolver.test.js | 82 ++++++--------------- 14 files changed, 286 insertions(+), 474 deletions(-) delete mode 100644 packages/ono/src/bundler.js delete mode 100644 packages/ono/src/utils.browser.js create mode 100644 packages/ono/test/builder.test.js delete mode 100644 packages/ono/test/bundler.test.js diff --git a/.gitignore b/.gitignore index 709c948..fa2a8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -83,8 +83,8 @@ out dist .output -# Mini JSX temporary files -.mini-jsx-tmp.js +# Ono per-build temp directory +.ono/ # Gatsby files .cache/ diff --git a/packages/ono/package.json b/packages/ono/package.json index ecbec6b..a5abf8b 100644 --- a/packages/ono/package.json +++ b/packages/ono/package.json @@ -9,7 +9,6 @@ "./jsx-runtime": "./src/jsx-runtime.js", "./renderer": "./src/renderer.js", "./transformer": "./src/transformer.js", - "./bundler": "./src/bundler.js", "./barrels": "./src/barrels.js", "./browser/compiler": "./src/browser/compiler.js", "./browser/unocss": "./src/browser/unocss.js" diff --git a/packages/ono/src/barrels.js b/packages/ono/src/barrels.js index c291c3e..109687f 100644 --- a/packages/ono/src/barrels.js +++ b/packages/ono/src/barrels.js @@ -2,10 +2,18 @@ * Barrels - Auto-generated barrel files with type inference */ import { readdir, writeFile } from "node:fs/promises"; -import { join, basename, dirname, relative } from "node:path"; -import { bundle } from "./bundler.js"; -import { getInlineRuntime } from "./builder.js"; -import { toCamelCase, cleanupTempFile, isJSXFile } from "./utils.js"; +import { join, basename, relative } from "node:path"; +import { importJSXModule } from "./builder.js"; +import { isJSXFile } from "./utils.js"; + +/** + * Convert kebab-case or snake_case to camelCase + * @param {string} str - String to convert + * @returns {string} camelCase string + */ +function toCamelCase(str) { + return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase()); +} /** * Infer TypeScript type from a JavaScript value @@ -89,32 +97,17 @@ async function getBarrelEntries(barrelDir) { } /** - * Load meta from an entry file by bundling and evaluating + * Load meta from an entry file by compiling and evaluating it * @param {string} entryPath - Path to the entry file * @returns {Promise} Meta object or null */ async function loadMeta(entryPath) { - const tempDir = dirname(entryPath); - const tempFile = join(tempDir, `_temp_meta_${Date.now()}.js`); - try { - // Bundle the file to resolve imports - const bundledCode = await bundle(entryPath); - - // Add the shared JSX runtime and extract meta - const code = `${await getInlineRuntime()}\n${bundledCode}\n`; - - await writeFile(tempFile, code); - - const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`); - const module = await import(moduleUrl.href); + const module = await importJSXModule(entryPath); return module.meta || null; } catch (error) { console.warn(`Warning: Could not load meta from ${entryPath}:`, error.message); return null; - } finally { - // Clean up - await cleanupTempFile(tempFile); } } diff --git a/packages/ono/src/builder.js b/packages/ono/src/builder.js index fd444e3..d8047c3 100644 --- a/packages/ono/src/builder.js +++ b/packages/ono/src/builder.js @@ -1,29 +1,97 @@ /** * Build utilities for Ono SSG + * + * Pages are compiled file-by-file into a per-build temp directory that + * mirrors the project layout, then the entry is imported with Node's own + * module resolution. No bundling: evaluation order, cycles, and package + * imports are all handled by Node. */ -import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { readFile, writeFile, mkdir, rm } from "node:fs/promises"; import { resolve, join, dirname, basename, relative } from "node:path"; +import { pathToFileURL } from "node:url"; import { renderToString } from "./renderer.js"; import { generateCSSFromFiles, loadUnoConfig } from "./unocss.js"; -import { bundle } from "./bundler.js"; -import { cleanupTempFile, getFilesRecursively, isJSXFile, isHTMLFile } from "./utils.js"; +import { transformJSX } from "./transformer.js"; +import { collectModules } from "./resolver.js"; +import { getFilesRecursively, isJSXFile, isHTMLFile } from "./utils.js"; import { DIRS } from "./constants.js"; -const JSX_RUNTIME_URL = new URL("./jsx-runtime.js", import.meta.url); -let runtimeSourceCache; -let tempFileCounter = 0; +/** Import statement for the real JSX runtime, injected into compiled pages */ +const JSX_RUNTIME_IMPORT = `import { h, Fragment } from ${JSON.stringify( + new URL("./jsx-runtime.js", import.meta.url).href, +)};\n`; + +/** Directory (inside the project) that holds per-build temp output */ +const TEMP_DIR = ".ono"; + +let buildCounter = 0; /** - * Get the JSX runtime source with export keywords stripped, ready to be - * injected into compiled pages. jsx-runtime.js is the single source of truth. - * @returns {Promise} Runtime source code + * Rewrite relative .jsx/.tsx/.ts import specifiers to .js so they resolve + * against the compiled files in the temp directory. */ -export async function getInlineRuntime() { - if (!runtimeSourceCache) { - const source = await readFile(JSX_RUNTIME_URL, "utf-8"); - runtimeSourceCache = source.replace(/^export /gm, ""); +function rewriteRelativeImports(code) { + return code.replace( + /^((?:import|export)[^'"]*['"])(\.[^'"]+)\.(?:jsx|tsx|ts)(['"])/gm, + (_match, head, base, tail) => `${head}${base}.js${tail}`, + ); +} + +/** True when the file imports the runtime itself (skip injection) */ +function importsOwnRuntime(source) { + return /from\s+['"]@hashrock\/ono(?:\/jsx-runtime(?:\.js)?)?['"]/.test(source); +} + +/** + * Compile an entry file and its local imports into a fresh temp directory. + * A unique directory per build doubles as ESM cache-busting for rebuilds. + * @returns {Promise<{tempRoot: string, entryPath: string}>} + */ +async function compileToTemp(entryFile) { + const cwd = process.cwd(); + const files = await collectModules(entryFile); + const tempRoot = join(cwd, TEMP_DIR, `build-${process.pid}-${buildCounter++}`); + + let entryPath; + for (const file of files) { + const rel = relative(cwd, file); + if (rel.startsWith("..")) { + throw new Error( + `Cannot compile ${file}: imports outside the project directory are not supported`, + ); + } + + const source = await readFile(file, "utf-8"); + let code = rewriteRelativeImports(transformJSX(source, file)); + if (!importsOwnRuntime(source)) { + code = JSX_RUNTIME_IMPORT + code; + } + + const outPath = join(tempRoot, rel.replace(/\.(jsx|tsx|ts)$/, ".js")); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, code); + + if (file === entryFile) { + entryPath = outPath; + } + } + + return { tempRoot, entryPath }; +} + +/** + * Compile a JSX entry file (with its local imports) and import it + * @param {string} entryFile - Path to the entry file + * @returns {Promise} The imported module namespace + */ +export async function importJSXModule(entryFile) { + const resolvedEntry = resolve(process.cwd(), entryFile); + const { tempRoot, entryPath } = await compileToTemp(resolvedEntry); + try { + return await import(pathToFileURL(entryPath).href); + } finally { + await rm(tempRoot, { recursive: true, force: true }); } - return runtimeSourceCache; } /** @@ -41,20 +109,7 @@ export async function buildFile(inputFile, options = {}) { const outDir = resolve(process.cwd(), outputDir); const resolvedInput = resolve(process.cwd(), inputFile); - // Bundle the file with all its dependencies - const bundledCode = await bundle(resolvedInput); - - // Inject the JSX runtime - const codeWithRuntime = (await getInlineRuntime()) + "\n" + bundledCode; - - // Write transformed JS temporarily - const tempFile = join(outDir, `_temp_${Date.now()}_${tempFileCounter++}.js`); - await mkdir(dirname(tempFile), { recursive: true }); - await writeFile(tempFile, codeWithRuntime); - - // Import and render - const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`); - const module = await import(moduleUrl.href); + const module = await importJSXModule(resolvedInput); const App = module.default; if (!App) { @@ -81,9 +136,6 @@ export async function buildFile(inputFile, options = {}) { console.log(`✓ Built successfully: ${relative(process.cwd(), outputPath)}`); } - // Clean up temp file - await cleanupTempFile(tempFile); - return { outputPath, html }; } diff --git a/packages/ono/src/bundler.js b/packages/ono/src/bundler.js deleted file mode 100644 index 52c6ca4..0000000 --- a/packages/ono/src/bundler.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Bundler - Bundle JSX files into a single executable module - */ - -import fs from "node:fs/promises"; -import { collectDependencies } from "./resolver.js"; -import { transformJSX } from "./transformer.js"; - -/** - * Bundle a JSX file and all its dependencies - * @param {string} entryFile - Absolute path to entry file - * @returns {Promise} Bundled JavaScript code - */ -export async function bundle(entryFile) { - // Collect all dependencies - const { order } = await collectDependencies(entryFile); - - const modules = []; - - // Process each module in dependency order - for (let i = 0; i < order.length; i++) { - const filePath = order[i]; - const isEntry = i === order.length - 1; // Last file is entry - - // Read the file - const source = await fs.readFile(filePath, "utf-8"); - - // Transform JSX to JS - const transformed = transformJSX(source, filePath); - - // Remove import statements (they're already resolved) - const withoutImports = removeImports(transformed); - - // Remove export default from non-entry files - const withoutExports = isEntry ? withoutImports : removeExportDefault(withoutImports); - - modules.push({ - path: filePath, - code: withoutExports, - isEntry - }); - } - - // Combine all modules into one - const bundledCode = modules.map(m => m.code).join("\n\n"); - - return bundledCode; -} - -/** - * Remove import statements from code (but keep package imports) - * @param {string} code - JavaScript code - * @returns {string} Code with relative imports removed, package imports kept - */ -function removeImports(code) { - // Remove only relative import statements, keep package imports - const lines = code.split("\n"); - const filteredLines = lines.filter(line => { - const trimmed = line.trim(); - if (!trimmed.startsWith("import ")) return true; - - // Keep package imports (not starting with . or /) - const importMatch = trimmed.match(/from\s+['"]([^'"]+)['"]/); - if (importMatch && !importMatch[1].startsWith('.') && !importMatch[1].startsWith('/')) { - return true; // Keep package import - } - - return false; // Remove relative import - }); - - return filteredLines.join("\n"); -} - -/** - * Remove export default from code - * @param {string} code - JavaScript code - * @returns {string} Code without export default - */ -function removeExportDefault(code) { - return code.replace(/export\s+default\s+/g, ""); -} diff --git a/packages/ono/src/commands/build.js b/packages/ono/src/commands/build.js index c201774..d88af09 100644 --- a/packages/ono/src/commands/build.js +++ b/packages/ono/src/commands/build.js @@ -1,28 +1,36 @@ /** * Build command for Ono CLI */ -import { resolve } from "node:path"; +import { resolve, join } from "node:path"; import { copyFile, mkdir, readdir, stat } from "node:fs/promises"; import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { parseArgs } from "node:util"; import { buildFile, buildFiles, generateUnoCSS } from "../builder.js"; import { generateBarrels } from "../barrels.js"; -import { DIRS } from "../constants.js"; +import { DIRS, PORTS } from "../constants.js"; /** - * Parse build command arguments + * Parse arguments shared by the build and dev commands * @param {string[]} args - Command line arguments - * @returns {{ input: string, outputDir: string, showHelp: boolean }} + * @returns {{ input: string, outputDir: string, port: number, showHelp: boolean }} */ -export function parseBuildArgs(args) { - const showHelp = args.includes("--help") || args.includes("-h"); - const nonOptionArgs = args.filter((arg) => !arg.startsWith("--")); - const input = nonOptionArgs[0] || DIRS.PAGES; - const outputDir = args.includes("--output") - ? args[args.indexOf("--output") + 1] - : DIRS.OUTPUT; - - return { input, outputDir, showHelp }; +export function parseCommandArgs(args) { + const { values, positionals } = parseArgs({ + args, + options: { + output: { type: "string", default: DIRS.OUTPUT }, + port: { type: "string", default: String(PORTS.SERVER) }, + help: { type: "boolean", short: "h", default: false }, + }, + allowPositionals: true, + }); + + return { + input: positionals[0] || DIRS.PAGES, + outputDir: values.output, + port: Number.parseInt(values.port, 10), + showHelp: values.help, + }; } /** @@ -99,7 +107,7 @@ export async function initializeBarrels() { * @returns {Promise} */ export async function runBuildCommand(args) { - const { input, outputDir, showHelp } = parseBuildArgs(args); + const { input, outputDir, showHelp } = parseCommandArgs(args); if (showHelp) { showBuildHelp(); diff --git a/packages/ono/src/commands/dev.js b/packages/ono/src/commands/dev.js index 489501c..22f0d5e 100644 --- a/packages/ono/src/commands/dev.js +++ b/packages/ono/src/commands/dev.js @@ -6,26 +6,7 @@ import { stat } from "node:fs/promises"; import { createDevServer } from "../server.js"; import { buildFile, buildFiles, generateUnoCSS } from "../builder.js"; import { watchFile, watchFiles, createWebSocketServer } from "../watcher.js"; -import { copyPublicFiles, initializeBarrels } from "./build.js"; -import { DIRS, PORTS } from "../constants.js"; - -/** - * Parse dev command arguments - * @param {string[]} args - Command line arguments - * @returns {{ input: string, port: number, outputDir: string }} - */ -export function parseDevArgs(args) { - const nonOptionArgs = args.filter((arg) => !arg.startsWith("--")); - const input = nonOptionArgs[0] || DIRS.PAGES; - const port = args.includes("--port") - ? parseInt(args[args.indexOf("--port") + 1]) - : PORTS.SERVER; - const outputDir = args.includes("--output") - ? args[args.indexOf("--output") + 1] - : DIRS.OUTPUT; - - return { input, port, outputDir }; -} +import { copyPublicFiles, initializeBarrels, parseCommandArgs } from "./build.js"; /** * Run the dev command @@ -33,7 +14,7 @@ export function parseDevArgs(args) { * @returns {Promise} */ export async function runDevCommand(args) { - const { input, port, outputDir } = parseDevArgs(args); + const { input, port, outputDir } = parseCommandArgs(args); // Generate barrel files if barrels directory exists await initializeBarrels(); diff --git a/packages/ono/src/resolver.js b/packages/ono/src/resolver.js index 10ad294..f512bbb 100644 --- a/packages/ono/src/resolver.js +++ b/packages/ono/src/resolver.js @@ -1,17 +1,20 @@ /** - * Module Resolver - Parse imports and resolve dependencies + * Module Resolver - collect the local import graph of an entry file. + * + * Only relative specifiers are followed; package imports and evaluation + * order are left to Node's own module resolution. */ import fs from "node:fs/promises"; import path from "node:path"; /** - * Parse import statements from source code + * Parse import specifiers from source code * @param {string} code - Source code - * @returns {Array} Array of import objects with specifier + * @returns {string[]} Array of import specifiers */ export function parseImports(code) { - const imports = []; + const specifiers = []; // Match various import patterns: // import foo from "bar" @@ -22,12 +25,10 @@ export function parseImports(code) { let match; while ((match = importRegex.exec(code)) !== null) { - imports.push({ - specifier: match[1] - }); + specifiers.push(match[1]); } - return imports; + return specifiers; } /** @@ -37,70 +38,25 @@ export function parseImports(code) { * @returns {string} Absolute path to the imported file */ export function resolveImportPath(importPath, fromFile) { - // If it's already an absolute path, return as-is if (path.isAbsolute(importPath)) { return importPath; } - - // For relative imports, resolve relative to the importing file - const dir = path.dirname(fromFile); - return path.resolve(dir, importPath); -} - -/** - * Topological sort for dependency graph - * @param {Map} graph - Dependency graph (file -> [dependencies]) - * @returns {Array} Sorted array of file paths - */ -function topologicalSort(graph) { - const sorted = []; - const visited = new Set(); - const visiting = new Set(); - - function visit(node) { - if (visited.has(node)) return; - if (visiting.has(node)) { - throw new Error(`Circular dependency detected: ${node}`); - } - - visiting.add(node); - - const deps = graph.get(node) || []; - for (const dep of deps) { - visit(dep); - } - - visiting.delete(node); - visited.add(node); - sorted.push(node); - } - - // Visit all nodes - for (const node of graph.keys()) { - visit(node); - } - - return sorted; + return path.resolve(path.dirname(fromFile), importPath); } /** - * Collect all dependencies recursively + * Collect the entry file and all transitively imported local files * @param {string} entryFile - Absolute path to entry file - * @returns {Object} Object with modules (Set), graph (Map), and order (Array) + * @returns {Promise>} Absolute paths of all local modules */ -export async function collectDependencies(entryFile) { +export async function collectModules(entryFile) { const modules = new Set(); - const graph = new Map(); const queue = [entryFile]; - // BFS to collect all dependencies while (queue.length > 0) { const currentFile = queue.shift(); - - // Skip if already processed if (modules.has(currentFile)) continue; - // Read the file let source; try { source = await fs.readFile(currentFile, "utf-8"); @@ -108,30 +64,14 @@ export async function collectDependencies(entryFile) { throw new Error(`Cannot read file: ${currentFile}\n${error.message}`); } - // Parse imports - const imports = parseImports(source); - const dependencies = []; + modules.add(currentFile); - for (const imp of imports) { - // Only process relative imports (skip node_modules, etc.) - if (imp.specifier.startsWith(".")) { - const resolvedPath = resolveImportPath(imp.specifier, currentFile); - dependencies.push(resolvedPath); - queue.push(resolvedPath); + for (const specifier of parseImports(source)) { + if (specifier.startsWith(".")) { + queue.push(resolveImportPath(specifier, currentFile)); } } - - // Add to modules and graph - modules.add(currentFile); - graph.set(currentFile, dependencies); } - // Sort dependencies topologically - const order = topologicalSort(graph); - - return { - modules, - graph, - order - }; + return modules; } diff --git a/packages/ono/src/transformer.js b/packages/ono/src/transformer.js index 7ea1440..63e4c9a 100644 --- a/packages/ono/src/transformer.js +++ b/packages/ono/src/transformer.js @@ -29,23 +29,3 @@ export function transformJSX(source, filename = 'input.jsx') { return result.outputText; } - -/** - * Transform JSX file and add necessary imports if not present - * @param {string} source - JSX source code - * @param {string} [filename='input.jsx'] - Optional filename - * @returns {string} Transformed JavaScript with imports - */ -export function transformJSXWithImports(source, filename = 'input.jsx') { - let transformedCode = transformJSX(source, filename); - - // Check if the code uses 'h' function (JSX was transformed) - if (transformedCode.includes('h(') && !source.includes('import') && !source.includes('from')) { - // Add import statement for h function (and Fragment if used) - const usesFragment = transformedCode.includes('Fragment'); - const imports = usesFragment ? '{ h, Fragment }' : '{ h }'; - transformedCode = `import ${imports} from './jsx-runtime.js';\n${transformedCode}`; - } - - return transformedCode; -} diff --git a/packages/ono/src/utils.browser.js b/packages/ono/src/utils.browser.js deleted file mode 100644 index f605514..0000000 --- a/packages/ono/src/utils.browser.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Browser-compatible utilities for Ono SSG - * These functions can be used in both Node.js and browser environments. - */ - -/** - * Check if a filename has a JSX extension (.jsx or .tsx) - * @param {string} filename - The filename to check - * @returns {boolean} True if the file has a JSX extension - */ -export function isJSXFile(filename) { - return filename.endsWith(".jsx") || filename.endsWith(".tsx"); -} - -/** - * Check if a filename has an HTML extension - * @param {string} filename - The filename to check - * @returns {boolean} True if the file has an HTML extension - */ -export function isHTMLFile(filename) { - return filename.endsWith(".html"); -} - -/** - * Convert kebab-case or snake_case to camelCase - * @param {string} str - String to convert - * @returns {string} camelCase string - */ -export function toCamelCase(str) { - return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase()); -} - -/** - * Create a debounced version of a function - * @param {Function} fn - Function to debounce - * @param {number} delay - Delay in milliseconds - * @returns {Function} Debounced function - */ -export function debounce(fn, delay) { - let timeoutId; - return (...args) => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => fn(...args), delay); - }; -} diff --git a/packages/ono/src/utils.js b/packages/ono/src/utils.js index a4172ea..af042cb 100644 --- a/packages/ono/src/utils.js +++ b/packages/ono/src/utils.js @@ -1,35 +1,29 @@ /** - * Node.js utilities for Ono SSG - * This file contains utilities that require Node.js runtime. - * For browser-compatible utilities, use utils.browser.js + * Shared utilities for Ono SSG */ -import { unlink } from "node:fs/promises"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; -// Re-export browser-compatible utilities for backward compatibility -export { - isJSXFile, - isHTMLFile, - toCamelCase, - debounce, -} from "./utils.browser.js"; +/** + * Check if a filename has a JSX extension (.jsx or .tsx) + * @param {string} filename - The filename to check + * @returns {boolean} True if the file has a JSX extension + */ +export function isJSXFile(filename) { + return filename.endsWith(".jsx") || filename.endsWith(".tsx"); +} /** - * Clean up a temporary file, silently ignoring errors - * @param {string} tempFile - Path to the temporary file to delete - * @returns {Promise} + * Check if a filename has an HTML extension + * @param {string} filename - The filename to check + * @returns {boolean} True if the file has an HTML extension */ -export async function cleanupTempFile(tempFile) { - try { - await unlink(tempFile); - } catch { - // Ignore cleanup errors - } +export function isHTMLFile(filename) { + return filename.endsWith(".html"); } /** - * Get all files with specified extensions recursively from a directory + * Get all files matching a predicate recursively from a directory * @param {string} dir - Directory to search * @param {(filename: string) => boolean} predicate - Function to test filenames * @returns {Promise} Array of file paths diff --git a/packages/ono/test/builder.test.js b/packages/ono/test/builder.test.js new file mode 100644 index 0000000..b40ccdc --- /dev/null +++ b/packages/ono/test/builder.test.js @@ -0,0 +1,107 @@ +import { test, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; +import path from "node:path"; +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { buildFile, buildFiles, importJSXModule } from "../src/builder.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixturesDir = path.join(__dirname, "fixtures"); +const outDir = path.join(__dirname, "builder-test-tmp"); + +beforeEach(async () => { + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); +}); + +afterEach(async () => { + await rm(outDir, { recursive: true, force: true }); +}); + +test("importJSXModule - returns the module namespace", async () => { + const module = await importJSXModule(path.join(fixturesDir, "App.jsx")); + assert.strictEqual(typeof module.default, "function"); +}); + +test("importJSXModule - rejects on non-existent file", async () => { + await assert.rejects( + () => importJSXModule(path.join(fixturesDir, "NonExistent.jsx")), + /Cannot read file/i + ); +}); + +test("importJSXModule - cleans up its temp directory", async () => { + await importJSXModule(path.join(fixturesDir, "App.jsx")); + const tempDir = path.join(process.cwd(), ".ono"); + if (existsSync(tempDir)) { + const { readdir } = await import("node:fs/promises"); + const leftovers = await readdir(tempDir); + assert.deepStrictEqual(leftovers, []); + } +}); + +test("buildFile - single file with imports renders all components", async () => { + const { html, outputPath } = await buildFile(path.join(fixturesDir, "App.jsx"), { + outputDir: outDir, + silent: true, + }); + + assert.ok(html.includes("

App

")); + assert.ok(html.includes("My Card")); + assert.ok(html.includes("")); + assert.strictEqual(outputPath, path.join(outDir, "App.html")); + assert.ok(existsSync(outputPath)); +}); + +test("buildFile - JSX fragments render without a wrapper", async () => { + const pageDir = path.join(outDir, "src"); + await mkdir(pageDir, { recursive: true }); + const page = path.join(pageDir, "fragment.jsx"); + await writeFile( + page, + `export default function Page() { + return ( + <> +

Title

+

Body

+ + ); +}` + ); + + const { html } = await buildFile(page, { outputDir: outDir, silent: true }); + assert.strictEqual(html, "

Title

Body

"); +}); + +test("buildFile - rejects when there is no default export", async () => { + const page = path.join(outDir, "no-default.jsx"); + await writeFile(page, `export const meta = { title: "x" };`); + + await assert.rejects( + () => buildFile(page, { outputDir: outDir, silent: true }), + /No default export/i + ); +}); + +test("buildFiles - preserves nested directory structure", async () => { + const pagesDir = path.join(outDir, "pages"); + await mkdir(path.join(pagesDir, "blog"), { recursive: true }); + await writeFile( + path.join(pagesDir, "index.jsx"), + `export default () =>

Home

;` + ); + await writeFile( + path.join(pagesDir, "blog", "first-post.jsx"), + `export default () =>
Post
;` + ); + + const results = await buildFiles(pagesDir, { outputDir: outDir, silent: true }); + const outputs = results.map((r) => r.outputPath).sort(); + + assert.deepStrictEqual(outputs, [ + path.join(outDir, "blog", "first-post.html"), + path.join(outDir, "index.html"), + ]); + assert.ok(existsSync(path.join(outDir, "blog", "first-post.html"))); +}); diff --git a/packages/ono/test/bundler.test.js b/packages/ono/test/bundler.test.js deleted file mode 100644 index fba12f8..0000000 --- a/packages/ono/test/bundler.test.js +++ /dev/null @@ -1,78 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { bundle } from "../src/bundler.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const fixturesDir = path.join(__dirname, "fixtures"); - -test("bundle - single file no imports", async () => { - const entryFile = path.join(fixturesDir, "NoImports.jsx"); - const result = await bundle(entryFile); - - // Should contain the h function - assert.ok(result.includes("h(")); - assert.ok(result.includes("Hello")); - - // Should be valid JavaScript (no JSX) - assert.ok(!result.includes("
")); -}); - -test("bundle - with imports", async () => { - const entryFile = path.join(fixturesDir, "Card.jsx"); - const result = await bundle(entryFile); - - // Should contain both Card and Button code - assert.ok(result.includes("Card")); - assert.ok(result.includes("Button")); - - // Should have h function calls - assert.ok(result.includes("h(")); - - // Should not have import statements in output - assert.ok(!result.includes('import Button')); -}); - -test("bundle - nested imports", async () => { - const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await bundle(entryFile); - - // Should contain all components - assert.ok(result.includes("App")); - assert.ok(result.includes("Card")); - assert.ok(result.includes("Button")); - - // Should be transformed (no JSX) - assert.ok(result.includes("h(")); -}); - -test("bundle - correct module order", async () => { - const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await bundle(entryFile); - - // Button should come before Card (Card depends on Button) - const buttonIndex = result.indexOf("function Button"); - const cardIndex = result.indexOf("function Card"); - const appIndex = result.indexOf("function App"); - - assert.ok(buttonIndex < cardIndex, "Button should come before Card"); - assert.ok(cardIndex < appIndex, "Card should come before App"); -}); - -test("bundle - export default at the end", async () => { - const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await bundle(entryFile); - - // Should have export default at the end - assert.ok(result.includes("export default App")); -}); - -test("bundle - handles non-existent file", async () => { - const entryFile = path.join(fixturesDir, "NonExistent.jsx"); - - await assert.rejects( - async () => await bundle(entryFile), - /Cannot read file/i - ); -}); diff --git a/packages/ono/test/resolver.test.js b/packages/ono/test/resolver.test.js index 448857f..4911396 100644 --- a/packages/ono/test/resolver.test.js +++ b/packages/ono/test/resolver.test.js @@ -2,7 +2,7 @@ import { test } from "node:test"; import assert from "node:assert"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { parseImports, resolveImportPath, collectDependencies } from "../src/resolver.js"; +import { parseImports, resolveImportPath, collectModules } from "../src/resolver.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.join(__dirname, "fixtures"); @@ -11,8 +11,7 @@ test("parseImports - single default import", () => { const code = `import Button from "./Button.jsx";`; const result = parseImports(code); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].specifier, "./Button.jsx"); + assert.deepStrictEqual(result, ["./Button.jsx"]); }); test("parseImports - multiple imports", () => { @@ -23,9 +22,7 @@ import Button from "./Button.jsx"; const result = parseImports(code); - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0].specifier, "./Card.jsx"); - assert.strictEqual(result[1].specifier, "./Button.jsx"); + assert.deepStrictEqual(result, ["./Card.jsx", "./Button.jsx"]); }); test("parseImports - no imports", () => { @@ -39,8 +36,7 @@ test("parseImports - named imports", () => { const code = `import { foo, bar } from "./utils.js";`; const result = parseImports(code); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].specifier, "./utils.js"); + assert.deepStrictEqual(result, ["./utils.js"]); }); test("parseImports - mixed imports", () => { @@ -84,79 +80,45 @@ test("resolveImportPath - absolute stays absolute", () => { assert.strictEqual(result, importPath); }); -test("collectDependencies - no imports", async () => { +test("collectModules - no imports", async () => { const entryFile = path.join(fixturesDir, "NoImports.jsx"); - const result = await collectDependencies(entryFile); + const result = await collectModules(entryFile); - assert.strictEqual(result.modules.size, 1); - assert.ok(result.modules.has(entryFile)); + assert.strictEqual(result.size, 1); + assert.ok(result.has(entryFile)); }); -test("collectDependencies - single level imports", async () => { +test("collectModules - single level imports", async () => { const entryFile = path.join(fixturesDir, "Card.jsx"); - const result = await collectDependencies(entryFile); + const result = await collectModules(entryFile); // Card.jsx imports Button.jsx - assert.strictEqual(result.modules.size, 2); - assert.ok(result.modules.has(entryFile)); + assert.strictEqual(result.size, 2); + assert.ok(result.has(entryFile)); const buttonPath = path.join(fixturesDir, "Button.jsx"); - assert.ok(result.modules.has(buttonPath)); + assert.ok(result.has(buttonPath)); }); -test("collectDependencies - nested imports", async () => { +test("collectModules - nested imports", async () => { const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await collectDependencies(entryFile); + const result = await collectModules(entryFile); // App.jsx imports Card.jsx and Button.jsx // Card.jsx also imports Button.jsx // Total: App.jsx, Card.jsx, Button.jsx (3 unique files) - assert.strictEqual(result.modules.size, 3); + assert.strictEqual(result.size, 3); - const appPath = path.join(fixturesDir, "App.jsx"); - const cardPath = path.join(fixturesDir, "Card.jsx"); - const buttonPath = path.join(fixturesDir, "Button.jsx"); - - assert.ok(result.modules.has(appPath)); - assert.ok(result.modules.has(cardPath)); - assert.ok(result.modules.has(buttonPath)); -}); - -test("collectDependencies - returns dependency graph", async () => { - const entryFile = path.join(fixturesDir, "Card.jsx"); - const result = await collectDependencies(entryFile); - - const cardDeps = result.graph.get(entryFile); - assert.ok(cardDeps); - assert.strictEqual(cardDeps.length, 1); - - const buttonPath = path.join(fixturesDir, "Button.jsx"); - assert.ok(cardDeps.includes(buttonPath)); -}); - -test("collectDependencies - correct evaluation order", async () => { - const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await collectDependencies(entryFile); - - // Button should come before Card (Card depends on Button) - // Card should come before App (App depends on Card) - const buttonPath = path.join(fixturesDir, "Button.jsx"); - const cardPath = path.join(fixturesDir, "Card.jsx"); - const appPath = path.join(fixturesDir, "App.jsx"); - - const buttonIndex = result.order.indexOf(buttonPath); - const cardIndex = result.order.indexOf(cardPath); - const appIndex = result.order.indexOf(appPath); - - assert.ok(buttonIndex < cardIndex, "Button should come before Card"); - assert.ok(cardIndex < appIndex, "Card should come before App"); + assert.ok(result.has(path.join(fixturesDir, "App.jsx"))); + assert.ok(result.has(path.join(fixturesDir, "Card.jsx"))); + assert.ok(result.has(path.join(fixturesDir, "Button.jsx"))); }); -test("collectDependencies - handles non-existent file", async () => { +test("collectModules - handles non-existent file", async () => { const entryFile = path.join(fixturesDir, "NonExistent.jsx"); await assert.rejects( - async () => await collectDependencies(entryFile), - /ENOENT|no such file/i + async () => await collectModules(entryFile), + /Cannot read file/i ); }); From 69af222fcb67f952ef56e08e26dff47c034d9fc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 11:43:15 +0000 Subject: [PATCH 4/9] refactor: drop h3 and ws, make live reload actually work via SSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the simplification plan: - The dev server is now plain node:http (~100 lines): static files, directory index support for the new nested output, and a simple path-traversal guard. - Live reload moves from WebSocket to Server-Sent Events on the same port. The server injects the EventSource snippet into served HTML — previously no client script was ever injected anywhere, so the ws broadcast on port 35729 had no listeners and live reload silently did nothing. It works end-to-end now, and the extra port is gone. - UnoCSS dependencies are narrowed to what is actually used: @unocss/core + @unocss/preset-uno + @unocss/reset instead of the unocss meta package (@unocss/core was previously an undeclared transitive dependency of the browser compiler). The reset CSS is resolved with createRequire instead of a ../node_modules path hack that failed under pnpm and silently produced an empty reset. - Runtime dependencies are now typescript + @unocss/* only. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- packages/ono/package.json | 7 +- packages/ono/src/commands/dev.js | 8 +-- packages/ono/src/constants.js | 19 ------ packages/ono/src/server.js | 107 ++++++++++++++++++++----------- packages/ono/src/unocss.js | 22 +++---- packages/ono/src/watcher.js | 53 ++++----------- pnpm-lock.yaml | 52 +++++---------- 7 files changed, 113 insertions(+), 155 deletions(-) diff --git a/packages/ono/package.json b/packages/ono/package.json index a5abf8b..de07051 100644 --- a/packages/ono/package.json +++ b/packages/ono/package.json @@ -51,10 +51,9 @@ "node": ">=18.0.0" }, "dependencies": { + "@unocss/core": "^66.5.4", "@unocss/preset-uno": "^66.5.4", - "h3": "^2.0.1-rc.5", - "typescript": "^5.9.3", - "unocss": "^66.5.4", - "ws": "^8.18.3" + "@unocss/reset": "^66.5.4", + "typescript": "^5.9.3" } } diff --git a/packages/ono/src/commands/dev.js b/packages/ono/src/commands/dev.js index 22f0d5e..4b571bd 100644 --- a/packages/ono/src/commands/dev.js +++ b/packages/ono/src/commands/dev.js @@ -5,7 +5,7 @@ import { resolve, relative } from "node:path"; import { stat } from "node:fs/promises"; import { createDevServer } from "../server.js"; import { buildFile, buildFiles, generateUnoCSS } from "../builder.js"; -import { watchFile, watchFiles, createWebSocketServer } from "../watcher.js"; +import { watchFile, watchFiles } from "../watcher.js"; import { copyPublicFiles, initializeBarrels, parseCommandArgs } from "./build.js"; /** @@ -30,14 +30,12 @@ export async function runDevCommand(args) { await copyPublicFiles(outputDir); await generateUnoCSS({ outputDir }); - const { wss } = createWebSocketServer(); - const mode = isDirectory ? "pages" : "single"; const indexFile = isDirectory ? "index.html" : relative(outputDir, initialBuild[0].outputPath); - const { port: serverPort } = await createDevServer({ + const { port: serverPort, reload } = await createDevServer({ outputDir, port, mode, @@ -46,7 +44,7 @@ export async function runDevCommand(args) { const watchOpts = { outputDir, - wss, + reload, onRebuild: () => copyPublicFiles(outputDir), }; await (isDirectory ? watchFiles(input, watchOpts) : watchFile(input, watchOpts)); diff --git a/packages/ono/src/constants.js b/packages/ono/src/constants.js index 56a5c42..859e66d 100644 --- a/packages/ono/src/constants.js +++ b/packages/ono/src/constants.js @@ -8,8 +8,6 @@ export const PORTS = { /** Default HTTP server port */ SERVER: 3000, - /** Default WebSocket port for live reload */ - WEBSOCKET: 35729, }; /** @@ -74,20 +72,3 @@ export const MIME_TYPES = { ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", }; - -/** - * Node.js error codes - */ -export const ERROR_CODES = { - PORT_IN_USE: "EADDRINUSE", - FILE_NOT_FOUND: "ENOENT", -}; - -/** - * HTTP status codes - */ -export const HTTP_STATUS = { - OK: 200, - NOT_FOUND: 404, - SERVER_ERROR: 500, -}; diff --git a/packages/ono/src/server.js b/packages/ono/src/server.js index 58f58f9..94c7285 100644 --- a/packages/ono/src/server.js +++ b/packages/ono/src/server.js @@ -1,12 +1,23 @@ /** - * Dev server using h3 + * Dev server - static files plus SSE live reload, no dependencies. + * + * The server injects a small EventSource script into served HTML and + * exposes a reload() function that broadcasts to connected browsers. */ -import { createApp, eventHandler, setResponseHeader, createError } from "h3"; -import { toNodeHandler } from "h3/node"; import { createServer } from "node:http"; -import { resolve, join, extname } from "node:path"; +import { resolve, join, extname, normalize } from "node:path"; import { readFile } from "node:fs/promises"; -import { DIRS, PORTS, MIME_TYPES, HTTP_STATUS, ERROR_CODES } from "./constants.js"; +import { DIRS, PORTS, MIME_TYPES } from "./constants.js"; + +const RELOAD_PATH = "/__ono_reload"; +const RELOAD_SCRIPT = ``; + +function injectReloadScript(html) { + if (/<\/body>/i.test(html)) { + return html.replace(/<\/body>/i, `${RELOAD_SCRIPT}`); + } + return html + RELOAD_SCRIPT; +} /** * Create a development server @@ -15,50 +26,72 @@ import { DIRS, PORTS, MIME_TYPES, HTTP_STATUS, ERROR_CODES } from "./constants.j * @param {number} [options.port] - HTTP port * @param {string} [options.mode] - Server mode: 'pages' or 'single' * @param {string} [options.indexFile] - Index file for single mode - * @returns {Promise<{server: import('http').Server, app: any, port: number}>} + * @returns {Promise<{server: import('http').Server, port: number, reload: () => void}>} */ export async function createDevServer(options) { const { outputDir = DIRS.OUTPUT, port = PORTS.SERVER, mode = "pages", indexFile = "index.html" } = options; const outDir = resolve(process.cwd(), outputDir); + const clients = new Set(); + + const server = createServer(async (req, res) => { + const url = (req.url || "/").split("?")[0]; - const app = createApp(); + // SSE endpoint for live reload + if (url === RELOAD_PATH) { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + res.write(": connected\n\n"); + clients.add(res); + req.on("close", () => clients.delete(res)); + return; + } - app.use( - "/**", - eventHandler(async (event) => { - const url = event.path || "/"; - const isFavicon = url === "/favicon.ico"; - const rootFile = mode === "single" ? indexFile : "index.html"; - const filePath = join(outDir, url === "/" || url === "" ? rootFile : url); + const rootFile = mode === "single" ? indexFile : "index.html"; + const relPath = url === "/" ? rootFile : url.endsWith("/") ? join(url, "index.html") : url; + const filePath = normalize(join(outDir, relPath)); - try { - const content = await readFile(filePath); - setResponseHeader(event, "Content-Type", MIME_TYPES[extname(filePath)] || "application/octet-stream"); - return content; - } catch (error) { - if (!isFavicon) console.error("Server error:", error); - if (error.code === ERROR_CODES.FILE_NOT_FOUND) { - throw createError({ - statusCode: HTTP_STATUS.NOT_FOUND, - statusMessage: "Not Found", - message: isFavicon ? "Favicon not found" : `File not found: ${error.path}`, - }); - } - throw createError({ - statusCode: HTTP_STATUS.SERVER_ERROR, - statusMessage: "Internal Server Error", - message: error.message, - }); + if (!filePath.startsWith(outDir)) { + res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Forbidden"); + return; + } + + try { + const content = await readFile(filePath); + const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream"; + res.writeHead(200, { "Content-Type": contentType }); + + if (filePath.endsWith(".html")) { + res.end(injectReloadScript(content.toString())); + } else { + res.end(content); } - }) - ); + } catch (error) { + if (error.code === "ENOENT") { + res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(`Not Found: ${url}`); + } else { + console.error("Server error:", error); + res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(`Server Error: ${error.message}`); + } + } + }); - const server = createServer(toNodeHandler(app)); + /** Tell all connected browsers to reload */ + const reload = () => { + for (const client of clients) { + client.write("data: reload\n\n"); + } + }; return new Promise((resolvePromise, rejectPromise) => { const onError = (err) => { - if (err.code === ERROR_CODES.PORT_IN_USE) { + if (err.code === "EADDRINUSE") { const nextPort = port + 1; console.log(`ℹ️ Port ${port} is busy, using port ${nextPort} instead`); server.removeListener("error", onError); @@ -70,7 +103,7 @@ export async function createDevServer(options) { server.once("error", onError); server.listen(port, () => { server.removeListener("error", onError); - resolvePromise({ server, app, port }); + resolvePromise({ server, port, reload }); }); }); } diff --git a/packages/ono/src/unocss.js b/packages/ono/src/unocss.js index e2514bb..9d5e512 100644 --- a/packages/ono/src/unocss.js +++ b/packages/ono/src/unocss.js @@ -1,32 +1,26 @@ /** - * UnoCSS Integration for Mini JSX + * UnoCSS Integration for Ono */ -import { createGenerator, presetUno } from "unocss"; +import { createGenerator } from "@unocss/core"; +import { presetUno } from "@unocss/preset-uno"; import fs from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { pathToFileURL } from "node:url"; +import { createRequire } from "node:module"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); /** * Get the Tailwind reset CSS * @returns {Promise} Reset CSS content */ async function getResetCSS() { - const resetPath = path.resolve(__dirname, "../node_modules/@unocss/reset/tailwind.css"); try { - return await fs.readFile(resetPath, "utf-8"); + return await fs.readFile(require.resolve("@unocss/reset/tailwind.css"), "utf-8"); } catch { - // Fallback: try to find it relative to the package - try { - const fallbackPath = new URL("../node_modules/@unocss/reset/tailwind.css", import.meta.url); - return await fs.readFile(fileURLToPath(fallbackPath), "utf-8"); - } catch { - return ""; - } + return ""; } } diff --git a/packages/ono/src/watcher.js b/packages/ono/src/watcher.js index 64b3179..0f8ddf4 100644 --- a/packages/ono/src/watcher.js +++ b/packages/ono/src/watcher.js @@ -3,39 +3,10 @@ */ import { watch } from "node:fs"; import { resolve, join, relative } from "node:path"; -import { WebSocketServer } from "ws"; import { buildFile, buildFiles, generateUnoCSS } from "./builder.js"; import { generateBarrel } from "./barrels.js"; import { isJSXFile } from "./utils.js"; -import { PORTS, TIMING, DIRS, ERROR_CODES } from "./constants.js"; - -/** - * Create a WebSocket server for live reload - * @param {number} [port] - Port number for WebSocket server - * @returns {{wss: WebSocketServer, port: number}} - */ -export function createWebSocketServer(port = PORTS.WEBSOCKET) { - try { - return { wss: new WebSocketServer({ port }), port }; - } catch (error) { - if (error.code !== ERROR_CODES.PORT_IN_USE) throw error; - const actualPort = port + 1; - console.log(`ℹ️ WebSocket port ${port} is busy, using port ${actualPort} instead`); - return { wss: new WebSocketServer({ port: actualPort }), port: actualPort }; - } -} - -/** - * Broadcast reload message to all connected clients - * @param {WebSocketServer} wss - WebSocket server instance - */ -export function broadcastReload(wss) { - wss.clients.forEach((client) => { - if (client.readyState === 1) { - client.send("reload"); - } - }); -} +import { TIMING, DIRS } from "./constants.js"; /** * Create a debounced async runner that logs errors instead of throwing. @@ -55,11 +26,11 @@ function debounce(fn, ms = TIMING.DEBOUNCE_MS) { } /** - * Notify clients and trigger onRebuild callback. + * Trigger onRebuild callback and browser reload. */ -async function afterRebuild({ onRebuild, wss }) { +async function afterRebuild({ onRebuild, reload }) { if (onRebuild) await onRebuild(); - if (wss) broadcastReload(wss); + if (reload) reload(); } /** @@ -68,11 +39,11 @@ async function afterRebuild({ onRebuild, wss }) { * @param {Object} options - Watch options * @param {string} [options.outputDir] - Output directory * @param {Function} [options.onRebuild] - Callback after rebuild - * @param {WebSocketServer} [options.wss] - WebSocket server for live reload + * @param {Function} [options.reload] - Live-reload broadcast from the dev server * @returns {Promise<{watcher: any, publicWatcher?: any, barrelsWatcher?: any}>} */ export async function watchFiles(inputPattern, options = {}) { - const { outputDir = DIRS.OUTPUT, onRebuild, wss } = options; + const { outputDir = DIRS.OUTPUT, onRebuild, reload } = options; const pagesDir = resolve(process.cwd(), inputPattern); const buildOpts = { outputDir, inputRoot: pagesDir, silent: false }; @@ -86,7 +57,7 @@ export async function watchFiles(inputPattern, options = {}) { console.log("🔄 Rebuilding...\n"); await buildFile(file, buildOpts); await generateUnoCSS({ outputDir }); - await afterRebuild({ onRebuild, wss }); + await afterRebuild({ onRebuild, reload }); }); const rebuildAll = debounce(async (reason) => { @@ -94,7 +65,7 @@ export async function watchFiles(inputPattern, options = {}) { console.log("🔄 Rebuilding...\n"); await buildFiles(inputPattern, buildOpts); await generateUnoCSS({ outputDir }); - await afterRebuild({ onRebuild, wss }); + await afterRebuild({ onRebuild, reload }); }); const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => { @@ -118,7 +89,7 @@ export async function watchFiles(inputPattern, options = {}) { await generateBarrel(barrelDir); await buildFiles(inputPattern, buildOpts); await generateUnoCSS({ outputDir }); - await afterRebuild({ onRebuild, wss }); + await afterRebuild({ onRebuild, reload }); }); let barrelsWatcher; @@ -142,11 +113,11 @@ export async function watchFiles(inputPattern, options = {}) { * @param {Object} options - Watch options * @param {string} [options.outputDir] - Output directory * @param {Function} [options.onRebuild] - Callback after rebuild - * @param {WebSocketServer} [options.wss] - WebSocket server for live reload + * @param {Function} [options.reload] - Live-reload broadcast from the dev server * @returns {Promise<{watcher: any}>} */ export async function watchFile(inputFile, options = {}) { - const { outputDir = DIRS.OUTPUT, onRebuild, wss } = options; + const { outputDir = DIRS.OUTPUT, onRebuild, reload } = options; const buildOpts = { outputDir, silent: false }; const resolvedInput = resolve(process.cwd(), inputFile); @@ -157,7 +128,7 @@ export async function watchFile(inputFile, options = {}) { console.log("🔄 Rebuilding...\n"); await buildFile(resolvedInput, buildOpts); await generateUnoCSS({ outputDir }); - await afterRebuild({ onRebuild, wss }); + await afterRebuild({ onRebuild, reload }); }); return { watcher: watch(resolvedInput, rebuild) }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ea75a2..343a6d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,21 +18,18 @@ importers: packages/ono: dependencies: + '@unocss/core': + specifier: ^66.5.4 + version: 66.5.12 '@unocss/preset-uno': specifier: ^66.5.4 version: 66.5.12 - h3: - specifier: ^2.0.1-rc.5 - version: 2.0.1-rc.7 + '@unocss/reset': + specifier: ^66.5.4 + version: 66.5.12 typescript: specifier: ^5.9.3 version: 5.9.3 - unocss: - specifier: ^66.5.4 - version: 66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)) - ws: - specifier: ^8.18.3 - version: 8.18.3 packages/ono-lp: dependencies: @@ -408,56 +405,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -710,15 +718,6 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} - h3@2.0.1-rc.7: - resolution: {integrity: sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA==} - engines: {node: '>=20.11.1'} - peerDependencies: - crossws: ^0.4.1 - peerDependenciesMeta: - crossws: - optional: true - he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -822,9 +821,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.7.12: - resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} - secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -836,11 +832,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - srvx@0.10.0: - resolution: {integrity: sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA==} - engines: {node: '>=20.16.0'} - hasBin: true - swr@2.3.8: resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} peerDependencies: @@ -1533,11 +1524,6 @@ snapshots: dependencies: duplexer: 0.1.2 - h3@2.0.1-rc.7: - dependencies: - rou3: 0.7.12 - srvx: 0.10.0 - he@1.2.0: {} jiti@2.6.1: {} @@ -1652,8 +1638,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 - rou3@0.7.12: {} - secure-json-parse@2.7.0: {} sirv@3.0.2: @@ -1664,8 +1648,6 @@ snapshots: source-map-js@1.2.1: {} - srvx@0.10.0: {} - swr@2.3.8(react@19.2.3): dependencies: dequal: 2.0.3 From 5dce9cc45dd18e266ec9adfc34c3150e3d5ef4c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 11:46:38 +0000 Subject: [PATCH 5/9] test: replace custom snapshot framework with node:test built-in snapshots Phase 4 of the simplification plan: - The 139-line hand-rolled snapshot-utils.js and its 37 .snap files are replaced by t.assert.snapshot (stable in Node 22), with snapshots stored next to each test file. Update via 'npm run test:update' (--test-update-snapshots) instead of an env var. SNAPSHOT_TESTING.md is no longer needed. - Snapshot cases are folded into the matching unit test files (transformer, renderer); cases that only restated an existing unit assertion are dropped. unocss and cli snapshot suites become unocss.test.js / cli.test.js. The cli suite also drops tests for the 'serve' and 'watch' commands, which no longer exist. - engines is raised to node >=22.3.0 (required for built-in snapshots; the test scripts already assumed a modern runner). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- packages/ono/SNAPSHOT_TESTING.md | 154 ------------- packages/ono/package.json | 10 +- .../cli.snapshot.build_command_help.snap | 14 -- ...snapshot.build_non-existent_directory.snap | 4 - .../cli.snapshot.build_non-existent_file.snap | 4 - .../cli.snapshot.help_message.snap | 20 -- .../cli.snapshot.help_message_short_flag.snap | 20 -- .../cli.snapshot.no_command_provided.snap | 20 -- .../cli.snapshot.serve_command.snap | 5 - .../cli.snapshot.unknown_command.snap | 5 - ...i.snapshot.version_flag_not_supported.snap | 5 - ...shot.watch_command_on_empty_directory.snap | 5 - ...derer.snapshot.HTML_entities_escaping.snap | 1 - .../renderer.snapshot.boolean_attributes.snap | 1 - .../renderer.snapshot.complex_form.snap | 16 -- .../renderer.snapshot.component_function.snap | 10 - ...erer.snapshot.element_with_attributes.snap | 2 - .../renderer.snapshot.element_with_text.snap | 1 - .../renderer.snapshot.nested_elements.snap | 9 - .../renderer.snapshot.simple_element.snap | 2 - .../renderer.snapshot.style_object.snap | 1 - .../transformer.snapshot.JSX_fragments.snap | 4 - ...sformer.snapshot.JSX_with_expressions.snap | 4 - .../transformer.snapshot.JSX_with_props.snap | 1 - ...apshot.complex_component_with_imports.snap | 15 -- ....snapshot.component_function_with_JSX.snap | 5 - ...former.snapshot.conditional_rendering.snap | 7 - .../transformer.snapshot.nested_JSX.snap | 9 - ...ransformer.snapshot.self-closing_tags.snap | 6 - ...ansformer.snapshot.simple_JSX_element.snap | 1 - ...ransformer.snapshot.style_prop_object.snap | 6 - ...unocss.snapshot.basic_utility_classes.snap | 10 - .../unocss.snapshot.blog_layout.snap | 39 ---- .../unocss.snapshot.color_utilities.snap | 14 -- .../unocss.snapshot.form_elements.snap | 26 --- .../unocss.snapshot.layout_classes.snap | 30 --- .../unocss.snapshot.responsive_design.snap | 19 -- .../unocss.snapshot.spacing_and_sizing.snap | 18 -- .../unocss.snapshot.typography.snap | 21 -- packages/ono/test/cli.snapshot.test.js | 218 ------------------ packages/ono/test/cli.test.js | 100 ++++++++ packages/ono/test/cli.test.js.snapshot | 158 +++++++++++++ packages/ono/test/renderer.snapshot.test.js | 143 ------------ packages/ono/test/renderer.test.js | 90 ++++++++ packages/ono/test/renderer.test.js.snapshot | 19 ++ packages/ono/test/snapshot-utils.js | 140 ----------- .../ono/test/transformer.snapshot.test.js | 166 ------------- packages/ono/test/transformer.test.js | 110 +++++++++ .../ono/test/transformer.test.js.snapshot | 63 +++++ ...unocss.snapshot.test.js => unocss.test.js} | 58 ++--- packages/ono/test/unocss.test.js.snapshot | 189 +++++++++++++++ 51 files changed, 753 insertions(+), 1245 deletions(-) delete mode 100644 packages/ono/SNAPSHOT_TESTING.md delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.build_command_help.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_directory.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_file.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.help_message.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.help_message_short_flag.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.no_command_provided.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.serve_command.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.unknown_command.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.version_flag_not_supported.snap delete mode 100644 packages/ono/test/__snapshots__/cli.snapshot.watch_command_on_empty_directory.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.HTML_entities_escaping.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.boolean_attributes.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.complex_form.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.component_function.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.element_with_attributes.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.element_with_text.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.nested_elements.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.simple_element.snap delete mode 100644 packages/ono/test/__snapshots__/renderer.snapshot.style_object.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.JSX_fragments.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_expressions.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_props.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.complex_component_with_imports.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.component_function_with_JSX.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.conditional_rendering.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.nested_JSX.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.self-closing_tags.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.simple_JSX_element.snap delete mode 100644 packages/ono/test/__snapshots__/transformer.snapshot.style_prop_object.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.basic_utility_classes.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.blog_layout.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.color_utilities.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.form_elements.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.layout_classes.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.responsive_design.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.spacing_and_sizing.snap delete mode 100644 packages/ono/test/__snapshots__/unocss.snapshot.typography.snap delete mode 100644 packages/ono/test/cli.snapshot.test.js create mode 100644 packages/ono/test/cli.test.js create mode 100644 packages/ono/test/cli.test.js.snapshot delete mode 100644 packages/ono/test/renderer.snapshot.test.js create mode 100644 packages/ono/test/renderer.test.js.snapshot delete mode 100644 packages/ono/test/snapshot-utils.js delete mode 100644 packages/ono/test/transformer.snapshot.test.js create mode 100644 packages/ono/test/transformer.test.js.snapshot rename packages/ono/test/{unocss.snapshot.test.js => unocss.test.js} (66%) create mode 100644 packages/ono/test/unocss.test.js.snapshot diff --git a/packages/ono/SNAPSHOT_TESTING.md b/packages/ono/SNAPSHOT_TESTING.md deleted file mode 100644 index 92d576c..0000000 --- a/packages/ono/SNAPSHOT_TESTING.md +++ /dev/null @@ -1,154 +0,0 @@ -# Snapshot Testing Guide - -This project uses snapshot testing to ensure consistent output across different components of the Ono SSG framework. - -## What is Snapshot Testing? - -Snapshot testing captures the output of your code at a specific point in time and stores it as a "snapshot" file. Future test runs compare the current output against the stored snapshot to detect unexpected changes. - -## Available Snapshot Tests - -### 1. HTML Renderer Snapshots (`test/renderer.snapshot.test.js`) -Tests the HTML output from JSX elements to ensure consistent rendering: -- Simple elements -- Elements with attributes -- Nested structures -- Component functions -- Boolean attributes -- Style objects -- Complex forms -- HTML entity escaping - -### 2. JSX Transformer Snapshots (`test/transformer.snapshot.test.js`) -Tests the JavaScript code generated from JSX transformation: -- Simple JSX elements -- JSX with props -- Nested JSX structures -- Component functions -- JSX with expressions -- Self-closing tags -- JSX fragments -- Complex components with imports -- Style prop objects -- Conditional rendering - -### 3. UnoCSS Generation Snapshots (`test/unocss.snapshot.test.js`) -Tests the CSS output generated from HTML content: -- Basic utility classes -- Layout classes -- Responsive design -- Color utilities -- Spacing and sizing -- Typography -- Form elements -- Blog layout - -## Running Snapshot Tests - -### Run all snapshot tests -```bash -npm run test:snapshot -``` - -### Update snapshots when intentional changes are made -```bash -npm run test:snapshot:update -``` - -### Run all tests (regular + snapshot) -```bash -npm run test:all -``` - -## Snapshot Files - -Snapshot files are stored in `test/__snapshots__/` with the naming convention: -``` -{test-file-name}.{test-name}.snap -``` - -For example: -- `renderer.snapshot.simple_element.snap` -- `transformer.snapshot.JSX_with_props.snap` -- `unocss.snapshot.basic_utility_classes.snap` - -## When to Update Snapshots - -Update snapshots when you've intentionally changed: - -1. **HTML Output**: Modified the renderer logic -2. **JSX Transformation**: Updated the JSX-to-JS transformation -3. **CSS Generation**: Changed UnoCSS configuration or generation logic - -### ⚠️ Important: Review Changes Before Updating - -Always review the differences between old and new snapshots before updating: - -1. Run tests to see failures -2. Manually verify the changes are intentional -3. Update snapshots using `npm run test:snapshot:update` - -## Example Workflow - -### Adding a New Feature -1. Write your code -2. Add/modify snapshot tests if needed -3. Run `npm run test:snapshot` -4. If snapshots fail due to intentional changes: - - Review the changes carefully - - Run `npm run test:snapshot:update` - - Commit both code and snapshot changes - -### Debugging Snapshot Failures -When snapshot tests fail: - -1. **Check the console output** - it shows expected vs actual -2. **Verify if the change was intentional**: - - If yes: update snapshots - - If no: fix your code -3. **For complex diffs**, you can examine snapshot files directly - -## Best Practices - -1. **Keep snapshots small and focused** - Test specific functionality -2. **Review snapshot changes in PRs** - Don't blindly update snapshots -3. **Use descriptive test names** - Makes snapshot files easier to understand -4. **Commit snapshot files** - They're part of your test suite -5. **Regular cleanup** - Remove unused snapshot files - -## Custom Snapshot Utilities - -The project includes custom snapshot utilities in `test/snapshot-utils.js`: - -- `matchSnapshot()` - Generic snapshot matching -- `matchHTMLSnapshot()` - HTML-specific formatting -- `matchCodeSnapshot()` - JavaScript code formatting - -### Example Usage - -```javascript -import { matchHTMLSnapshot } from "./snapshot-utils.js"; - -test("my html test", async () => { - const html = renderComponent(); - await matchHTMLSnapshot(html, "my-test-file.js", "my html test"); -}); -``` - -## Troubleshooting - -### Snapshot test fails with "No such file or directory" -- Make sure you're running tests from the project root -- Ensure `test/__snapshots__/` directory exists (it's created automatically) - -### Snapshots not updating -- Use the exact command: `npm run test:snapshot:update` -- Check file permissions on the `test/__snapshots__/` directory - -### Large snapshot diffs -- Consider breaking down large components into smaller, testable units -- Use more specific test names to identify which part changed - ---- - -**Note**: Snapshot testing complements, but doesn't replace, traditional unit tests. Use both approaches for comprehensive test coverage. \ No newline at end of file diff --git a/packages/ono/package.json b/packages/ono/package.json index de07051..a678b72 100644 --- a/packages/ono/package.json +++ b/packages/ono/package.json @@ -22,11 +22,9 @@ "LICENSE" ], "scripts": { - "test": "node --test test/**/*.test.js", - "test:watch": "node --test --watch test/**/*.test.js", - "test:snapshot": "node --test test/**/*.snapshot.test.js", - "test:snapshot:update": "UPDATE_SNAPSHOTS=true node --test test/**/*.snapshot.test.js", - "test:all": "npm run test && npm run test:snapshot" + "test": "node --test test/*.test.js", + "test:watch": "node --test --watch test/*.test.js", + "test:update": "node --test --test-update-snapshots test/*.test.js" }, "keywords": [ "jsx", @@ -48,7 +46,7 @@ }, "homepage": "https://github.com/hashrock/ono#readme", "engines": { - "node": ">=18.0.0" + "node": ">=22.3.0" }, "dependencies": { "@unocss/core": "^66.5.4", diff --git a/packages/ono/test/__snapshots__/cli.snapshot.build_command_help.snap b/packages/ono/test/__snapshots__/cli.snapshot.build_command_help.snap deleted file mode 100644 index 1eb518c..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.build_command_help.snap +++ /dev/null @@ -1,14 +0,0 @@ -Exit Code: 0 -STDOUT: -Usage: ono build [input] [options] -Build JSX files to static HTML -Arguments: - input File or directory to build (default: pages) -Options: - --output Output directory (default: dist) - --help Show this help message -Examples: - ono build Build all pages in pages/ directory - ono build pages/index.jsx Build a single file - ono build --output build Build to build/ directory -STDERR: \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_directory.snap b/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_directory.snap deleted file mode 100644 index 52d899b..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_directory.snap +++ /dev/null @@ -1,4 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Error: ENOENT: no such file or directory, stat '/PATH/test-tmp/non-existent-dir' \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_file.snap b/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_file.snap deleted file mode 100644 index b33aeb5..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.build_non-existent_file.snap +++ /dev/null @@ -1,4 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Error: ENOENT: no such file or directory, stat '/PATH/test-tmp/non-existent.jsx' \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.help_message.snap b/packages/ono/test/__snapshots__/cli.snapshot.help_message.snap deleted file mode 100644 index e265ae2..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.help_message.snap +++ /dev/null @@ -1,20 +0,0 @@ -Exit Code: 0 -STDOUT: -Ono - Minimalist SSG Framework -Usage: - ono [options] -Commands: - build [input] Build JSX files to static HTML - input: file or directory (default: pages) - dev [input] Build, watch, and serve with live reload - input: file or directory (default: pages) -Options: - --output Output directory (default: dist) - --port Server port (default: 3000) - --help Show this help message -Examples: - ono build Build all pages in pages/ directory - ono build pages/index.jsx Build a single file - ono dev Start dev server with live reload - ono dev --port 8080 Start dev server on port 8080 -STDERR: \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.help_message_short_flag.snap b/packages/ono/test/__snapshots__/cli.snapshot.help_message_short_flag.snap deleted file mode 100644 index e265ae2..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.help_message_short_flag.snap +++ /dev/null @@ -1,20 +0,0 @@ -Exit Code: 0 -STDOUT: -Ono - Minimalist SSG Framework -Usage: - ono [options] -Commands: - build [input] Build JSX files to static HTML - input: file or directory (default: pages) - dev [input] Build, watch, and serve with live reload - input: file or directory (default: pages) -Options: - --output Output directory (default: dist) - --port Server port (default: 3000) - --help Show this help message -Examples: - ono build Build all pages in pages/ directory - ono build pages/index.jsx Build a single file - ono dev Start dev server with live reload - ono dev --port 8080 Start dev server on port 8080 -STDERR: \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.no_command_provided.snap b/packages/ono/test/__snapshots__/cli.snapshot.no_command_provided.snap deleted file mode 100644 index e265ae2..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.no_command_provided.snap +++ /dev/null @@ -1,20 +0,0 @@ -Exit Code: 0 -STDOUT: -Ono - Minimalist SSG Framework -Usage: - ono [options] -Commands: - build [input] Build JSX files to static HTML - input: file or directory (default: pages) - dev [input] Build, watch, and serve with live reload - input: file or directory (default: pages) -Options: - --output Output directory (default: dist) - --port Server port (default: 3000) - --help Show this help message -Examples: - ono build Build all pages in pages/ directory - ono build pages/index.jsx Build a single file - ono dev Start dev server with live reload - ono dev --port 8080 Start dev server on port 8080 -STDERR: \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.serve_command.snap b/packages/ono/test/__snapshots__/cli.snapshot.serve_command.snap deleted file mode 100644 index 322e1d1..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.serve_command.snap +++ /dev/null @@ -1,5 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Unknown command: serve -Run "ono --help" for usage information \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.unknown_command.snap b/packages/ono/test/__snapshots__/cli.snapshot.unknown_command.snap deleted file mode 100644 index 58acc2e..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.unknown_command.snap +++ /dev/null @@ -1,5 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Unknown command: unknown-command -Run "ono --help" for usage information \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.version_flag_not_supported.snap b/packages/ono/test/__snapshots__/cli.snapshot.version_flag_not_supported.snap deleted file mode 100644 index fc00d92..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.version_flag_not_supported.snap +++ /dev/null @@ -1,5 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Unknown command: --version -Run "ono --help" for usage information \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/cli.snapshot.watch_command_on_empty_directory.snap b/packages/ono/test/__snapshots__/cli.snapshot.watch_command_on_empty_directory.snap deleted file mode 100644 index 13773e1..0000000 --- a/packages/ono/test/__snapshots__/cli.snapshot.watch_command_on_empty_directory.snap +++ /dev/null @@ -1,5 +0,0 @@ -Exit Code: 1 -STDOUT: -STDERR: -Unknown command: watch -Run "ono --help" for usage information \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.HTML_entities_escaping.snap b/packages/ono/test/__snapshots__/renderer.snapshot.HTML_entities_escaping.snap deleted file mode 100644 index 1e26344..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.HTML_entities_escaping.snap +++ /dev/null @@ -1 +0,0 @@ -
Content with <em>HTML</em> & "special" characters
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.boolean_attributes.snap b/packages/ono/test/__snapshots__/renderer.snapshot.boolean_attributes.snap deleted file mode 100644 index 24d8f32..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.boolean_attributes.snap +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.complex_form.snap b/packages/ono/test/__snapshots__/renderer.snapshot.complex_form.snap deleted file mode 100644 index d849960..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.complex_form.snap +++ /dev/null @@ -1,16 +0,0 @@ -
-
- - -
-
- - -
-
- - -
- -
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.component_function.snap b/packages/ono/test/__snapshots__/renderer.snapshot.component_function.snap deleted file mode 100644 index 6842eab..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.component_function.snap +++ /dev/null @@ -1,10 +0,0 @@ -
-
-

My First Post

-

By John Doe

-
-
-

This is the first paragraph.

-

This is the second paragraph.

-
-
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.element_with_attributes.snap b/packages/ono/test/__snapshots__/renderer.snapshot.element_with_attributes.snap deleted file mode 100644 index 9e0ac17..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.element_with_attributes.snap +++ /dev/null @@ -1,2 +0,0 @@ -
-
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.element_with_text.snap b/packages/ono/test/__snapshots__/renderer.snapshot.element_with_text.snap deleted file mode 100644 index 34fa97d..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.element_with_text.snap +++ /dev/null @@ -1 +0,0 @@ -
Hello World
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.nested_elements.snap b/packages/ono/test/__snapshots__/renderer.snapshot.nested_elements.snap deleted file mode 100644 index 254e9b4..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.nested_elements.snap +++ /dev/null @@ -1,9 +0,0 @@ -
-

Title

-

This is a paragraph with bold text and regular text.

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.simple_element.snap b/packages/ono/test/__snapshots__/renderer.snapshot.simple_element.snap deleted file mode 100644 index 094418b..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.simple_element.snap +++ /dev/null @@ -1,2 +0,0 @@ -
-
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/renderer.snapshot.style_object.snap b/packages/ono/test/__snapshots__/renderer.snapshot.style_object.snap deleted file mode 100644 index 0675fd3..0000000 --- a/packages/ono/test/__snapshots__/renderer.snapshot.style_object.snap +++ /dev/null @@ -1 +0,0 @@ -
Styled content
\ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_fragments.snap b/packages/ono/test/__snapshots__/transformer.snapshot.JSX_fragments.snap deleted file mode 100644 index 0c71f4b..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_fragments.snap +++ /dev/null @@ -1,4 +0,0 @@ -const content = (h(Fragment, null, - h("h1", null, "Title"), - h("p", null, "First paragraph"), - h("p", null, "Second paragraph"))); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_expressions.snap b/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_expressions.snap deleted file mode 100644 index 8ced704..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_expressions.snap +++ /dev/null @@ -1,4 +0,0 @@ -const todos = items.map(item => (h("li", { key: item.id, className: item.completed ? 'completed' : '' }, - h("input", { type: "checkbox", checked: item.completed, onChange: () => toggle(item.id) }), - h("span", null, item.text), - h("button", { onClick: () => remove(item.id) }, "\u00D7")))); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_props.snap b/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_props.snap deleted file mode 100644 index 714132f..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.JSX_with_props.snap +++ /dev/null @@ -1 +0,0 @@ -const elem = h("div", { className: "test", id: "main" }, "Hello"); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.complex_component_with_imports.snap b/packages/ono/test/__snapshots__/transformer.snapshot.complex_component_with_imports.snap deleted file mode 100644 index aae58fc..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.complex_component_with_imports.snap +++ /dev/null @@ -1,15 +0,0 @@ -import { useState } from 'react'; -import { Header } from './components/Header'; -import { Footer } from './components/Footer'; -export default function App() { - const [count, setCount] = useState(0); - return (h("div", { className: "app" }, - h(Header, { title: "My App" }), - h("main", null, - h("h1", null, - "Counter: ", - count), - h("button", { onClick: () => setCount(count + 1) }, "Increment"), - h("button", { onClick: () => setCount(count - 1) }, "Decrement")), - h(Footer, null))); -} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.component_function_with_JSX.snap b/packages/ono/test/__snapshots__/transformer.snapshot.component_function_with_JSX.snap deleted file mode 100644 index cc18556..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.component_function_with_JSX.snap +++ /dev/null @@ -1,5 +0,0 @@ -function BlogPost({ title, children }) { - return (h("article", { className: "blog-post" }, - h("h1", null, title), - h("div", { className: "content" }, children))); -} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.conditional_rendering.snap b/packages/ono/test/__snapshots__/transformer.snapshot.conditional_rendering.snap deleted file mode 100644 index a99ef58..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.conditional_rendering.snap +++ /dev/null @@ -1,7 +0,0 @@ -const content = (h("div", null, - isLoggedIn ? (h("div", null, - h("h1", null, "Welcome back!"), - h("button", { onClick: logout }, "Logout"))) : (h("div", null, - h("h1", null, "Please log in"), - h("button", { onClick: login }, "Login"))), - showMessage && h("p", { className: "message" }, message))); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.nested_JSX.snap b/packages/ono/test/__snapshots__/transformer.snapshot.nested_JSX.snap deleted file mode 100644 index 27d90f5..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.nested_JSX.snap +++ /dev/null @@ -1,9 +0,0 @@ -const elem = (h("div", { className: "container" }, - h("h1", null, "Title"), - h("p", null, - "Paragraph with ", - h("strong", null, "bold"), - " text"), - h("ul", null, - h("li", null, "Item 1"), - h("li", null, "Item 2")))); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.self-closing_tags.snap b/packages/ono/test/__snapshots__/transformer.snapshot.self-closing_tags.snap deleted file mode 100644 index 220a315..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.self-closing_tags.snap +++ /dev/null @@ -1,6 +0,0 @@ -const form = (h("form", null, - h("input", { type: "text", name: "name", required: true }), - h("input", { type: "email", name: "email", required: true }), - h("br", null), - h("hr", null), - h("img", { src: "/logo.png", alt: "Logo" }))); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.simple_JSX_element.snap b/packages/ono/test/__snapshots__/transformer.snapshot.simple_JSX_element.snap deleted file mode 100644 index 640c052..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.simple_JSX_element.snap +++ /dev/null @@ -1 +0,0 @@ -const elem = h("div", null, "Hello"); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/transformer.snapshot.style_prop_object.snap b/packages/ono/test/__snapshots__/transformer.snapshot.style_prop_object.snap deleted file mode 100644 index dc74980..0000000 --- a/packages/ono/test/__snapshots__/transformer.snapshot.style_prop_object.snap +++ /dev/null @@ -1,6 +0,0 @@ -const styledDiv = (h("div", { style: { - backgroundColor: 'red', - fontSize: '16px', - padding: '10px', - border: '1px solid black' - } }, "Styled content")); \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.basic_utility_classes.snap b/packages/ono/test/__snapshots__/unocss.snapshot.basic_utility_classes.snap deleted file mode 100644 index acb8944..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.basic_utility_classes.snap +++ /dev/null @@ -1,10 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.m-2{margin:0.5rem;} -.bg-blue-100{--un-bg-opacity:1;background-color:rgb(219 234 254 / var(--un-bg-opacity));} -.p-4{padding:1rem;} -.text-2xl{font-size:1.5rem;line-height:2rem;} -.text-gray-600{--un-text-opacity:1;color:rgb(75 85 99 / var(--un-text-opacity));} -.text-red-500{--un-text-opacity:1;color:rgb(239 68 68 / var(--un-text-opacity));} -.font-bold{font-weight:700;} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.blog_layout.snap b/packages/ono/test/__snapshots__/unocss.snapshot.blog_layout.snap deleted file mode 100644 index c5f9b18..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.blog_layout.snap +++ /dev/null @@ -1,39 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.mx-2{margin-left:0.5rem;margin-right:0.5rem;} -.mx-auto{margin-left:auto;margin-right:auto;} -.mb-2{margin-bottom:0.5rem;} -.mb-4{margin-bottom:1rem;} -.mb-8{margin-bottom:2rem;} -.mt-12{margin-top:3rem;} -.mt-2{margin-top:0.5rem;} -.max-w-4xl{max-width:56rem;} -.flex{display:flex;} -.gap-2{gap:0.5rem;} -.space-y-8>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(2rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(2rem * var(--un-space-y-reverse));} -.border-t{border-top-width:1px;} -.border-gray-200{--un-border-opacity:1;border-color:rgb(229 231 235 / var(--un-border-opacity));} -.rounded{border-radius:0.25rem;} -.rounded-lg{border-radius:0.5rem;} -.bg-blue-100{--un-bg-opacity:1;background-color:rgb(219 234 254 / var(--un-bg-opacity));} -.bg-green-100{--un-bg-opacity:1;background-color:rgb(220 252 231 / var(--un-bg-opacity));} -.bg-white{--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity));} -.p-6{padding:1.5rem;} -.px-2{padding-left:0.5rem;padding-right:0.5rem;} -.px-4{padding-left:1rem;padding-right:1rem;} -.py-1{padding-top:0.25rem;padding-bottom:0.25rem;} -.py-8{padding-top:2rem;padding-bottom:2rem;} -.pt-8{padding-top:2rem;} -.text-center{text-align:center;} -.text-2xl{font-size:1.5rem;line-height:2rem;} -.text-4xl{font-size:2.25rem;line-height:2.5rem;} -.text-sm{font-size:0.875rem;line-height:1.25rem;} -.text-xs{font-size:0.75rem;line-height:1rem;} -.text-blue-700{--un-text-opacity:1;color:rgb(29 78 216 / var(--un-text-opacity));} -.text-gray-600{--un-text-opacity:1;color:rgb(75 85 99 / var(--un-text-opacity));} -.text-gray-900{--un-text-opacity:1;color:rgb(17 24 39 / var(--un-text-opacity));} -.text-green-700{--un-text-opacity:1;color:rgb(21 128 61 / var(--un-text-opacity));} -.hover\:text-blue-600:hover{--un-text-opacity:1;color:rgb(37 99 235 / var(--un-text-opacity));} -.font-bold{font-weight:700;} -.shadow-md{--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.color_utilities.snap b/packages/ono/test/__snapshots__/unocss.snapshot.color_utilities.snap deleted file mode 100644 index a2947c3..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.color_utilities.snap +++ /dev/null @@ -1,14 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.bg-black{--un-bg-opacity:1;background-color:rgb(0 0 0 / var(--un-bg-opacity));} -.bg-blue-500{--un-bg-opacity:1;background-color:rgb(59 130 246 / var(--un-bg-opacity));} -.hover\:bg-blue-700:hover{--un-bg-opacity:1;background-color:rgb(29 78 216 / var(--un-bg-opacity));} -.bg-opacity-50{--un-bg-opacity:0.5;} -.from-purple-400{--un-gradient-from-position:0%;--un-gradient-from:rgb(192 132 252 / var(--un-from-opacity, 1)) var(--un-gradient-from-position);--un-gradient-to-position:100%;--un-gradient-to:rgb(192 132 252 / 0) var(--un-gradient-to-position);--un-gradient-stops:var(--un-gradient-from), var(--un-gradient-to);} -.via-pink-500{--un-gradient-via-position:50%;--un-gradient-to:rgb(236 72 153 / 0);--un-gradient-stops:var(--un-gradient-from), rgb(236 72 153 / var(--un-via-opacity, 1)) var(--un-gradient-via-position), var(--un-gradient-to);} -.to-red-500{--un-gradient-to-position:100%;--un-gradient-to:rgb(239 68 68 / var(--un-to-opacity, 1)) var(--un-gradient-to-position);} -.bg-gradient-to-r{--un-gradient-shape:to right in oklch;--un-gradient:var(--un-gradient-shape), var(--un-gradient-stops);background-image:linear-gradient(var(--un-gradient));} -.text-green-200{--un-text-opacity:1;color:rgb(187 247 208 / var(--un-text-opacity));} -.text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} -.text-yellow-300{--un-text-opacity:1;color:rgb(253 224 71 / var(--un-text-opacity));} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.form_elements.snap b/packages/ono/test/__snapshots__/unocss.snapshot.form_elements.snap deleted file mode 100644 index cdcd2ed..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.form_elements.snap +++ /dev/null @@ -1,26 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.block{display:block;} -.max-w-md{max-width:28rem;} -.w-full{width:100%;} -.space-y-4>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1rem * var(--un-space-y-reverse));} -.border{border-width:1px;} -.border-gray-300{--un-border-opacity:1;border-color:rgb(209 213 219 / var(--un-border-opacity));} -.rounded{border-radius:0.25rem;} -.rounded-md{border-radius:0.375rem;} -.bg-blue-500{--un-bg-opacity:1;background-color:rgb(59 130 246 / var(--un-bg-opacity));} -.hover\:bg-blue-700:hover{--un-bg-opacity:1;background-color:rgb(29 78 216 / var(--un-bg-opacity));} -.px-3{padding-left:0.75rem;padding-right:0.75rem;} -.px-4{padding-left:1rem;padding-right:1rem;} -.py-2{padding-top:0.5rem;padding-bottom:0.5rem;} -.text-sm{font-size:0.875rem;line-height:1.25rem;} -.text-gray-700{--un-text-opacity:1;color:rgb(55 65 81 / var(--un-text-opacity));} -.text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} -.font-bold{font-weight:700;} -.font-medium{font-weight:500;} -.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px;} -.focus\:ring-2:focus{--un-ring-width:2px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} -.focus\:ring-blue-500:focus{--un-ring-opacity:1;--un-ring-color:rgb(59 130 246 / var(--un-ring-opacity));} -.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;} -.duration-200{transition-duration:200ms;} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.layout_classes.snap b/packages/ono/test/__snapshots__/unocss.snapshot.layout_classes.snap deleted file mode 100644 index 77a2345..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.layout_classes.snap +++ /dev/null @@ -1,30 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: shortcuts */ -.container{width:100%;} -@media (min-width: 640px){ -.container{max-width:640px;} -} -@media (min-width: 768px){ -.container{max-width:768px;} -} -@media (min-width: 1024px){ -.container{max-width:1024px;} -} -@media (min-width: 1280px){ -.container{max-width:1280px;} -} -@media (min-width: 1536px){ -.container{max-width:1536px;} -} -/* layer: default */ -.grid{display:grid;} -.col-span-2{grid-column:span 2/span 2;} -.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr));} -.mx-auto{margin-left:auto;margin-right:auto;} -.w-1\/2{width:50%;} -.flex{display:flex;} -.items-center{align-items:center;} -.justify-between{justify-content:space-between;} -.gap-4{gap:1rem;} -.px-4{padding-left:1rem;padding-right:1rem;} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.responsive_design.snap b/packages/ono/test/__snapshots__/unocss.snapshot.responsive_design.snap deleted file mode 100644 index e1d7f33..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.responsive_design.snap +++ /dev/null @@ -1,19 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.block{display:block;} -.w-full{width:100%;} -.text-sm{font-size:0.875rem;line-height:1.25rem;} -.text-xl{font-size:1.25rem;line-height:1.75rem;} -@media (min-width: 768px){ -.md\:hidden{display:none;} -.md\:w-1\/2{width:50%;} -.md\:text-2xl{font-size:1.5rem;line-height:2rem;} -.md\:text-base{font-size:1rem;line-height:1.5rem;} -} -@media (min-width: 1024px){ -.lg\:w-1\/3{width:33.3333333333%;} -.lg\:flex{display:flex;} -.lg\:text-3xl{font-size:1.875rem;line-height:2.25rem;} -.lg\:text-lg{font-size:1.125rem;line-height:1.75rem;} -} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.spacing_and_sizing.snap b/packages/ono/test/__snapshots__/unocss.snapshot.spacing_and_sizing.snap deleted file mode 100644 index 44792a5..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.spacing_and_sizing.snap +++ /dev/null @@ -1,18 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.m-1{margin:0.25rem;} -.m-4{margin:1rem;} -.h-32{height:8rem;} -.max-w-4xl{max-width:56rem;} -.min-h-screen{min-height:100vh;} -.w-64{width:16rem;} -.space-y-6>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1.5rem * var(--un-space-y-reverse));} -.border-2{border-width:2px;} -.border-gray-300{--un-border-opacity:1;border-color:rgb(209 213 219 / var(--un-border-opacity));} -.rounded-lg{border-radius:0.5rem;} -.p-2{padding:0.5rem;} -.p-8{padding:2rem;} -.px-3{padding-left:0.75rem;padding-right:0.75rem;} -.py-1{padding-top:0.25rem;padding-bottom:0.25rem;} -.text-xs{font-size:0.75rem;line-height:1rem;} \ No newline at end of file diff --git a/packages/ono/test/__snapshots__/unocss.snapshot.typography.snap b/packages/ono/test/__snapshots__/unocss.snapshot.typography.snap deleted file mode 100644 index 84976bd..0000000 --- a/packages/ono/test/__snapshots__/unocss.snapshot.typography.snap +++ /dev/null @@ -1,21 +0,0 @@ -/* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} -/* layer: default */ -.max-w-none{max-width:none;} -.border-l-4{border-left-width:4px;} -.border-gray-300{--un-border-opacity:1;border-color:rgb(209 213 219 / var(--un-border-opacity));} -.rounded{border-radius:0.25rem;} -.bg-gray-100{--un-bg-opacity:1;background-color:rgb(243 244 246 / var(--un-bg-opacity));} -.px-1{padding-left:0.25rem;padding-right:0.25rem;} -.pl-4{padding-left:1rem;} -.text-2xl{font-size:1.5rem;line-height:2rem;} -.text-4xl{font-size:2.25rem;line-height:2.5rem;} -.text-base{font-size:1rem;line-height:1.5rem;} -.text-sm{font-size:0.875rem;line-height:1.25rem;} -.font-bold{font-weight:700;} -.font-normal{font-weight:400;} -.font-semibold{font-weight:600;} -.leading-relaxed{line-height:1.625;} -.leading-tight{line-height:1.25;} -.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;} -.italic{font-style:italic;} \ No newline at end of file diff --git a/packages/ono/test/cli.snapshot.test.js b/packages/ono/test/cli.snapshot.test.js deleted file mode 100644 index fe62bab..0000000 --- a/packages/ono/test/cli.snapshot.test.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Snapshot tests for CLI commands - * These tests capture the actual CLI output and behavior - */ -import { test, beforeEach, afterEach } from "node:test"; -import { spawn } from "node:child_process"; -import { mkdir, rm } from "node:fs/promises"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { matchSnapshot } from "./snapshot-utils.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const CLI_PATH = resolve(__dirname, "../src/cli.js"); -const TEST_DIR = resolve(__dirname, "../test-tmp"); -const TEST_FILE = "cli.snapshot.test.js"; - -/** - * Helper to run CLI command and capture output - */ -function runCLI(args, options = {}) { - return new Promise((resolve, reject) => { - const child = spawn("node", [CLI_PATH, ...args], { - cwd: options.cwd || TEST_DIR, - env: { ...process.env, ...options.env }, - }); - - let stdout = ""; - let stderr = ""; - - child.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - child.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - child.on("close", (code) => { - resolve({ code, stdout, stderr }); - }); - - child.on("error", (error) => { - reject(error); - }); - - // For dev command, kill after timeout - if (options.timeout) { - setTimeout(() => { - child.kill("SIGTERM"); - }, options.timeout); - } - }); -} - -beforeEach(async () => { - await rm(TEST_DIR, { recursive: true, force: true }); - await mkdir(TEST_DIR, { recursive: true }); -}); - -afterEach(async () => { - await rm(TEST_DIR, { recursive: true, force: true }); -}); - -test("snapshot - help message", async () => { - const { code, stdout, stderr } = await runCLI(["--help"]); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "help message"); -}); - -test("snapshot - help message short flag", async () => { - const { code, stdout, stderr } = await runCLI(["-h"]); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "help message short flag"); -}); - -test("snapshot - no command provided", async () => { - const { code, stdout, stderr } = await runCLI([]); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "no command provided"); -}); - -test("snapshot - unknown command", async () => { - const { code, stdout, stderr } = await runCLI(["unknown-command"]); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "unknown command"); -}); - -test("snapshot - version flag (not supported)", async () => { - const { code, stdout, stderr } = await runCLI(["--version"]); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "version flag not supported"); -}); - -test("snapshot - build command help", async () => { - const { code, stdout, stderr } = await runCLI(["build", "--help"]); - - // Normalize file paths to be environment-independent - const normalizedStderr = stderr - .replace(/\/[^\s]+\/test-tmp\//g, "/PATH/test-tmp/"); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${normalizedStderr}`; - - await matchSnapshot(output, TEST_FILE, "build command help"); -}); - -test("snapshot - build non-existent file", async () => { - const { code, stdout, stderr } = await runCLI(["build", "non-existent.jsx"]); - - // Normalize file paths to be environment-independent - const normalizedStderr = stderr - .replace(/\/[^\s]+\/test-tmp\//g, "/PATH/test-tmp/"); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${normalizedStderr}`; - - await matchSnapshot(output, TEST_FILE, "build non-existent file"); -}); - -test("snapshot - build non-existent directory", async () => { - const { code, stdout, stderr } = await runCLI(["build", "non-existent-dir"]); - - // Normalize file paths to be environment-independent - const normalizedStderr = stderr - .replace(/\/[^\s]+\/test-tmp\//g, "/PATH/test-tmp/"); - - const output = `Exit Code: ${code} -STDOUT: -${stdout} -STDERR: -${normalizedStderr}`; - - await matchSnapshot(output, TEST_FILE, "build non-existent directory"); -}); - -test("snapshot - serve command", async () => { - // Create a simple dist directory with an HTML file - await mkdir(resolve(TEST_DIR, "dist"), { recursive: true }); - - // Use a random high port to avoid conflicts - const port = 9000 + Math.floor(Math.random() * 1000); - const { code, stdout, stderr } = await runCLI(["serve", "--port", port.toString()], { timeout: 1000 }); - - // Remove any dynamic content like timestamps, process IDs, and port numbers - const cleanedStdout = stdout - .replace(/Server running at http:\/\/localhost:\d+/g, "Server running at http://localhost:PORT") - .replace(/PID: \d+/g, "PID: XXXXX") - .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, "TIMESTAMP") - .replace(/port \d+/g, "port PORT"); - - const output = `Exit Code: ${code} -STDOUT: -${cleanedStdout} -STDERR: -${stderr}`; - - await matchSnapshot(output, TEST_FILE, "serve command"); -}); - -test("snapshot - watch command on empty directory", async () => { - const { code, stdout, stderr } = await runCLI(["watch"], { timeout: 1000 }); - - // Clean up any dynamic content - const cleanedStdout = stdout - .replace(/Found \d+ page\(s\)/g, "Found N page(s)") - .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, "TIMESTAMP") - .replace(/Watching.*for changes/g, "Watching ... for changes") - .replace(/port \d+/g, "port PORT") - .replace(/Server running at http:\/\/localhost:\d+/g, "Server running at http://localhost:PORT"); - - // Normalize file paths to be environment-independent - const normalizedStderr = stderr - .replace(/\/[^\s]+\/test-tmp\//g, "/PATH/test-tmp/"); - - const output = `Exit Code: ${code} -STDOUT: -${cleanedStdout} -STDERR: -${normalizedStderr}`; - - await matchSnapshot(output, TEST_FILE, "watch command on empty directory"); -}); \ No newline at end of file diff --git a/packages/ono/test/cli.test.js b/packages/ono/test/cli.test.js new file mode 100644 index 0000000..bd3ae8e --- /dev/null +++ b/packages/ono/test/cli.test.js @@ -0,0 +1,100 @@ +/** + * Snapshot tests for CLI commands + * These tests capture the actual CLI output and behavior. + * Update snapshots with: npm run test:update + */ +import { test, beforeEach, afterEach } from "node:test"; +import { spawn } from "node:child_process"; +import { mkdir, rm } from "node:fs/promises"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CLI_PATH = resolve(__dirname, "../src/cli.js"); +const TEST_DIR = resolve(__dirname, "../test-tmp"); + +// Keep CLI output readable in the snapshot file +const raw = { serializers: [(value) => value] }; + +/** + * Helper to run CLI command and capture output + */ +function runCLI(args, options = {}) { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn("node", [CLI_PATH, ...args], { + cwd: options.cwd || TEST_DIR, + env: { ...process.env, ...options.env }, + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + resolvePromise({ code, stdout, stderr }); + }); + + child.on("error", rejectPromise); + }); +} + +/** Normalize file paths so snapshots are environment-independent */ +function normalizePaths(output) { + return output.replace(/\/[^\s'"]+\/test-tmp\//g, "/PATH/test-tmp/"); +} + +function formatResult({ code, stdout, stderr }) { + return `Exit Code: ${code} +STDOUT: +${stdout} +STDERR: +${normalizePaths(stderr)}`; +} + +beforeEach(async () => { + await rm(TEST_DIR, { recursive: true, force: true }); + await mkdir(TEST_DIR, { recursive: true }); +}); + +afterEach(async () => { + await rm(TEST_DIR, { recursive: true, force: true }); +}); + +test("cli - help message", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["--help"])), raw); +}); + +test("cli - help message short flag", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["-h"])), raw); +}); + +test("cli - no command provided", async (t) => { + t.assert.snapshot(formatResult(await runCLI([])), raw); +}); + +test("cli - unknown command", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["unknown-command"])), raw); +}); + +test("cli - version flag (not supported)", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["--version"])), raw); +}); + +test("cli - build command help", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["build", "--help"])), raw); +}); + +test("cli - build non-existent file", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["build", "non-existent.jsx"])), raw); +}); + +test("cli - build non-existent directory", async (t) => { + t.assert.snapshot(formatResult(await runCLI(["build", "non-existent-dir"])), raw); +}); diff --git a/packages/ono/test/cli.test.js.snapshot b/packages/ono/test/cli.test.js.snapshot new file mode 100644 index 0000000..4288b85 --- /dev/null +++ b/packages/ono/test/cli.test.js.snapshot @@ -0,0 +1,158 @@ +exports[`cli - build command help 1`] = ` +Exit Code: 0 +STDOUT: + +Usage: ono build [input] [options] + +Build JSX files to static HTML + +Arguments: + input File or directory to build (default: pages) + +Options: + --output Output directory (default: dist) + --help Show this help message + +Examples: + ono build Build all pages in pages/ directory + ono build pages/index.jsx Build a single file + ono build --output build Build to build/ directory + + +STDERR: + +`; + +exports[`cli - build non-existent directory 1`] = ` +Exit Code: 1 +STDOUT: + +STDERR: +Error: ENOENT: no such file or directory, stat '/PATH/test-tmp/non-existent-dir' + +`; + +exports[`cli - build non-existent file 1`] = ` +Exit Code: 1 +STDOUT: + +STDERR: +Error: ENOENT: no such file or directory, stat '/PATH/test-tmp/non-existent.jsx' + +`; + +exports[`cli - help message 1`] = ` +Exit Code: 0 +STDOUT: + +Ono - Minimalist SSG Framework + +Usage: + ono [options] + +Commands: + build [input] Build JSX files to static HTML + input: file or directory (default: pages) + + dev [input] Build, watch, and serve with live reload + input: file or directory (default: pages) + +Options: + --output Output directory (default: dist) + --port Server port (default: 3000) + --help Show this help message + +Examples: + ono build Build all pages in pages/ directory + ono build pages/index.jsx Build a single file + ono dev Start dev server with live reload + ono dev --port 8080 Start dev server on port 8080 + + +STDERR: + +`; + +exports[`cli - help message short flag 1`] = ` +Exit Code: 0 +STDOUT: + +Ono - Minimalist SSG Framework + +Usage: + ono [options] + +Commands: + build [input] Build JSX files to static HTML + input: file or directory (default: pages) + + dev [input] Build, watch, and serve with live reload + input: file or directory (default: pages) + +Options: + --output Output directory (default: dist) + --port Server port (default: 3000) + --help Show this help message + +Examples: + ono build Build all pages in pages/ directory + ono build pages/index.jsx Build a single file + ono dev Start dev server with live reload + ono dev --port 8080 Start dev server on port 8080 + + +STDERR: + +`; + +exports[`cli - no command provided 1`] = ` +Exit Code: 0 +STDOUT: + +Ono - Minimalist SSG Framework + +Usage: + ono [options] + +Commands: + build [input] Build JSX files to static HTML + input: file or directory (default: pages) + + dev [input] Build, watch, and serve with live reload + input: file or directory (default: pages) + +Options: + --output Output directory (default: dist) + --port Server port (default: 3000) + --help Show this help message + +Examples: + ono build Build all pages in pages/ directory + ono build pages/index.jsx Build a single file + ono dev Start dev server with live reload + ono dev --port 8080 Start dev server on port 8080 + + +STDERR: + +`; + +exports[`cli - unknown command 1`] = ` +Exit Code: 1 +STDOUT: + +STDERR: +Unknown command: unknown-command +Run "ono --help" for usage information + +`; + +exports[`cli - version flag (not supported) 1`] = ` +Exit Code: 1 +STDOUT: + +STDERR: +Unknown command: --version +Run "ono --help" for usage information + +`; diff --git a/packages/ono/test/renderer.snapshot.test.js b/packages/ono/test/renderer.snapshot.test.js deleted file mode 100644 index c27a513..0000000 --- a/packages/ono/test/renderer.snapshot.test.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Snapshot tests for HTML renderer - */ -import { test } from "node:test"; -import { createElement } from "../src/jsx-runtime.js"; -import { renderToString } from "../src/renderer.js"; -import { matchHTMLSnapshot } from "./snapshot-utils.js"; - -const TEST_FILE = "renderer.snapshot.test.js"; - -test("snapshot - simple element", async () => { - const vnode = createElement("div"); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "simple element"); -}); - -test("snapshot - element with text", async () => { - const vnode = createElement("div", null, "Hello World"); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "element with text"); -}); - -test("snapshot - element with attributes", async () => { - const vnode = createElement("div", { - className: "container", - id: "main", - "data-testid": "test-element" - }); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "element with attributes"); -}); - -test("snapshot - nested elements", async () => { - const vnode = createElement("div", { className: "container" }, - createElement("h1", null, "Title"), - createElement("p", null, "This is a paragraph with ", - createElement("strong", null, "bold text"), - " and regular text." - ), - createElement("ul", null, - createElement("li", null, "Item 1"), - createElement("li", null, "Item 2"), - createElement("li", null, "Item 3") - ) - ); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "nested elements"); -}); - -test("snapshot - component function", async () => { - function BlogPost({ title, author, children }) { - return createElement("article", { className: "blog-post" }, - createElement("header", null, - createElement("h1", null, title), - createElement("p", { className: "author" }, "By ", author) - ), - createElement("div", { className: "content" }, children) - ); - } - - const vnode = BlogPost({ - title: "My First Post", - author: "John Doe", - children: [ - createElement("p", null, "This is the first paragraph."), - createElement("p", null, "This is the second paragraph.") - ] - }); - - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "component function"); -}); - -test("snapshot - boolean attributes", async () => { - const vnode = createElement("input", { - type: "checkbox", - checked: true, - disabled: false, - required: true - }); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "boolean attributes"); -}); - -test("snapshot - style object", async () => { - const vnode = createElement("div", { - style: { - backgroundColor: "red", - fontSize: "16px", - margin: "10px 20px" - } - }, "Styled content"); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "style object"); -}); - -test("snapshot - complex form", async () => { - const vnode = createElement("form", { className: "contact-form", method: "post" }, - createElement("div", { className: "form-group" }, - createElement("label", { htmlFor: "name" }, "Name:"), - createElement("input", { - type: "text", - id: "name", - name: "name", - required: true, - placeholder: "Enter your name" - }) - ), - createElement("div", { className: "form-group" }, - createElement("label", { htmlFor: "email" }, "Email:"), - createElement("input", { - type: "email", - id: "email", - name: "email", - required: true, - placeholder: "Enter your email" - }) - ), - createElement("div", { className: "form-group" }, - createElement("label", { htmlFor: "message" }, "Message:"), - createElement("textarea", { - id: "message", - name: "message", - rows: 4, - placeholder: "Enter your message" - }) - ), - createElement("button", { type: "submit", className: "btn btn-primary" }, "Send Message") - ); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "complex form"); -}); - -test("snapshot - HTML entities escaping", async () => { - const vnode = createElement("div", { - title: "This has \"quotes\" & ", - "data-value": "" - }, - "Content with HTML & \"special\" characters" - ); - const result = renderToString(vnode); - await matchHTMLSnapshot(result, TEST_FILE, "HTML entities escaping"); -}); \ No newline at end of file diff --git a/packages/ono/test/renderer.test.js b/packages/ono/test/renderer.test.js index ae4c4ce..0ad4cb3 100644 --- a/packages/ono/test/renderer.test.js +++ b/packages/ono/test/renderer.test.js @@ -212,3 +212,93 @@ test("renderToString - Fragment with mixed content", () => { const result = renderToString(vnode); assert.strictEqual(result, "Text beforeboldText after"); }); + +// --- Output snapshots (update with: npm run test:update) --- + +const raw = { serializers: [(value) => value] }; + +test("renderToString snapshot - nested elements", (t) => { + const vnode = createElement("div", { className: "container" }, + createElement("h1", null, "Title"), + createElement("p", null, "This is a paragraph with ", + createElement("strong", null, "bold text"), + " and regular text." + ), + createElement("ul", null, + createElement("li", null, "Item 1"), + createElement("li", null, "Item 2"), + createElement("li", null, "Item 3") + ) + ); + t.assert.snapshot(renderToString(vnode), raw); +}); + +test("renderToString snapshot - component function", (t) => { + function BlogPost({ title, author, children }) { + return createElement("article", { className: "blog-post" }, + createElement("header", null, + createElement("h1", null, title), + createElement("p", { className: "author" }, "By ", author) + ), + createElement("div", { className: "content" }, children) + ); + } + + const vnode = BlogPost({ + title: "My First Post", + author: "John Doe", + children: [ + createElement("p", null, "This is the first paragraph."), + createElement("p", null, "This is the second paragraph.") + ] + }); + + t.assert.snapshot(renderToString(vnode), raw); +}); + +test("renderToString snapshot - style object", (t) => { + const vnode = createElement("div", { + style: { + backgroundColor: "red", + fontSize: "16px", + margin: "10px 20px" + } + }, "Styled content"); + t.assert.snapshot(renderToString(vnode), raw); +}); + +test("renderToString snapshot - complex form", (t) => { + const vnode = createElement("form", { className: "contact-form", method: "post" }, + createElement("div", { className: "form-group" }, + createElement("label", { htmlFor: "name" }, "Name:"), + createElement("input", { + type: "text", + id: "name", + name: "name", + required: true, + placeholder: "Enter your name" + }) + ), + createElement("div", { className: "form-group" }, + createElement("label", { htmlFor: "message" }, "Message:"), + createElement("textarea", { + id: "message", + name: "message", + rows: 4, + placeholder: "Enter your message" + }) + ), + createElement("button", { type: "submit", className: "btn btn-primary" }, "Send Message") + ); + t.assert.snapshot(renderToString(vnode), raw); +}); + +test("renderToString snapshot - HTML entities escaping", (t) => { + const vnode = createElement("div", { + title: "This has \"quotes\" & ", + "data-value": "" + }, + "Content with HTML & \"special\" characters" + ); + t.assert.snapshot(renderToString(vnode), raw); +}); diff --git a/packages/ono/test/renderer.test.js.snapshot b/packages/ono/test/renderer.test.js.snapshot new file mode 100644 index 0000000..ff7c582 --- /dev/null +++ b/packages/ono/test/renderer.test.js.snapshot @@ -0,0 +1,19 @@ +exports[`renderToString snapshot - HTML entities escaping 1`] = ` +
Content with <em>HTML</em> & "special" characters
+`; + +exports[`renderToString snapshot - complex form 1`] = ` +
+`; + +exports[`renderToString snapshot - component function 1`] = ` +

My First Post

By John Doe

This is the first paragraph.

This is the second paragraph.

+`; + +exports[`renderToString snapshot - nested elements 1`] = ` +

Title

This is a paragraph with bold text and regular text.

  • Item 1
  • Item 2
  • Item 3
+`; + +exports[`renderToString snapshot - style object 1`] = ` +
Styled content
+`; diff --git a/packages/ono/test/snapshot-utils.js b/packages/ono/test/snapshot-utils.js deleted file mode 100644 index 01cf2a1..0000000 --- a/packages/ono/test/snapshot-utils.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Snapshot testing utilities for Node.js test runner - */ -import { readFile, writeFile, mkdir } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import { existsSync } from "node:fs"; -import assert from "node:assert"; - -const SNAPSHOTS_DIR = "test/__snapshots__"; - -/** - * Normalize content for snapshot comparison - * @param {string} content - Content to normalize - * @returns {string} Normalized content - */ -function normalizeContent(content) { - return content - .trim() - .replace(/\r\n/g, "\n") // Normalize line endings - .replace(/\s+$/gm, ""); // Remove trailing whitespace -} - -/** - * Generate snapshot file path - * @param {string} testFile - Test file name - * @param {string} testName - Test name - * @returns {string} Snapshot file path - */ -function getSnapshotPath(testFile, testName) { - const sanitizedTestName = testName.replace(/[^a-zA-Z0-9-_]/g, "_"); - const sanitizedTestFile = testFile.replace(/\.test\.js$/, ""); - return join(SNAPSHOTS_DIR, `${sanitizedTestFile}.${sanitizedTestName}.snap`); -} - -/** - * Read existing snapshot - * @param {string} snapshotPath - Path to snapshot file - * @returns {Promise} Snapshot content or null if not exists - */ -async function readSnapshot(snapshotPath) { - try { - return await readFile(snapshotPath, "utf-8"); - } catch (error) { - if (error.code === "ENOENT") { - return null; - } - throw error; - } -} - -/** - * Write snapshot to file - * @param {string} snapshotPath - Path to snapshot file - * @param {string} content - Content to write - */ -async function writeSnapshot(snapshotPath, content) { - await mkdir(dirname(snapshotPath), { recursive: true }); - await writeFile(snapshotPath, content, "utf-8"); -} - -/** - * Match content against snapshot - * @param {string} content - Actual content - * @param {string} testFile - Test file name (e.g., "renderer.test.js") - * @param {string} testName - Test name - * @param {object} options - Options - * @param {boolean} options.updateSnapshots - Update snapshots instead of comparing - */ -export async function matchSnapshot(content, testFile, testName, options = {}) { - const { updateSnapshots = process.env.UPDATE_SNAPSHOTS === "true" } = options; - - const normalizedContent = normalizeContent(content); - const snapshotPath = getSnapshotPath(testFile, testName); - - if (updateSnapshots) { - await writeSnapshot(snapshotPath, normalizedContent); - console.log(`📸 Updated snapshot: ${snapshotPath}`); - return; - } - - const existingSnapshot = await readSnapshot(snapshotPath); - - if (existingSnapshot === null) { - // First time running this test, create snapshot - await writeSnapshot(snapshotPath, normalizedContent); - console.log(`📸 Created snapshot: ${snapshotPath}`); - return; - } - - const normalizedSnapshot = normalizeContent(existingSnapshot); - - try { - assert.strictEqual(normalizedContent, normalizedSnapshot); - } catch (error) { - // Provide helpful diff information - console.error(`❌ Snapshot mismatch in ${testName}`); - console.error(`Expected (snapshot):`); - console.error(normalizedSnapshot); - console.error(`Actual:`); - console.error(normalizedContent); - console.error(`\nTo update snapshots, run: UPDATE_SNAPSHOTS=true npm test`); - throw error; - } -} - -/** - * Match HTML snapshot with formatting - * @param {string} html - HTML content - * @param {string} testFile - Test file name - * @param {string} testName - Test name - * @param {object} options - Options - */ -export async function matchHTMLSnapshot(html, testFile, testName, options = {}) { - // Basic HTML formatting for better readability - const formattedHTML = html - .replace(/>\n<") - .replace(/\n\s*\n/g, "\n") - .trim(); - - await matchSnapshot(formattedHTML, testFile, testName, options); -} - -/** - * Match JavaScript code snapshot - * @param {string} code - JavaScript code - * @param {string} testFile - Test file name - * @param {string} testName - Test name - * @param {object} options - Options - */ -export async function matchCodeSnapshot(code, testFile, testName, options = {}) { - await matchSnapshot(code, testFile, testName, options); -} - -/** - * Check if snapshots directory exists - * @returns {boolean} True if snapshots directory exists - */ -export function hasSnapshots() { - return existsSync(SNAPSHOTS_DIR); -} \ No newline at end of file diff --git a/packages/ono/test/transformer.snapshot.test.js b/packages/ono/test/transformer.snapshot.test.js deleted file mode 100644 index e9e1881..0000000 --- a/packages/ono/test/transformer.snapshot.test.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Snapshot tests for JSX transformer - */ -import { test } from "node:test"; -import { transformJSX } from "../src/transformer.js"; -import { matchCodeSnapshot } from "./snapshot-utils.js"; - -const TEST_FILE = "transformer.snapshot.test.js"; - -test("snapshot - simple JSX element", async () => { - const input = `const elem =
Hello
;`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "simple JSX element"); -}); - -test("snapshot - JSX with props", async () => { - const input = `const elem =
Hello
;`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "JSX with props"); -}); - -test("snapshot - nested JSX", async () => { - const input = ` -const elem = ( -
-

Title

-

Paragraph with bold text

-
    -
  • Item 1
  • -
  • Item 2
  • -
-
-);`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "nested JSX"); -}); - -test("snapshot - component function with JSX", async () => { - const input = ` -function BlogPost({ title, children }) { - return ( -
-

{title}

-
- {children} -
-
- ); -}`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "component function with JSX"); -}); - -test("snapshot - JSX with expressions", async () => { - const input = ` -const todos = items.map(item => ( -
  • - toggle(item.id)} - /> - {item.text} - -
  • -));`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "JSX with expressions"); -}); - -test("snapshot - self-closing tags", async () => { - const input = ` -const form = ( -
    - - -
    -
    - Logo -
    -);`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "self-closing tags"); -}); - -test("snapshot - JSX fragments", async () => { - const input = ` -const content = ( - <> -

    Title

    -

    First paragraph

    -

    Second paragraph

    - -);`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "JSX fragments"); -}); - -test("snapshot - complex component with imports", async () => { - const input = ` -import { useState } from 'react'; -import { Header } from './components/Header'; -import { Footer } from './components/Footer'; - -export default function App() { - const [count, setCount] = useState(0); - - return ( -
    -
    -
    -

    Counter: {count}

    - - -
    -
    -
    - ); -}`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "complex component with imports"); -}); - -test("snapshot - style prop object", async () => { - const input = ` -const styledDiv = ( -
    - Styled content -
    -);`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "style prop object"); -}); - -test("snapshot - conditional rendering", async () => { - const input = ` -const content = ( -
    - {isLoggedIn ? ( -
    -

    Welcome back!

    - -
    - ) : ( -
    -

    Please log in

    - -
    - )} - {showMessage &&

    {message}

    } -
    -);`; - const result = transformJSX(input); - await matchCodeSnapshot(result, TEST_FILE, "conditional rendering"); -}); \ No newline at end of file diff --git a/packages/ono/test/transformer.test.js b/packages/ono/test/transformer.test.js index 3e57eb7..0348d4a 100644 --- a/packages/ono/test/transformer.test.js +++ b/packages/ono/test/transformer.test.js @@ -154,3 +154,113 @@ test("transformJSX - nested Fragment", () => { assert.ok(result.includes('h("div"')); assert.ok(result.includes('h("span"')); }); + +// --- Output snapshots (update with: npm run test:update) --- + +const raw = { serializers: [(value) => value] }; + +test("transformJSX snapshot - component function with JSX", (t) => { + const input = ` +function BlogPost({ title, children }) { + return ( +
    +

    {title}

    +
    + {children} +
    +
    + ); +}`; + t.assert.snapshot(transformJSX(input), raw); +}); + +test("transformJSX snapshot - JSX with expressions", (t) => { + const input = ` +const todos = items.map(item => ( +
  • + toggle(item.id)} + /> + {item.text} + +
  • +));`; + t.assert.snapshot(transformJSX(input), raw); +}); + +test("transformJSX snapshot - JSX fragments", (t) => { + const input = ` +const content = ( + <> +

    Title

    +

    First paragraph

    +

    Second paragraph

    + +);`; + t.assert.snapshot(transformJSX(input), raw); +}); + +test("transformJSX snapshot - complex component with imports", (t) => { + const input = ` +import { useState } from 'react'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; + +export default function App() { + const [count, setCount] = useState(0); + + return ( +
    +
    +
    +

    Counter: {count}

    + +
    +
    +
    + ); +}`; + t.assert.snapshot(transformJSX(input), raw); +}); + +test("transformJSX snapshot - style prop object", (t) => { + const input = ` +const styledDiv = ( +
    + Styled content +
    +);`; + t.assert.snapshot(transformJSX(input), raw); +}); + +test("transformJSX snapshot - conditional rendering", (t) => { + const input = ` +const content = ( +
    + {isLoggedIn ? ( +
    +

    Welcome back!

    + +
    + ) : ( +
    +

    Please log in

    + +
    + )} + {showMessage &&

    {message}

    } +
    +);`; + t.assert.snapshot(transformJSX(input), raw); +}); diff --git a/packages/ono/test/transformer.test.js.snapshot b/packages/ono/test/transformer.test.js.snapshot new file mode 100644 index 0000000..0e0d55c --- /dev/null +++ b/packages/ono/test/transformer.test.js.snapshot @@ -0,0 +1,63 @@ +exports[`transformJSX snapshot - JSX fragments 1`] = ` +const content = (h(Fragment, null, + h("h1", null, "Title"), + h("p", null, "First paragraph"), + h("p", null, "Second paragraph"))); + +`; + +exports[`transformJSX snapshot - JSX with expressions 1`] = ` +const todos = items.map(item => (h("li", { key: item.id, className: item.completed ? 'completed' : '' }, + h("input", { type: "checkbox", checked: item.completed, onChange: () => toggle(item.id) }), + h("span", null, item.text), + h("button", { onClick: () => remove(item.id) }, "\\u00D7")))); + +`; + +exports[`transformJSX snapshot - complex component with imports 1`] = ` +import { useState } from 'react'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +export default function App() { + const [count, setCount] = useState(0); + return (h("div", { className: "app" }, + h(Header, { title: "My App" }), + h("main", null, + h("h1", null, + "Counter: ", + count), + h("button", { onClick: () => setCount(count + 1) }, "Increment")), + h(Footer, null))); +} + +`; + +exports[`transformJSX snapshot - component function with JSX 1`] = ` +function BlogPost({ title, children }) { + return (h("article", { className: "blog-post" }, + h("h1", null, title), + h("div", { className: "content" }, children))); +} + +`; + +exports[`transformJSX snapshot - conditional rendering 1`] = ` +const content = (h("div", null, + isLoggedIn ? (h("div", null, + h("h1", null, "Welcome back!"), + h("button", { onClick: logout }, "Logout"))) : (h("div", null, + h("h1", null, "Please log in"), + h("button", { onClick: login }, "Login"))), + showMessage && h("p", { className: "message" }, message))); + +`; + +exports[`transformJSX snapshot - style prop object 1`] = ` +const styledDiv = (h("div", { style: { + backgroundColor: 'red', + fontSize: '16px', + padding: '10px', + border: '1px solid black' + } }, "Styled content")); + +`; diff --git a/packages/ono/test/unocss.snapshot.test.js b/packages/ono/test/unocss.test.js similarity index 66% rename from packages/ono/test/unocss.snapshot.test.js rename to packages/ono/test/unocss.test.js index 69f8104..822df1b 100644 --- a/packages/ono/test/unocss.snapshot.test.js +++ b/packages/ono/test/unocss.test.js @@ -1,13 +1,14 @@ /** * Snapshot tests for UnoCSS generation + * Update snapshots with: npm run test:update */ import { test } from "node:test"; import { generateCSS } from "../src/unocss.js"; -import { matchCodeSnapshot } from "./snapshot-utils.js"; -const TEST_FILE = "unocss.snapshot.test.js"; +// Keep generated CSS readable in the snapshot file +const raw = { serializers: [(value) => value] }; -test("snapshot - basic utility classes", async () => { +test("unocss - basic utility classes", async (t) => { const htmlContent = `

    Title

    @@ -15,11 +16,10 @@ test("snapshot - basic utility classes", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "basic utility classes"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - layout classes", async () => { +test("unocss - layout classes", async (t) => { const htmlContent = `
    @@ -33,11 +33,10 @@ test("snapshot - layout classes", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "layout classes"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - responsive design", async () => { +test("unocss - responsive design", async (t) => { const htmlContent = `
    @@ -47,11 +46,10 @@ test("snapshot - responsive design", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "responsive design"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - color utilities", async () => { +test("unocss - color utilities", async (t) => { const htmlContent = `
    @@ -62,11 +60,10 @@ test("snapshot - color utilities", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "color utilities"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - spacing and sizing", async () => { +test("unocss - spacing and sizing", async (t) => { const htmlContent = `
    @@ -78,11 +75,10 @@ test("snapshot - spacing and sizing", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "spacing and sizing"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - typography", async () => { +test("unocss - typography", async (t) => { const htmlContent = `

    Main Title

    @@ -98,11 +94,10 @@ test("snapshot - typography", async () => {
    `; - const css = await generateCSS(htmlContent); - await matchCodeSnapshot(css, TEST_FILE, "typography"); + t.assert.snapshot(await generateCSS(htmlContent), raw); }); -test("snapshot - form elements", async () => { +test("unocss - form elements", async (t) => { const htmlContent = `
    @@ -112,13 +107,6 @@ test("snapshot - form elements", async () => { class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
    -
    - - -
    - ); -} - -export default Button; diff --git a/packages/ono/example/components/Card.jsx b/packages/ono/example/components/Card.jsx deleted file mode 100644 index 899415b..0000000 --- a/packages/ono/example/components/Card.jsx +++ /dev/null @@ -1,26 +0,0 @@ -// Card component demonstrating slots -function Card(props) { - return ( -
    - {props.title && ( -

    {props.title}

    - )} -
    - {props.children} -
    - {props.actions && ( -
    - {props.actions} -
    - )} -
    - ); -} - -export default Card; diff --git a/packages/ono/example/components/Layout.jsx b/packages/ono/example/components/Layout.jsx deleted file mode 100644 index 54f36a6..0000000 --- a/packages/ono/example/components/Layout.jsx +++ /dev/null @@ -1,46 +0,0 @@ -// Layout component with slot support -function Layout(props) { - return ( - - - - - {props.title || "Mini JSX Site"} - - - -
    -

    {props.title}

    - {props.header} -
    -
    - {props.children} -
    -
    - {props.footer ||

    © 2025 Mini JSX

    } -
    - - - ); -} - -export default Layout; diff --git a/packages/ono/example/index.html b/packages/ono/example/index.html deleted file mode 100644 index 5a8fdf9..0000000 --- a/packages/ono/example/index.html +++ /dev/null @@ -1,37 +0,0 @@ - -Mini JSX - SSG Example

    Mini JSX - SSG Example

    Hello, World!

    This example demonstrates:

    • External component imports
    • Slot-based composition (children, header, footer, actions)
    • Props handling
    • Nested components

    Feature: Slots

    Cards can have title slots, content slots (children), and action slots for flexible composition.

    Feature: External Imports

    Import components from separate files to keep your code organized. The build system will resolve and bundle them automatically.

    No title? No problem! Slots are optional.

    Built with Mini JSX 🚀

    A lightweight JSX library for static site generation

    - - \ No newline at end of file diff --git a/packages/ono/example/index.jsx b/packages/ono/example/index.jsx deleted file mode 100644 index 7bbc894..0000000 --- a/packages/ono/example/index.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// External imports -import Layout from "./components/Layout.jsx"; -import Card from "./components/Card.jsx"; -import Button from "./components/Button.jsx"; - -// Simple component -function Greeting(props) { - return
    Hello, {props.name}!
    ; -} - -// Main page demonstrating slots and external imports -function App() { - return ( - - Home | About - - } - footer={ -
    -

    Built with Mini JSX 🚀

    -

    A lightweight JSX library for static site generation

    -
    - } - > - - -

    This example demonstrates:

    -
      -
    • External component imports
    • -
    • Slot-based composition (children, header, footer, actions)
    • -
    • Props handling
    • -
    • Nested components
    • -
    - - - - -
    - } - > -

    - Cards can have title slots, content slots (children), - and action slots for flexible composition. -

    - - - -

    - Import components from separate files to keep your code organized. - The build system will resolve and bundle them automatically. -

    -
    - - -

    - No title? No problem! Slots are optional. -

    -
    - - ); -} - -export default App; diff --git a/packages/ono/example/output.html b/packages/ono/example/output.html deleted file mode 100644 index 34a50d6..0000000 --- a/packages/ono/example/output.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - Mini JSX - SSG Example - - - -
    -

    Mini JSX - SSG Example

    - -
    -
    -
    Hello, World!
    - -

    This example demonstrates:

    -
      -
    • External component imports
    • -
    • Slot-based composition (children, header, footer, actions)
    • -
    • Props handling
    • -
    • Nested components
    • -
    - -
    -

    Feature: Slots

    -
    -

    - Cards can have title slots, content slots (children), - and action slots for flexible composition. -

    -
    -
    -
    - - -
    -
    -
    - -
    -

    Feature: External Imports

    -
    -

    - Import components from separate files to keep your code organized. - The build system will resolve and bundle them automatically. -

    -
    -
    - -
    -
    -

    - No title? No problem! Slots are optional. -

    -
    -
    -
    -
    -
    -

    Built with Mini JSX 🚀

    -

    A lightweight JSX library for static site generation

    -
    -
    - - diff --git a/packages/ono/pages/about.jsx b/packages/ono/pages/about.jsx deleted file mode 100644 index cb98b8a..0000000 --- a/packages/ono/pages/about.jsx +++ /dev/null @@ -1,32 +0,0 @@ -export default function About() { - return ( - - - - - About - Mini JSX Pages Example - - - - - -
    -

    About Mini JSX

    -

    Mini JSX is a lightweight JSX library for static site generation.

    -

    Features

    -
      -
    • Simple JSX runtime
    • -
    • Fast bundler
    • -
    • Pages directory support
    • -
    • Live reload dev server
    • -
    • UnoCSS atomic CSS integration
    • -
    -
    - - - ); -} diff --git a/packages/ono/pages/blog/first-post.jsx b/packages/ono/pages/blog/first-post.jsx deleted file mode 100644 index 6c0bce1..0000000 --- a/packages/ono/pages/blog/first-post.jsx +++ /dev/null @@ -1,28 +0,0 @@ -export default function FirstPost() { - return ( - - - - - First Post - Blog - - - - - -
    -

    My First Blog Post

    -

    Published on October 29, 2025

    -

    Welcome to my first blog post built with Mini JSX!

    -

    This page is in a subdirectory (pages/blog/), and the output preserves the directory structure (dist/blog/first-post.html).

    -

    Why Mini JSX?

    -

    Mini JSX makes it easy to build static sites with JSX components, without the complexity of a full framework.

    -
    - - - ); -} diff --git a/packages/ono/pages/index.jsx b/packages/ono/pages/index.jsx deleted file mode 100644 index 5fa11ff..0000000 --- a/packages/ono/pages/index.jsx +++ /dev/null @@ -1,30 +0,0 @@ -export default function Home() { - return ( - - - - - Home - Mini JSX Pages Example - - - - - -
    -

    Welcome to Mini JSX

    -

    This is a multi-page static site built with Mini JSX!

    -
      -
    • Pages are automatically discovered from the pages/ directory
    • -
    • Directory structure is preserved in the output
    • -
    • Live reload works across all pages
    • -
    • Now with UnoCSS atomic CSS!
    • -
    -
    - - - ); -} diff --git a/packages/ono/public/css/style.css b/packages/ono/public/css/style.css deleted file mode 100644 index 85f5cc6..0000000 --- a/packages/ono/public/css/style.css +++ /dev/null @@ -1,40 +0,0 @@ -/* Global styles for Mini JSX site */ -body { - font-family: system-ui, -apple-system, sans-serif; - max-width: 800px; - margin: 0 auto; - padding: 2rem; - line-height: 1.6; - color: #333; -} - -nav { - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 2px solid #eee; -} - -nav a { - margin-right: 1rem; - color: #0070f3; - text-decoration: none; - font-weight: 500; -} - -nav a:hover { - text-decoration: underline; -} - -code { - background: #f4f4f4; - padding: 0.2rem 0.4rem; - border-radius: 3px; - font-family: 'Monaco', 'Courier New', monospace; - font-size: 0.9em; -} - -.meta { - color: #666; - font-size: 0.9rem; - font-style: italic; -} diff --git a/packages/ono/public/favicon.ico b/packages/ono/public/favicon.ico deleted file mode 100644 index 64ffac7..0000000 --- a/packages/ono/public/favicon.ico +++ /dev/null @@ -1 +0,0 @@ -# Placeholder favicon file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 343a6d4..fdbb787 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,46 +31,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/ono-lp: - dependencies: - '@ai-sdk/anthropic': - specifier: ^1.2.12 - version: 1.2.12(zod@3.25.76) - '@ai-sdk/google': - specifier: ^1.2.22 - version: 1.2.22(zod@3.25.76) - '@ai-sdk/openai': - specifier: ^1.3.24 - version: 1.3.24(zod@3.25.76) - '@hashrock/ono': - specifier: workspace:* - version: link:../ono - ai: - specifier: ^4.3.19 - version: 4.3.19(react@19.2.3)(zod@3.25.76) - commander: - specifier: ^12.0.0 - version: 12.1.0 - dotenv: - specifier: ^17.2.3 - version: 17.2.3 - node-html-markdown: - specifier: ^1.3.0 - version: 1.3.0 - zod: - specifier: ^3.25.76 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^22.0.0 - version: 22.19.3 - tsx: - specifier: ^4.19.0 - version: 4.21.0 - typescript: - specifier: ^5.7.0 - version: 5.9.3 - packages/repl: dependencies: '@hashrock/ono': @@ -98,50 +58,6 @@ importers: packages: - '@ai-sdk/anthropic@1.2.12': - resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - - '@ai-sdk/google@1.2.22': - resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - - '@ai-sdk/openai@1.3.24': - resolution: {integrity: sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - - '@ai-sdk/provider-utils@2.2.8': - resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - - '@ai-sdk/provider@1.1.3': - resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} - engines: {node: '>=18'} - - '@ai-sdk/react@1.2.12': - resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/ui-utils@1.2.11': - resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -361,10 +277,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -492,9 +404,6 @@ packages: cpu: [x64] os: [win32] - '@types/diff-match-patch@1.0.36': - resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -592,27 +501,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ai@4.3.19: - resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - react: - optional: true - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -620,10 +512,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -631,17 +519,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -654,40 +535,12 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} - engines: {node: '>=12'} - duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -718,10 +571,6 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -734,14 +583,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - - jsondiffpatch@0.6.0: - resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -766,16 +607,6 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-html-markdown@1.3.0: - resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==} - engines: {node: '>=10.0.0'} - - node-html-parser@6.1.13: - resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -805,10 +636,6 @@ packages: quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} - engines: {node: '>=0.10.0'} - readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -821,9 +648,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -832,15 +656,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - swr@2.3.8: - resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - throttleit@2.1.0: - resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} - engines: {node: '>=18'} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -891,11 +706,6 @@ packages: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@7.3.0: resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -951,62 +761,8 @@ packages: utf-8-validate: optional: true - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - snapshots: - '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/google@1.2.22(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai@1.3.24(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - nanoid: 3.3.11 - secure-json-parse: 2.7.0 - zod: 3.25.76 - - '@ai-sdk/provider@1.1.3': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@1.2.12(react@19.2.3)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) - react: 19.2.3 - swr: 2.3.8(react@19.2.3) - throttleit: 2.1.0 - optionalDependencies: - zod: 3.25.76 - - '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 @@ -1166,8 +922,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@opentelemetry/api@1.9.0': {} - '@polka/url@1.0.0-next.29': {} '@quansync/fs@1.0.0': @@ -1240,13 +994,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.54.0': optional: true - '@types/diff-match-patch@1.0.36': {} - '@types/estree@1.0.8': {} '@types/node@22.19.3': dependencies: undici-types: 6.21.0 + optional: true '@unocss/astro@66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0))': dependencies: @@ -1397,87 +1150,33 @@ snapshots: acorn@8.15.0: {} - ai@4.3.19(react@19.2.3)(zod@3.25.76): - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) - '@ai-sdk/react': 1.2.12(react@19.2.3)(zod@3.25.76) - '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) - '@opentelemetry/api': 1.9.0 - jsondiffpatch: 0.6.0 - zod: 3.25.76 - optionalDependencies: - react: 19.2.3 - - boolbase@1.0.0: {} - cac@6.7.14: {} - chalk@5.6.2: {} - chokidar@5.0.0: dependencies: readdirp: 5.0.0 colorette@2.0.20: {} - commander@12.1.0: {} - confbox@0.1.8: {} consola@3.4.2: {} - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - css-tree@3.1.0: dependencies: mdn-data: 2.12.2 source-map-js: 1.2.1 - css-what@6.2.2: {} - debug@4.4.3: dependencies: ms: 2.1.3 defu@6.1.4: {} - dequal@2.0.3: {} - destr@2.0.5: {} - diff-match-patch@1.0.5: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dotenv@17.2.3: {} - duplexer@0.1.2: {} - entities@4.5.0: {} - esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -1517,6 +1216,7 @@ snapshots: get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 + optional: true globals@11.12.0: {} @@ -1524,22 +1224,12 @@ snapshots: dependencies: duplexer: 0.1.2 - he@1.2.0: {} - jiti@2.6.1: {} js-tokens@4.0.0: {} jsesc@3.1.0: {} - json-schema@0.4.0: {} - - jsondiffpatch@0.6.0: - dependencies: - '@types/diff-match-patch': 1.0.36 - chalk: 5.6.2 - diff-match-patch: 1.0.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1561,19 +1251,6 @@ snapshots: node-fetch-native@1.6.7: {} - node-html-markdown@1.3.0: - dependencies: - node-html-parser: 6.1.13 - - node-html-parser@6.1.13: - dependencies: - css-select: 5.2.2 - he: 1.2.0 - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -1604,11 +1281,10 @@ snapshots: quansync@1.0.0: {} - react@19.2.3: {} - readdirp@5.0.0: {} - resolve-pkg-maps@1.0.0: {} + resolve-pkg-maps@1.0.0: + optional: true rollup@4.54.0: dependencies: @@ -1638,8 +1314,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 - secure-json-parse@2.7.0: {} - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -1648,14 +1322,6 @@ snapshots: source-map-js@1.2.1: {} - swr@2.3.8(react@19.2.3): - dependencies: - dequal: 2.0.3 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) - - throttleit@2.1.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -1671,6 +1337,7 @@ snapshots: get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 + optional: true typescript@5.9.3: {} @@ -1689,7 +1356,8 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.4.2 - undici-types@6.21.0: {} + undici-types@6.21.0: + optional: true unocss@66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)): dependencies: @@ -1723,10 +1391,6 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 - use-sync-external-store@1.6.0(react@19.2.3): - dependencies: - react: 19.2.3 - vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0): dependencies: esbuild: 0.27.2 @@ -1744,9 +1408,3 @@ snapshots: vue-flow-layout@0.2.0: {} ws@8.18.3: {} - - zod-to-json-schema@3.25.1(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} diff --git a/uno.config.js b/uno.config.js deleted file mode 100644 index 2bbf738..0000000 --- a/uno.config.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * UnoCSS Configuration - * @see https://unocss.dev/guide/config-file - */ -import { presetUno } from "unocss"; - -export default { - presets: [ - presetUno(), - ], - - // Custom theme colors, spacing, etc. - theme: { - colors: { - primary: "#0070f3", - secondary: "#666", - }, - }, - - // Custom shortcuts for common patterns - shortcuts: { - "btn": "px-4 py-2 rounded bg-primary text-white hover:opacity-80 cursor-pointer", - "container": "max-w-800px mx-auto px-4", - }, - - // Additional rules (optional) - rules: [], - - // Safelist classes that should always be generated - safelist: [], -}; From 91d655a28a34c92d01d148975dbd00f217cf2be4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 11:53:13 +0000 Subject: [PATCH 8/9] chore: drop unused ws/unocss deps from repl, mark plan as implemented Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- SIMPLIFICATION_PLAN.md | 8 + packages/repl/package.json | 4 +- pnpm-lock.yaml | 629 +------------------------------------ 3 files changed, 11 insertions(+), 630 deletions(-) diff --git a/SIMPLIFICATION_PLAN.md b/SIMPLIFICATION_PLAN.md index c746e9d..52994e2 100644 --- a/SIMPLIFICATION_PLAN.md +++ b/SIMPLIFICATION_PLAN.md @@ -1,5 +1,13 @@ # Ono シンプル化計画 +> **ステータス: 全フェーズ実装済み**(このブランチの後続コミットを参照)。 +> 実装中に追加で見つかった不具合も修正済み: +> ライブリロードはクライアントスクリプトが未注入で全く機能していなかった(SSE化と同時に修正)、 +> 生成バレルは `export ... from` がローカル束縛を作らないため import すると +> ReferenceError になっていた(import+export 形式に修正)。 +> 実測値: コア依存 5→4(typescript + @unocss/core/preset-uno/reset)、 +> リポジトリ全体で -3,179行/+1,516行、既知バグ 5件解消。 + 全体設計をレビューした結果に基づく、設計・実装・機能のシンプル化計画。 「最小限の依存関係・高い移植性・シンプルなアーキテクチャ」というコア哲学に照らして、 **現状がその哲学からずれている箇所**を特定し、フェーズごとに解消する。 diff --git a/packages/repl/package.json b/packages/repl/package.json index 269753e..b6f46dd 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -15,8 +15,6 @@ "@hashrock/ono": "workspace:*", "@unocss/core": "^66.5.4", "@unocss/preset-uno": "^66.5.4", - "typescript": "^5.9.3", - "unocss": "^66.5.4", - "ws": "^8.18.3" + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdbb787..a5637c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,12 +45,6 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 - unocss: - specifier: ^66.5.4 - version: 66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)) - ws: - specifier: ^8.18.3 - version: 8.18.3 devDependencies: vite: specifier: ^7.1.12 @@ -58,47 +52,6 @@ importers: packages: - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.7': - resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.7': - resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} - '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -255,34 +208,9 @@ packages: cpu: [x64] os: [win32] - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - - '@quansync/fs@1.0.0': - resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@rollup/rollup-android-arm-eabi@4.54.0': resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} cpu: [arm] @@ -410,68 +338,21 @@ packages: '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} - '@unocss/astro@66.5.12': - resolution: {integrity: sha512-ynhlljsTGTHAcQHbpqxe3IXEDXjPm9IdeDWAhPet7UiGXhW230vEZ+1/OoARqLysVSVz4pPb81MDgS167Oo4Nw==} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 - peerDependenciesMeta: - vite: - optional: true - - '@unocss/cli@66.5.12': - resolution: {integrity: sha512-aqjhYSiYneGfzXH6iYCY4StVN1QyeRKLhuBPkJO7gzad+1RNeqH2se1l4c5Fnvf+1rU9xRM8cw1CUSIn9UOxYQ==} - engines: {node: '>=14'} - hasBin: true - - '@unocss/config@66.5.12': - resolution: {integrity: sha512-rgV7Jj1nBZsLgk/FIFMDzKVLzIZlbKT5T0SB+odo9xZUsN5xwZZMl7I8TfZj5VxQaYqFEgSpS/Y4QCWlZ+7scQ==} - engines: {node: '>=14'} - '@unocss/core@66.5.12': resolution: {integrity: sha512-/m6su0OXcCYRwIMf8sobBjZTC25iBLUnQVcfyvHOJwLJzOMr8dtNmZbqTs7+Kouz40jlPF7pR+ufFrN+s5ZD7g==} '@unocss/extractor-arbitrary-variants@66.5.12': resolution: {integrity: sha512-UGzHhLaaSu/YT0rmXtdoE1ttLvwWsI/RVTwNNy3QnL/y4Hvmo7T1MtG5Ri5btfqfDWPzrQLQiTvI8loGCD8lFQ==} - '@unocss/inspector@66.5.12': - resolution: {integrity: sha512-X8Ygo842Yy0g46JNlgUGvqDhvr5BuVfFwMJeWSFJBYHzPKsZFxTU29aGxNDNDascTnNdWjZZqerPpG5esa+K2Q==} - - '@unocss/postcss@66.5.12': - resolution: {integrity: sha512-fTGrn19I45jzoP9Jsxty9/PXix3PFftj3tgrIsYjZ0R4tpCffW0s7X/iEl3GwfR45kpe5NlQ5ghskd3CFHUp+Q==} - engines: {node: '>=14'} - peerDependencies: - postcss: ^8.4.21 - - '@unocss/preset-attributify@66.5.12': - resolution: {integrity: sha512-9h/Zgiztzjp1Zf/c/DHAgm1bvzh5oLxAHhPMHmEFjNO335vEjd+PUZBzXXymKM+VoBlMz5DADpAVlTvq1N1aJA==} - - '@unocss/preset-icons@66.5.12': - resolution: {integrity: sha512-3bgkN8tTrcOSGuBcJSDrtDfBt7WU3chFjfw7zo4ign+Z0L6qANB2O62AOdOMJOxKjlppJ6a8AceHthhPZP2PDA==} - '@unocss/preset-mini@66.5.12': resolution: {integrity: sha512-JEyhb0vKIguaZnrGw0CXcgU6/9cWubVL8BTiLl26hsC+6vFHVSnaDHIWOJ8sTShzEQjPSxKDlAj/lGQCC2+88Q==} - '@unocss/preset-tagify@66.5.12': - resolution: {integrity: sha512-gzQ+986lNxpqMeGxeYlDRpfrzcRt2DFjVpfmuNYD6daK4AFRbetQbhynnZyf8zwf++2YUDGf6xI9TfTTSG2QQA==} - - '@unocss/preset-typography@66.5.12': - resolution: {integrity: sha512-ckOD1coTCLXhO3oDCINqm0W292dgtYWtUYeQneNARJz3jjdNqANFPOP/y9Kpfe7WGNegVySRlDizi/L6VSdqJQ==} - '@unocss/preset-uno@66.5.12': resolution: {integrity: sha512-jTLhDeRqhTrCSbEgCQIg0K0PLFDtukG4eeOH5ff7Q4CtmkmsCUK0pqeXegi6ZCyatDwm72qc2WABMSqDMBdhtw==} - '@unocss/preset-web-fonts@66.5.12': - resolution: {integrity: sha512-NSUf+H5X0jZ1PLWW6D5ldBERERpbH8VvkpJJhxNTCS54Lj5vJiZ1S06UYxBB57vuUOaHpQOGTbKUSc204LCqdw==} - '@unocss/preset-wind3@66.5.12': resolution: {integrity: sha512-SUzX12aQcM1ikzfv4rqwd/xuXtK5GKvhV0/JjvtG/kDTMGaKv161F2ytduj+2pBHtpJO5fUmreCD5ycTUIkxhQ==} - '@unocss/preset-wind4@66.5.12': - resolution: {integrity: sha512-JVddnLJ6NOk7hOXA0Y8SYbQEu+JpURbE9o/IHVCkRClVRkE81b9KgJf7WQa/8KIr1O20wRRFdt9QRH4m3pZJ/A==} - - '@unocss/preset-wind@66.5.12': - resolution: {integrity: sha512-wp1/8JqQriv1AqpxskKbZYD9TNqZLQ9VBr7nNN6OkiPXBE1egEwnyb/fY+sS7IpEgwi4N9uehwQgk0/xs84SWg==} - '@unocss/reset@66.5.12': resolution: {integrity: sha512-wGTMu1sXVdxnzAonzHk/yUsyDyGrr8OiXCDSC7pVNep6eXhhf0g85v/Gx9FoAjZRyCppm6ePDWXtWYS8zglfCQ==} @@ -479,68 +360,6 @@ packages: resolution: {integrity: sha512-2UQvdjS6nD3QHLEwcXlDhXFNiOUQDuOC+itX4tjqvnjP/hj5A99WEUHemb8WEHAlHAt7khe9591+BkHHo3BX/w==} engines: {node: '>=14'} - '@unocss/transformer-attributify-jsx@66.5.12': - resolution: {integrity: sha512-h88voRNzSDDBf8In9A/wT0x7IlpRSnOnS62hBIcWk3Ci6w2+I/5eMFP+Rl1kY3zAz4hJ1/Ei6d9Rup3eS5037w==} - - '@unocss/transformer-compile-class@66.5.12': - resolution: {integrity: sha512-EV9LCrIfwUrevHOAhcQD/4HO5NdDzd1ALXNSDbaRxPjDVquWIRs/DujUmihyV2wqu2qEnkOumC+kyDPfZ7/u3w==} - - '@unocss/transformer-directives@66.5.12': - resolution: {integrity: sha512-oRTqR2a5du6b1md549JUX8doXcXY0XNTkiar7R0HZInF4ic0BbjG+nflifd1UtTbI1TUOtcZLQHm+/4tQqM4MA==} - - '@unocss/transformer-variant-group@66.5.12': - resolution: {integrity: sha512-iNHzliFCIVjbbmM9PVexqFhPa1t6C/6Ma3ZtkQRMq9KD2YsLvxdabvESEbjHA3iooR+bjPkiROC9whyRLWnyqQ==} - - '@unocss/vite@66.5.12': - resolution: {integrity: sha512-BSbUmUCLF3303Cu0y+gbhibXkXPpcR6lVFNN2g06EXTDNJEoS/1VKvZEUBU8RP8d1mLkv5mqN4FzdltZ+vA3uw==} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -563,62 +382,18 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - gzip-size@6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} - - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - perfect-debounce@2.0.0: - resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -626,20 +401,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - quansync@1.0.0: - resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} - - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -648,26 +413,14 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -678,34 +431,9 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - - unconfig-core@7.4.2: - resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} - - unconfig@7.4.2: - resolution: {integrity: sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unocss@66.5.12: - resolution: {integrity: sha512-3WdSuM+SOjVpXDtffTuSvYTMuufpFzBehu2b4Tr7DcoIUxGouZn3mdxCLx3PiEuK0ih40Fo7Sjm+J4mccHfwLg==} - engines: {node: '>=14'} - peerDependencies: - '@unocss/webpack': 66.5.12 - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 - peerDependenciesMeta: - '@unocss/webpack': - optional: true - vite: - optional: true - - unplugin-utils@0.3.1: - resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} - engines: {node: '>=20.19.0'} - vite@7.3.0: resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -746,77 +474,8 @@ packages: yaml: optional: true - vue-flow-layout@0.2.0: - resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - snapshots: - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.0.2 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/generator@7.28.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/parser@7.27.7': - dependencies: - '@babel/types': 7.28.5 - - '@babel/parser@7.28.5': - dependencies: - '@babel/types': 7.28.5 - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.7 - '@babel/types': 7.28.5 - - '@babel/traverse@7.27.7': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/parser': 7.27.7 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - debug: 4.4.3 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@esbuild/aix-ppc64@0.27.2': optional: true @@ -895,39 +554,8 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.1.0': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - mlly: 1.8.0 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@polka/url@1.0.0-next.29': {} - - '@quansync/fs@1.0.0': - dependencies: - quansync: 1.0.0 - '@rollup/rollup-android-arm-eabi@4.54.0': optional: true @@ -1001,111 +629,29 @@ snapshots: undici-types: 6.21.0 optional: true - '@unocss/astro@66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/reset': 66.5.12 - '@unocss/vite': 66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)) - optionalDependencies: - vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0) - - '@unocss/cli@66.5.12': - dependencies: - '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.5.12 - '@unocss/core': 66.5.12 - '@unocss/preset-uno': 66.5.12 - cac: 6.7.14 - chokidar: 5.0.0 - colorette: 2.0.20 - consola: 3.4.2 - magic-string: 0.30.21 - pathe: 2.0.3 - perfect-debounce: 2.0.0 - tinyglobby: 0.2.15 - unplugin-utils: 0.3.1 - - '@unocss/config@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - unconfig: 7.4.2 - '@unocss/core@66.5.12': {} '@unocss/extractor-arbitrary-variants@66.5.12': dependencies: '@unocss/core': 66.5.12 - '@unocss/inspector@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/rule-utils': 66.5.12 - colorette: 2.0.20 - gzip-size: 6.0.0 - sirv: 3.0.2 - vue-flow-layout: 0.2.0 - - '@unocss/postcss@66.5.12(postcss@8.5.6)': - dependencies: - '@unocss/config': 66.5.12 - '@unocss/core': 66.5.12 - '@unocss/rule-utils': 66.5.12 - css-tree: 3.1.0 - postcss: 8.5.6 - tinyglobby: 0.2.15 - - '@unocss/preset-attributify@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - - '@unocss/preset-icons@66.5.12': - dependencies: - '@iconify/utils': 3.1.0 - '@unocss/core': 66.5.12 - ofetch: 1.5.1 - '@unocss/preset-mini@66.5.12': dependencies: '@unocss/core': 66.5.12 '@unocss/extractor-arbitrary-variants': 66.5.12 '@unocss/rule-utils': 66.5.12 - '@unocss/preset-tagify@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - - '@unocss/preset-typography@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/rule-utils': 66.5.12 - '@unocss/preset-uno@66.5.12': dependencies: '@unocss/core': 66.5.12 '@unocss/preset-wind3': 66.5.12 - '@unocss/preset-web-fonts@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - ofetch: 1.5.1 - '@unocss/preset-wind3@66.5.12': dependencies: '@unocss/core': 66.5.12 '@unocss/preset-mini': 66.5.12 '@unocss/rule-utils': 66.5.12 - '@unocss/preset-wind4@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/extractor-arbitrary-variants': 66.5.12 - '@unocss/rule-utils': 66.5.12 - - '@unocss/preset-wind@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/preset-wind3': 66.5.12 - '@unocss/reset@66.5.12': {} '@unocss/rule-utils@66.5.12': @@ -1113,70 +659,6 @@ snapshots: '@unocss/core': 66.5.12 magic-string: 0.30.21 - '@unocss/transformer-attributify-jsx@66.5.12': - dependencies: - '@babel/parser': 7.27.7 - '@babel/traverse': 7.27.7 - '@unocss/core': 66.5.12 - transitivePeerDependencies: - - supports-color - - '@unocss/transformer-compile-class@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - - '@unocss/transformer-directives@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - '@unocss/rule-utils': 66.5.12 - css-tree: 3.1.0 - - '@unocss/transformer-variant-group@66.5.12': - dependencies: - '@unocss/core': 66.5.12 - - '@unocss/vite@66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.5.12 - '@unocss/core': 66.5.12 - '@unocss/inspector': 66.5.12 - chokidar: 5.0.0 - magic-string: 0.30.21 - pathe: 2.0.3 - tinyglobby: 0.2.15 - unplugin-utils: 0.3.1 - vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0) - - acorn@8.15.0: {} - - cac@6.7.14: {} - - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - - colorette@2.0.20: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - css-tree@3.1.0: - dependencies: - mdn-data: 2.12.2 - source-map-js: 1.2.1 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - defu@6.1.4: {} - - destr@2.0.5: {} - - duplexer@0.1.2: {} - esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -1218,71 +700,25 @@ snapshots: resolve-pkg-maps: 1.0.0 optional: true - globals@11.12.0: {} - - gzip-size@6.0.0: - dependencies: - duplexer: 0.1.2 - - jiti@2.6.1: {} - - js-tokens@4.0.0: {} - - jsesc@3.1.0: {} + jiti@2.6.1: + optional: true magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - mdn-data@2.12.2: {} - - mlly@1.8.0: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - - mrmime@2.0.1: {} - - ms@2.1.3: {} - nanoid@3.3.11: {} - node-fetch-native@1.6.7: {} - - ofetch@1.5.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.1 - - package-manager-detector@1.6.0: {} - - pathe@2.0.3: {} - - perfect-debounce@2.0.0: {} - picocolors@1.1.1: {} picomatch@4.0.3: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - quansync@1.0.0: {} - - readdirp@5.0.0: {} - resolve-pkg-maps@1.0.0: optional: true @@ -1314,23 +750,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - source-map-js@1.2.1: {} - tinyexec@1.0.2: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - totalist@3.0.1: {} - tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -1341,56 +767,9 @@ snapshots: typescript@5.9.3: {} - ufo@1.6.1: {} - - unconfig-core@7.4.2: - dependencies: - '@quansync/fs': 1.0.0 - quansync: 1.0.0 - - unconfig@7.4.2: - dependencies: - '@quansync/fs': 1.0.0 - defu: 6.1.4 - jiti: 2.6.1 - quansync: 1.0.0 - unconfig-core: 7.4.2 - undici-types@6.21.0: optional: true - unocss@66.5.12(postcss@8.5.6)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)): - dependencies: - '@unocss/astro': 66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)) - '@unocss/cli': 66.5.12 - '@unocss/core': 66.5.12 - '@unocss/postcss': 66.5.12(postcss@8.5.6) - '@unocss/preset-attributify': 66.5.12 - '@unocss/preset-icons': 66.5.12 - '@unocss/preset-mini': 66.5.12 - '@unocss/preset-tagify': 66.5.12 - '@unocss/preset-typography': 66.5.12 - '@unocss/preset-uno': 66.5.12 - '@unocss/preset-web-fonts': 66.5.12 - '@unocss/preset-wind': 66.5.12 - '@unocss/preset-wind3': 66.5.12 - '@unocss/preset-wind4': 66.5.12 - '@unocss/transformer-attributify-jsx': 66.5.12 - '@unocss/transformer-compile-class': 66.5.12 - '@unocss/transformer-directives': 66.5.12 - '@unocss/transformer-variant-group': 66.5.12 - '@unocss/vite': 66.5.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0)) - optionalDependencies: - vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0) - transitivePeerDependencies: - - postcss - - supports-color - - unplugin-utils@0.3.1: - dependencies: - pathe: 2.0.3 - picomatch: 4.0.3 - vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(tsx@4.21.0): dependencies: esbuild: 0.27.2 @@ -1404,7 +783,3 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 - - vue-flow-layout@0.2.0: {} - - ws@8.18.3: {} From dd5f8e11ed3a4931ebce67df1e61a20aa2e71fe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 12:26:42 +0000 Subject: [PATCH 9/9] feat: parser-combinator mini bundler shared by Node build and the REPL Replaces the Node-native module resolution (and the REPL's regex-based module rewriting) with one browser-compatible mini bundler, per owner direction on PR #21. - src/parser.js: small parser-combinator core plus an ES module-syntax parser. Only import/export declarations (and top-level function names) are parsed; the surrounding code is skipped by a scanner that understands strings, template literals, comments, and regex literals, so 'import' inside a string or comment is never misparsed. No regular expressions over source code. - src/bundler.js: pure, host-agnostic bundle({entry, load, resolve}). Modules are wrapped in factories and linked with a tiny lazy require, which removes the need for topological sorting and gives CommonJS-like cycle tolerance (function exports are hoisted to survive cycles, like ESM). Package imports are either hoisted to the bundle top (Node) or rejected with a clear error (browser). - builder.js bundles to a single temp file under .ono/ and imports it; the temp-directory tree mirror and .jsx->.js specifier rewriting are gone. resolver.js is superseded and removed. - browser/compiler.js now delegates to the shared bundler, deleting its ~150 lines of regex import/export rewriting and manual topo sort. The render-candidate heuristic no longer needs to regex the entry source. - New test suites for the parser (string/comment/regex-literal traps, every import/export form) and the bundler (cycles, barrels, hoisting), plus browser-compiler tests that now run in Node since the module has no Node dependencies. Verified: 152 unit tests, example + barrels build, dev server SSE reload, and the built REPL compiling in headless Chromium. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01L5ggPYmgrEz4Yd9V3D8fbK --- ONO.md | 1 + SIMPLIFICATION_PLAN.md | 3 + packages/ono/package.json | 2 + packages/ono/src/browser/compiler.js | 249 +++--------- packages/ono/src/builder.js | 106 +++--- packages/ono/src/bundler.js | 230 +++++++++++ packages/ono/src/parser.js | 550 +++++++++++++++++++++++++++ packages/ono/src/resolver.js | 79 ---- packages/ono/test/bundler.test.js | 208 ++++++++++ packages/ono/test/compiler.test.js | 87 +++++ packages/ono/test/parser.test.js | 192 ++++++++++ packages/ono/test/resolver.test.js | 139 ------- 12 files changed, 1376 insertions(+), 470 deletions(-) create mode 100644 packages/ono/src/bundler.js create mode 100644 packages/ono/src/parser.js delete mode 100644 packages/ono/src/resolver.js create mode 100644 packages/ono/test/bundler.test.js create mode 100644 packages/ono/test/compiler.test.js create mode 100644 packages/ono/test/parser.test.js delete mode 100644 packages/ono/test/resolver.test.js diff --git a/ONO.md b/ONO.md index d339cda..2e89381 100644 --- a/ONO.md +++ b/ONO.md @@ -98,6 +98,7 @@ export default function Page() { ```js import { renderToString } from "@hashrock/ono/renderer"; import { transformJSX } from "@hashrock/ono/transformer"; +import { bundle } from "@hashrock/ono/bundler"; // ブラウザ互換ミニバンドラ ``` ## 重要なポイント diff --git a/SIMPLIFICATION_PLAN.md b/SIMPLIFICATION_PLAN.md index 52994e2..1883cfa 100644 --- a/SIMPLIFICATION_PLAN.md +++ b/SIMPLIFICATION_PLAN.md @@ -1,6 +1,9 @@ # Ono シンプル化計画 > **ステータス: 全フェーズ実装済み**(このブランチの後続コミットを参照)。 +> Phase 2 の「Nodeのモジュール解決に委譲」はオーナーの方針変更により置き換え済み: +> 現在はパーサコンビネータ(`src/parser.js`)でモジュール構文を解析する +> **ブラウザ互換ミニバンドラ**(`src/bundler.js`)を Node ビルドと REPL が共用する。 > 実装中に追加で見つかった不具合も修正済み: > ライブリロードはクライアントスクリプトが未注入で全く機能していなかった(SSE化と同時に修正)、 > 生成バレルは `export ... from` がローカル束縛を作らないため import すると diff --git a/packages/ono/package.json b/packages/ono/package.json index a678b72..b8c5a5f 100644 --- a/packages/ono/package.json +++ b/packages/ono/package.json @@ -9,6 +9,8 @@ "./jsx-runtime": "./src/jsx-runtime.js", "./renderer": "./src/renderer.js", "./transformer": "./src/transformer.js", + "./parser": "./src/parser.js", + "./bundler": "./src/bundler.js", "./barrels": "./src/barrels.js", "./browser/compiler": "./src/browser/compiler.js", "./browser/unocss": "./src/browser/unocss.js" diff --git a/packages/ono/src/browser/compiler.js b/packages/ono/src/browser/compiler.js index 6215eaf..061a8d4 100644 --- a/packages/ono/src/browser/compiler.js +++ b/packages/ono/src/browser/compiler.js @@ -1,188 +1,73 @@ /** - * Browser compiler utilities shared with the REPL worker. + * Browser compiler - shared with the REPL worker. + * + * Uses the same mini bundler as the Node build (bundler.js/parser.js). + * Package imports are rejected — the browser has no module resolution — + * and the bundle is evaluated with new Function, with the JSX runtime + * passed in as parameters. */ import { transformJSX } from '../transformer.js'; import { renderToString } from '../renderer.js'; -import { h } from '../jsx-runtime.js'; +import { h, Fragment } from '../jsx-runtime.js'; +import { bundle } from '../bundler.js'; import { getUnoGenerator } from './unocss.js'; -function resolveImport(from, to) { - if (to.startsWith('./') || to.startsWith('../')) { - const fromParts = from.split('/').slice(0, -1); - const targetParts = to.split('/'); - const resolved = [...fromParts]; - - for (const part of targetParts) { - if (!part || part === '.') continue; - if (part === '..') { - resolved.pop(); - } else { - resolved.push(part); - } +/** Join a relative specifier against the importing file's virtual path */ +function resolveImport(fromId, specifier) { + const fromParts = fromId.split('/').slice(0, -1); + const targetParts = specifier.split('/'); + const resolved = [...fromParts]; + + for (const part of targetParts) { + if (!part || part === '.') continue; + if (part === '..') { + resolved.pop(); + } else { + resolved.push(part); } - - return resolved.join('/'); } - return to; + return resolved.join('/'); } -function topoSortModules(files, entryPoint) { - const filenames = Object.keys(files); - const dependencyMap = new Map(); - const importPattern = /import\s+(?:[\s\S]+?)?from\s+['"]([^'"]+)['"]/g; - - for (const filename of filenames) { - const source = files[filename] || ''; - const deps = []; - source.replace(importPattern, (match, specifier) => { - const resolved = resolveImport(filename, specifier); - if (files[resolved]) { - deps.push(resolved); - } - return match; - }); - dependencyMap.set(filename, deps); - } - - const visited = new Set(); - const order = []; - - const visit = (file) => { - if (!files[file] || visited.has(file)) return; - visited.add(file); - const deps = dependencyMap.get(file) || []; - for (const dep of deps) { - visit(dep); - } - order.push(file); - }; - - if (entryPoint) { - visit(entryPoint); - } - - for (const filename of filenames) { - if (!visited.has(filename)) { - visit(filename); - } +async function bundleProject(files, entryPoint) { + if (!(entryPoint in files)) { + throw new Error(`Entry point '${entryPoint}' not found`); } - return order; -} - -function bundleModules(files, entryPoint) { - const order = topoSortModules(files, entryPoint); - let bundledCode = 'const __modules = {};\n'; - - for (const filename of order) { - const transformedCode = transformJSX(files[filename], filename); - let code = transformedCode; - const exportMappings = new Map(); - - const addExport = (exportName, localName = exportName) => { - if (!exportName || exportMappings.has(exportName)) return; - exportMappings.set(exportName, localName); - }; - - code = code.replace(/import\s+{([^}]+)}\s+from\s+['"]([^'"]+)['"]/g, (match, imports, path) => { - const resolvedPath = resolveImport(filename, path); - const importNames = imports.split(',').map(part => part.trim()).filter(Boolean); - - return importNames - .map(name => { - const [local, alias] = name.split(/\s+as\s+/).map(token => token && token.trim()); - const localName = alias || local; - const exportName = local; - return `const ${localName} = __modules['${resolvedPath}']['${exportName}'];`; - }) - .join('\n'); - }); - - code = code.replace(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => { - const resolvedPath = resolveImport(filename, path); - return `const ${localName} = __modules['${resolvedPath}']['default'];`; - }); - - code = code.replace(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => { - const resolvedPath = resolveImport(filename, path); - return `const ${localName} = __modules['${resolvedPath}'];`; - }); - - code = code.replace(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => { - addExport('default', name); - return `function ${name}`; - }); - - code = code.replace(/export\s+default\s+([A-Za-z_$][\w$]*)/g, (match, name) => { - addExport('default', name); - return `${name}`; - }); - - code = code.replace(/export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/g, (match, kind, name) => { - addExport(name); - return `${kind} ${name}`; - }); - - code = code.replace(/export\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => { - addExport(name); - return `function ${name}`; - }); - - code = code.replace(/export\s+class\s+([A-Za-z_$][\w$]*)/g, (match, name) => { - addExport(name); - return `class ${name}`; - }); - - code = code.replace(/export\s*{\s*([^}]+)\s*};?/g, (match, names) => { - names - .split(',') - .map(part => part.trim()) - .filter(Boolean) - .forEach(part => { - const [local, alias] = part.split(/\s+as\s+/).map(token => token && token.trim()); - addExport(alias || local, local); - }); - return ''; - }); - - code = code.replace(/export\s*{\s*};?/g, ''); - - if (filename === entryPoint) { - const functionMatches = code.matchAll(/function\s+([A-Za-z_$][\w$]*)/g); - for (const match of functionMatches) { - const name = match[1]; - addExport(name); + const { code } = await bundle({ + entry: entryPoint, + resolve: (specifier, fromId) => { + const resolved = resolveImport(fromId, specifier); + if (!(resolved in files)) { + throw new Error(`Cannot find module '${specifier}' imported from '${fromId}'`); } - } - - const exportEntries = Array.from(exportMappings.entries()).map(([exportName, localName]) => { - if (exportName === 'default') { - return `'default': ${localName}`; - } - if (exportName === localName) { - return exportName; - } - return `'${exportName}': ${localName}`; - }); - - const exportsObject = exportEntries.length > 0 ? `{ ${exportEntries.join(', ')} }` : '{}'; - - bundledCode += ` -__modules['${filename}'] = (() => { - ${code} - return ${exportsObject}; -})(); -`; - } + return resolved; + }, + load: (id) => transformJSX(files[id], id), + onExternal: 'error', + exposeEntryFunctions: true, + }); + + return code; +} - bundledCode += `const __entry = __modules['${entryPoint}'];\n`; +function evaluateEntryModule(bundledCode) { + const getEntryModule = new Function('h', 'Fragment', ` + ${bundledCode} + return __ono_entry; + `); - return bundledCode; + return getEntryModule(h, Fragment); } -function extractRenderCandidate(entryModule, entryCode) { +/** + * Pick what to render from the entry module: the default export if there + * is one, otherwise the last exported function (REPL snippets usually + * define components and finish with an App function), otherwise any value. + */ +function extractRenderCandidate(entryModule) { if (!entryModule || typeof entryModule !== 'object') { return null; } @@ -195,21 +80,9 @@ function extractRenderCandidate(entryModule, entryCode) { return () => defaultExport; } - const lastCallMatch = entryCode.match(/([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*$/m); - if (lastCallMatch) { - const lastCallName = lastCallMatch[1]; - const candidate = entryModule[lastCallName]; - if (typeof candidate === 'function') { - return candidate; - } - if (candidate !== undefined) { - return () => candidate; - } - } - - const fallbackFunction = Object.values(entryModule).find(value => typeof value === 'function'); - if (fallbackFunction) { - return fallbackFunction; + const functions = Object.values(entryModule).filter((value) => typeof value === 'function'); + if (functions.length > 0) { + return functions[functions.length - 1]; } const firstValue = Object.values(entryModule)[0]; @@ -220,20 +93,10 @@ function extractRenderCandidate(entryModule, entryCode) { return null; } -function evaluateEntryModule(bundledCode) { - const getEntryModule = new Function('h', ` - ${bundledCode} - return __entry; - `); - - return getEntryModule(h); -} - export async function compileProject(files, entryPoint = 'index.jsx', options = {}) { - const bundled = bundleModules(files, entryPoint); + const bundled = await bundleProject(files, entryPoint); const entryModule = evaluateEntryModule(bundled); - const entryCode = files[entryPoint] || ''; - const renderCandidate = extractRenderCandidate(entryModule, entryCode); + const renderCandidate = extractRenderCandidate(entryModule); if (!renderCandidate) { throw new Error('Unable to determine a render function in the entry module.'); diff --git a/packages/ono/src/builder.js b/packages/ono/src/builder.js index d8047c3..98c2d1b 100644 --- a/packages/ono/src/builder.js +++ b/packages/ono/src/builder.js @@ -1,10 +1,10 @@ /** * Build utilities for Ono SSG * - * Pages are compiled file-by-file into a per-build temp directory that - * mirrors the project layout, then the entry is imported with Node's own - * module resolution. No bundling: evaluation order, cycles, and package - * imports are all handled by Node. + * Pages are bundled with the browser-compatible mini bundler + * (bundler.js + parser.js), written to a single temp file, and imported. + * Package (bare) imports are hoisted to the top of the bundle where + * Node's own resolution handles them. */ import { readFile, writeFile, mkdir, rm } from "node:fs/promises"; import { resolve, join, dirname, basename, relative } from "node:path"; @@ -12,7 +12,7 @@ import { pathToFileURL } from "node:url"; import { renderToString } from "./renderer.js"; import { generateCSSFromFiles, loadUnoConfig } from "./unocss.js"; import { transformJSX } from "./transformer.js"; -import { collectModules } from "./resolver.js"; +import { bundle } from "./bundler.js"; import { getFilesRecursively, isJSXFile, isHTMLFile } from "./utils.js"; import { DIRS } from "./constants.js"; @@ -21,76 +21,64 @@ const JSX_RUNTIME_IMPORT = `import { h, Fragment } from ${JSON.stringify( new URL("./jsx-runtime.js", import.meta.url).href, )};\n`; -/** Directory (inside the project) that holds per-build temp output */ +/** Directory (inside the project) that holds per-build temp files */ const TEMP_DIR = ".ono"; let buildCounter = 0; /** - * Rewrite relative .jsx/.tsx/.ts import specifiers to .js so they resolve - * against the compiled files in the temp directory. + * Bundle an entry file into a single ES module source string + * @param {string} entryFile - Absolute path to the entry file + * @returns {Promise} Bundled module source */ -function rewriteRelativeImports(code) { - return code.replace( - /^((?:import|export)[^'"]*['"])(\.[^'"]+)\.(?:jsx|tsx|ts)(['"])/gm, - (_match, head, base, tail) => `${head}${base}.js${tail}`, - ); -} - -/** True when the file imports the runtime itself (skip injection) */ -function importsOwnRuntime(source) { - return /from\s+['"]@hashrock\/ono(?:\/jsx-runtime(?:\.js)?)?['"]/.test(source); -} - -/** - * Compile an entry file and its local imports into a fresh temp directory. - * A unique directory per build doubles as ESM cache-busting for rebuilds. - * @returns {Promise<{tempRoot: string, entryPath: string}>} - */ -async function compileToTemp(entryFile) { - const cwd = process.cwd(); - const files = await collectModules(entryFile); - const tempRoot = join(cwd, TEMP_DIR, `build-${process.pid}-${buildCounter++}`); - - let entryPath; - for (const file of files) { - const rel = relative(cwd, file); - if (rel.startsWith("..")) { - throw new Error( - `Cannot compile ${file}: imports outside the project directory are not supported`, - ); - } - - const source = await readFile(file, "utf-8"); - let code = rewriteRelativeImports(transformJSX(source, file)); - if (!importsOwnRuntime(source)) { - code = JSX_RUNTIME_IMPORT + code; - } - - const outPath = join(tempRoot, rel.replace(/\.(jsx|tsx|ts)$/, ".js")); - await mkdir(dirname(outPath), { recursive: true }); - await writeFile(outPath, code); - - if (file === entryFile) { - entryPath = outPath; - } - } - - return { tempRoot, entryPath }; +export async function compileBundle(entryFile) { + const { code, entryExports, externalBindings } = await bundle({ + entry: entryFile, + resolve: (specifier, fromId) => resolve(dirname(fromId), specifier), + load: async (id) => { + let source; + try { + source = await readFile(id, "utf-8"); + } catch (error) { + throw new Error(`Cannot read file: ${id}\n${error.message}`); + } + return transformJSX(source, id); + }, + }); + + // Provide h/Fragment unless a page pulls in the runtime itself + const header = + externalBindings.has("h") || externalBindings.has("Fragment") ? "" : JSX_RUNTIME_IMPORT; + + // Re-export the entry's exports so the bundle behaves like the entry module + const footer = entryExports + .map((name) => + name === "default" + ? "export default __ono_entry.default;" + : `export const ${name} = __ono_entry[${JSON.stringify(name)}];`, + ) + .join("\n"); + + return `${header}${code}\n${footer}\n`; } /** - * Compile a JSX entry file (with its local imports) and import it + * Bundle a JSX entry file (with its local imports) and import it. + * A unique temp file per build doubles as ESM cache-busting for rebuilds. * @param {string} entryFile - Path to the entry file * @returns {Promise} The imported module namespace */ export async function importJSXModule(entryFile) { const resolvedEntry = resolve(process.cwd(), entryFile); - const { tempRoot, entryPath } = await compileToTemp(resolvedEntry); + const code = await compileBundle(resolvedEntry); + + const tempFile = join(process.cwd(), TEMP_DIR, `build-${process.pid}-${buildCounter++}.js`); + await mkdir(dirname(tempFile), { recursive: true }); + await writeFile(tempFile, code); try { - return await import(pathToFileURL(entryPath).href); + return await import(pathToFileURL(tempFile).href); } finally { - await rm(tempRoot, { recursive: true, force: true }); + await rm(tempFile, { force: true }); } } diff --git a/packages/ono/src/bundler.js b/packages/ono/src/bundler.js new file mode 100644 index 0000000..d25547f --- /dev/null +++ b/packages/ono/src/bundler.js @@ -0,0 +1,230 @@ +/** + * Mini bundler - browser-compatible. + * + * Each module is wrapped in a factory function and linked with a tiny + * lazy require(), so no topological sort is needed and import cycles + * behave like CommonJS. Module syntax is parsed with the parser + * combinators in parser.js — no regular expressions over source code. + * + * The host environment supplies I/O: + * - load(id): return the module's JavaScript source (JSX already transformed) + * - resolve(specifier, fromId): turn a relative specifier into a module id + * + * Known limitations (deliberate, for simplicity): no top-level await, + * no destructuring in exported declarations, `export *` copies a + * snapshot of the source module. + */ +import { parseModule, isIdentifierName } from "./parser.js"; + +const isRelative = (specifier) => + specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/"); + +/** Apply text replacements (non-overlapping) to a source string */ +function applyEdits(source, edits) { + let result = source; + for (const edit of [...edits].sort((a, b) => b.start - a.start)) { + result = result.slice(0, edit.start) + edit.text + result.slice(edit.end); + } + return result; +} + +/** Generate the require/destructuring lines that replace an import statement */ +function importReplacement(imp, requireCall) { + const parts = []; + if (imp.defaultBinding) parts.push(`const ${imp.defaultBinding} = ${requireCall}.default;`); + if (imp.namespace) parts.push(`const ${imp.namespace} = ${requireCall};`); + if (imp.named && imp.named.length > 0) { + const bindings = imp.named + .map(({ imported, local }) => + imported === local ? imported : `${JSON.stringify(imported)}: ${local}`, + ) + .join(", "); + parts.push(`const { ${bindings} } = ${requireCall};`); + } + if (parts.length === 0) parts.push(`${requireCall};`); // side-effect only + return parts.join(" "); +} + +/** Record the top-level names an import statement binds */ +function collectBindings(imp, bindings) { + if (imp.defaultBinding) bindings.add(imp.defaultBinding); + if (imp.namespace) bindings.add(imp.namespace); + for (const { local } of imp.named ?? []) bindings.add(local); +} + +const LINKER_RUNTIME = `const __ono_cache = new Map(); +function __ono_require(id) { + let record = __ono_cache.get(id); + if (!record) { + record = { exports: {} }; + __ono_cache.set(id, record); + __ono_modules[id](record.exports, __ono_require); + } + return record.exports; +} +function __ono_export_star(target, source) { + for (const key of Object.keys(source)) { + if (key !== "default") target[key] = source[key]; + } +}`; + +/** + * Bundle an entry module and its local imports into one script. + * + * @param {Object} options + * @param {string} options.entry - Module id of the entry point + * @param {(id: string) => string | Promise} options.load - Return a module's JS source + * @param {(specifier: string, fromId: string) => string} options.resolve - Resolve a relative specifier + * @param {"hoist"|"error"} [options.onExternal] - Bare (package) imports: hoist to the + * bundle top (needs an ESM host, e.g. Node) or throw (e.g. the browser REPL) + * @param {boolean} [options.exposeEntryFunctions] - Also export the entry's top-level + * function declarations (REPL convenience for code without exports) + * @returns {Promise<{code: string, entryId: string, entryExports: string[], externalBindings: Set}>} + * `code` defines __ono_modules/__ono_require and ends by evaluating the + * entry into `__ono_entry`. The caller decides how to expose it. + */ +export async function bundle(options) { + const { entry, load, resolve, onExternal = "hoist", exposeEntryFunctions = false } = options; + + const moduleCodes = new Map(); + const externalImports = []; + const externalBindings = new Set(); + const entryExports = []; + const addEntryExport = (name) => { + if (!entryExports.includes(name)) entryExports.push(name); + }; + + const queue = [entry]; + while (queue.length > 0) { + const id = queue.shift(); + if (moduleCodes.has(id)) continue; + + const source = await load(id); + let parsed; + try { + parsed = parseModule(source); + } catch (error) { + throw new Error(`${error.message} (in ${id})`); + } + + const isEntry = id === entry; + const edits = []; + // Function declarations are hoisted, so their export assignments go at + // the top of the factory — this keeps them visible across import cycles, + // mirroring ESM hoisting. Everything else is assigned at the end. + const head = []; + const tail = []; + + for (const imp of parsed.imports) { + if (isRelative(imp.specifier)) { + const depId = resolve(imp.specifier, id); + queue.push(depId); + const requireCall = `__ono_require(${JSON.stringify(depId)})`; + edits.push({ start: imp.start, end: imp.end, text: importReplacement(imp, requireCall) }); + } else if (onExternal === "hoist") { + // Package import: move it to the top of the bundle, outside the factories + const statement = source.slice(imp.start, imp.end).trim(); + if (!externalImports.includes(statement)) externalImports.push(statement); + collectBindings(imp, externalBindings); + edits.push({ start: imp.start, end: imp.end, text: "" }); + } else { + throw new Error(`Cannot bundle package import "${imp.specifier}" (in ${id})`); + } + } + + for (const exp of parsed.exports) { + switch (exp.type) { + case "exportStarFrom": { + if (!isRelative(exp.specifier)) { + throw new Error(`"export * from" a package is not supported (in ${id})`); + } + const depId = resolve(exp.specifier, id); + queue.push(depId); + const requireCall = `__ono_require(${JSON.stringify(depId)})`; + const text = exp.alias + ? `__ono_exports[${JSON.stringify(exp.alias)}] = ${requireCall};` + : `__ono_export_star(__ono_exports, ${requireCall});`; + edits.push({ start: exp.start, end: exp.end, text }); + if (isEntry && exp.alias) addEntryExport(exp.alias); + break; + } + case "exportNamedFrom": { + if (!isRelative(exp.specifier)) { + throw new Error(`Re-exporting from a package is not supported (in ${id})`); + } + const depId = resolve(exp.specifier, id); + queue.push(depId); + const requireCall = `__ono_require(${JSON.stringify(depId)})`; + const text = exp.named + .map( + ({ local, exported }) => + `__ono_exports[${JSON.stringify(exported)}] = ${requireCall}[${JSON.stringify(local)}];`, + ) + .join(" "); + edits.push({ start: exp.start, end: exp.end, text }); + if (isEntry) exp.named.forEach(({ exported }) => addEntryExport(exported)); + break; + } + case "exportNamed": { + edits.push({ start: exp.start, end: exp.end, text: "" }); + for (const { local, exported } of exp.named) { + tail.push(`__ono_exports[${JSON.stringify(exported)}] = ${local};`); + if (isEntry) addEntryExport(exported); + } + break; + } + case "exportDefaultDeclaration": { + edits.push({ start: exp.start, end: exp.headerEnd, text: "" }); + const target = exp.declarationKind === "function" ? head : tail; + target.push(`__ono_exports.default = ${exp.name};`); + if (isEntry) addEntryExport("default"); + break; + } + case "exportDefaultExpression": { + edits.push({ start: exp.start, end: exp.headerEnd, text: "__ono_exports.default =" }); + if (isEntry) addEntryExport("default"); + break; + } + case "exportDeclaration": { + edits.push({ start: exp.start, end: exp.headerEnd, text: "" }); + const target = exp.declarationKind === "function" ? head : tail; + for (const name of exp.names) { + target.push(`__ono_exports[${JSON.stringify(name)}] = ${name};`); + if (isEntry) addEntryExport(name); + } + break; + } + } + } + + if (exposeEntryFunctions && isEntry) { + for (const name of parsed.topLevelFunctions) { + if (!entryExports.includes(name)) { + tail.push(`__ono_exports[${JSON.stringify(name)}] = ${name};`); + addEntryExport(name); + } + } + } + + let code = applyEdits(source, edits); + if (head.length > 0) code = `${head.join("\n")}\n${code}`; + if (tail.length > 0) code += `\n${tail.join("\n")}`; + moduleCodes.set(id, code); + } + + const parts = []; + if (externalImports.length > 0) parts.push(externalImports.join("\n")); + parts.push("const __ono_modules = {};"); + for (const [id, code] of moduleCodes) { + parts.push(`__ono_modules[${JSON.stringify(id)}] = function (__ono_exports, __ono_require) {\n${code}\n};`); + } + parts.push(LINKER_RUNTIME); + parts.push(`const __ono_entry = __ono_require(${JSON.stringify(entry)});`); + + return { + code: parts.join("\n\n"), + entryId: entry, + entryExports: entryExports.filter((name) => name === "default" || isIdentifierName(name)), + externalBindings, + }; +} diff --git a/packages/ono/src/parser.js b/packages/ono/src/parser.js new file mode 100644 index 0000000..8e7bfa2 --- /dev/null +++ b/packages/ono/src/parser.js @@ -0,0 +1,550 @@ +/** + * Parser combinators and an ES module-syntax parser. + * + * Parses only what the bundler needs — import/export declarations and + * top-level function names — using parser combinators instead of regular + * expressions. The rest of the source is skipped by a small scanner that + * understands strings, template literals, comments, and (heuristically) + * regex literals, so an "import" inside a string is never misparsed. + * + * Browser-compatible: no Node.js APIs. + */ + +// --- Combinator core ------------------------------------------------------- +// A parser is a function (input, pos) => result. +// Success: { ok: true, value, pos } Failure: { ok: false, expected, pos } + +const ok = (value, pos) => ({ ok: true, value, pos }); +const fail = (expected, pos) => ({ ok: false, expected, pos }); + +/** Match an exact string */ +export function str(expected) { + return (input, pos) => + input.startsWith(expected, pos) + ? ok(expected, pos + expected.length) + : fail(expected, pos); +} + +/** Run parsers in order, collecting their values */ +export function seq(...parsers) { + return (input, pos) => { + const values = []; + let current = pos; + for (const parser of parsers) { + const result = parser(input, current); + if (!result.ok) return result; + values.push(result.value); + current = result.pos; + } + return ok(values, current); + }; +} + +/** Try parsers in order, returning the first success */ +export function alt(...parsers) { + return (input, pos) => { + let furthest = null; + for (const parser of parsers) { + const result = parser(input, pos); + if (result.ok) return result; + if (!furthest || result.pos > furthest.pos) furthest = result; + } + return furthest || fail("no alternative matched", pos); + }; +} + +/** Match zero or more repetitions */ +export function many(parser) { + return (input, pos) => { + const values = []; + let current = pos; + for (;;) { + const result = parser(input, current); + if (!result.ok || result.pos === current) return ok(values, current); + values.push(result.value); + current = result.pos; + } + }; +} + +/** Make a parser optional (yields null when it fails) */ +export function opt(parser) { + return (input, pos) => { + const result = parser(input, pos); + return result.ok ? result : ok(null, pos); + }; +} + +/** Transform a parser's value */ +export function map(parser, fn) { + return (input, pos) => { + const result = parser(input, pos); + return result.ok ? ok(fn(result.value), result.pos) : result; + }; +} + +/** Zero or more items separated by a separator */ +export function sepBy(item, separator) { + return (input, pos) => { + const first = item(input, pos); + if (!first.ok) return ok([], pos); + const rest = many(map(seq(separator, item), ([, value]) => value)); + const result = rest(input, first.pos); + return ok([first.value, ...result.value], result.pos); + }; +} + +// --- Lexical helpers ------------------------------------------------------- + +const isWs = (c) => c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v"; +const isIdStart = (c) => (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_" || c === "$"; +const isIdChar = (c) => isIdStart(c) || (c >= "0" && c <= "9"); + +/** True when the whole string is a valid (ASCII) identifier name */ +export function isIdentifierName(name) { + if (name.length === 0 || !isIdStart(name[0])) return false; + for (let i = 1; i < name.length; i++) { + if (!isIdChar(name[i])) return false; + } + return true; +} + +/** Skip whitespace and comments, returning the new position */ +export function skipTrivia(input, pos) { + for (;;) { + while (pos < input.length && isWs(input[pos])) pos++; + if (input.startsWith("//", pos)) { + while (pos < input.length && input[pos] !== "\n") pos++; + continue; + } + if (input.startsWith("/*", pos)) { + const end = input.indexOf("*/", pos + 2); + pos = end === -1 ? input.length : end + 2; + continue; + } + return pos; + } +} + +/** Wrap a parser so it skips leading trivia */ +function token(parser) { + return (input, pos) => parser(input, skipTrivia(input, pos)); +} + +/** Any identifier-like word (including reserved words) */ +const wordRaw = (input, pos) => { + if (pos >= input.length || !isIdStart(input[pos])) return fail("identifier", pos); + let end = pos + 1; + while (end < input.length && isIdChar(input[end])) end++; + return ok(input.slice(pos, end), end); +}; + +const word = token(wordRaw); + +/** A specific keyword (whole word) */ +const kw = (expected) => + token((input, pos) => { + const result = wordRaw(input, pos); + return result.ok && result.value === expected ? result : fail(`"${expected}"`, pos); + }); + +const punct = (s) => token(str(s)); + +/** A single- or double-quoted string literal, yielding its contents */ +const stringLit = token((input, pos) => { + const quote = input[pos]; + if (quote !== '"' && quote !== "'") return fail("string literal", pos); + let out = ""; + let i = pos + 1; + while (i < input.length) { + const c = input[i]; + if (c === "\\") { + out += input[i + 1] ?? ""; + i += 2; + } else if (c === quote) { + return ok(out, i + 1); + } else if (c === "\n") { + break; + } else { + out += c; + i++; + } + } + return fail("closing quote", pos); +}); + +// --- Import declaration grammar ------------------------------------------- + +// { a, b as c, default as d } +const importSpecifier = map( + seq(word, opt(seq(kw("as"), word))), + ([imported, alias]) => ({ imported, local: alias ? alias[1] : imported }), +); + +const namedList = (specifier) => + map( + seq(punct("{"), sepBy(specifier, punct(",")), opt(punct(",")), punct("}")), + ([, specs]) => specs, + ); + +const namespaceImport = map(seq(punct("*"), kw("as"), word), ([, , name]) => name); + +const importClause = alt( + map(namespaceImport, (namespace) => ({ namespace })), + map(namedList(importSpecifier), (named) => ({ named })), + map( + seq( + word, + opt( + seq( + punct(","), + alt( + map(namespaceImport, (namespace) => ({ namespace })), + map(namedList(importSpecifier), (named) => ({ named })), + ), + ), + ), + ), + ([defaultBinding, rest]) => ({ defaultBinding, ...(rest ? rest[1] : {}) }), + ), +); + +const importDecl = map( + seq( + kw("import"), + alt( + map(stringLit, (specifier) => ({ specifier, sideEffect: true })), + map(seq(importClause, kw("from"), stringLit), ([clause, , specifier]) => ({ + ...clause, + specifier, + })), + ), + opt(punct(";")), + ), + ([, decl]) => ({ type: "import", ...decl }), +); + +// --- Export declaration grammar -------------------------------------------- + +// { a, b as c } +const exportSpecifier = map( + seq(word, opt(seq(kw("as"), word))), + ([local, alias]) => ({ local, exported: alias ? alias[1] : local }), +); + +const exportStarFrom = map( + seq(punct("*"), opt(seq(kw("as"), word)), kw("from"), stringLit, opt(punct(";"))), + ([, alias, , specifier]) => ({ + type: "exportStarFrom", + specifier, + alias: alias ? alias[1] : null, + }), +); + +const exportNamedFrom = map( + seq(namedList(exportSpecifier), kw("from"), stringLit, opt(punct(";"))), + ([named, , specifier]) => ({ type: "exportNamedFrom", named, specifier }), +); + +const exportNamed = map( + seq(namedList(exportSpecifier), opt(punct(";"))), + ([named]) => ({ type: "exportNamed", named }), +); + +// function Foo / async function Foo / class Foo (name optional) +const functionOrClassHead = alt( + map(seq(opt(kw("async")), kw("function"), opt(punct("*")), opt(word)), ([, , , name]) => ({ + kind: "function", + name, + })), + map(seq(kw("class"), opt(word)), ([, name]) => ({ kind: "class", name })), +); + +/** + * Scan `const a = ..., b = ...;` collecting declared names. + * Initializers are skipped with depth/string tracking, not parsed. + */ +function scanDeclaratorNames(input, pos) { + const names = []; + for (;;) { + pos = skipTrivia(input, pos); + const c = input[pos]; + if (c === "{" || c === "[") { + throw new Error("Destructuring in an exported declaration is not supported by the Ono bundler"); + } + const nameResult = wordRaw(input, pos); + if (!nameResult.ok) break; + names.push(nameResult.value); + pos = nameResult.pos; + + // Skip to the next top-level "," (next declarator) or ";" (end) + let depth = 0; + let done = false; + while (pos < input.length) { + pos = skipTrivia(input, pos); + const ch = input[pos]; + if (ch === undefined) break; + if (ch === '"' || ch === "'") { + pos = skipStringLiteral(input, pos); + } else if (ch === "`") { + pos = skipTemplateLiteral(input, pos); + } else if (ch === "(" || ch === "[" || ch === "{") { + depth++; + pos++; + } else if (ch === ")" || ch === "]" || ch === "}") { + depth--; + pos++; + } else if (depth === 0 && ch === ",") { + pos++; + break; + } else if (depth === 0 && ch === ";") { + done = true; + break; + } else { + pos++; + } + } + if (done || pos >= input.length) break; + } + return names; +} + +/** + * Parse an export declaration starting at the `export` keyword. + * For `export ` forms only the header span is consumed + * (the declaration itself stays in place); `consumeTo` says where the + * scanner should continue. + */ +function exportDecl(input, pos) { + const head = kw("export")(input, pos); + if (!head.ok) return head; + const afterExport = head.pos; + + const fromForm = alt(exportStarFrom, exportNamedFrom, exportNamed)(input, afterExport); + if (fromForm.ok) { + return ok({ ...fromForm.value, consumeTo: fromForm.pos }, fromForm.pos); + } + + const defaultKw = kw("default")(input, afterExport); + if (defaultKw.ok) { + const headerEnd = defaultKw.pos; + const decl = functionOrClassHead(input, headerEnd); + if (decl.ok && decl.value.name) { + // export default function Foo() {} — keep the declaration, strip keywords + return ok( + { + type: "exportDefaultDeclaration", + name: decl.value.name, + declarationKind: decl.value.kind, + headerEnd, + consumeTo: headerEnd, + }, + headerEnd, + ); + } + // export default (or anonymous function/class) + return ok({ type: "exportDefaultExpression", headerEnd, consumeTo: headerEnd }, headerEnd); + } + + const fnHead = functionOrClassHead(input, afterExport); + if (fnHead.ok && fnHead.value.name) { + return ok( + { + type: "exportDeclaration", + names: [fnHead.value.name], + declarationKind: fnHead.value.kind, + headerEnd: afterExport, + consumeTo: afterExport, + }, + afterExport, + ); + } + + const kind = alt(kw("const"), kw("let"), kw("var"))(input, afterExport); + if (kind.ok) { + const names = scanDeclaratorNames(input, kind.pos); + if (names.length > 0) { + return ok( + { + type: "exportDeclaration", + names, + declarationKind: "variable", + headerEnd: afterExport, + consumeTo: afterExport, + }, + afterExport, + ); + } + } + + return fail("export declaration", afterExport); +} + +// --- Source scanner --------------------------------------------------------- + +function skipStringLiteral(input, pos) { + const quote = input[pos]; + let i = pos + 1; + while (i < input.length) { + const c = input[i]; + if (c === "\\") i += 2; + else if (c === quote) return i + 1; + else if (c === "\n") return i; // unterminated — bail out + else i++; + } + return i; +} + +function skipTemplateLiteral(input, pos) { + let i = pos + 1; + while (i < input.length) { + const c = input[i]; + if (c === "\\") { + i += 2; + } else if (c === "`") { + return i + 1; + } else if (c === "$" && input[i + 1] === "{") { + // Skip the embedded expression (may contain nested strings/templates) + i += 2; + let depth = 1; + while (i < input.length && depth > 0) { + i = skipTrivia(input, i); + const e = input[i]; + if (e === undefined) break; + if (e === '"' || e === "'") i = skipStringLiteral(input, i); + else if (e === "`") i = skipTemplateLiteral(input, i); + else { + if (e === "{") depth++; + else if (e === "}") depth--; + i++; + } + } + } else { + i++; + } + } + return i; +} + +function skipRegexLiteral(input, pos) { + let i = pos + 1; + let inClass = false; + while (i < input.length) { + const c = input[i]; + if (c === "\\") i += 2; + else if (c === "[") { inClass = true; i++; } + else if (c === "]") { inClass = false; i++; } + else if (c === "/" && !inClass) { + i++; + while (i < input.length && isIdChar(input[i])) i++; // flags + return i; + } else if (c === "\n") { + return i; // not a regex after all — bail out + } else { + i++; + } + } + return i; +} + +const REGEX_ALLOWED_AFTER_WORD = new Set([ + "return", "typeof", "case", "in", "of", "new", "delete", "void", + "instanceof", "do", "else", "yield", "await", +]); +const REGEX_ALLOWED_AFTER_CHAR = new Set([...("=([{,;:!&|?+-*%^~<>")]); + +/** True when the previous token puts us in expression position */ +function isExpressionContext(lastToken) { + return ( + REGEX_ALLOWED_AFTER_CHAR.has(lastToken) || + REGEX_ALLOWED_AFTER_WORD.has(lastToken) + ); +} + +/** + * Parse the module syntax of a JavaScript source file. + * @param {string} source - JavaScript source (post JSX transform) + * @returns {{imports: object[], exports: object[], topLevelFunctions: string[]}} + * Each statement carries a `start`/`end` (or `headerEnd`) span into `source`. + */ +export function parseModule(source) { + const imports = []; + const exportStatements = []; + const topLevelFunctions = []; + + let pos = 0; + let braceDepth = 0; + let lastToken = ""; // previous significant token (word or single char) + + while (pos < source.length) { + pos = skipTrivia(source, pos); + if (pos >= source.length) break; + const c = source[pos]; + + if (c === '"' || c === "'") { + pos = skipStringLiteral(source, pos); + lastToken = c; + continue; + } + if (c === "`") { + pos = skipTemplateLiteral(source, pos); + lastToken = c; + continue; + } + if (c === "/") { + // skipTrivia already handled comments, so this is division or a regex + if (lastToken === "" || REGEX_ALLOWED_AFTER_CHAR.has(lastToken) || REGEX_ALLOWED_AFTER_WORD.has(lastToken)) { + pos = skipRegexLiteral(source, pos); + } else { + pos++; + } + lastToken = "/"; + continue; + } + + if (isIdStart(c)) { + const start = pos; + let end = pos + 1; + while (end < source.length && isIdChar(source[end])) end++; + const wordText = source.slice(start, end); + + if (wordText === "import") { + const result = importDecl(source, start); + if (result.ok) { + imports.push({ ...result.value, start, end: result.pos }); + pos = result.pos; + lastToken = ";"; + continue; + } + // dynamic import() or import.meta — fall through + } else if (wordText === "export" && braceDepth === 0) { + const result = exportDecl(source, start); + if (result.ok) { + const { consumeTo, ...statement } = result.value; + exportStatements.push({ ...statement, start, end: result.pos }); + pos = consumeTo; + lastToken = ";"; + continue; + } + } else if (wordText === "function" && braceDepth === 0 && lastToken !== "async" && !isExpressionContext(lastToken)) { + // A function *declaration* — `const f = function foo() {}` is skipped + const name = word(source, end); + if (name.ok) topLevelFunctions.push(name.value); + } else if (wordText === "async" && braceDepth === 0 && !isExpressionContext(lastToken)) { + const fn = seq(kw("function"), opt(punct("*")), word)(source, end); + if (fn.ok) topLevelFunctions.push(fn.value[2]); + } + + pos = end; + lastToken = wordText; + continue; + } + + if (c === "{") braceDepth++; + else if (c === "}") braceDepth = Math.max(0, braceDepth - 1); + lastToken = c; + pos++; + } + + return { imports, exports: exportStatements, topLevelFunctions }; +} diff --git a/packages/ono/src/resolver.js b/packages/ono/src/resolver.js deleted file mode 100644 index 0305009..0000000 --- a/packages/ono/src/resolver.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Module Resolver - collect the local import graph of an entry file. - * - * Only relative specifiers are followed; package imports and evaluation - * order are left to Node's own module resolution. - */ - -import fs from "node:fs/promises"; -import path from "node:path"; - -/** - * Parse import specifiers from source code - * @param {string} code - Source code - * @returns {string[]} Array of import specifiers - */ -export function parseImports(code) { - const specifiers = []; - - // Match import and re-export patterns: - // import foo from "bar" - // import { foo } from "bar" - // import * as foo from "bar" - // import "bar" - // export { foo } from "bar" - // export * from "bar" - const importRegex = /(?:import|export)\s+(?:[\w{},\s*]+\s+from\s+)?['"]([^'"]+)['"]/g; - - let match; - while ((match = importRegex.exec(code)) !== null) { - specifiers.push(match[1]); - } - - return specifiers; -} - -/** - * Resolve import path relative to the importing file - * @param {string} importPath - Import specifier (e.g., "./Button.jsx") - * @param {string} fromFile - Absolute path of the file doing the import - * @returns {string} Absolute path to the imported file - */ -export function resolveImportPath(importPath, fromFile) { - if (path.isAbsolute(importPath)) { - return importPath; - } - return path.resolve(path.dirname(fromFile), importPath); -} - -/** - * Collect the entry file and all transitively imported local files - * @param {string} entryFile - Absolute path to entry file - * @returns {Promise>} Absolute paths of all local modules - */ -export async function collectModules(entryFile) { - const modules = new Set(); - const queue = [entryFile]; - - while (queue.length > 0) { - const currentFile = queue.shift(); - if (modules.has(currentFile)) continue; - - let source; - try { - source = await fs.readFile(currentFile, "utf-8"); - } catch (error) { - throw new Error(`Cannot read file: ${currentFile}\n${error.message}`); - } - - modules.add(currentFile); - - for (const specifier of parseImports(source)) { - if (specifier.startsWith(".")) { - queue.push(resolveImportPath(specifier, currentFile)); - } - } - } - - return modules; -} diff --git a/packages/ono/test/bundler.test.js b/packages/ono/test/bundler.test.js new file mode 100644 index 0000000..f03645e --- /dev/null +++ b/packages/ono/test/bundler.test.js @@ -0,0 +1,208 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { bundle } from "../src/bundler.js"; + +/** Bundle a virtual file system (browser-style ids) */ +function bundleFiles(files, entry, options = {}) { + return bundle({ + entry, + resolve: (specifier, fromId) => { + const fromParts = fromId.split("/").slice(0, -1); + for (const part of specifier.split("/")) { + if (!part || part === ".") continue; + if (part === "..") fromParts.pop(); + else fromParts.push(part); + } + return fromParts.join("/"); + }, + load: (id) => { + if (!(id in files)) throw new Error(`Cannot read file: ${id}`); + return files[id]; + }, + onExternal: options.onExternal ?? "error", + exposeEntryFunctions: options.exposeEntryFunctions ?? false, + }); +} + +/** Evaluate a bundle (no externals) and return the entry module object */ +async function evaluate(files, entry, options) { + const { code, ...rest } = await bundleFiles(files, entry, options); + const entryModule = new Function(`${code}\nreturn __ono_entry;`)(); + return { entryModule, code, ...rest }; +} + +test("bundle - default and named imports across modules", async () => { + const { entryModule } = await evaluate( + { + "lib.js": `export default 10;\nexport const twice = (n) => n * 2;`, + "index.js": `import ten, { twice } from "./lib.js";\nexport default twice(ten);`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.default, 20); +}); + +test("bundle - namespace import and aliases", async () => { + const { entryModule } = await evaluate( + { + "lib.js": `export const a = 1;\nexport const b = 2;`, + "index.js": `import * as lib from "./lib.js";\nimport { a as first } from "./lib.js";\nexport const sum = lib.a + lib.b + first;`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.sum, 4); +}); + +test("bundle - modules are evaluated once and in dependency order", async () => { + const { entryModule } = await evaluate( + { + "log.js": `export const order = [];`, + "a.js": `import { order } from "./log.js";\norder.push("a");\nexport const a = true;`, + "b.js": `import { order } from "./log.js";\nimport { a } from "./a.js";\norder.push("b");\nexport const b = a;`, + "index.js": `import { order } from "./log.js";\nimport { b } from "./b.js";\nimport { a } from "./a.js";\nexport const result = order.join(",");`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.result, "a,b"); +}); + +test("bundle - nested directory resolution", async () => { + const { entryModule } = await evaluate( + { + "components/Button.js": `export default () => "button";`, + "components/Card.js": `import Button from "./Button.js";\nexport default () => "card+" + Button();`, + "index.js": `import Card from "./components/Card.js";\nexport default Card();`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.default, "card+button"); +}); + +test("bundle - re-exports (barrel pattern)", async () => { + const { entryModule, entryExports } = await evaluate( + { + "posts/hello.js": `export const meta = { title: "Hello" };\nexport default () => "post";`, + "blog.js": `import hello, { meta as helloMeta } from "./posts/hello.js";\nexport { hello, helloMeta };`, + "index.js": `export { hello, helloMeta } from "./blog.js";`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.hello(), "post"); + assert.strictEqual(entryModule.helloMeta.title, "Hello"); + assert.deepStrictEqual(entryExports, ["hello", "helloMeta"]); +}); + +test("bundle - export star from", async () => { + const { entryModule } = await evaluate( + { + "lib.js": `export const x = 1;\nexport const y = 2;\nexport default "ignored";`, + "index.js": `export * from "./lib.js";`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.x, 1); + assert.strictEqual(entryModule.y, 2); + assert.strictEqual(entryModule.default, undefined); +}); + +test("bundle - import cycles resolve like CommonJS", async () => { + const { entryModule } = await evaluate( + { + "a.js": `import { bName } from "./b.js";\nexport function aName() { return "a->" + bName(); }`, + "b.js": `import { aName } from "./a.js";\nexport function bName() { return "b"; }\nexport const viaA = () => aName();`, + "index.js": `import { aName } from "./a.js";\nimport { viaA } from "./b.js";\nexport const result = viaA();`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.result, "a->b"); +}); + +test("bundle - side-effect import evaluates the module", async () => { + const { entryModule } = await evaluate( + { + "setup.js": `globalThis.__ono_test_flag = "ready";`, + "index.js": `import "./setup.js";\nexport const flag = globalThis.__ono_test_flag;`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.flag, "ready"); + delete globalThis.__ono_test_flag; +}); + +test("bundle - default export declaration keeps its binding usable", async () => { + const { entryModule } = await evaluate( + { + "index.js": `export default function App() { return helper(); }\nfunction helper() { return App.name; }`, + }, + "index.js", + ); + + assert.strictEqual(entryModule.default(), "App"); +}); + +test("bundle - exposeEntryFunctions exports unexported entry functions", async () => { + const { entryModule } = await evaluate( + { + "index.js": `function App() { return "rendered"; }\nApp();`, + }, + "index.js", + { exposeEntryFunctions: true }, + ); + + assert.strictEqual(entryModule.App(), "rendered"); +}); + +test("bundle - onExternal error rejects package imports", async () => { + await assert.rejects( + () => + bundleFiles( + { "index.js": `import React from "react";` }, + "index.js", + { onExternal: "error" }, + ), + /Cannot bundle package import "react"/, + ); +}); + +test("bundle - onExternal hoist moves package imports to the bundle top", async () => { + const { code, externalBindings } = await bundleFiles( + { + "index.js": `import { readFile } from "node:fs/promises";\nexport default readFile;`, + }, + "index.js", + { onExternal: "hoist" }, + ); + + assert.ok(code.startsWith(`import { readFile } from "node:fs/promises";`)); + assert.ok(externalBindings.has("readFile")); +}); + +test("bundle - missing module rejects with the loader's error", async () => { + await assert.rejects( + () => + bundleFiles( + { "index.js": `import missing from "./missing.js";` }, + "index.js", + ), + /Cannot read file: missing.js/, + ); +}); + +test("bundle - entryExports lists the entry's export names", async () => { + const { entryExports } = await bundleFiles( + { + "index.js": `export default 1;\nexport const meta = {};\nexport function helper() {}`, + }, + "index.js", + ); + + assert.deepStrictEqual(entryExports, ["default", "meta", "helper"]); +}); diff --git a/packages/ono/test/compiler.test.js b/packages/ono/test/compiler.test.js new file mode 100644 index 0000000..8e3490e --- /dev/null +++ b/packages/ono/test/compiler.test.js @@ -0,0 +1,87 @@ +/** + * Tests for the browser compiler (REPL path). + * browser/compiler.js has no Node dependencies, so it can be tested here. + */ +import { test } from "node:test"; +import assert from "node:assert"; +import { compileProject } from "../src/browser/compiler.js"; + +test("compileProject - default export page", async () => { + const files = { + "index.jsx": `export default function App() { + return
    Hello REPL
    ; +}`, + }; + + const { html } = await compileProject(files, "index.jsx", { enableUno: false }); + assert.strictEqual(html, `
    Hello REPL
    `); +}); + +test("compileProject - multi-file project with imports", async () => { + const files = { + "components/Card.jsx": `export function Card(props) { + return
    {props.children}
    ; +}`, + "index.jsx": `import { Card } from './components/Card.jsx'; + +export default function App() { + return

    content

    ; +}`, + }; + + const { html } = await compileProject(files, "index.jsx", { enableUno: false }); + assert.strictEqual(html, `

    content

    `); +}); + +test("compileProject - REPL style entry without exports", async () => { + const files = { + "index.jsx": `function App() { + return ( + <> +

    Title

    +

    Fragment in the REPL

    + + ); +} + +App()`, + }; + + const { html } = await compileProject(files, "index.jsx", { enableUno: false }); + assert.strictEqual(html, "

    Title

    Fragment in the REPL

    "); +}); + +test("compileProject - generates UnoCSS when enabled", async () => { + const files = { + "index.jsx": `export default function App() { + return
    styled
    ; +}`, + }; + + const { css } = await compileProject(files, "index.jsx"); + assert.ok(css.includes(".text-red-500")); +}); + +test("compileProject - missing import produces a clear error", async () => { + const files = { + "index.jsx": `import Missing from './missing.jsx'; +export default () => ;`, + }; + + await assert.rejects( + () => compileProject(files, "index.jsx", { enableUno: false }), + /Cannot find module '.\/missing.jsx'/, + ); +}); + +test("compileProject - package imports are rejected in the browser", async () => { + const files = { + "index.jsx": `import React from 'react'; +export default () =>
    ;`, + }; + + await assert.rejects( + () => compileProject(files, "index.jsx", { enableUno: false }), + /Cannot bundle package import "react"/, + ); +}); diff --git a/packages/ono/test/parser.test.js b/packages/ono/test/parser.test.js new file mode 100644 index 0000000..cc3a4f4 --- /dev/null +++ b/packages/ono/test/parser.test.js @@ -0,0 +1,192 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { parseModule, isIdentifierName } from "../src/parser.js"; + +// --- imports --------------------------------------------------------------- + +test("parseModule - default import", () => { + const { imports } = parseModule(`import Button from "./Button.jsx";`); + + assert.strictEqual(imports.length, 1); + assert.strictEqual(imports[0].specifier, "./Button.jsx"); + assert.strictEqual(imports[0].defaultBinding, "Button"); +}); + +test("parseModule - named imports with aliases", () => { + const { imports } = parseModule(`import { foo, bar as baz, default as d } from "./utils.js";`); + + assert.deepStrictEqual(imports[0].named, [ + { imported: "foo", local: "foo" }, + { imported: "bar", local: "baz" }, + { imported: "default", local: "d" }, + ]); +}); + +test("parseModule - namespace import", () => { + const { imports } = parseModule(`import * as utils from "./utils.js";`); + + assert.strictEqual(imports[0].namespace, "utils"); + assert.strictEqual(imports[0].specifier, "./utils.js"); +}); + +test("parseModule - default plus named", () => { + const { imports } = parseModule(`import App, { helper } from "./app.js";`); + + assert.strictEqual(imports[0].defaultBinding, "App"); + assert.deepStrictEqual(imports[0].named, [{ imported: "helper", local: "helper" }]); +}); + +test("parseModule - side-effect import", () => { + const { imports } = parseModule(`import "./setup.js";`); + + assert.strictEqual(imports[0].sideEffect, true); + assert.strictEqual(imports[0].specifier, "./setup.js"); +}); + +test("parseModule - multiline import", () => { + const source = `import { + one, + two as three, +} from "./numbers.js";`; + const { imports } = parseModule(source); + + assert.deepStrictEqual(imports[0].named, [ + { imported: "one", local: "one" }, + { imported: "two", local: "three" }, + ]); + assert.strictEqual(source.slice(imports[0].start, imports[0].end), source); +}); + +test("parseModule - import inside a string is ignored", () => { + const { imports } = parseModule(`const s = 'import fake from "./fake.js"';`); + + assert.strictEqual(imports.length, 0); +}); + +test("parseModule - import inside a template literal is ignored", () => { + const source = "const s = `import fake from \"./fake.js\" ${'import x from \"./y\"'}`;"; + const { imports } = parseModule(source); + + assert.strictEqual(imports.length, 0); +}); + +test("parseModule - import inside comments is ignored", () => { + const source = `// import a from "./a.js" +/* import b from "./b.js" */ +import real from "./real.js";`; + const { imports } = parseModule(source); + + assert.strictEqual(imports.length, 1); + assert.strictEqual(imports[0].specifier, "./real.js"); +}); + +test("parseModule - dynamic import and import.meta are ignored", () => { + const source = `const mod = import("./dynamic.js"); +const url = import.meta.url;`; + const { imports } = parseModule(source); + + assert.strictEqual(imports.length, 0); +}); + +test("parseModule - regex literal containing quotes does not break scanning", () => { + const source = `const re = /['"{]/g; +import real from "./real.js";`; + const { imports } = parseModule(source); + + assert.strictEqual(imports.length, 1); +}); + +// --- exports --------------------------------------------------------------- + +test("parseModule - export default function declaration", () => { + const source = `export default function App() { return h("div"); }`; + const { exports } = parseModule(source); + + assert.strictEqual(exports[0].type, "exportDefaultDeclaration"); + assert.strictEqual(exports[0].name, "App"); +}); + +test("parseModule - export default expression", () => { + const { exports } = parseModule(`export default App;`); + + assert.strictEqual(exports[0].type, "exportDefaultExpression"); +}); + +test("parseModule - export default anonymous function", () => { + const { exports } = parseModule(`export default function () { return 1; }`); + + assert.strictEqual(exports[0].type, "exportDefaultExpression"); +}); + +test("parseModule - export function and class declarations", () => { + const source = `export function foo() {} +export async function bar() {} +export class Baz {}`; + const { exports } = parseModule(source); + + assert.deepStrictEqual( + exports.map((e) => e.names[0]), + ["foo", "bar", "Baz"], + ); + assert.ok(exports.every((e) => e.type === "exportDeclaration")); +}); + +test("parseModule - export const with multiple declarators", () => { + const source = `export const a = fn(1, { b: 2 }), c = [3, 4];`; + const { exports } = parseModule(source); + + assert.deepStrictEqual(exports[0].names, ["a", "c"]); +}); + +test("parseModule - export named list", () => { + const { exports } = parseModule(`const a = 1; export { a, a as b };`); + + assert.strictEqual(exports[0].type, "exportNamed"); + assert.deepStrictEqual(exports[0].named, [ + { local: "a", exported: "a" }, + { local: "a", exported: "b" }, + ]); +}); + +test("parseModule - re-export from (barrel files)", () => { + const source = `export { default as helloWorld, meta as helloWorldMeta } from './blog/hello-world.tsx';`; + const { exports } = parseModule(source); + + assert.strictEqual(exports[0].type, "exportNamedFrom"); + assert.strictEqual(exports[0].specifier, "./blog/hello-world.tsx"); + assert.deepStrictEqual(exports[0].named, [ + { local: "default", exported: "helloWorld" }, + { local: "meta", exported: "helloWorldMeta" }, + ]); +}); + +test("parseModule - export star from", () => { + const { exports } = parseModule(`export * from "./all.js";`); + + assert.strictEqual(exports[0].type, "exportStarFrom"); + assert.strictEqual(exports[0].specifier, "./all.js"); +}); + +test("parseModule - destructuring export is rejected with a clear error", () => { + assert.throws(() => parseModule(`export const { a } = obj;`), /Destructuring/); +}); + +// --- top-level functions ----------------------------------------------------- + +test("parseModule - collects top-level function declarations", () => { + const source = `function App() { return 1; } +async function load() {} +function helper() { function inner() {} } +const expr = function named() {}; +App();`; + const { topLevelFunctions } = parseModule(source); + + assert.deepStrictEqual(topLevelFunctions, ["App", "load", "helper"]); +}); + +test("isIdentifierName", () => { + assert.ok(isIdentifierName("valid_$Name1")); + assert.ok(!isIdentifierName("1bad")); + assert.ok(!isIdentifierName("has-dash")); + assert.ok(!isIdentifierName("")); +}); diff --git a/packages/ono/test/resolver.test.js b/packages/ono/test/resolver.test.js deleted file mode 100644 index 1dbbfdf..0000000 --- a/packages/ono/test/resolver.test.js +++ /dev/null @@ -1,139 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { parseImports, resolveImportPath, collectModules } from "../src/resolver.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const fixturesDir = path.join(__dirname, "fixtures"); - -test("parseImports - single default import", () => { - const code = `import Button from "./Button.jsx";`; - const result = parseImports(code); - - assert.deepStrictEqual(result, ["./Button.jsx"]); -}); - -test("parseImports - multiple imports", () => { - const code = ` -import Card from "./Card.jsx"; -import Button from "./Button.jsx"; - `.trim(); - - const result = parseImports(code); - - assert.deepStrictEqual(result, ["./Card.jsx", "./Button.jsx"]); -}); - -test("parseImports - no imports", () => { - const code = `function test() { return
    ; }`; - const result = parseImports(code); - - assert.strictEqual(result.length, 0); -}); - -test("parseImports - named imports", () => { - const code = `import { foo, bar } from "./utils.js";`; - const result = parseImports(code); - - assert.deepStrictEqual(result, ["./utils.js"]); -}); - -test("parseImports - re-exports (barrel files)", () => { - const code = ` -export { default as helloWorld, meta as helloWorldMeta } from './blog/hello-world.tsx'; -export * from './blog/getting-started.tsx'; -export const entries = ['hello-world']; - `.trim(); - - const result = parseImports(code); - - assert.deepStrictEqual(result, [ - "./blog/hello-world.tsx", - "./blog/getting-started.tsx", - ]); -}); - -test("parseImports - mixed imports", () => { - const code = ` -import React from "react"; -import Button from "./Button.jsx"; -import { helper } from "./utils.js"; - `.trim(); - - const result = parseImports(code); - - assert.strictEqual(result.length, 3); -}); - -test("resolveImportPath - relative path same directory", () => { - const fromFile = path.join(fixturesDir, "App.jsx"); - const importPath = "./Button.jsx"; - - const result = resolveImportPath(importPath, fromFile); - const expected = path.join(fixturesDir, "Button.jsx"); - - assert.strictEqual(result, expected); -}); - -test("resolveImportPath - relative path parent directory", () => { - const fromFile = path.join(fixturesDir, "sub", "Component.jsx"); - const importPath = "../Button.jsx"; - - const result = resolveImportPath(importPath, fromFile); - const expected = path.join(fixturesDir, "Button.jsx"); - - assert.strictEqual(result, expected); -}); - -test("resolveImportPath - absolute stays absolute", () => { - const fromFile = path.join(fixturesDir, "App.jsx"); - const importPath = "/absolute/path/Button.jsx"; - - const result = resolveImportPath(importPath, fromFile); - - assert.strictEqual(result, importPath); -}); - -test("collectModules - no imports", async () => { - const entryFile = path.join(fixturesDir, "NoImports.jsx"); - const result = await collectModules(entryFile); - - assert.strictEqual(result.size, 1); - assert.ok(result.has(entryFile)); -}); - -test("collectModules - single level imports", async () => { - const entryFile = path.join(fixturesDir, "Card.jsx"); - const result = await collectModules(entryFile); - - // Card.jsx imports Button.jsx - assert.strictEqual(result.size, 2); - assert.ok(result.has(entryFile)); - - const buttonPath = path.join(fixturesDir, "Button.jsx"); - assert.ok(result.has(buttonPath)); -}); - -test("collectModules - nested imports", async () => { - const entryFile = path.join(fixturesDir, "App.jsx"); - const result = await collectModules(entryFile); - - // App.jsx imports Card.jsx and Button.jsx - // Card.jsx also imports Button.jsx - // Total: App.jsx, Card.jsx, Button.jsx (3 unique files) - assert.strictEqual(result.size, 3); - - assert.ok(result.has(path.join(fixturesDir, "App.jsx"))); - assert.ok(result.has(path.join(fixturesDir, "Card.jsx"))); - assert.ok(result.has(path.join(fixturesDir, "Button.jsx"))); -}); - -test("collectModules - handles non-existent file", async () => { - const entryFile = path.join(fixturesDir, "NonExistent.jsx"); - - await assert.rejects( - async () => await collectModules(entryFile), - /Cannot read file/i - ); -});