Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
NODE_VERSION: 24.x

jobs:
# ── Frontend ──────────────────────────────────────────────────────────────
frontend:
name: Frontend
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
with:
version: 10.33.0

- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint (Biome)
run: pnpm lint

- name: Type-check
run: pnpm exec tsc --noEmit

- name: Build
run: pnpm build

# ── Backend (Rust) ────────────────────────────────────────────────────────
backend:
name: Backend
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

# Tauri v2 system dependencies required on Linux
- name: Cache apt packages
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
with:
path: /var/cache/apt/archives
key: apt-tauri-${{ runner.os }}

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
curl \
file \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
patchelf \
wget

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
workspaces: src-tauri -> target
cache-on-failure: true
shared-key: lineup-rust

- name: Check
working-directory: src-tauri
run: cargo check

- name: Clippy
working-directory: src-tauri
run: cargo clippy

- name: Test
working-directory: src-tauri
run: cargo test
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Changelog

All notable changes to Lineup are documented here.

---

## [1.1.0] — 2026-04-30

### Added

#### TypeScript support
- Lineup can now scan TypeScript projects (`.ts` and `.tsx` files) for V8 JIT memory layout inefficiencies. V8 stores `number` properties as unboxed 8-byte doubles while all other property types are stored as 4-byte tagged compressed pointers (Node.js 14+, 64-bit). Interleaving `number` and non-`number` properties creates the same hidden padding waste as Go struct misalignment.
- Analyzes three TypeScript constructs: `class` declarations (instance properties only; static properties are skipped), `interface` declarations, and object `type` aliases (`type Foo = { ... }`).
- Supports both exported and non-exported declarations, including `export default class`.
- New `ts_parser.rs` Rust module: walks `.ts`/`.tsx` files using `oxc_parser` 0.128 (pure Rust, no C build step). Auto-skips `node_modules/`, `dist/`, `build/`, `.next/`, `.nuxt/`, `coverage/`, and `.cache/` in addition to `.gitignore` exclusions.
- New TypeScript analysis pipeline in `analyzer.rs`: `ts_type_info()` V8 size model, `analyze_ts_files()`, and `build_ts_def()`. Shares the same `align_up`, `struct_layout`, and optimal-sort primitives as the Go pipeline.
- TypeScript declarations that `extend` or `implement` other types are flagged `~approximate` because parent properties affect V8 hidden-class layout but are not resolved across files.

#### Language selector
- New **Language** toggle (`Go` / `TypeScript`) in the Scan Options modal. Arch selector is hidden when TypeScript is selected (V8 always uses 64-bit pointer compression).
- New **Default Language** setting in the Settings page (`go` by default).
- `ScanOptions` now carries a `language` field passed to the Tauri `scan_repo` command.

#### Declaration kind
- `declaration_kind` field added to all result types: `"struct"` for Go, `"class"` / `"interface"` / `"type"` for TypeScript.
- Kind badge shown next to the type name in `StructCard` and `StructDetail` for non-Go results.
- Toolbar on the Scan Results screen shows `"declarations"` instead of `"structs"` for TypeScript scans.

### Changed

- **Database schema** — two additive `ALTER TABLE` migrations applied at startup:
- `scans.language TEXT NOT NULL DEFAULT 'go'`
- `struct_results.declaration_kind TEXT NOT NULL DEFAULT 'struct'`
- Both migrations are no-ops on existing databases (duplicate-column errors are silently ignored).
- `save_scan` and `save_struct_result` in `db.rs` now accept and store `language` / `declaration_kind` respectively.
- `get_history`, `get_scan_detail`, and `get_struct_detail` queries updated to select and map the new columns.
- `ScanSummary`, `StructSummary`, and `StructDetail` Rust and TypeScript types updated with new fields.
- `AppSettings` TypeScript interface gains `defaultLanguage: string`.
- Re-scan pre-fills the `language` of the original scan (same as it does for arch and ignore patterns).

### Dependencies

- Added `oxc_allocator`, `oxc_parser`, `oxc_ast`, `oxc_span` at version `0.128.0` (pure Rust TypeScript/JSX parser).

### UI improvements

- **History card language badge** — the language is now displayed as a bordered monospace pill (`GO` / `TS`) next to the repo path instead of inline parenthetical text. The path remains truncatable while the badge is always fully visible.
- **Language-aware ignore pattern placeholders** — the ignore patterns textarea in the Scan Options modal and in Settings now shows language-appropriate placeholder examples. Go shows `vendor/`, `generated/`, `_test\.go$`; TypeScript shows `\.test\.tsx?$`, `\.spec\.tsx?$`, `\.d\.ts$`. The modal placeholder updates reactively when the language toggle is switched.

