Skip to content

chore: migrate from eslint+prettier to Vite+#8

Merged
jakeklassen merged 3 commits into
mainfrom
chore/vite-plus
Mar 13, 2026
Merged

chore: migrate from eslint+prettier to Vite+#8
jakeklassen merged 3 commits into
mainfrom
chore/vite-plus

Conversation

@jakeklassen

@jakeklassen jakeklassen commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace eslint and prettier with Vite+ unified toolchain (oxlint for linting, oxfmt for formatting)
  • Standardize all package lint scripts to vp lint / vp lint --fix
  • Add per-package .oxlintrc.json (with extends) and .oxfmtrc.json (full copies, no nested config support) for VS Code multi-root workspace
  • Configure VS Code workspace for oxc extension (type-aware linting, oxfmt as default formatter)
  • Tune lint rules: disable consistent-type-definitions and array-type, allow numbers in template expressions
  • Fix all lint warnings across the codebase (erasing ops, no-new-array, no-explicit-any disables, etc.)
  • Remove orphaned ecs-benchmark CI workflow
  • Delete stale docs — content moved to GitHub issues Performance improvement plan: Phases 2-3 #6 and Hierarchy and scene graph support #7

Test plan

  • vp lint passes with 0 errors
  • vp test passes
  • Pre-commit hook (vp check --fix) runs successfully
  • CI passes
  • VS Code shows diagnostics correctly in multi-root workspace

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Adopted a unified Vite+ toolchain for build, lint, test, and packaging workflows.
    • Enabled type-aware linting and formatter integration for consistent IDE/save-time fixes.
  • Chores

    • Migrated npm scripts to vp-based commands and switched dev tooling to the new ecosystem.
    • Consolidated formatting/linting configs and standardized project formatting rules.
  • Documentation

    • Added unified toolchain documentation and updated multiple READMEs; removed some legacy planning/benchmark docs.

Replace eslint and prettier with Vite+ unified toolchain (oxlint for
linting, oxfmt for formatting). Standardize all package lint scripts
to use `vp lint` / `vp lint --fix`. Add per-package oxlint/oxfmt
configs for VS Code multi-root workspace support. Remove orphaned
ecs-benchmark CI workflow. Delete stale docs (benchmark.md,
hierarchy-ideas.md, performance-plan.md, tasks.md) — content moved
to GitHub issues #6 and #7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify

netlify Bot commented Mar 13, 2026

Copy link
Copy Markdown

Deploy Preview for objecs ready!

Name Link
🔨 Latest commit 8684cfb
🔍 Latest deploy log https://app.netlify.com/projects/objecs/deploys/69b41d6bb0df2400083c3588
😎 Deploy Preview https://deploy-preview-8--objecs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces the ESLint/Prettier/Vitest toolchain with a vite-plus (VP) centered toolchain: adds root VP/vite-plus config, OxLint/OxFmt configs, per-package ox configs, updates scripts/dependencies to VP/vite-plus, normalizes formatting and lint directives, and updates documentation and workspace/editor settings.

Changes

Cohort / File(s) Summary
Root VP & Lint Config
vite.config.ts, .oxlintrc.json, .oxfmtrc.json
Adds centralized VP-based config and comprehensive OxLint/OxFmt rules (type-aware linting, many rule overrides) and formatting defaults.
Hooks & Skills
.vite-hooks/pre-commit, .claude/skills/vite-plus
Adds pre-commit hook invoking vp staged and a vite-plus skill reference.
Workspace & Editor
workspace.code-workspace, .vscode/settings.json, pnpm-workspace.yaml
Updates workspace and editor settings to use OxC formatter, enable format-on-save/fixes, and adds workspace pnpm metadata.
Root/package.json & Scripts
package.json, packages/*/package.json (multiple packages listed)
Replaces eslint/prettier/vitest scripts with vp equivalents; adds vite-plus/vp-related deps and removes ESLint/Prettier packages.
Per-package Ox configs
packages/*/.oxlintrc.json, packages/*/.oxfmtrc.json (benchmark, examples, ecs-benchmark, objecs, perf-proofs, game-benchmark)
Adds package-level OxLint/OxFmt files that extend the root config.
Removed ESLint configs / Prettier files
eslint.config.mjs (deleted), .prettierrc (deleted), various packages/*/.prettierrc (deleted)
Deletes legacy ESLint and Prettier configuration files.
Vite -> Vite-Plus migration
packages/examples/vite.config.ts, packages/objecs/vite.config.ts, packages/examples/src/vite-env.d.ts, vite.config.ts
Switches imports from vite/vite types to vite-plus/vite-plus client and adds pack/lint/format config in root vite.config.ts.
Tests & Test Runner imports
packages/objecs/src/archetype.test.ts, packages/objecs/src/world.test.ts
Updates test imports from vitest to vite-plus/test.
Codebase lint pragma changes
many packages/examples/**, packages/game-benchmark/**, packages/perf-proofs/**
Replaces eslint-disable-next-line with oxlint-disable-next-line and minor refactors (Array.from, small API usage tweaks).
Docs added/removed/edited
AGENTS.md (new), CLAUDE.md, benchmark.md (removed block), hierarchy-ideas.md (deleted), performance-plan.md (deleted), tasks.md (removed item)
Adds Vite+ toolchain docs and removes obsolete benchmark/performance planning docs.
Formatting/HTML normalization
packages/examples/**/index.html, packages/examples/public/css/demo.css, packages/examples/src/style.css
Normalizes DOCTYPE casing, indentation, string quoting, and general formatting across HTML/CSS files.
Build tooling changes for packages/objecs
packages/objecs/package.json, packages/objecs/tsup.config.ts (deleted), packages/objecs/vite.config.ts
Shifts build/test/lint scripts to VP; removes tsup config and updates exports/module/type entries and pack config.
CI/workflow removal
packages/ecs-benchmark/.github/workflows/main.yml (deleted)
Removes the package-level GitHub Actions workflow.

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer (editor/terminal)
  participant Git as Git (pre-commit)
  participant VP as vp CLI (vite-plus)
  participant OxLint as OxLint
  participant Editor as Editor/VSCode (oxc.vscode)

  Dev->>Git: git commit
  Git->>VP: run "vp staged"
  VP->>OxLint: lint staged files (type-aware)
  OxLint-->>VP: lint results & fixes
  VP->>Git: allow/deny commit
  Dev->>Editor: save file
  Editor->>VP: source.fixAll.oxc / format on save
  VP->>OxLint: apply fixes & format
  OxLint-->>Editor: updated file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I nibbled at lint rules, hopped through configs bright,
Swapped ESLint for VP and OxC by moonlight.
Pre-commit I planted, formatters in tow,
A tidy code burrow where clean breezes blow.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: migrating the project's tooling from ESLint and Prettier to Vite+. The title is concise, specific, and clearly conveys the main objective without unnecessary details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/vite-plus
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

jakeklassen and others added 2 commits March 13, 2026 10:08
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move build config into vite.config.ts pack section, update package.json
exports to match tsdown output extensions (.mjs/.cjs/.d.mts/.d.cts),
and remove tsup dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/examples/src/lib/event-emitter.ts (1)

28-35: ⚠️ Potential issue | 🔴 Critical

Spread args when invoking listeners.

