ci: add lint and typecheck workflow (closes #147) (#369)

* fix: resolve lint and type errors to enable CI

- Remove stale biome-ignore suppressions in bootstrap.ts, r3f.d.ts,
  and parametric-node-renderer.tsx (rules no longer fire).
- Replace forEach callbacks that return values with for...of loops
  in node-actions.ts and build-collider-world.ts
  (lint/suspicious/useIterableCallbackReturn).
- Rewrite assign-in-expression guards in ceiling/tool.tsx to explicit
  if-statements (lint/suspicious/noAssignInExpressions).
- Hoist useMemo/useCallback above early return in chimney/panel.tsx
  (lint/correctness/useHookAtTopLevel).
- Add `^build` to turbo check-types dependsOn so packages/core dist is
  up-to-date before type checking — this resolves all 32 type errors
  which were caused by stale dist, not missing symbols.

* ci: add lint and typecheck workflow on push/PR

Adds a quality gate that runs `bun run check` (Biome) and
`bun run check-types` (TypeScript) on every push and PR to main.
Uses concurrency groups to cancel stale runs. Closes #147.

---------

Co-authored-by: Pascal <open@pascal.app>
This commit is contained in:
Aymeric Rabot
2026-06-06 09:05:16 -04:00
committed by GitHub
co-authored by Pascal
parent f01c443f2a
commit 8f2e5678e8
9 changed files with 54 additions and 29 deletions
@@ -883,8 +883,10 @@ export const applyNodeChangesAction = (
return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections }
})
nodesToMarkDirty.forEach((id) => get().markDirty(id))
parentsToMarkDirty.forEach((id) => {
for (const id of nodesToMarkDirty) {
get().markDirty(id)
}
for (const id of parentsToMarkDirty) {
get().markDirty(id)
const parent = get().nodes[id]
if (parent && 'children' in parent && Array.isArray(parent.children)) {
@@ -892,7 +894,7 @@ export const applyNodeChangesAction = (
get().markDirty(childId as AnyNodeId)
}
}
})
}
}
export const updateNodesAction = (
@@ -294,7 +294,9 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
}
const mergedGeometry = mergeGeometries(geometries, false)
geometries.forEach((geometry) => geometry.dispose())
for (const geometry of geometries) {
geometry.dispose()
}
if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
mergedGeometry?.dispose()
+2 -2
View File
@@ -250,8 +250,8 @@ export const CeilingTool: React.FC = () => {
if (points.length === 0) {
mainLineRef.current.visible = false
closingLineRef.current.visible = false
groundMainLineRef.current && (groundMainLineRef.current.visible = false)
groundClosingLineRef.current && (groundClosingLineRef.current.visible = false)
if (groundMainLineRef.current) groundMainLineRef.current.visible = false
if (groundClosingLineRef.current) groundClosingLineRef.current.visible = false
return
}
const ceilingY = levelY + CEILING_HEIGHT
+15 -14
View File
@@ -133,6 +133,21 @@ export default function ChimneyPanel() {
}
}, [selectedId, node, deleteNode, setSelection])
// Match the current store node against the preset table so the
// segmented control highlights "the preset you'd land on if you
// applied X again". Compare against the store node, not the live-
// override-merged `node`, so the highlight is stable across slider
// drags. Null means the user has tweaked away from any preset; the
// segmented control will then render with no segment selected.
const activePreset = useMemo(() => detectActiveChimneyPreset(storeNode), [storeNode])
const applyPreset = useCallback(
(key: ChimneyPresetKey) => {
commitProp(chimneyPresets[key] as Partial<ChimneyNode>)
triggerSFX('sfx:item-pick')
},
[commitProp],
)
if (!(node && node.type === 'chimney' && selectedId)) return null
const scenestate = useScene.getState()
@@ -322,20 +337,6 @@ export default function ChimneyPanel() {
}
// Match the current store node against the preset table so the
// segmented control highlights "the preset you'd land on if you
// applied X again". Compare against the store node, not the live-
// override-merged `node`, so the highlight is stable across slider
// drags. Null means the user has tweaked away from any preset; the
// segmented control will then render with no segment selected.
const activePreset = useMemo(() => detectActiveChimneyPreset(storeNode), [storeNode])
const applyPreset = useCallback(
(key: ChimneyPresetKey) => {
commitProp(chimneyPresets[key] as Partial<ChimneyNode>)
triggerSFX('sfx:item-pick')
},
[commitProp],
)
return (
<PanelWrapper
icon="/icons/roof.png"
@@ -50,9 +50,6 @@ type RenderableNode = AnyNode & {
export const ParametricNodeRenderer = ({ node }: { node: AnyNode }) => {
const ref = useRef<Group>(null!)
const n = node as RenderableNode
// biome-ignore lint/suspicious/noExplicitAny: useNodeEvents is keyed by
// literal kind; the registry path passes a runtime kind union. Routing
// through the type cast is safer than widening the hook signature.
const handlers = useNodeEvents(node as any, node.type as any)
const liveTransform = useLiveTransforms((s) => s.get(node.id as AnyNodeId))
// Registry arrow handles (rotation gizmo, position-affecting patches)
-3
View File
@@ -78,21 +78,18 @@ interface ThreeJSXElements {
}
declare module 'react' {
// biome-ignore lint/style/noNamespace: Required for JSX module augmentation
namespace JSX {
interface IntrinsicElements extends ThreeJSXElements {}
}
}
declare module 'react/jsx-runtime' {
// biome-ignore lint/style/noNamespace: Required for JSX module augmentation
namespace JSX {
interface IntrinsicElements extends ThreeJSXElements {}
}
}
declare module 'react/jsx-dev-runtime' {
// biome-ignore lint/style/noNamespace: Required for JSX module augmentation
namespace JSX {
interface IntrinsicElements extends ThreeJSXElements {}
}