### Internal

- **`PARSER_LANGUAGE` type narrowed to `"GO" | "TS"`** — language values are now uppercase string literals throughout the frontend (`types.ts`, `util.ts`, settings store, all components and pages) and stored as `"GO"` / `"TS"` in the SQLite database. The database `DEFAULT` and Rust fallback values updated to match.
- **GitHub Actions CI pipeline** (`.github/workflows/ci.yml`) — two jobs running on `ubuntu-24.04`:
- *Frontend*: pnpm install (frozen lockfile) → Biome lint → `tsc --noEmit` type-check → Vite build. Uses `actions/setup-node` pnpm store cache.
- *Backend*: apt system-dep cache → Tauri Linux deps → `cargo check` → `cargo clippy` → `cargo test`. Uses `Swatinem/rust-cache` with `cache-on-failure` and a shared cache key across branches.
- All action references pinned to full commit SHAs. Triggers on push to `main` and `develop`, and on PRs targeting `main`.

---

## [1.0.0] — initial release

- Go struct padding analysis for `amd64` and `arm64` targets.
- Three-pane results UI (file tree / declaration list / detail panel).
- Scan history persisted to local SQLite database.
- Re-scan with pre-filled options.
- Configurable ignore patterns (regex).
- Copy optimized definition to clipboard.
- Dark / light theme.
95 changes: 79 additions & 16 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For user-facing documentation see [README.md](README.md).
| Persistence | SQLite via [rusqlite](https://github.com/rusqlite/rusqlite) (bundled) |
| Directory walking | [ignore](https://crates.io/crates/ignore) |
| Regex | [regex](https://crates.io/crates/regex) |
| TypeScript parsing | [oxc_parser](https://crates.io/crates/oxc_parser) 0.128 (pure Rust) |

---

Expand Down Expand Up @@ -127,8 +128,8 @@ lineup/
│ │ ├── FileTree.tsx # Left-pane file tree on results screen
│ │ ├── HistoryCard.tsx # Scan history card on home screen
│ │ ├── ProgressModal.tsx # Scan progress overlay with cancel
│ │ ├── ScanOptionsModal.tsx# Pre-scan config (arch, ignore patterns)
│ │ ├── StructCard.tsx # Per-struct summary card
│ │ ├── ScanOptionsModal.tsx# Pre-scan config (language, arch, ignore patterns)
│ │ ├── StructCard.tsx # Per-declaration summary card
│ │ └── StructDetail.tsx # Right-pane detail view with copy button
│ ├── pages/
│ │ ├── Home.tsx # Home / history screen
Expand All @@ -150,7 +151,8 @@ lineup/
│ ├── lib.rs # Tauri commands, managed state, app setup
│ ├── db.rs # SQLite schema, queries, data types
│ ├── parser.rs # Go source walker and struct parser
│ └── analyzer.rs # Padding analyzer and optimal-order engine
│ ├── ts_parser.rs # TypeScript source walker and declaration parser
│ └── analyzer.rs # Padding analyzer and optimal-order engine (Go + TS)
├── public/ # Static assets
├── biome.json # Biome linter / formatter config
├── vite.config.ts
Expand All @@ -166,6 +168,8 @@ lineup/

Defines `AppState` (the Tauri managed state), all `#[tauri::command]` handlers, and the `tauri::Builder` setup. `AppState` holds a `Mutex<rusqlite::Connection>` and an `Arc<AtomicBool>` cancel flag.

`ScanOptions` carries a `language` field (`"GO"` or `"TS"`) that controls which walker and analyzer are invoked by `scan_repo`.

### `db.rs`

All database logic. Initializes the schema on first run and exposes typed functions for every persistence operation. The SQLite file is stored in the platform app-data directory resolved by `tauri::Manager::path().app_data_dir()`.
Expand All @@ -178,10 +182,11 @@ All database logic. Initializes the schema on first run and exposes typed functi
| `repo_path` | TEXT | Absolute path to the scanned directory |
| `scanned_at` | INTEGER | Unix timestamp (seconds) |
| `total_structs` | INTEGER | |
| `padded_structs` | INTEGER | Structs with bytes_saved > 0 |
| `bytes_saved` | INTEGER | Sum across all structs |
| `padded_structs` | INTEGER | Declarations with bytes_saved > 0 |
| `bytes_saved` | INTEGER | Sum across all declarations |
| `ignore_patterns` | TEXT | JSON array of regex strings |
| `target_arch` | TEXT | `"amd64"` or `"arm64"` |
| `language` | TEXT | `"GO"` or `"TS"` (added in 1.1.0; defaults to `"GO"` for existing rows) |

**Schema — `struct_results` table**

Expand All @@ -199,21 +204,50 @@ All database logic. Initializes the schema on first run and exposes typed functi
| `optimized_def` | TEXT | Reordered definition with added comment header |
| `has_generics` | INTEGER | Boolean (0/1); sizes are approximate |
| `has_embedded` | INTEGER | Boolean (0/1) |
| `declaration_kind` | TEXT | `"struct"` for Go; `"class"`, `"interface"`, or `"type"` for TypeScript (added in 1.1.0; defaults to `"struct"` for existing rows) |

### `parser.rs`

Walks a repository using `ignore::Walk` — automatically respects `.gitignore` and skips `vendor/`. User-supplied ignore patterns (regex strings) are compiled into a `RegexSet` before the walk begins and tested against each file's repo-relative path. Uses brace-counting (not pure regex) to reliably extract `type Name[TypeParams] struct { ... }` blocks including generic structs. Captures doc comments (`//`-prefixed lines immediately preceding `type Name struct`) and preserves inline field comments and struct tags.
Walks a **Go** repository using `ignore::Walk` — automatically respects `.gitignore` and skips `vendor/` and `testdata/`. User-supplied ignore patterns (regex strings) are compiled into a `RegexSet` before the walk begins and tested against each file's repo-relative path. Uses brace-counting (not pure regex) to reliably extract `type Name[TypeParams] struct { ... }` blocks including generic structs. Captures doc comments (`//`-prefixed lines immediately preceding `type Name struct`) and preserves inline field comments and struct tags.

### `ts_parser.rs`

Walks a **TypeScript** repository using the same `ignore::Walk` infrastructure. Hard-skips `node_modules/`, `dist/`, `build/`, `.next/`, `.nuxt/`, `coverage/`, and `.cache/` in addition to `.gitignore` exclusions. Accepts `.ts` and `.tsx` files.

Parsing is done with [`oxc_parser`](https://crates.io/crates/oxc_parser) (pure Rust, no C build step). The parser produces a typed AST from which the following top-level constructs are extracted:

- `class` declarations — instance `PropertyDefinition` nodes (static properties are skipped)
- `interface` declarations — `TSPropertySignature` nodes
- object `type` aliases (`type Foo = { ... }`) — `TSPropertySignature` nodes inside `TSTypeLiteral`

Exported and non-exported variants, including `export default class`, are all handled. For each property the raw source line is captured via `span` byte offsets for use in reconstructed definitions.

### `analyzer.rs`

Contains two independent analysis pipelines that share the same core layout primitives (`align_up`, `struct_layout`, optimal-sort by align desc / size desc / name asc).

**Go pipeline (`analyze_files`)**

Two-pass analysis:

1. **Pass 1** — parse all files and build a `HashMap<String, StructInfo>` registry mapping type names to their computed size and alignment.
2. **Pass 2** — for each struct, resolve embedded types recursively from the registry, compute current size (with padding), compute optimal size (fields sorted by alignment descending, ties broken by size then name), and generate the `optimized_def` string.
1. **Pass 1** — parse all files and build a `HashMap<String, TypeInfo>` registry mapping type names to their computed size and alignment.
2. **Pass 2** — for each struct, resolve embedded types recursively from the registry, compute current size (with padding), compute optimal size, and generate the `optimized_def` string with a `// Reordered for optimal memory alignment` header.

The `Arch` enum (`Amd64` | `Arm64`) selects the type-size/alignment table. Generic type parameters and unresolved types default to size 8 / align 8 and set `approximate = true`.

**TypeScript pipeline (`analyze_ts_files`)**

Single-pass analysis using the V8 type-size model:

The `Arch` enum (`Amd64` | `Arm64`) selects the type-size/alignment table. Current tables are identical for both 64-bit targets. Generic type parameters and unresolved types default to size 8 / align 8 and are flagged `has_generics = true`.
| TypeScript type | V8 representation | Size | Align |
|---|---|---|---|
| `number` | Unboxed Double | 8 B | 8 |
| `any`, `unknown`, `never`, `void` | Tagged pointer (conservative) | 4 B | 4 |
| Everything else | Tagged/compressed pointer | 4 B | 4 |

**Type table (amd64 / arm64)**
Types with generics (`has_generics`) or that extend/implement other types (`has_embedded`) are flagged `approximate = true`. The `declaration_kind` field on `AnalyzedStruct` is set to `"class"`, `"interface"`, or `"type"`. The `optimized_def` header is `// Reordered for optimal V8 memory layout`.

**Go type table (amd64 / arm64)**

| Go type(s) | Size (bytes) | Align (bytes) |
|---|---|---|
Expand All @@ -230,9 +264,34 @@ The `Arch` enum (`Amd64` | `Arm64`) selects the type-size/alignment table. Curre

---

## Tauri Command API
## Data & Log Locations

The bundle identifier is **`com.capabletechnology.lineup`**.

### SQLite database

The database file is created on first launch at `{app_data_dir}/lineup.db`.

| Platform | Path |
|---|---|
| macOS | `~/Library/Application Support/com.capabletechnology.lineup/lineup.db` |
| Linux | `$XDG_DATA_HOME/com.capabletechnology.lineup/lineup.db` (typically `~/.local/share/com.capabletechnology.lineup/lineup.db`) |
| Windows | `%APPDATA%\com.capabletechnology.lineup\lineup.db` (e.g. `C:\Users\<user>\AppData\Roaming\com.capabletechnology.lineup\lineup.db`) |

All commands are invoked from the frontend via `@tauri-apps/api/core` `invoke()`. Errors are returned as rejected promises (the Rust `Err(String)` maps to a JS string rejection).
Deleting the file resets all scan history. The app will re-create the schema automatically on next launch.

### Logs

Lineup does not use `tauri-plugin-log` — no log files are written to disk.

- **Development** (`pnpm tauri:dev`): Rust `println!`/`eprintln!` output appears in the terminal that launched the dev command. Frontend `console.*` output appears in the WebView DevTools (right-click → Inspect, or `Ctrl+Shift+I` / `Cmd+Option+I`).
- **Production**: stdout/stderr is captured by the host OS. Use the platform system log viewer to inspect it:

| Platform | How to view |
|---|---|
| macOS | **Console.app** → filter by `com.capabletechnology.lineup`, or `log stream --predicate 'process == "Lineup"'` in Terminal |
| Linux | `journalctl -f` (if launched via systemd) or redirect stdout/stderr when launching from the command line |
| Windows | **Event Viewer** → Windows Logs → Application, or launch from a terminal to capture stdout/stderr directly | via `@tauri-apps/api/core` `invoke()`. Errors are returned as rejected promises (the Rust `Err(String)` maps to a JS string rejection).

---

Expand Down Expand Up @@ -264,7 +323,8 @@ invoke<ScanSummary>('scan_repo', {
```ts
interface ScanOptions {
ignore_patterns: string[]; // regex strings; matched against repo-relative file paths
target_arch: string; // "amd64" | "arm64"
target_arch: string; // "amd64" | "arm64" (used only when language is "go")
language: string; // "GO" | "TS"
}
```

Expand Down Expand Up @@ -314,6 +374,7 @@ interface ScanSummary {
bytes_saved: number;
ignore_patterns: string[];
target_arch: string; // "amd64" | "arm64"
language: string; // "GO" | "TS"
}
```

Expand Down Expand Up @@ -364,6 +425,7 @@ interface StructSummary {
bytes_saved: number;
has_generics: boolean;
has_embedded: boolean;
declaration_kind: string; // "struct" | "class" | "interface" | "type"
}
```

Expand All @@ -389,10 +451,11 @@ interface StructDetail {
current_size: number;
optimal_size: number;
bytes_saved: number;
current_def: string; // original struct source
optimized_def: string; // reordered struct with "// Reordered for optimal memory alignment" header
current_def: string; // original source
optimized_def: string; // reordered source with comment header
has_generics: boolean;
has_embedded: boolean;
declaration_kind: string; // "struct" | "class" | "interface" | "type"
}
```

Expand Down Expand Up @@ -451,4 +514,4 @@ Run `pnpm format` and `pnpm lint` before submitting a pull request.
- Follow the output's suggestions to install the missing dependencies before re-running any build

**Database errors on startup**
- The SQLite file lives in the platform app-data directory. Delete `lineup.db` in that directory to reset state (all scan history will be lost).
- The SQLite file lives at the path shown in the [Data & Log Locations](#data--log-locations) section above. Delete `lineup.db` in that directory to reset state (all scan history will be lost).
Loading
Loading