From 5620e3983c33576a924058f4f1fb2f026501e4e0 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:48:32 +0200 Subject: [PATCH] fix(pkg): ship src/logger.ts and src/util for the raw-source ./tui entry --- packages/opencode/package.json | 2 + .../opencode/src/tests/tui-packaging.test.ts | 144 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 packages/opencode/src/tests/tui-packaging.test.ts diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 10c00f5..d3f6c1c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -40,6 +40,8 @@ "src/tui/command-dialogs.tsx", "src/sidebar-state.ts", "src/tui-preferences.ts", + "src/logger.ts", + "src/util", "src/rpc", "README.md", "LICENSE" diff --git a/packages/opencode/src/tests/tui-packaging.test.ts b/packages/opencode/src/tests/tui-packaging.test.ts new file mode 100644 index 0000000..cf626f1 --- /dev/null +++ b/packages/opencode/src/tests/tui-packaging.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, test } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join, relative, resolve } from 'node:path' + +// --------------------------------------------------------------------------- +// Evergreen regression check: the ./tui export ships raw source +// (exports["./tui"].import === "./src/tui.tsx"), so every src/ file +// transitively reachable from that entry must be listed in package.json +// "files" — otherwise the published tarball is missing modules and the +// ./tui import throws ERR_MODULE_NOT_FOUND at load time. This test walks +// the import graph dynamically from package.json and asserts no reachable +// src/ file is uncovered. +// --------------------------------------------------------------------------- + +const PKG_DIR = join(import.meta.dir!, '..', '..') + +function readJson(path: string): any { + return JSON.parse(readFileSync(path, 'utf8')) +} + +// Collect every relative import/export specifier in a source file. +// Covers: +// import ... from './x' +// import type ... from './x' +// export ... from './x' +// export type ... from './x' +// import './x' (side-effect) +// import('./x') (dynamic) +function relativeSpecs(source: string): string[] { + const seen = new Set() + // static from-based (single- or multi-line; captures the string body) + for (const m of source.matchAll(/from\s+(['"])(\.[^'"]*)\1/g)) { + seen.add(m[2]!) + } + // dynamic import() calls + for (const m of source.matchAll(/import\s*\(\s*(['"])(\.[^'"]*)\1\s*\)/g)) { + seen.add(m[2]!) + } + // bare side-effect imports + for (const m of source.matchAll(/import\s+(['"])(\.[^'"]*)\1/g)) { + seen.add(m[2]!) + } + return [...seen] +} + +// Resolve a relative specifier against an importer directory using the +// same resolution rules TypeScript (and Bun's runtime loader) follows +// for .ts/.tsx source files. +function resolveRelSpec(spec: string, fromDir: string): string | null { + // Order matters: exact match, then .ts / .tsx, then index files, + // then .js/.jsx→.ts/.tsx rewrites (the source tree uses .js specifiers). + const candidates = [spec] + + if (spec.endsWith('.js')) { + const base = spec.slice(0, -3) + candidates.push(`${base}.ts`, `${base}.tsx`) + } else if (spec.endsWith('.jsx')) { + const base = spec.slice(0, -4) + candidates.push(`${base}.tsx`) + } + + candidates.push( + `${spec}.ts`, + `${spec}.tsx`, + `${spec}/index.ts`, + `${spec}/index.tsx`, + ) + + for (const c of candidates) { + const p = resolve(fromDir, c) + if (existsSync(p)) return p + } + return null +} + +// BFS the transitive relative-import graph starting from entryRel (a +// package-relative path like "src/tui.tsx"). Returns the set of posix +// package-relative paths for every src/ file reached. +function collectReachableSrcFiles(entryRel: string): Set { + const pkgSrc = join(PKG_DIR, 'src') + const visited = new Set() + const queue = [entryRel] + + while (queue.length > 0) { + const f = queue.shift()! + const abs = resolve(PKG_DIR, f) + if (visited.has(abs)) continue + visited.add(abs) + + let source: string + try { + source = readFileSync(abs, 'utf8') + } catch { + continue + } + + const fromDir = dirname(abs) + for (const spec of relativeSpecs(source)) { + const resolved = resolveRelSpec(spec, fromDir) + if (resolved?.startsWith(pkgSrc) && !visited.has(resolved)) { + queue.push(relative(PKG_DIR, resolved)) + } + } + } + + // Posix-relative paths (Bun on Windows still uses / in package.json paths) + return new Set( + [...visited].map((f) => relative(PKG_DIR, f).replaceAll('\\', '/')), + ) +} + +// A reachable src/ file is covered when some "files" entry E satisfies +// relPath === E OR relPath.startsWith(E.replace(/\/$/, '') + '/') +function uncoveredFiles(reachable: Set, files: string[]): string[] { + return [...reachable] + .filter((rel) => { + for (const e of files) { + const dir = e.replace(/\/$/, '') + if (rel === e || rel.startsWith(`${dir}/`)) return false // covered + } + return true // uncovered + }) + .sort() +} + +describe('tui packaging (raw-source ./tui entry)', () => { + test('every reachable src/ file is covered by package.json files', () => { + const pkg = readJson(join(PKG_DIR, 'package.json')) + const tuiEntry: string = pkg.exports['./tui'].import + + if (!tuiEntry.startsWith('./')) { + throw new Error( + `Expected exports["./tui"].import to be a raw-source entry like "./src/tui.tsx", got ${JSON.stringify(tuiEntry)}`, + ) + } + + // Strip the "./" prefix to get a package-relative path ("src/tui.tsx") + const entryRel = tuiEntry.slice(2) + const reachable = collectReachableSrcFiles(entryRel) + const uncovered = uncoveredFiles(reachable, pkg.files) + + expect(uncovered).toEqual([]) + }) +})