At line 34, listener(args) passes the tuple as a single argument instead of spreading it, causing listeners to receive the wrong payload shape. Single-arg events will receive an array instead of the value; multi-arg events will fail entirely.

Proposed fix
 	public emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>) {
 		if (!this.has(event)) {
 			return;
 		}

 		for (const listener of this.get(event) ?? []) {
-			listener(args);
+			listener(...args);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/examples/src/lib/event-emitter.ts` around lines 28 - 35, The emit
method is calling listener(args) which passes the entire args tuple as one
parameter; change the invocation in EventEmitter.emit to spread the tuple so
listeners receive the actual parameters by calling listener(...args) (locate the
emit method and the loop over this.get(event) listeners where listener(args) is
used).
CLAUDE.md (1)

20-57: ⚠️ Potential issue | 🟡 Minor

Update "Common Commands" section to use vp workflow instead of pnpm directly.

Lines 20–57 teach developers to use pnpm install, pnpm lint, and pnpm --filter ... commands, but line 228 explicitly states "Do not use pnpm, npm, or Yarn directly." Replace the Common Commands section with vp equivalents (vp install, vp lint, vp run, etc.) to align with the Vite+ guidance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 20 - 57, Update the "Common Commands" examples under
the "Root-level", "Core Library (packages/objecs)", "Examples
(packages/examples)", "Benchmarks", and "Game Benchmarks" sections to use the
project's vp workflow instead of pnpm: replace occurrences like `pnpm install`,
`pnpm lint`, `pnpm lint:fix`, `pnpm --filter <pkg> build/test/run/lint`, and
`pnpm --filter <pkg> start` with the corresponding `vp` commands (`vp install`,
`vp lint`, `vp lint:fix`, `vp --filter <pkg> run build` / `vp --filter <pkg> run
test` / `vp --filter <pkg> run` / `vp --filter <pkg> run lint`, or `vp --filter
<pkg> run start`), and adjust single-command examples (e.g., `pnpm --filter
examples dev` → `vp --filter examples run dev`) so the documentation aligns with
the "Do not use pnpm, npm, or Yarn directly" guidance.
🧹 Nitpick comments (8)
packages/game-benchmark/src/games/mutation/miniplex/index.ts (1)

124-133: Type assertion is safe but could be cleaner.

The type assertion (entity as Entity & { position: { x: number } }).position.x works at runtime because all entities are created with position (lines 71-80). However, iterating over queries that don't require position (e.g., shielded, poisonedEntities) with this assumption couples the iteration logic to the entity creation logic.

For a benchmark file this is acceptable, but consider documenting this invariant with a comment if it's intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/game-benchmark/src/games/mutation/miniplex/index.ts` around lines
124 - 133, The iteration uses a type assertion on entity to read position.x
inside iterateSystem (sum += (entity as Entity & { position: { x: number }
}).position.x) which relies on all entities being created with position (lines
~71-80); make this invariant explicit by adding a short comment above the
iterateSystem block (or immediately before the assertion) stating that every
entity produced by the benchmark always has a position property so the cast is
intentional and safe, mentioning the related queries/collections (queries,
shielded, poisonedEntities) to avoid surprising future readers.
packages/examples/src/demos/shmup/systems/sprite-animation-system.ts (1)

30-35: Guard these frame lookups instead of suppressing them.

Both indexed reads can still come back undefined at runtime, and the next assignment would propagate that bad state into the sprite data. A small guard/invariant here is safer than carrying the !s forward.

Suggested change
-				const frameIndex =
-					// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-					spriteAnimation.frameSequence[spriteAnimation.currentFrame]!;
-
-				// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-				const frame = spriteAnimation.frames[frameIndex]!;
+				const frameIndex =
+					spriteAnimation.frameSequence[spriteAnimation.currentFrame];
+				const frame =
+					frameIndex === undefined
+						? undefined
+						: spriteAnimation.frames[frameIndex];
+				if (frame === undefined) {
+					world.removeEntityComponents(entity, "spriteAnimation");
+					continue;
+				}

As per coding guidelines, Remove components via world.removeEntityComponents(entity, 'component') which uses delete; never set a component value to undefined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/examples/src/demos/shmup/systems/sprite-animation-system.ts` around
lines 30 - 35, The indexed reads using
spriteAnimation.frameSequence[spriteAnimation.currentFrame] and
spriteAnimation.frames[frameIndex] must be guarded instead of using non-null
assertions: check that frameIndex is not undefined and that frame exists before
assigning into sprite data; if either lookup fails, do not set
spriteAnimation.currentFrame/frame or sprite data—instead remove the sprite
component using world.removeEntityComponents(entity, 'spriteAnimation') (or the
appropriate component name) to avoid storing undefined. Update the logic in
sprite-animation-system.ts around the frameIndex/frame resolution (symbols:
spriteAnimation, frameSequence, currentFrame, frames, frameIndex, frame) to
perform these runtime checks and call world.removeEntityComponents when the
invariants are violated.
packages/perf-proofs/src/array/for-of-vs-for-index-vs-foreach.ts (1)

12-15: Model the benchmark entity from EntityBase.

This benchmark is meant to approximate ECS-style entity iteration, but the local Entity shape drops the base entity contract entirely. Extending EntityBase here keeps the measured object layout closer to the entities used elsewhere in the repo.

Suggested change
+import type { EntityBase } from "objecs";
 import { run, bench, summary } from "mitata";
 
-type Entity = {
+type Entity = EntityBase & {
 	position: { x: number; y: number };
 	velocity: { x: number; y: number };
 };

As per coding guidelines, Entities must extend EntityBase and components must be JSON-compatible values (no functions/symbols).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/perf-proofs/src/array/for-of-vs-for-index-vs-foreach.ts` around
lines 12 - 15, Replace the local Entity type with one that extends the repo's
EntityBase so the benchmark uses the same entity layout as other code: import
EntityBase if necessary and declare type Entity = EntityBase & { position: { x:
number; y: number }; velocity: { x: number; y: number } }; ensure both position
and velocity remain plain JSON-compatible objects (no functions/symbols) so the
Entity shape used in for-of-vs-for-index-vs-foreach.ts matches the ECS-style
entities elsewhere.
packages/examples/src/demos/shmup/systems/text-blink-animation-system.ts (1)

27-32: Avoid writing an unresolved color back into the component.

These !-suppressed lookups can still yield undefined from a bad sequence index or missing color key. Guard the lookup and keep the previous color when the animation data is inconsistent.

Suggested change
-			const colorSequence =
-				// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-				textBlinkAnimation.colorSequence[textBlinkAnimation.currentColorIndex]!;
-
-			// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-			textBlinkAnimation.color = textBlinkAnimation.colors[colorSequence]!;
+			const colorSequence =
+				textBlinkAnimation.colorSequence[textBlinkAnimation.currentColorIndex];
+			const color =
+				colorSequence === undefined
+					? undefined
+					: textBlinkAnimation.colors[colorSequence];
+			if (color === undefined) {
+				continue;
+			}
+			textBlinkAnimation.color = color;

As per coding guidelines, Remove components via world.removeEntityComponents(entity, 'component') which uses delete; never set a component value to undefined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/examples/src/demos/shmup/systems/text-blink-animation-system.ts`
around lines 27 - 32, Guard the two non-null assertions when resolving the next
blink color: read const seqKey =
textBlinkAnimation.colorSequence[textBlinkAnimation.currentColorIndex] and const
resolved = textBlinkAnimation.colors[seqKey], then only assign
textBlinkAnimation.color = resolved if resolved !== undefined; otherwise leave
the existing textBlinkAnimation.color unchanged (do not set it to undefined or
delete the property). This uses the existing identifiers
textBlinkAnimation.colorSequence, textBlinkAnimation.currentColorIndex,
textBlinkAnimation.colors and textBlinkAnimation.color to locate and fix the
code.
pnpm-workspace.yaml (1)

11-14: Pin the shared Vite+ catalog entries instead of latest.

Using mutable tags here means a clean install or lockfile refresh can move the lint/build toolchain for the whole workspace without any manifest change. For a repo-wide tooling migration, it’s safer to pin the tested versions and bump them intentionally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pnpm-workspace.yaml` around lines 11 - 14, The catalog entries "vite",
"vitest", and "vite-plus" currently point to mutable tags
(npm:`@voidzero-dev/vite-plus-core`@latest,
npm:`@voidzero-dev/vite-plus-test`@latest, vite-plus: latest); change each to a
pinned version string (e.g. replace "@latest" and "latest" with exact semver
versions like "1.2.3" or the commit SHA you verified) so the workspace toolchain
is deterministic, then run pnpm install to update the lockfile and commit the
updated pnpm-lock.yaml alongside the manifest change.
.oxlintrc.json (1)

177-177: Move @typescript-eslint/prefer-for-of exception to package-level override in .oxlintrc.json.

The global "error" setting at line 177 forces 8 inline oxlint-disable-next-line suppressions across packages/perf-proofs/**. These files benchmark different iteration strategies, so the rule enforces noise rather than guidance. A package-level override eliminates the scattered disables:

Suggested override
 	"overrides": [
+		{
+			"files": ["packages/perf-proofs/**/*.{ts,tsx,mts,cts}"],
+			"rules": {
+				"@typescript-eslint/prefer-for-of": "off"
+			}
+		},
 		{
 			"files": ["**/*.{js,mjs,cjs,ts,mts,cts}"],
 			"rules": {
 				"depend/ban-dependencies": "error"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.oxlintrc.json at line 177, The project-wide rule
"@typescript-eslint/prefer-for-of" is set to "error" in .oxlintrc.json causing
multiple inline oxlint-disable-next-line comments in packages/perf-proofs/**;
remove or relax that global rule and add a package-level override for the
perf-proofs package that disables or changes "@typescript-eslint/prefer-for-of"
for "packages/perf-proofs/**" so benchmarks can use alternative iteration
without inline suppressions; update the "overrides" array in .oxlintrc.json to
include a new object targeting "packages/perf-proofs/**" with the rule adjusted
and remove the need for the eight inline disables.
packages/game-benchmark/src/games/mutation/objecs/index.ts (1)

92-95: Tighten the factory return type instead of asserting non-null here.

Entity[OptionalComponent] is optional, so the ! suppresses a real type mismatch. Use a mapped type to ensure factories return non-null values, aligning with the guideline to never set component values to undefined.

♻️ Suggested type tightening
-	const componentFactories: Record<
-		OptionalComponent,
-		() => Entity[OptionalComponent]
-	> = {
+	const componentFactories: {
+		[K in OptionalComponent]: () => NonNullable<Entity[K]>;
+	} = {
 		shield: () => ({ strength: 10 + Math.random() * 90 }),
 		poisoned: () => ({ tickDamage: 1 + Math.random() * 5 }),
 		stunned: () => ({ duration: 0.5 + Math.random() * 2 }),
 		buff: () => ({ multiplier: 1.1 + Math.random() * 0.9 }),
 	};
@@
 			} else {
 				world.addEntityComponents(
 					entity,
 					comp,
-					// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-					componentFactories[comp]()!,
+					componentFactories[comp](),
 				);
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/game-benchmark/src/games/mutation/objecs/index.ts` around lines 92 -
95, The factory map componentFactories currently types return values as
Entity[OptionalComponent] (which is optional) and relies on non-null assertions;
change the return type to guarantee non-null by using a mapped/indexed type such
as mapping over OptionalComponent (e.g. [K in OptionalComponent]: () =>
NonNullable<Entity[K]>) so each factory returns a concrete component value (not
undefined), and remove any trailing `!` assertions in the factory
implementations to satisfy the "never undefined" guideline; keep references to
componentFactories, OptionalComponent and Entity when updating the type.
packages/objecs/vite.config.ts (1)

2-5: Consider adding a brief explanation for the lint suppression.

The oxlint-disable-next-line comment suppresses typescript/no-unsafe-assignment. While this may be necessary due to defineConfig's return type inference, a brief inline comment explaining why it's needed would help future maintainers understand the suppression isn't masking a real issue.

📝 Suggested clarification
-// oxlint-disable-next-line typescript/no-unsafe-assignment
+// oxlint-disable-next-line typescript/no-unsafe-assignment -- defineConfig returns untyped config object
 const config = defineConfig({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/objecs/vite.config.ts` around lines 2 - 5, Add a short inline
rationale immediately after the oxlint-disable-next-line
typescript/no-unsafe-assignment comment explaining why the suppression is
necessary (e.g., defineConfig's generic/third-party return type causes a false
positive and the assignment is safe), so future maintainers know this isn't
hiding a real type bug; reference the suppression and the defineConfig usage in
your note and keep it concise (one sentence) describing the specific reason and
that it was reviewed/intentional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vscode/settings.json:
- Line 2: The settings entry "js/ts.preferences.importModuleSpecifierEnding" is
not recognized by VS Code; replace it with the documented keys
"javascript.preferences.importModuleSpecifierEnding" and
"typescript.preferences.importModuleSpecifierEnding" and set both to the same
value ("js") so the preference is applied for both JS and TS editors; update the
.vscode/settings.json entry that currently contains
"js/ts.preferences.importModuleSpecifierEnding" accordingly.

In `@CLAUDE.md`:
- Around line 228-234: The guidance contradicts Oxc docs: update the bullet
about "Type-Aware Linting" to state definitively whether oxlint-tsgolint must be
present or is auto-managed by Vite+; specifically either (A) change the sentence
to "Vite+ automatically installs and manages oxlint-tsgolint for type-aware
linting, so you do not need to add it manually" or (B) change it to "Install
oxlint-tsgolint as a devDependency (e.g., oxlint-tsgolint@0.16.0) to enable vp
lint --type-aware", and ensure the term "oxlint-tsgolint" and the command "vp
lint --type-aware" are mentioned exactly so readers know the dependency and the
command to run.

In `@packages/examples/src/demos/basic/index.html`:
- Around line 19-20: The CSS block setting "width: 640;" and "height: 480;" is
invalid and ignored by browsers; update the CSS for the canvas (the rules
containing width: 640 and height: 480) to use explicit units (e.g. "width:
640px;" and "height: 480px;") or remove those declarations entirely since the
<canvas> element already has width="640" and height="480" HTML attributes that
determine its drawing size.

In `@packages/examples/src/demos/shmup/components/text-blink-animation.ts`:
- Around line 14-16: The code currently uses non-null assertions on
colorSequence/currentColorIndex and durationMs which can yield undefined color
or Infinity frameRate; update the logic in the text-blink-animation function to
defensively validate inputs: ensure colorSequence is non-empty (fallback to a
safe default sequence), clamp or validate currentColorIndex against
colorSequence.length so colors[colorSequence[currentColorIndex]] cannot be
undefined, and ensure durationMs and computed divisor (colorSequence.length) are
> 0 (fallback to a sensible positive value) before computing frameRate; replace
the "!" assertions with safe access and explicit defaults so the returned
component never contains undefined values.

In `@packages/objecs/src/archetype.ts`:
- Around line 146-152: The chained .without() currently overwrites prior
exclusions by passing only the new components array (the line constructing the
new archetype uses "without: components as any"); modify the creation so it
preserves and merges prior exclusions with the newly-specified ones (e.g.,
combine this.#without with components into a deduplicated list/set) before
assigning to the new archetype's without field so successive
.without("b").without("c") retains both exclusions; update the code around the
Archetype cloning/creation (the method named without / the constructor call that
sets world, components, without) to perform this merge and dedupe.

In `@packages/perf-proofs/src/map-set/map-vs-object-lookup.ts`:
- Line 16: The object lookup in the "known-hit" loop should be a pure lookup to
match the Map side: replace the nullish coalescing use (obj[k] ?? 0) with a
non-null assertion (obj[k]!) so the benchmark measures the same pure read as
map.get(k)!; update the loop that compares obj and map lookups to use obj[k]!
(keeping the existing obj: Record<string, number | undefined> declaration).

In `@vite.config.ts`:
- Around line 191-201: The current override uses jsPlugins:
["eslint-plugin-depend"] which the Oxlint language server ignores, so the
depend/ban-dependencies rule only appears in CLI; to fix, move the plugin into
ESLint's standard plugin configuration (e.g., add "plugins": ["depend"] inside
the same override or at top-level) and remove jsPlugins, ensure the rule
"depend/ban-dependencies" remains in the override's rules, and confirm the
plugin is installed (package.json) so VS Code LSP will load and surface the rule
diagnostics.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 20-57: Update the "Common Commands" examples under the
"Root-level", "Core Library (packages/objecs)", "Examples (packages/examples)",
"Benchmarks", and "Game Benchmarks" sections to use the project's vp workflow
instead of pnpm: replace occurrences like `pnpm install`, `pnpm lint`, `pnpm
lint:fix`, `pnpm --filter <pkg> build/test/run/lint`, and `pnpm --filter <pkg>
start` with the corresponding `vp` commands (`vp install`, `vp lint`, `vp
lint:fix`, `vp --filter <pkg> run build` / `vp --filter <pkg> run test` / `vp
--filter <pkg> run` / `vp --filter <pkg> run lint`, or `vp --filter <pkg> run
start`), and adjust single-command examples (e.g., `pnpm --filter examples dev`
→ `vp --filter examples run dev`) so the documentation aligns with the "Do not
use pnpm, npm, or Yarn directly" guidance.

In `@packages/examples/src/lib/event-emitter.ts`:
- Around line 28-35: The emit method is calling listener(args) which passes the
entire args tuple as one parameter; change the invocation in EventEmitter.emit
to spread the tuple so listeners receive the actual parameters by calling
listener(...args) (locate the emit method and the loop over this.get(event)
listeners where listener(args) is used).

---

Nitpick comments:
In @.oxlintrc.json:
- Line 177: The project-wide rule "@typescript-eslint/prefer-for-of" is set to
"error" in .oxlintrc.json causing multiple inline oxlint-disable-next-line
comments in packages/perf-proofs/**; remove or relax that global rule and add a
package-level override for the perf-proofs package that disables or changes
"@typescript-eslint/prefer-for-of" for "packages/perf-proofs/**" so benchmarks
can use alternative iteration without inline suppressions; update the
"overrides" array in .oxlintrc.json to include a new object targeting
"packages/perf-proofs/**" with the rule adjusted and remove the need for the
eight inline disables.

In `@packages/examples/src/demos/shmup/systems/sprite-animation-system.ts`:
- Around line 30-35: The indexed reads using
spriteAnimation.frameSequence[spriteAnimation.currentFrame] and
spriteAnimation.frames[frameIndex] must be guarded instead of using non-null
assertions: check that frameIndex is not undefined and that frame exists before
assigning into sprite data; if either lookup fails, do not set
spriteAnimation.currentFrame/frame or sprite data—instead remove the sprite
component using world.removeEntityComponents(entity, 'spriteAnimation') (or the
appropriate component name) to avoid storing undefined. Update the logic in
sprite-animation-system.ts around the frameIndex/frame resolution (symbols:
spriteAnimation, frameSequence, currentFrame, frames, frameIndex, frame) to
perform these runtime checks and call world.removeEntityComponents when the
invariants are violated.

In `@packages/examples/src/demos/shmup/systems/text-blink-animation-system.ts`:
- Around line 27-32: Guard the two non-null assertions when resolving the next
blink color: read const seqKey =
textBlinkAnimation.colorSequence[textBlinkAnimation.currentColorIndex] and const
resolved = textBlinkAnimation.colors[seqKey], then only assign
textBlinkAnimation.color = resolved if resolved !== undefined; otherwise leave
the existing textBlinkAnimation.color unchanged (do not set it to undefined or
delete the property). This uses the existing identifiers
textBlinkAnimation.colorSequence, textBlinkAnimation.currentColorIndex,
textBlinkAnimation.colors and textBlinkAnimation.color to locate and fix the
code.

In `@packages/game-benchmark/src/games/mutation/miniplex/index.ts`:
- Around line 124-133: The iteration uses a type assertion on entity to read
position.x inside iterateSystem (sum += (entity as Entity & { position: { x:
number } }).position.x) which relies on all entities being created with position
(lines ~71-80); make this invariant explicit by adding a short comment above the
iterateSystem block (or immediately before the assertion) stating that every
entity produced by the benchmark always has a position property so the cast is
intentional and safe, mentioning the related queries/collections (queries,
shielded, poisonedEntities) to avoid surprising future readers.

In `@packages/game-benchmark/src/games/mutation/objecs/index.ts`:
- Around line 92-95: The factory map componentFactories currently types return
values as Entity[OptionalComponent] (which is optional) and relies on non-null
assertions; change the return type to guarantee non-null by using a
mapped/indexed type such as mapping over OptionalComponent (e.g. [K in
OptionalComponent]: () => NonNullable<Entity[K]>) so each factory returns a
concrete component value (not undefined), and remove any trailing `!` assertions
in the factory implementations to satisfy the "never undefined" guideline; keep
references to componentFactories, OptionalComponent and Entity when updating the
type.

In `@packages/objecs/vite.config.ts`:
- Around line 2-5: Add a short inline rationale immediately after the
oxlint-disable-next-line typescript/no-unsafe-assignment comment explaining why
the suppression is necessary (e.g., defineConfig's generic/third-party return
type causes a false positive and the assignment is safe), so future maintainers
know this isn't hiding a real type bug; reference the suppression and the
defineConfig usage in your note and keep it concise (one sentence) describing
the specific reason and that it was reviewed/intentional.

In `@packages/perf-proofs/src/array/for-of-vs-for-index-vs-foreach.ts`:
- Around line 12-15: Replace the local Entity type with one that extends the
repo's EntityBase so the benchmark uses the same entity layout as other code:
import EntityBase if necessary and declare type Entity = EntityBase & {
position: { x: number; y: number }; velocity: { x: number; y: number } }; ensure
both position and velocity remain plain JSON-compatible objects (no
functions/symbols) so the Entity shape used in for-of-vs-for-index-vs-foreach.ts
matches the ECS-style entities elsewhere.

In `@pnpm-workspace.yaml`:
- Around line 11-14: The catalog entries "vite", "vitest", and "vite-plus"
currently point to mutable tags (npm:`@voidzero-dev/vite-plus-core`@latest,
npm:`@voidzero-dev/vite-plus-test`@latest, vite-plus: latest); change each to a
pinned version string (e.g. replace "@latest" and "latest" with exact semver
versions like "1.2.3" or the commit SHA you verified) so the workspace toolchain
is deterministic, then run pnpm install to update the lockfile and commit the
updated pnpm-lock.yaml alongside the manifest change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8dd8e256-4465-4636-9d83-ff726809817a

📥 Commits

Reviewing files that changed from the base of the PR and between 6251bb9 and 452591a.

⛔ Files ignored due to path filters (2)
  • packages/examples/public/js/feather.min.js is excluded by !**/*.min.js
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (101)
  • .claude/skills/vite-plus
  • .oxfmtrc.json
  • .oxlintrc.json
  • .prettierrc
  • .vite-hooks/pre-commit
  • .vscode/settings.json
  • AGENTS.md
  • CLAUDE.md
  • benchmark.md
  • eslint.config.mjs
  • hierarchy-ideas.md
  • jekyll.config.yml
  • netlify.toml
  • package.json
  • packages/benchmark/.oxfmtrc.json
  • packages/benchmark/.oxlintrc.json
  • packages/benchmark/.prettierrc
  • packages/benchmark/.vscode/settings.json
  • packages/benchmark/package.json
  • packages/benchmark/renovate.json
  • packages/ecs-benchmark/.github/workflows/main.yml
  • packages/ecs-benchmark/.oxfmtrc.json
  • packages/ecs-benchmark/.oxlintrc.json
  • packages/ecs-benchmark/.prettierrc
  • packages/ecs-benchmark/package.json
  • packages/examples/.oxfmtrc.json
  • packages/examples/.oxlintrc.json
  • packages/examples/.prettierrc
  • packages/examples/index.html
  • packages/examples/package.json
  • packages/examples/public/css/demo.css
  • packages/examples/public/js/feather-init.js
  • packages/examples/src/demos/basic/index.html
  • packages/examples/src/demos/bouncy-rectangles/index.html
  • packages/examples/src/demos/debug-rendering/index.html
  • packages/examples/src/demos/debug-rendering/systems/sprite-animation-system.ts
  • packages/examples/src/demos/falling-sand/index.html
  • packages/examples/src/demos/falling-sand/main.ts
  • packages/examples/src/demos/pixel-text/index.html
  • packages/examples/src/demos/shmup/components/sprite-outline-animation.ts
  • packages/examples/src/demos/shmup/components/text-blink-animation.ts
  • packages/examples/src/demos/shmup/content.ts
  • packages/examples/src/demos/shmup/enemy/switch-enemy-to-attach-mode.ts
  • packages/examples/src/demos/shmup/index.html
  • packages/examples/src/demos/shmup/scene.ts
  • packages/examples/src/demos/shmup/systems/destroy-boss-event-system.ts
  • packages/examples/src/demos/shmup/systems/sprite-animation-system.ts
  • packages/examples/src/demos/shmup/systems/sprite-outline-animation-system.ts
  • packages/examples/src/demos/shmup/systems/sprite-outline-rendering-system.ts
  • packages/examples/src/demos/shmup/systems/text-blink-animation-system.ts
  • packages/examples/src/demos/sprite-animation/index.html
  • packages/examples/src/demos/sprite-tweening/index.html
  • packages/examples/src/demos/sprite-tweening/main.ts
  • packages/examples/src/lib/canvas.ts
  • packages/examples/src/lib/event-emitter.ts
  • packages/examples/src/lib/pixel-text/renderer.ts
  • packages/examples/src/style.css
  • packages/examples/src/vite-env.d.ts
  • packages/examples/tsconfig.json
  • packages/examples/vite.config.ts
  • packages/game-benchmark/.oxfmtrc.json
  • packages/game-benchmark/.oxlintrc.json
  • packages/game-benchmark/README.md
  • packages/game-benchmark/package.json
  • packages/game-benchmark/src/games/ant-simulation/miniplex/index.ts
  • packages/game-benchmark/src/games/ant-simulation/objecs/index.ts
  • packages/game-benchmark/src/games/ant-simulation/systems/render-system.ts
  • packages/game-benchmark/src/games/ant-simulation/systems/steering-system.ts
  • packages/game-benchmark/src/games/boids/miniplex/index.ts
  • packages/game-benchmark/src/games/boids/objecs/index.ts
  • packages/game-benchmark/src/games/boids/systems/explosion-system.ts
  • packages/game-benchmark/src/games/mutation/miniplex/index.ts
  • packages/game-benchmark/src/games/mutation/objecs/index.ts
  • packages/game-benchmark/src/index.ts
  • packages/game-benchmark/src/profiler.ts
  • packages/objecs/.oxfmtrc.json
  • packages/objecs/.oxlintrc.json
  • packages/objecs/.prettierrc
  • packages/objecs/README.md
  • packages/objecs/package.json
  • packages/objecs/src/archetype.test.ts
  • packages/objecs/src/archetype.ts
  • packages/objecs/src/world.test.ts
  • packages/objecs/src/world.ts
  • packages/objecs/vite.config.ts
  • packages/perf-proofs/.oxfmtrc.json
  • packages/perf-proofs/.oxlintrc.json
  • packages/perf-proofs/package.json
  • packages/perf-proofs/src/array/every-vs-for-loop.ts
  • packages/perf-proofs/src/array/for-of-vs-for-index-vs-foreach.ts
  • packages/perf-proofs/src/array/fused-every-some.ts
  • packages/perf-proofs/src/iteration/iterator-protocol-overhead.ts
  • packages/perf-proofs/src/map-set/map-vs-object-lookup.ts
  • packages/perf-proofs/src/set/set-alloc-vs-alternatives.ts
  • packages/perf-proofs/src/set/set-vs-array-iteration.ts
  • packages/perf-proofs/src/set/small-set-vs-small-array.ts
  • performance-plan.md
  • pnpm-workspace.yaml
  • tasks.md
  • vite.config.ts
  • workspace.code-workspace
💤 Files with no reviewable changes (11)
  • benchmark.md
  • eslint.config.mjs
  • packages/ecs-benchmark/.github/workflows/main.yml
  • .prettierrc
  • packages/examples/.prettierrc
  • packages/benchmark/.prettierrc
  • packages/objecs/.prettierrc
  • packages/ecs-benchmark/.prettierrc
  • performance-plan.md
  • hierarchy-ideas.md
  • tasks.md

Comment thread .vscode/settings.json
@@ -1,3 +1,21 @@
{
"typescript.preferences.importModuleSpecifierEnding": "js"
"js/ts.preferences.importModuleSpecifierEnding": "js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

VS Code javascript.preferences.importModuleSpecifierEnding typescript.preferences.importModuleSpecifierEnding documentation

💡 Result:

These VS Code settings control how auto-imports (IntelliSense / Quick Fix import) choose the ending of generated module specifiers (e.g. whether to drop index and/or include the .js extension):

  • javascript.preferences.importModuleSpecifierEnding (JavaScript) — valid values:

    • "auto" (default; uses project jsconfig), "minimal" (drop /index.js/), "index" (keep /index), "js" (include the .js extension). [1]
  • typescript.preferences.importModuleSpecifierEnding (TypeScript) — same intent; docs describe:

    • "auto", "minimal" (e.g. ./component/index.js./component), "index" (→ ./component/index), "js" (don’t shorten; include the .js or .ts extension). [2]

Example workspace/user settings:

{
  "javascript.preferences.importModuleSpecifierEnding": "js",
  "typescript.preferences.importModuleSpecifierEnding": "js"
}

VS Code also lists both settings among JS/TS preferences that can be configured per-language. [3]

Sources: [1] (code.visualstudio.com) [2] (github.com) [3] (code.visualstudio.com)

Citations:


🏁 Script executed:

# Check if there's any VS Code settings reference or documentation in the repository
find . -type f -name "*.json" -o -name "*.md" | xargs grep -l "importModuleSpecifierEnding" 2>/dev/null | head -20

Repository: jakeklassen/objecs

Length of output: 85


🏁 Script executed:

cat .vscode/settings.json

Repository: jakeklassen/objecs

Length of output: 587


Use the documented JS and TS setting keys here.

VS Code documents separate javascript.preferences.importModuleSpecifierEnding and typescript.preferences.importModuleSpecifierEnding keys for this preference. The combined js/ts.preferences.importModuleSpecifierEnding entry is not recognized and will be ignored.

Suggested fix
-	"js/ts.preferences.importModuleSpecifierEnding": "js",
+	"javascript.preferences.importModuleSpecifierEnding": "js",
+	"typescript.preferences.importModuleSpecifierEnding": "js",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"js/ts.preferences.importModuleSpecifierEnding": "js",
"javascript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.importModuleSpecifierEnding": "js",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vscode/settings.json at line 2, The settings entry
"js/ts.preferences.importModuleSpecifierEnding" is not recognized by VS Code;
replace it with the documented keys
"javascript.preferences.importModuleSpecifierEnding" and
"typescript.preferences.importModuleSpecifierEnding" and set both to the same
value ("js") so the preference is applied for both JS and TS editors; update the
.vscode/settings.json entry that currently contains
"js/ts.preferences.importModuleSpecifierEnding" accordingly.

Comment thread CLAUDE.md
Comment on lines +228 to +234
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. They do not exist. Use `vp test` and `vp lint` instead.
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vp test` command, run it using `vp run test`.
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
- **Use Vite+ wrappers for one-off binaries:** Use `vp dlx` instead of package-manager-specific `dlx`/`npx` commands.
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let me check the exact content of CLAUDE.md around the mentioned lines
head -n 250 CLAUDE.md | tail -n +220

Repository: jakeklassen/objecs

Length of output: 1953


🌐 Web query:

oxlint-tsgolint type-aware linting requirement

💡 Result:

Oxlint’s type-aware linting is provided by the separate oxlint-tsgolint backend (tsgolint). To use it you must:

  • Install the extra dependency: oxlint-tsgolint@latest (dev dependency). [1]
  • Enable type-aware mode: run oxlint --type-aware (or set options.typeAware: true in the root config). [1]
  • Have a TypeScript project it can build type info for (it constructs TS “programs” via typescript-go). In practice that means it will use your tsconfig.json (defaults to ./tsconfig.json; you can pass a path with --tsconfig). [1], [2]
  • Meet the TypeScript compatibility requirement: the type-aware engine is powered by typescript-go and requires TypeScript 7.0+; some legacy tsconfig options may be unsupported. [1]

If you also want TypeScript compiler diagnostics during linting, add --type-check (or options.typeCheck: true). [1]

Sources: [1] Oxlint “Type-Aware Linting” docs, [2] Oxlint CLI docs.


🏁 Script executed:

# Check for oxlint-tsgolint in package.json
grep -A 5 -B 5 "oxlint-tsgolint" package.json 2>/dev/null || echo "Not found in package.json root"

Repository: jakeklassen/objecs

Length of output: 308


🏁 Script executed:

# Check if there are multiple package.json files
find . -name "package.json" -type f | head -20

Repository: jakeklassen/objecs

Length of output: 287


The type-aware linting guidance contradicts the actual setup and Oxc documentation.

The statement "There is no need to install oxlint-tsgolint" is inaccurate. Oxc's official documentation explicitly requires installing oxlint-tsgolint as a dev dependency for type-aware linting, and the repository already includes it (oxlint-tsgolint@0.16.0 in devDependencies). Either clarify that Vite+ automatically installs and manages oxlint-tsgolint as part of its type-aware setup, or revise the guidance to reflect the actual requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 228 - 234, The guidance contradicts Oxc docs: update
the bullet about "Type-Aware Linting" to state definitively whether
oxlint-tsgolint must be present or is auto-managed by Vite+; specifically either
(A) change the sentence to "Vite+ automatically installs and manages
oxlint-tsgolint for type-aware linting, so you do not need to add it manually"
or (B) change it to "Install oxlint-tsgolint as a devDependency (e.g.,
oxlint-tsgolint@0.16.0) to enable vp lint --type-aware", and ensure the term
"oxlint-tsgolint" and the command "vp lint --type-aware" are mentioned exactly
so readers know the dependency and the command to run.

Comment on lines +19 to +20
width: 640;
height: 480;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/examples/src/demos/basic/index.html | head -30

Repository: jakeklassen/objecs

Length of output: 1040


🏁 Script executed:

# Also verify CSS spec on unitless values
web_search

Repository: jakeklassen/objecs

Length of output: 112


🌐 Web query:

CSS unitless width height values invalid specification

💡 Result:

In CSS, width/height do not accept unitless numbers. Their value syntax is auto | <length-percentage> | …, so values like width: 100; or height: 20; are invalid and get ignored by the browser (the declaration is dropped). [1]

The main exception people remember is unitless zero: 0 may be written without a unit (so width: 0; is valid). [2]

One subtlety: even though width: 0; is valid, 0 inside calc() is still a <number>, not a <length>, so width: calc(0 + 5px) is invalid (mixing <number> and <length>). [3]

Sources

[1] CSS Sizing Module Level 3 – width/height value definition. (w3.org)
[2] MDN <length> – unit optional for 0. (developer.mozilla.org)
[3] CSS Values & Units (example showing calc(0 + 5px) invalid). (w3.org)

Citations:


🏁 Script executed:

grep -n "canvas" packages/examples/src/demos/basic/index.html

Repository: jakeklassen/objecs

Length of output: 138


Add units to the canvas size CSS or remove these declarations.

width: 640 and height: 480 are invalid CSS and are ignored by the browser. The <canvas> element already has width="640" and height="480" HTML attributes that set the size correctly, so either delete these CSS rules or add units: 640px and 480px.

💡 Suggested fix
-				width: 640;
-				height: 480;
+				width: 640px;
+				height: 480px;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
width: 640;
height: 480;
width: 640px;
height: 480px;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/examples/src/demos/basic/index.html` around lines 19 - 20, The CSS
block setting "width: 640;" and "height: 480;" is invalid and ignored by
browsers; update the CSS for the canvas (the rules containing width: 640 and
height: 480) to use explicit units (e.g. "width: 640px;" and "height: 480px;")
or remove those declarations entirely since the <canvas> element already has
width="640" and height="480" HTML attributes that determine its drawing size.

Comment on lines +14 to 16
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
const color = colors[colorSequence[currentColorIndex]!]!;
const frameRate = durationMs / 1_000 / colorSequence.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/examples/src/demos/shmup/components/text-blink-animation.ts

Repository: jakeklassen/objecs

Length of output: 852


🏁 Script executed:

cd packages/examples && find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: jakeklassen/objecs

Length of output: 1002


🏁 Script executed:

rg "textBlinkAnimationFactory" -A 5 -B 5

Repository: jakeklassen/objecs

Length of output: 20132


Add defensive guards to prevent undefined component values.

Lines 14-16 suppress non-null assertions on array access, relying on implicit assumptions that colorSequence is non-empty and indices are in bounds. While all current callers satisfy these invariants, the function should guard its inputs to ensure the returned component never contains undefined values, per the guideline that components must be JSON-compatible and set explicitly (never undefined). If colorSequence is empty or contains out-of-range indices, color becomes undefined despite the NonNullable return type, and frameRate becomes Infinity.

💡 Proposed fix
 export function textBlinkAnimationFactory({
 	colors,
 	colorSequence,
 	durationMs,
 }: {
 	colors: HexColor[];
 	colorSequence: number[];
 	durationMs: number;
 }): NonNullable<Entity["textBlinkAnimation"]> {
 	const delta = 0;
 	const currentColorIndex = 0;
-	// oxlint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-	const color = colors[colorSequence[currentColorIndex]!]!;
+	const initialColorIndex = colorSequence[currentColorIndex];
+	if (initialColorIndex === undefined) {
+		throw new Error("textBlinkAnimationFactory requires a non-empty colorSequence");
+	}
+	const color = colors[initialColorIndex];
+	if (color === undefined) {
+		throw new Error(`textBlinkAnimationFactory received an invalid color index: ${initialColorIndex}`);
+	}
 	const frameRate = durationMs / 1_000 / colorSequence.length;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
const color = colors[colorSequence[currentColorIndex]!]!;
const frameRate = durationMs / 1_000 / colorSequence.length;
const initialColorIndex = colorSequence[currentColorIndex];
if (initialColorIndex === undefined) {
throw new Error("textBlinkAnimationFactory requires a non-empty colorSequence");
}
const color = colors[initialColorIndex];
if (color === undefined) {
throw new Error(`textBlinkAnimationFactory received an invalid color index: ${initialColorIndex}`);
}
const frameRate = durationMs / 1_000 / colorSequence.length;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/examples/src/demos/shmup/components/text-blink-animation.ts` around
lines 14 - 16, The code currently uses non-null assertions on
colorSequence/currentColorIndex and durationMs which can yield undefined color
or Infinity frameRate; update the logic in the text-blink-animation function to
defensively validate inputs: ensure colorSequence is non-empty (fallback to a
safe default sequence), clamp or validate currentColorIndex against
colorSequence.length so colors[colorSequence[currentColorIndex]] cannot be
undefined, and ensure durationMs and computed divisor (colorSequence.length) are
> 0 (fallback to a sensible positive value) before computing frameRate; replace
the "!" assertions with safe access and explicit defaults so the returned
component never contains undefined values.

Comment on lines 146 to 152
entities,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
world: this.#world as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
components: this.#components as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
without: components as any,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/objecs/src/archetype.ts | head -200

Repository: jakeklassen/objecs

Length of output: 5341


Preserve prior exclusions when chaining .without().

The without() method passes only the newly specified components to exclude, discarding prior exclusions. When chaining .without("b").without("c"), the resulting archetype remembers only "c" in its exclusion list, allowing entities with "b" to match. This breaks the expected semantic of successive filters.

Minimal fix
+		const excluding = [...(this.#excluding ?? []), ...components];
+
 		const archetype = new Archetype<
 			SafeEntity<
 				Omit<Entity, (typeof components)[number]>,
 				Exclude<Components[number], (typeof components)[number]>
 			>,
 			Array<Exclude<Components[number], (typeof components)[number]>>
 		>({
 			entities,
 			world: this.#world as any,
 			components: this.#components as any,
-			without: components as any,
+			without: excluding as any,
 		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
entities,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
world: this.#world as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
components: this.#components as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// oxlint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
without: components as any,
const excluding = [...(this.#excluding ?? []), ...components];
const archetype = new Archetype<
SafeEntity<
Omit<Entity, (typeof components)[number]>,
Exclude<Components[number], (typeof components)[number]>
>,
Array<Exclude<Components[number], (typeof components)[number]>>
>({
entities,
// oxlint-disable-next-line `@typescript-eslint/no-unsafe-assignment`, `@typescript-eslint/no-explicit-any`
world: this.#world as any,
// oxlint-disable-next-line `@typescript-eslint/no-unsafe-assignment`, `@typescript-eslint/no-explicit-any`
components: this.#components as any,
// oxlint-disable-next-line `@typescript-eslint/no-unsafe-assignment`, `@typescript-eslint/no-explicit-any`
without: excluding as any,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/objecs/src/archetype.ts` around lines 146 - 152, The chained
.without() currently overwrites prior exclusions by passing only the new
components array (the line constructing the new archetype uses "without:
components as any"); modify the creation so it preserves and merges prior
exclusions with the newly-specified ones (e.g., combine this.#without with
components into a deduplicated list/set) before assigning to the new archetype's
without field so successive .without("b").without("c") retains both exclusions;
update the code around the Archetype cloning/creation (the method named without
/ the constructor call that sets world, components, without) to perform this
merge and dedupe.


const map = new Map<string, number>();
const obj: Record<string, number> = {};
const obj: Record<string, number | undefined> = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/perf-proofs/src/map-set/map-vs-object-lookup.ts

Repository: jakeklassen/objecs

Length of output: 2594


Keep the known-hit object loop as a pure lookup.

Line 16's wider type is fine for the miss case, but line 58's obj[k] ?? 0 adds an extra nullish coalescing operation only on the object side. This measures a different operation than map.get(k)!, biasing the benchmark. Use obj[k]! instead to keep both sides as pure lookups, matching the pattern used correctly in the "set + get cycle" benchmark (line 85).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/perf-proofs/src/map-set/map-vs-object-lookup.ts` at line 16, The
object lookup in the "known-hit" loop should be a pure lookup to match the Map
side: replace the nullish coalescing use (obj[k] ?? 0) with a non-null assertion
(obj[k]!) so the benchmark measures the same pure read as map.get(k)!; update
the loop that compares obj and map lookups to use obj[k]! (keeping the existing
obj: Record<string, number | undefined> declaration).

Comment thread vite.config.ts
Comment on lines +191 to +201
overrides: [
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
rules: {
"depend/ban-dependencies": "error",
},
jsPlugins: ["eslint-plugin-depend"],
env: {
browser: true,
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Oxlint jsPlugins language server LSP support documentation

💡 Result:

  • JS Plugins (Oxlint jsPlugins) docs: Oxlint can load ESLint-compatible JS plugins by listing them in .oxlintrc.json under jsPlugins, then enabling their rules under rules (supports specifiers like ./plugin.js, eslint-plugin-foo, @scope/eslint-plugin, plus optional aliases). [1]
  • LSP (Oxlint language server) usage: Editor integrations run Oxlint’s language server via oxlint --lsp (so oxlint must be installed in the project). [2]
  • LSP capabilities + configuration knobs (including config path): The oxc_language_server README documents supported LSP features (diagnostics, code actions like source.fixAll.oxc, formatting for oxfmt) and workspace options such as configPath (to point at a specific oxlint config / control nested config behavior), typeAware, and more. [3]

Sources
[1] (oxc.rs)
[2] (oxc.rs)
[3] (github.com)

Citations:


🌐 Web query:

Oxc configuration jsPlugins linter LSP editor integration

💡 Result:

Oxc (Oxlint) config: plugins vs jsPlugins

  • Native (Rust) plugins: configure with plugins in .oxlintrc.json. Note that setting plugins overwrites the default plugin set (so include everything you want enabled). [2]
  • JavaScript plugins (jsPlugins, experimental): add them under jsPlugins in .oxlintrc.json (string paths or objects with aliases). These are meant for ESLint-plugin compatibility / advanced integrations. [2], [3]
  • Important limitation: jsPlugins are currently not supported in the language server (LSP). If you rely on jsPlugins, they may work via CLI linting but won’t be reflected through editor LSP diagnostics. [2]

Also: Oxlint config files are JSON-only (but support comments, i.e., JSONC-style). [4]

LSP + editor integration (diagnostics / fix-on-save)

  • Editor extensions generally run oxlint --lsp from your project, so oxlint must be installed locally in the repo (not just globally). [1]
  • VS Code / Cursor: install the official extension oxc.oxc-vscode, and (optionally) enable fix-on-save via:
    • .vscode/extensions.json recommendations
    • .vscode/settings.json editor.codeActionsOnSave: { "source.fixAll.oxc": "always" } [1]

Practical takeaway

  • If you need the best editor/LSP experience today: prefer native plugins for rules you want surfaced in-editor. [2]
  • Use jsPlugins only when you specifically need JS/ESLint plugin compatibility—and expect that LSP/editor diagnostics won’t include those rules yet. [2], [3]

Sources:
[1] Oxc docs – Setup editors (Oxlint LSP + VS Code settings) (oxc.rs)
[2] Oxc docs – Oxlint configuration (plugins, jsPlugins, and LSP limitation) (oxc.rs)
[3] Oxc docs – JS Plugins guide (oxc.rs)
[4] Oxc docs – Config format notes (JSON w/ comments) (oxc.rs)

Citations:


🌐 Web query:

Oxlint VS Code extension --lsp flag behavior

💡 Result:

The Oxc (Oxlint) VS Code extension starts Oxlint in Language Server (LSP) mode by always appending --lsp to whatever “oxlint binary” it resolved for your workspace (so Oxlint must be installed locally in the project for the extension to work as intended). [1]

Concretely, the extension launches one of these command shapes: [2]

  • If the resolved “binary” looks like a Node script (e.g. ends in .js/.cjs/.mjs, or is the node_modules/<tool>/bin/<tool> style path), it runs:
    • node <binaryPath> --lsp
  • Otherwise it runs the binary directly:
    • <binaryPath> --lsp
    • On Windows it may run via a shell and quotes the path: "${binaryPath}" --lsp

So: in the VS Code extension, --lsp is not a “lint option”; it’s the switch that makes Oxlint act as a long-running LSP server over stdio (and the extension relies on that). [1], [2]

Sources: [1] [2]


eslint-plugin-depend via jsPlugins will not report in editor LSP, only in CLI.

jsPlugins are not supported in Oxlint's language server. The depend/ban-dependencies rule will run in vp lint (CLI) but never appear in VS Code diagnostics, undermining the PR's goal to standardize workspace diagnostics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vite.config.ts` around lines 191 - 201, The current override uses jsPlugins:
["eslint-plugin-depend"] which the Oxlint language server ignores, so the
depend/ban-dependencies rule only appears in CLI; to fix, move the plugin into
ESLint's standard plugin configuration (e.g., add "plugins": ["depend"] inside
the same override or at top-level) and remove jsPlugins, ensure the rule
"depend/ban-dependencies" remains in the override's rules, and confirm the
plugin is installed (package.json) so VS Code LSP will load and surface the rule
diagnostics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/objecs/package.json (1)

63-65: Pin concrete versions in the workspace catalog to improve reproducibility.

Lines 63-65 reference @latest aliases for vite and vitest. The workspace catalog in pnpm-workspace.yaml (lines 12-14) is the source of these moving targets. Update the catalog entries to use concrete versions instead of @latest, then ensure package manifests reference the catalog consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/objecs/package.json` around lines 63 - 65, The workspace is using
moving targets for tool aliases: change the workspace catalog entries that
define the catalog aliases for "vite" and "vitest" from using `@latest` to pinned
concrete versions, then update the package manifest entries "vite" and "vitest"
(and ensure "vite-plus" still references the catalog entry if intended) to
reference those catalog keys consistently; specifically edit the catalog
definitions (the catalog items that provided
npm:`@voidzero-dev/vite-plus-core`@latest and
npm:`@voidzero-dev/vite-plus-test`@latest) to use explicit semver or exact version
strings and ensure the package.json entries ("vite", "vitest") no longer point
to `@latest` but to the catalog alias or the concrete versions you pinned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/objecs/package.json`:
- Around line 63-65: The workspace is using moving targets for tool aliases:
change the workspace catalog entries that define the catalog aliases for "vite"
and "vitest" from using `@latest` to pinned concrete versions, then update the
package manifest entries "vite" and "vitest" (and ensure "vite-plus" still
references the catalog entry if intended) to reference those catalog keys
consistently; specifically edit the catalog definitions (the catalog items that
provided npm:`@voidzero-dev/vite-plus-core`@latest and
npm:`@voidzero-dev/vite-plus-test`@latest) to use explicit semver or exact version
strings and ensure the package.json entries ("vite", "vitest") no longer point
to `@latest` but to the catalog alias or the concrete versions you pinned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b68bf794-0277-4683-ac2b-7eff02a7e52c

📥 Commits

Reviewing files that changed from the base of the PR and between 452591a and 8684cfb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/objecs/package.json
  • packages/objecs/tsup.config.ts
  • packages/objecs/vite.config.ts
💤 Files with no reviewable changes (1)
  • packages/objecs/tsup.config.ts

@jakeklassen jakeklassen merged commit 0eb2092 into main Mar 13, 2026
7 checks passed
@jakeklassen jakeklassen deleted the chore/vite-plus branch March 13, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant