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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
- name: Check
run: vp check

- name: Check className overrides on components/ui do not grow
run: vp run lint:restyle-ceiling

- name: Typecheck
run: vpr typecheck

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Full glossary with file links: `docs/internals/glossary.md`
## Taste

- Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb.
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits, add a variant to the component, not classes at the call site. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` reports violations and CI caps their count.
- Inferred types over annotations. `any` is the enemy.
- Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior.
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.
Expand Down
42 changes: 21 additions & 21 deletions apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7155,27 +7155,27 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
const values = Map.prototype.values;
yield* Effect.acquireRelease(
Effect.sync(() =>
vi
.spyOn(Map.prototype, "values")
.mockImplementation(function (this: Map<unknown, unknown>) {
const iterator = values.call(this);
const next = iterator.next.bind(iterator);
iterator.next = () => {
const result = next();
const value: unknown = result.value;
if (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
value.id.startsWith("history-part-")
) {
visitedHistoryParts += 1;
}
return result;
};
return iterator;
}),
vi.spyOn(Map.prototype, "values").mockImplementation(function (
this: Map<unknown, unknown>,
) {
const iterator = values.call(this);
const next = iterator.next.bind(iterator);
iterator.next = () => {
const result = next();
const value: unknown = result.value;
if (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
value.id.startsWith("history-part-")
) {
visitedHistoryParts += 1;
}
return result;
};
return iterator;
}),
),
(spy) => Effect.sync(() => spy.mockRestore()),
);
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints",
"knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts",
"lint:mobile": "node scripts/mobile-native-static-check.ts",
"lint:restyle-ceiling": "node scripts/lint-restyle-ceiling.ts",
"test": "vp run -r test",
"test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml",
"test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test",
Expand All @@ -51,6 +52,7 @@
},
"devDependencies": {
"@effect/tsgo": "catalog:",
"@shadcn/lint": "0.1.5",
"@types/node": "catalog:",
"knip": "6.34.0",
"typescript": "catalog:",
Expand Down
1,226 changes: 753 additions & 473 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ catalog:
lightningcss: 1.33.0
tailwindcss: 4.3.3
typescript: 7.0.2
vite: npm:@voidzero-dev/vite-plus-core@0.3.0
vite-plus: 0.3.0
vite: npm:@voidzero-dev/vite-plus-core@0.3.3
vite-plus: 0.3.3
yaml: ^2.9.0

minimumReleaseAgeExclude:
Expand Down Expand Up @@ -94,6 +94,8 @@ minimumReleaseAgeExclude:
- "@distilled.cloud/prisma@1.0.0-rc.12"
- "@distilled.cloud/railway@1.0.0-rc.12"
- "@distilled.cloud/stripe@1.0.0-rc.12"
- "@shadcn/lint@0.1.5"
- cn@0.3.2

overrides:
# Earlier React Navigation declarations create a circular type alias in TypeScript 7.
Expand Down
24 changes: 24 additions & 0 deletions scripts/lint-restyle-ceiling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { assert, describe, it } from "@effect/vitest";

import { countRestyleFindings, evaluateCeiling } from "./lint-restyle-ceiling.ts";

describe("lint-restyle-ceiling", () => {
it("counts only no-restyle diagnostics", () => {
const count = countRestyleFindings({
diagnostics: [
{ code: "shadcn(no-restyle)" },
{ code: "react(refs)" },
{ code: "shadcn(no-restyle)" },
],
});
assert.strictEqual(count, 2);
});

it("fails above the ceiling and passes at or below it", () => {
assert.isFalse(evaluateCeiling(11, 10).ok);
assert.isTrue(evaluateCeiling(10, 10).ok);
const below = evaluateCeiling(7, 10);
assert.isTrue(below.ok);
assert.include(below.message, "Lower RESTYLE_CEILING");
});
});
66 changes: 66 additions & 0 deletions scripts/lint-restyle-ceiling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node
// @effect-diagnostics nodeBuiltinImport:off - one-shot CI gate over a child process.
import * as NodeChildProcess from "node:child_process";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

// `shadcn/no-restyle` stays a warning while the existing className overrides on
// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
// migrate a file, and delete this script when the rule becomes an error.
export const RESTYLE_CEILING = 1247;

const RULE = "shadcn(no-restyle)";

const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");

interface LintReport {
readonly diagnostics: ReadonlyArray<{ readonly code: string }>;
}

export function countRestyleFindings(report: LintReport): number {
return report.diagnostics.filter((diagnostic) => diagnostic.code === RULE).length;
}

export function evaluateCeiling(
count: number,
ceiling: number,
): { readonly ok: boolean; readonly message: string } {
if (count > ceiling) {
return {
ok: false,
message:
`${RULE}: ${count} findings exceed the ceiling of ${ceiling}. ` +
"Use a variant or size on the components/ui export instead of a className override " +
"(run `vp lint apps/web/src` for the list).",
};
}
const slack = ceiling - count;
return {
ok: true,
message:
slack === 0
? `${RULE}: ${count} findings, at the ceiling.`
: `${RULE}: ${count} findings, ${slack} below the ceiling of ${ceiling}. Lower RESTYLE_CEILING in scripts/lint-restyle-ceiling.ts to ${count}.`,
};
}

function main() {
const result = NodeChildProcess.spawnSync("vp", ["lint", "--format", "json", "apps/web/src"], {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
});
if (result.error) throw result.error;
const report = JSON.parse(result.stdout) as LintReport;
const verdict = evaluateCeiling(countRestyleFindings(report), RESTYLE_CEILING);
process.stdout.write(`${verdict.message}\n`);
process.exitCode = verdict.ok ? 0 : 1;
}

if (
process.argv[1] !== undefined &&
NodePath.resolve(process.argv[1]) === NodeURL.fileURLToPath(import.meta.url)
) {
main();
}
16 changes: 15 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ export default defineConfig({
"apps/mobile/uniwind-types.d.ts",
],
plugins: ["eslint", "oxc", "react", "unicorn", "typescript"],
jsPlugins: ["./oxlint-plugin-t3code/index.ts"],
jsPlugins: ["./oxlint-plugin-t3code/index.ts", "@shadcn/lint"],
settings: {
shadcn: { ui: "~/components/ui" },
},
categories: {
correctness: "warn",
suspicious: "warn",
Expand Down Expand Up @@ -167,6 +170,17 @@ export default defineConfig({
files: ["apps/mobile/src/**"],
rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" },
},
{
// components/ui exports own their look. App code picks a variant or size instead
// of restyling with className; layout classes (width, flex, margin, position) stay
// allowed because placement belongs to the parent. Warn-only until the existing
// overrides are migrated to variants; the ceiling below stops the count growing.
files: ["apps/web/src/**"],
excludeFiles: ["apps/web/src/components/ui/**"],
rules: {
"shadcn/no-restyle": ["warn", { allow: ["layout"] }],
},
},
{
// Shared client code must not call APIs missing from Hermes. Our ESNext
// TypeScript target accepts them even when they would crash mobile at launch.
Expand Down
Loading