| author | |
| committer | |
| log | 194f838e7636f97739c440c7855aed195dbd7f52 |
| tree | 636e1e0ee32d20178fb127362450ef5e0e2a9f0a |
| parent | ec44bc56f598be16ffbc22ed0e4e095447043844 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>21 files changed, 258 insertions(+), 231 deletions(-)
apps/desktop/src/preview-pip-preload.ts-1| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics globalDate:off - This isolated Electron preload does not run inside an Effect runtime. | ||
| 2 | import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts"; | 1 | import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts"; |
| 3 | import { contextBridge, ipcRenderer } from "electron"; | 2 | import { contextBridge, ipcRenderer } from "electron"; |
| 4 | 3 |
apps/mobile/metro.config.js+1-2| ... | @@ -36,8 +36,7 @@ config.resolver = { | ... | @@ -36,8 +36,7 @@ config.resolver = { |
| 36 | new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`), | 36 | new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`), |
| 37 | ], | 37 | ], |
| 38 | extraNodeModules: { | 38 | extraNodeModules: { |
| 39 | // oxlint-disable-next-line unicorn/no-useless-fallback-in-spread | 39 | ...config.resolver?.extraNodeModules, |
| 40 | ...(config.resolver?.extraNodeModules ?? {}), | ||
| 41 | shiki: mobileShikiRoot, | 40 | shiki: mobileShikiRoot, |
| 42 | "@shikijs/core": resolveShikiDependencyRoot("@shikijs/core"), | 41 | "@shikijs/core": resolveShikiDependencyRoot("@shikijs/core"), |
| 43 | "@shikijs/engine-javascript": resolveShikiDependencyRoot("@shikijs/engine-javascript"), | 42 | "@shikijs/engine-javascript": resolveShikiDependencyRoot("@shikijs/engine-javascript"), |
apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx+10-8| ... | @@ -9,10 +9,10 @@ const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([ | ... | @@ -9,10 +9,10 @@ const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([ |
| 9 | StyleSheet.create({}), | 9 | StyleSheet.create({}), |
| 10 | ]); | 10 | ]); |
| 11 | 11 | ||
| 12 | const textDefaults: TextProps = { | 12 | const textDefaults = { |
| 13 | allowFontScaling: true, | 13 | allowFontScaling: true, |
| 14 | selectable: true, | 14 | selectable: true, |
| 15 | }; | 15 | } satisfies TextProps; |
| 16 | 16 | ||
| 17 | const useTextAncestorContext = () => React.useContext(TextAncestorContext); | 17 | const useTextAncestorContext = () => React.useContext(TextAncestorContext); |
| 18 | 18 | ||
| ... | @@ -28,7 +28,11 @@ export type ContextMenuActionEvent = { | ... | @@ -28,7 +28,11 @@ export type ContextMenuActionEvent = { |
| 28 | nativeEvent: { target: number; actionIdentifier: string }; | 28 | nativeEvent: { target: number; actionIdentifier: string }; |
| 29 | }; | 29 | }; |
| 30 | 30 | ||
| 31 | export type MarkdownTextPrimitiveProps = TextProps & { | 31 | /** |
| 32 | * `onTextLayout` is not offered: the native view reports plain line strings | ||
| 33 | * while the React Native Text fallback reports measured `TextLayoutLine`s. | ||
| 34 | */ | ||
| 35 | export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & { | ||
| 32 | uiTextView?: boolean; | 36 | uiTextView?: boolean; |
| 33 | contextMenuConfig?: string; | 37 | contextMenuConfig?: string; |
| 34 | onContextMenuAction?: (event: ContextMenuActionEvent) => void; | 38 | onContextMenuAction?: (event: ContextMenuActionEvent) => void; |
| ... | @@ -75,16 +79,14 @@ function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPr | ... | @@ -75,16 +79,14 @@ function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPr |
| 75 | }); | 79 | }); |
| 76 | 80 | ||
| 77 | if (!isAncestor) { | 81 | if (!isAncestor) { |
| 82 | // Press handlers are delivered by the text runs; the container never sees them. | ||
| 83 | const { onPress: _onPress, onLongPress: _onLongPress, ...containerProps } = rest; | ||
| 78 | return ( | 84 | return ( |
| 79 | <TextAncestorContext.Provider value={contextValue}> | 85 | <TextAncestorContext.Provider value={contextValue}> |
| 80 | <T3MarkdownTextNativeComponent | 86 | <T3MarkdownTextNativeComponent |
| 81 | {...textDefaults} | 87 | {...textDefaults} |
| 82 | {...rest} | 88 | {...containerProps} |
| 83 | // ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'} | ||
| 84 | style={[flattenedStyle]} | 89 | style={[flattenedStyle]} |
| 85 | // @ts-expect-error Weirdness | ||
| 86 | onPress={undefined} | ||
| 87 | onLongPress={undefined} | ||
| 88 | > | 90 | > |
| 89 | {nativeChildren} | 91 | {nativeChildren} |
| 90 | </T3MarkdownTextNativeComponent> | 92 | </T3MarkdownTextNativeComponent> |
apps/server/scripts/migrate-dev-db.ts-1| ... | @@ -24,7 +24,6 @@ | ... | @@ -24,7 +24,6 @@ |
| 24 | * cursors never rewind. | 24 | * cursors never rewind. |
| 25 | */ | 25 | */ |
| 26 | 26 | ||
| 27 | // @effect-diagnostics nodeBuiltinImport:off - node:os resolves the shared T3 home guard. | ||
| 28 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | 27 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 29 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 28 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 30 | import * as NodeOS from "node:os"; | 29 | import * as NodeOS from "node:os"; |
apps/server/scripts/t3-sqlite-state.ts-1| ... | @@ -1,6 +1,5 @@ | ... | @@ -1,6 +1,5 @@ |
| 1 | #!/usr/bin/env node | 1 | #!/usr/bin/env node |
| 2 | 2 | ||
| 3 | // @effect-diagnostics nodeBuiltinImport:off - node:os resolves the shared T3 home guard. | ||
| 4 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 6 | import * as NodeOS from "node:os"; | 5 | import * as NodeOS from "node:os"; |
apps/server/src/assets/AttachmentUpload.ts-1| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | import * as NodeCrypto from "node:crypto"; | 1 | import * as NodeCrypto from "node:crypto"; |
| 3 | 2 | ||
| 4 | import { | 3 | import { |
apps/server/src/http.ts+1-2| ... | @@ -73,8 +73,7 @@ export function downloadContentDisposition(fileName?: string): string { | ... | @@ -73,8 +73,7 @@ export function downloadContentDisposition(fileName?: string): string { |
| 73 | return "attachment"; | 73 | return "attachment"; |
| 74 | } | 74 | } |
| 75 | // toWellFormed: encodeURIComponent throws URIError on unpaired surrogates. | 75 | // toWellFormed: encodeURIComponent throws URIError on unpaired surrogates. |
| 76 | // eslint-disable-next-line no-control-regex -- Header filenames must strip ASCII controls. | 76 | const sanitized = fileName.toWellFormed().replace(/[\p{Cc}"\\]/gu, "_"); |
| 77 | const sanitized = fileName.toWellFormed().replace(/[\u0000-\u001f"\\]/g, "_"); | ||
| 78 | const asciiFallback = sanitized.replace(/[^\u0020-\u007e]/g, "_"); | 77 | const asciiFallback = sanitized.replace(/[^\u0020-\u007e]/g, "_"); |
| 79 | const needsExtended = asciiFallback !== sanitized; | 78 | const needsExtended = asciiFallback !== sanitized; |
| 80 | const extendedName = encodeURIComponent(sanitized).replace( | 79 | const extendedName = encodeURIComponent(sanitized).replace( |
apps/server/src/relay/AgentAwarenessRelay.test.ts+37-49| ... | @@ -358,58 +358,45 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -358,58 +358,45 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 358 | ).toEqual([activeThreadId]); | 358 | ).toEqual([activeThreadId]); |
| 359 | }); | 359 | }); |
| 360 | 360 | ||
| 361 | it("signs the activity publish JWT and rejects tampering", async () => { | 361 | it.effect("signs the activity publish JWT and rejects tampering", () => |
| 362 | const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { | 362 | Effect.gen(function* () { |
| 363 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, | 363 | const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { |
| 364 | publicKeyEncoding: { format: "pem", type: "spki" }, | 364 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, |
| 365 | }); | 365 | publicKeyEncoding: { format: "pem", type: "spki" }, |
| 366 | const payload = { | 366 | }); |
| 367 | iss: "t3-env:env", | 367 | const payload = { |
| 368 | aud: "https://relay.example.test", | 368 | iss: "t3-env:env", |
| 369 | sub: "env", | 369 | aud: "https://relay.example.test", |
| 370 | jti: "nonce-1", | 370 | sub: "env", |
| 371 | iat: 100, | 371 | jti: "nonce-1", |
| 372 | exp: 200, | 372 | iat: 100, |
| 373 | environmentId: state.environmentId, | 373 | exp: 200, |
| 374 | threadId: state.threadId, | 374 | environmentId: state.environmentId, |
| 375 | state, | 375 | threadId: state.threadId, |
| 376 | } satisfies RelayAgentActivityPublishProofPayload; | 376 | state, |
| 377 | const proof = await Effect.runPromise( | 377 | } satisfies RelayAgentActivityPublishProofPayload; |
| 378 | AgentAwarenessRelay.signRelayAgentActivityPublishProof({ | 378 | const proof = yield* AgentAwarenessRelay.signRelayAgentActivityPublishProof({ |
| 379 | privateKey: keyPair.privateKey, | 379 | privateKey: keyPair.privateKey, |
| 380 | payload, | 380 | payload, |
| 381 | }), | 381 | }); |
| 382 | ); | 382 | const verify = (token: string) => |
| 383 | |||
| 384 | await expect( | ||
| 385 | Effect.runPromise( | ||
| 386 | verifyRelayJwt({ | ||
| 387 | publicKey: keyPair.publicKey, | ||
| 388 | token: proof, | ||
| 389 | typ: RELAY_ACTIVITY_PUBLISH_TYP, | ||
| 390 | issuer: "t3-env:env", | ||
| 391 | audience: "https://relay.example.test", | ||
| 392 | nowEpochSeconds: 150, | ||
| 393 | }), | ||
| 394 | ), | ||
| 395 | ).resolves.toMatchObject({ jti: "nonce-1", state }); | ||
| 396 | await expect( | ||
| 397 | Effect.runPromise( | ||
| 398 | verifyRelayJwt({ | 383 | verifyRelayJwt({ |
| 399 | publicKey: keyPair.publicKey, | 384 | publicKey: keyPair.publicKey, |
| 400 | token: (() => { | 385 | token, |
| 401 | const [header, body, signature = ""] = proof.split("."); | ||
| 402 | const corruptedSignature = `${signature.startsWith("a") ? "b" : "a"}${signature.slice(1)}`; | ||
| 403 | return `${header}.${body}.${corruptedSignature}`; | ||
| 404 | })(), | ||
| 405 | typ: RELAY_ACTIVITY_PUBLISH_TYP, | 386 | typ: RELAY_ACTIVITY_PUBLISH_TYP, |
| 406 | issuer: "t3-env:env", | 387 | issuer: "t3-env:env", |
| 407 | audience: "https://relay.example.test", | 388 | audience: "https://relay.example.test", |
| 408 | nowEpochSeconds: 150, | 389 | nowEpochSeconds: 150, |
| 409 | }), | 390 | }); |
| 410 | ), | 391 | |
| 411 | ).rejects.toBeDefined(); | 392 | expect(yield* verify(proof)).toMatchObject({ jti: "nonce-1", state }); |
| 412 | }); | 393 | |
| 394 | const [header, body, signature = ""] = proof.split("."); | ||
| 395 | const corruptedSignature = `${signature.startsWith("a") ? "b" : "a"}${signature.slice(1)}`; | ||
| 396 | const rejection = yield* Effect.flip(verify(`${header}.${body}.${corruptedSignature}`)); | ||
| 397 | expect(rejection).toBeDefined(); | ||
| 398 | }), | ||
| 399 | ); | ||
| 413 | 400 | ||
| 414 | it.effect("keeps the orchestration listener armed until relay config is installed", () => | 401 | it.effect("keeps the orchestration listener armed until relay config is installed", () => |
| 415 | Effect.scoped( | 402 | Effect.scoped( |
| ... | @@ -557,10 +544,11 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -557,10 +544,11 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 557 | Effect.scoped( | 544 | Effect.scoped( |
| 558 | Effect.gen(function* () { | 545 | Effect.gen(function* () { |
| 559 | const originalFetch = globalThis.fetch; | 546 | const originalFetch = globalThis.fetch; |
| 560 | const context = yield* Effect.context<never>(); | ||
| 561 | const runFork = Effect.runForkWith(context); | ||
| 562 | const events = yield* Queue.unbounded<OrchestrationEvent>(); | 547 | const events = yield* Queue.unbounded<OrchestrationEvent>(); |
| 563 | const fetchSeen = yield* Deferred.make<URL>(); | 548 | let resolveFetchSeen: (url: URL) => void = () => {}; |
| 549 | const fetchSeen = new Promise<URL>((resolve) => { | ||
| 550 | resolveFetchSeen = resolve; | ||
| 551 | }); | ||
| 564 | const userSpans: Array<string> = []; | 552 | const userSpans: Array<string> = []; |
| 565 | const productSpans: Array<string> = []; | 553 | const productSpans: Array<string> = []; |
| 566 | const collectingTracer = (spans: Array<string>) => | 554 | const collectingTracer = (spans: Array<string>) => |
| ... | @@ -648,7 +636,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -648,7 +636,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 648 | ? input | 636 | ? input |
| 649 | : (input as unknown as { readonly url: string }).url, | 637 | : (input as unknown as { readonly url: string }).url, |
| 650 | ); | 638 | ); |
| 651 | runFork(Deferred.succeed(fetchSeen, url)); | 639 | resolveFetchSeen(url); |
| 652 | return Promise.resolve(Response.json({ ok: true, deliveries: [] })); | 640 | return Promise.resolve(Response.json({ ok: true, deliveries: [] })); |
| 653 | }) as unknown as typeof fetch; | 641 | }) as unknown as typeof fetch; |
| 654 | yield* Effect.addFinalizer(() => | 642 | yield* Effect.addFinalizer(() => |
| ... | @@ -707,7 +695,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -707,7 +695,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 707 | occurredAt: now, | 695 | occurredAt: now, |
| 708 | } as unknown as OrchestrationEvent); | 696 | } as unknown as OrchestrationEvent); |
| 709 | 697 | ||
| 710 | const url = yield* Deferred.await(fetchSeen).pipe(Effect.timeout("2 seconds")); | 698 | const url = yield* Effect.promise(() => fetchSeen).pipe(Effect.timeout("2 seconds")); |
| 711 | expect(url.origin).toBe("https://transport.example.test"); | 699 | expect(url.origin).toBe("https://transport.example.test"); |
| 712 | expect(productSpans).toContain("makePublishProof"); | 700 | expect(productSpans).toContain("makePublishProof"); |
| 713 | expect(userSpans).not.toContain("makePublishProof"); | 701 | expect(userSpans).not.toContain("makePublishProof"); |
apps/server/src/server.test.ts+8-8| ... | @@ -56,7 +56,6 @@ import * as Effect from "effect/Effect"; | ... | @@ -56,7 +56,6 @@ import * as Effect from "effect/Effect"; |
| 56 | import * as FileSystem from "effect/FileSystem"; | 56 | import * as FileSystem from "effect/FileSystem"; |
| 57 | import * as Fiber from "effect/Fiber"; | 57 | import * as Fiber from "effect/Fiber"; |
| 58 | import * as Layer from "effect/Layer"; | 58 | import * as Layer from "effect/Layer"; |
| 59 | import * as ManagedRuntime from "effect/ManagedRuntime"; | ||
| 60 | import * as Option from "effect/Option"; | 59 | import * as Option from "effect/Option"; |
| 61 | import * as Path from "effect/Path"; | 60 | import * as Path from "effect/Path"; |
| 62 | import * as PubSub from "effect/PubSub"; | 61 | import * as PubSub from "effect/PubSub"; |
| ... | @@ -398,7 +397,9 @@ const makeBrowserOtlpPayload = (spanName: string) => | ... | @@ -398,7 +397,9 @@ const makeBrowserOtlpPayload = (spanName: string) => |
| 398 | ({ close }) => Effect.promise(close), | 397 | ({ close }) => Effect.promise(close), |
| 399 | ); | 398 | ); |
| 400 | 399 | ||
| 401 | const runtime = ManagedRuntime.make( | 400 | // The exporter's batch fiber is forked while the layer builds and ticks on |
| 401 | // a wall-clock interval, so the whole tracer runs on the live clock. | ||
| 402 | yield* Layer.build( | ||
| 402 | OtlpTracer.layer({ | 403 | OtlpTracer.layer({ |
| 403 | url: collector.url, | 404 | url: collector.url, |
| 404 | exportInterval: "10 millis", | 405 | exportInterval: "10 millis", |
| ... | @@ -411,14 +412,13 @@ const makeBrowserOtlpPayload = (spanName: string) => | ... | @@ -411,14 +412,13 @@ const makeBrowserOtlpPayload = (spanName: string) => |
| 411 | }, | 412 | }, |
| 412 | }, | 413 | }, |
| 413 | }).pipe(Layer.provide(browserOtlpTracingLayer)), | 414 | }).pipe(Layer.provide(browserOtlpTracingLayer)), |
| 415 | ).pipe( | ||
| 416 | Effect.flatMap((tracing) => | ||
| 417 | Effect.void.pipe(Effect.withSpan(spanName), Effect.provideContext(tracing)), | ||
| 418 | ), | ||
| 419 | TestClock.withLive, | ||
| 414 | ); | 420 | ); |
| 415 | 421 | ||
| 416 | try { | ||
| 417 | yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); | ||
| 418 | } finally { | ||
| 419 | yield* Effect.promise(() => runtime.dispose()); | ||
| 420 | } | ||
| 421 | |||
| 422 | const request = yield* Effect.raceFirst( | 422 | const request = yield* Effect.raceFirst( |
| 423 | Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), | 423 | Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), |
| 424 | Effect.sleep(Duration.seconds(1)).pipe( | 424 | Effect.sleep(Duration.seconds(1)).pipe( |
apps/server/src/serviceLauncher.ts-1| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | // @effect-diagnostics globalDate:off | ||
| 3 | // @effect-diagnostics globalTimers:off | 2 | // @effect-diagnostics globalTimers:off |
| 4 | // This file is shipped as a standalone bundle and copied to a stable path by | 3 | // This file is shipped as a standalone bundle and copied to a stable path by |
| 5 | // `t3 service update`. Keep runtime imports limited to Node built-ins. | 4 | // `t3 service update`. Keep runtime imports limited to Node built-ins. |
apps/web/src/components/settings/ThemeSearchSection.tsx+6-7| ... | @@ -204,13 +204,12 @@ export function ThemeSearchSection({ | ... | @@ -204,13 +204,12 @@ export function ThemeSearchSection({ |
| 204 | return; | 204 | return; |
| 205 | } | 205 | } |
| 206 | void runSearch(debouncedQuery); | 206 | void runSearch(debouncedQuery); |
| 207 | // `installingId` and `sortBy` are deliberately not dependencies: the | 207 | // `sortBy` is deliberately not a direct dependency: the guards above read |
| 208 | // guards above read the current values from the fresh render closure. An | 208 | // the current value from the fresh render closure. An install finishing |
| 209 | // install finishing reruns the search only when the query or sort changed | 209 | // reruns the search only when the query or sort changed while it was in |
| 210 | // while it was in flight (checked via lastSearchKeyRef, recorded only | 210 | // flight (checked via lastSearchKeyRef, recorded only once a search |
| 211 | // once a search succeeds), so the install error the user needs to see is | 211 | // succeeds), so the install error the user needs to see is preserved |
| 212 | // preserved across that rerun. | 212 | // across that rerun. |
| 213 | // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| 214 | }, [open, query, debouncedQuery, installingId, runSearch]); | 213 | }, [open, query, debouncedQuery, installingId, runSearch]); |
| 215 | 214 | ||
| 216 | const handleSortChange = useCallback((value: OpenVsxThemeSort | null) => { | 215 | const handleSortChange = useCallback((value: OpenVsxThemeSort | null) => { |
apps/web/src/hooks/useTheme.ts+1-2| ... | @@ -356,8 +356,7 @@ function applyTheme(theme: Theme, { suppressTransitions = false, preservePreview | ... | @@ -356,8 +356,7 @@ function applyTheme(theme: Theme, { suppressTransitions = false, preservePreview |
| 356 | syncDesktopTheme(theme, followSystem, appearanceMode); | 356 | syncDesktopTheme(theme, followSystem, appearanceMode); |
| 357 | if (suppressTransitions) { | 357 | if (suppressTransitions) { |
| 358 | // Force a reflow so the no-transitions class takes effect before removal | 358 | // Force a reflow so the no-transitions class takes effect before removal |
| 359 | // oxlint-disable-next-line no-unused-expressions | 359 | void document.documentElement.offsetHeight; |
| 360 | document.documentElement.offsetHeight; | ||
| 361 | requestAnimationFrame(() => { | 360 | requestAnimationFrame(() => { |
| 362 | document.documentElement.classList.remove("no-transitions"); | 361 | document.documentElement.classList.remove("no-transitions"); |
| 363 | }); | 362 | }); |
oxlint-plugin-t3code/rules/no-global-process-runtime.ts+1-20| ... | @@ -4,23 +4,8 @@ import * as Option from "effect/Option"; | ... | @@ -4,23 +4,8 @@ import * as Option from "effect/Option"; |
| 4 | import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts"; | 4 | import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts"; |
| 5 | 5 | ||
| 6 | const RUNTIME_PROPERTIES = new Set(["platform", "arch"]); | 6 | const RUNTIME_PROPERTIES = new Set(["platform", "arch"]); |
| 7 | const HOST_PROCESS_REFERENCE_FILE = "packages/shared/src/hostProcess.ts"; | ||
| 8 | const NODE_OS_MODULES = new Set(["node:os", "os"]); | 7 | const NODE_OS_MODULES = new Set(["node:os", "os"]); |
| 9 | 8 | ||
| 10 | const normalizePath = (path: string) => path.replaceAll("\\", "/"); | ||
| 11 | |||
| 12 | const toRepoPath = (filename: string, cwd: string) => { | ||
| 13 | const normalizedFilename = normalizePath(filename); | ||
| 14 | const normalizedCwd = normalizePath(cwd).replace(/\/+$/u, ""); | ||
| 15 | const prefix = `${normalizedCwd}/`; | ||
| 16 | return normalizedFilename.startsWith(prefix) | ||
| 17 | ? normalizedFilename.slice(prefix.length) | ||
| 18 | : normalizedFilename; | ||
| 19 | }; | ||
| 20 | |||
| 21 | const isHostProcessReferenceFile = (filename: string, cwd: string) => | ||
| 22 | toRepoPath(filename, cwd) === HOST_PROCESS_REFERENCE_FILE; | ||
| 23 | |||
| 24 | const isGlobalProcessObject = (node: unknown): boolean => { | 9 | const isGlobalProcessObject = (node: unknown): boolean => { |
| 25 | const expression = unwrapExpression(node); | 10 | const expression = unwrapExpression(node); |
| 26 | if (isIdentifier(expression, "process")) return true; | 11 | if (isIdentifier(expression, "process")) return true; |
| ... | @@ -48,7 +33,7 @@ export default defineRule({ | ... | @@ -48,7 +33,7 @@ export default defineRule({ |
| 48 | type: "problem", | 33 | type: "problem", |
| 49 | docs: { | 34 | docs: { |
| 50 | description: | 35 | description: |
| 51 | "Disallow direct host runtime platform/architecture reads outside the shared host process references.", | 36 | "Disallow direct host runtime platform/architecture reads; the shared host process references are exempted in the lint config.", |
| 52 | }, | 37 | }, |
| 53 | }, | 38 | }, |
| 54 | createOnce(context) { | 39 | createOnce(context) { |
| ... | @@ -117,8 +102,6 @@ export default defineRule({ | ... | @@ -117,8 +102,6 @@ export default defineRule({ |
| 117 | before: resetBindings, | 102 | before: resetBindings, |
| 118 | ImportDeclaration: trackImportDeclaration, | 103 | ImportDeclaration: trackImportDeclaration, |
| 119 | MemberExpression(node) { | 104 | MemberExpression(node) { |
| 120 | if (isHostProcessReferenceFile(context.filename, context.cwd)) return; | ||
| 121 | |||
| 122 | const property = getPropertyName(node.property); | 105 | const property = getPropertyName(node.property); |
| 123 | if (Option.isNone(property) || !RUNTIME_PROPERTIES.has(property.value)) return; | 106 | if (Option.isNone(property) || !RUNTIME_PROPERTIES.has(property.value)) return; |
| 124 | if (!isGlobalProcessObject(node.object)) return; | 107 | if (!isGlobalProcessObject(node.object)) return; |
| ... | @@ -129,8 +112,6 @@ export default defineRule({ | ... | @@ -129,8 +112,6 @@ export default defineRule({ |
| 129 | }); | 112 | }); |
| 130 | }, | 113 | }, |
| 131 | CallExpression(node) { | 114 | CallExpression(node) { |
| 132 | if (isHostProcessReferenceFile(context.filename, context.cwd)) return; | ||
| 133 | |||
| 134 | const property = getNodeOsRuntimeCall(node.callee); | 115 | const property = getNodeOsRuntimeCall(node.callee); |
| 135 | if (Option.isNone(property)) return; | 116 | if (Option.isNone(property)) return; |
| 136 | 117 |
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.test.ts+31| ... | @@ -71,3 +71,34 @@ productionRule.valid( | ... | @@ -71,3 +71,34 @@ productionRule.valid( |
| 71 | export const main = () => Effect.runPromise(Effect.void); | 71 | export const main = () => Effect.runPromise(Effect.void); |
| 72 | `, | 72 | `, |
| 73 | ); | 73 | ); |
| 74 | |||
| 75 | const legacyRule = createOxlintRuleHarness("t3code/no-manual-effect-runtime-in-tests", { | ||
| 76 | filename: "legacy.test.ts", | ||
| 77 | ruleOptions: [{ maxOccurrences: 2 }], | ||
| 78 | }); | ||
| 79 | |||
| 80 | describe("t3code/no-manual-effect-runtime-in-tests with maxOccurrences", () => { | ||
| 81 | legacyRule.valid( | ||
| 82 | "allows occurrences up to the ceiling", | ||
| 83 | ` | ||
| 84 | import * as Effect from "effect/Effect"; | ||
| 85 | |||
| 86 | test("first", () => Effect.runSync(Effect.void)); | ||
| 87 | test("second", () => Effect.runSync(Effect.void)); | ||
| 88 | `, | ||
| 89 | ); | ||
| 90 | |||
| 91 | legacyRule.invalid( | ||
| 92 | "reports occurrences beyond the ceiling", | ||
| 93 | ` | ||
| 94 | import * as Effect from "effect/Effect"; | ||
| 95 | |||
| 96 | test("first", () => Effect.runSync(Effect.void)); | ||
| 97 | test("second", () => Effect.runSync(Effect.void)); | ||
| 98 | test("third", () => Effect.runSync(Effect.void)); | ||
| 99 | `, | ||
| 100 | (output) => { | ||
| 101 | assert.equal(output.match(/no-manual-effect-runtime-in-tests/g)?.length, 1); | ||
| 102 | }, | ||
| 103 | ); | ||
| 104 | }); |
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts+27-39| ... | @@ -19,44 +19,17 @@ const EFFECT_RUNTIME_METHODS = new Set([ | ... | @@ -19,44 +19,17 @@ const EFFECT_RUNTIME_METHODS = new Set([ |
| 19 | "runSyncWith", | 19 | "runSyncWith", |
| 20 | ]); | 20 | ]); |
| 21 | 21 | ||
| 22 | // Existing manual runners are tracked as debt. The rule permits no net-new | 22 | // Existing manual runners are tracked as debt through the `maxOccurrences` |
| 23 | // occurrences in these files, while unlisted test files must have zero. | 23 | // option, set per file in the lint config. The rule permits no net-new |
| 24 | const LEGACY_BASELINE = new Map<string, number>([ | 24 | // occurrences in those files, while every other test file must have zero. |
| 25 | ["apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts", 1], | 25 | const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => { |
| 26 | ["apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts", 2], | 26 | const [first] = options; |
| 27 | ["apps/mobile/src/state/use-remote-environment-registry.test.ts", 2], | 27 | return typeof first === "object" && |
| 28 | ["apps/server/src/orchestration/commandInvariants.test.ts", 6], | 28 | first !== null && |
| 29 | ["apps/server/src/orchestration/Layers/CheckpointReactor.test.ts", 42], | 29 | "maxOccurrences" in first && |
| 30 | ["apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts", 5], | 30 | typeof first.maxOccurrences === "number" |
| 31 | ["apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts", 4], | 31 | ? first.maxOccurrences |
| 32 | ["apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts", 70], | 32 | : 0; |
| 33 | ["apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts", 31], | ||
| 34 | ["apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts", 2], | ||
| 35 | ["apps/server/src/orchestration/projector.test.ts", 20], | ||
| 36 | ["apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts", 4], | ||
| 37 | ["apps/server/src/provider/acp/CursorAcpSupport.test.ts", 1], | ||
| 38 | ["apps/server/src/provider/Layers/ClaudeAdapter.test.ts", 2], | ||
| 39 | ["apps/server/src/provider/Layers/CodexAdapter.test.ts", 1], | ||
| 40 | ["apps/server/src/provider/Layers/CodexSessionRuntime.test.ts", 5], | ||
| 41 | ["apps/server/src/provider/Layers/CursorAdapter.test.ts", 1], | ||
| 42 | ["apps/server/src/provider/Layers/CursorProvider.test.ts", 4], | ||
| 43 | ["apps/server/src/provider/Layers/ProviderService.test.ts", 2], | ||
| 44 | ["apps/server/src/provider/Layers/ProviderSessionReaper.test.ts", 21], | ||
| 45 | ["apps/server/src/relay/AgentAwarenessRelay.test.ts", 4], | ||
| 46 | ["apps/server/src/server.test.ts", 1], | ||
| 47 | ["apps/web/src/cloud/dpop.test.ts", 2], | ||
| 48 | ["apps/web/src/environments/runtime/service.addSavedEnvironment.test.ts", 1], | ||
| 49 | ["oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.test.ts", 7], | ||
| 50 | ["packages/client-runtime/src/relay/managedRelayState.test.ts", 1], | ||
| 51 | ["packages/client-runtime/src/wsTransport.test.ts", 2], | ||
| 52 | ]); | ||
| 53 | |||
| 54 | const baselineFor = (filename: string): number => { | ||
| 55 | const normalized = filename.replaceAll("\\", "/"); | ||
| 56 | for (const [suffix, count] of LEGACY_BASELINE) { | ||
| 57 | if (normalized.endsWith(suffix)) return count; | ||
| 58 | } | ||
| 59 | return 0; | ||
| 60 | }; | 33 | }; |
| 61 | 34 | ||
| 62 | const manualRunnerName = (callee: unknown): Option.Option<string> => { | 35 | const manualRunnerName = (callee: unknown): Option.Option<string> => { |
| ... | @@ -87,11 +60,26 @@ export default defineRule({ | ... | @@ -87,11 +60,26 @@ export default defineRule({ |
| 87 | description: | 60 | description: |
| 88 | "Disallow manually creating or running Effect runtimes in tests; use @effect/vitest.", | 61 | "Disallow manually creating or running Effect runtimes in tests; use @effect/vitest.", |
| 89 | }, | 62 | }, |
| 63 | schema: [ | ||
| 64 | { | ||
| 65 | type: "object", | ||
| 66 | properties: { | ||
| 67 | maxOccurrences: { | ||
| 68 | type: "integer", | ||
| 69 | minimum: 0, | ||
| 70 | description: | ||
| 71 | "Legacy debt ceiling for this file: occurrences beyond this count are reported.", | ||
| 72 | }, | ||
| 73 | }, | ||
| 74 | additionalProperties: false, | ||
| 75 | }, | ||
| 76 | ], | ||
| 77 | defaultOptions: [{ maxOccurrences: 0 }], | ||
| 90 | }, | 78 | }, |
| 91 | create(context) { | 79 | create(context) { |
| 92 | if (!TEST_FILE_PATTERN.test(context.filename)) return {}; | 80 | if (!TEST_FILE_PATTERN.test(context.filename)) return {}; |
| 93 | 81 | ||
| 94 | const allowedCount = baselineFor(context.filename); | 82 | const allowedCount = readMaxOccurrences(context.options); |
| 95 | let occurrenceCount = 0; | 83 | let occurrenceCount = 0; |
| 96 | 84 | ||
| 97 | return { | 85 | return { |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.test.ts+7-20| ... | @@ -7,15 +7,11 @@ const guardedMobileFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-them | ... | @@ -7,15 +7,11 @@ const guardedMobileFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-them |
| 7 | }); | 7 | }); |
| 8 | const reviewedInteropFile = createOxlintRuleHarness( | 8 | const reviewedInteropFile = createOxlintRuleHarness( |
| 9 | "t3code/no-mobile-uniwind-theme-escape-hatches", | 9 | "t3code/no-mobile-uniwind-theme-escape-hatches", |
| 10 | { filename: "apps/mobile/src/features/home/HomeHeader.tsx" }, | 10 | { |
| 11 | filename: "apps/mobile/src/features/home/HomeHeader.tsx", | ||
| 12 | ruleOptions: [{ allowUniwindTheme: true }], | ||
| 13 | }, | ||
| 11 | ); | 14 | ); |
| 12 | const gitOverlayInteropFile = createOxlintRuleHarness( | ||
| 13 | "t3code/no-mobile-uniwind-theme-escape-hatches", | ||
| 14 | { filename: "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx" }, | ||
| 15 | ); | ||
| 16 | const webFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", { | ||
| 17 | filename: "apps/web/src/ThemeSurface.tsx", | ||
| 18 | }); | ||
| 19 | 15 | ||
| 20 | describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { | 16 | describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { |
| 21 | guardedMobileFile.valid( | 17 | guardedMobileFile.valid( |
| ... | @@ -93,18 +89,9 @@ describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { | ... | @@ -93,18 +89,9 @@ describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { |
| 93 | `, | 89 | `, |
| 94 | ); | 90 | ); |
| 95 | 91 | ||
| 96 | gitOverlayInteropFile.valid( | 92 | reviewedInteropFile.invalid( |
| 97 | "allows the native liquid-glass theme boundary", | 93 | "still reports appearance variants in reviewed interop boundaries", |
| 98 | ` | 94 | `const surface = <View className="bg-white dark:bg-black" />;`, |
| 99 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | ||
| 100 | |||
| 101 | export const tint = useUniwindTheme()["--color-glass-surface"]; | ||
| 102 | `, | ||
| 103 | ); | ||
| 104 | |||
| 105 | webFile.valid( | ||
| 106 | "does not impose the mobile custom-theme policy on web code", | ||
| 107 | `const surface = <div className="bg-white dark:bg-black" />;`, | ||
| 108 | ); | 95 | ); |
| 109 | 96 | ||
| 110 | guardedMobileFile.invalid( | 97 | guardedMobileFile.invalid( |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.ts+29-42| ... | @@ -3,44 +3,9 @@ import * as Option from "effect/Option"; | ... | @@ -3,44 +3,9 @@ import * as Option from "effect/Option"; |
| 3 | 3 | ||
| 4 | import { getPropertyName, unwrapExpression } from "../utils.ts"; | 4 | import { getPropertyName, unwrapExpression } from "../utils.ts"; |
| 5 | 5 | ||
| 6 | const MOBILE_SOURCE_MARKER = "/apps/mobile/src/"; | ||
| 7 | const APPEARANCE_VARIANT_PATTERN = /\b(?:dark|light):(?=\S)/u; | 6 | const APPEARANCE_VARIANT_PATTERN = /\b(?:dark|light):(?=\S)/u; |
| 8 | const APPEARANCE_VARIANT_MESSAGE = | 7 | const APPEARANCE_VARIANT_MESSAGE = |
| 9 | "dark:/light: utilities do not follow registered custom themes; use an adaptive semantic token."; | 8 | "dark:/light: utilities do not follow registered custom themes; use an adaptive semantic token."; |
| 10 | const THEME_INTEROP_ALLOWLIST = new Set([ | ||
| 11 | "features/archive/ArchivedThreadsScreen.tsx", | ||
| 12 | "features/connection/ConnectionsNewRouteScreen.tsx", | ||
| 13 | "features/files/FileMarkdownPreview.tsx", | ||
| 14 | "features/files/SourceFileSurface.tsx", | ||
| 15 | "features/files/ThreadFilesRouteScreen.tsx", | ||
| 16 | "features/files/thread-file-navigator-pane.tsx", | ||
| 17 | "features/home/HomeHeader.tsx", | ||
| 18 | "features/review/ReviewSheet.tsx", | ||
| 19 | "features/review/useNativeReviewDiffBridge.ts", | ||
| 20 | "features/settings/SettingsEnvironmentsRouteScreen.tsx", | ||
| 21 | "features/settings/appearance/components/AppearancePreviews.tsx", | ||
| 22 | "features/settings/appearance/components/FontSizeSliderRow.tsx", | ||
| 23 | "features/threads/GitActionProgressOverlay.tsx", | ||
| 24 | "features/threads/NewTaskContextPickerScreens.tsx", | ||
| 25 | "features/threads/NewTaskDraftScreen.tsx", | ||
| 26 | "features/threads/ThreadComposer.tsx", | ||
| 27 | "features/threads/ThreadFeed.tsx", | ||
| 28 | "features/threads/ThreadSettingsSheet.tsx", | ||
| 29 | "features/threads/git/GitOverviewSheet.tsx", | ||
| 30 | "features/threads/thread-list-items.tsx", | ||
| 31 | "features/threads/thread-list-v2-items.tsx", | ||
| 32 | "lib/useMobileNavigationTheme.ts", | ||
| 33 | "native/T3ComposerEditor.ios.tsx", | ||
| 34 | "native/T3ComposerEditor.native.tsx", | ||
| 35 | ]); | ||
| 36 | |||
| 37 | const mobileSourcePath = (filename: string): string | undefined => { | ||
| 38 | const normalized = `/${filename.replaceAll("\\", "/")}`; | ||
| 39 | const markerIndex = normalized.lastIndexOf(MOBILE_SOURCE_MARKER); | ||
| 40 | return markerIndex === -1 | ||
| 41 | ? undefined | ||
| 42 | : normalized.slice(markerIndex + MOBILE_SOURCE_MARKER.length); | ||
| 43 | }; | ||
| 44 | 9 | ||
| 45 | const literalStringValue = (node: unknown): Option.Option<string> => { | 10 | const literalStringValue = (node: unknown): Option.Option<string> => { |
| 46 | if (typeof node !== "object" || node === null) return Option.none(); | 11 | if (typeof node !== "object" || node === null) return Option.none(); |
| ... | @@ -54,18 +19,40 @@ const reportsAppearanceVariant = (value: string) => APPEARANCE_VARIANT_PATTERN.t | ... | @@ -54,18 +19,40 @@ const reportsAppearanceVariant = (value: string) => APPEARANCE_VARIANT_PATTERN.t |
| 54 | const importsModule = (source: string, modulePath: string): boolean => | 19 | const importsModule = (source: string, modulePath: string): boolean => |
| 55 | source.replace(/\.[cm]?[jt]sx?$/u, "").endsWith(modulePath); | 20 | source.replace(/\.[cm]?[jt]sx?$/u, "").endsWith(modulePath); |
| 56 | 21 | ||
| 22 | const readAllowUniwindTheme = (options: ReadonlyArray<unknown>): boolean => { | ||
| 23 | const [first] = options; | ||
| 24 | return ( | ||
| 25 | typeof first === "object" && | ||
| 26 | first !== null && | ||
| 27 | "allowUniwindTheme" in first && | ||
| 28 | first.allowUniwindTheme === true | ||
| 29 | ); | ||
| 30 | }; | ||
| 31 | |||
| 57 | export default defineRule({ | 32 | export default defineRule({ |
| 58 | meta: { | 33 | meta: { |
| 59 | type: "problem", | 34 | type: "problem", |
| 60 | docs: { | 35 | docs: { |
| 61 | description: | 36 | description: |
| 62 | "Keep mobile theme styling on semantic Uniwind classes and reviewed native interop boundaries.", | 37 | "Keep mobile theme styling on semantic Uniwind classes. Scope the rule to mobile sources and exempt reviewed native interop boundaries in the lint config.", |
| 63 | }, | 38 | }, |
| 39 | schema: [ | ||
| 40 | { | ||
| 41 | type: "object", | ||
| 42 | properties: { | ||
| 43 | allowUniwindTheme: { | ||
| 44 | type: "boolean", | ||
| 45 | description: | ||
| 46 | "Permit useUniwindTheme in a reviewed native/third-party interop boundary that cannot consume a className.", | ||
| 47 | }, | ||
| 48 | }, | ||
| 49 | additionalProperties: false, | ||
| 50 | }, | ||
| 51 | ], | ||
| 52 | defaultOptions: [{ allowUniwindTheme: false }], | ||
| 64 | }, | 53 | }, |
| 65 | create(context) { | 54 | create(context) { |
| 66 | const sourcePath = mobileSourcePath(context.filename); | 55 | const allowUniwindTheme = readAllowUniwindTheme(context.options); |
| 67 | if (sourcePath === undefined) return {}; | ||
| 68 | |||
| 69 | const uniwindNamespaces = new Set<Variable>(); | 56 | const uniwindNamespaces = new Set<Variable>(); |
| 70 | 57 | ||
| 71 | const resolveVariable = (node: unknown): Variable | undefined => { | 58 | const resolveVariable = (node: unknown): Variable | undefined => { |
| ... | @@ -131,13 +118,13 @@ export default defineRule({ | ... | @@ -131,13 +118,13 @@ export default defineRule({ |
| 131 | 118 | ||
| 132 | if ( | 119 | if ( |
| 133 | !isTypeOnly && | 120 | !isTypeOnly && |
| 134 | importsModule(source.value, "/useUniwindTheme") && | 121 | !allowUniwindTheme && |
| 135 | !THEME_INTEROP_ALLOWLIST.has(sourcePath) | 122 | importsModule(source.value, "/useUniwindTheme") |
| 136 | ) { | 123 | ) { |
| 137 | context.report({ | 124 | context.report({ |
| 138 | node: specifier, | 125 | node: specifier, |
| 139 | message: | 126 | message: |
| 140 | "Use className for theme styling, or review and add this native/third-party interop boundary to the lint allowlist.", | 127 | "Use className for theme styling, or review this native/third-party interop boundary and enable allowUniwindTheme for it in the lint config.", |
| 141 | }); | 128 | }); |
| 142 | } | 129 | } |
| 143 | } | 130 | } |
oxlint-plugin-t3code/test/utils.ts+3-1| ... | @@ -61,6 +61,8 @@ interface RuleHarness { | ... | @@ -61,6 +61,8 @@ interface RuleHarness { |
| 61 | 61 | ||
| 62 | interface RuleHarnessOptions { | 62 | interface RuleHarnessOptions { |
| 63 | readonly filename?: string; | 63 | readonly filename?: string; |
| 64 | /** Rule options, as they would appear after the severity in the lint config. */ | ||
| 65 | readonly ruleOptions?: ReadonlyArray<unknown>; | ||
| 64 | } | 66 | } |
| 65 | 67 | ||
| 66 | const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => | 68 | const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => |
| ... | @@ -111,7 +113,7 @@ export const createOxlintRuleHarness = ( | ... | @@ -111,7 +113,7 @@ export const createOxlintRuleHarness = ( |
| 111 | configPath, | 113 | configPath, |
| 112 | yield* encodeOxlintConfig({ | 114 | yield* encodeOxlintConfig({ |
| 113 | jsPlugins: [{ name: "t3code", specifier: pluginPath }], | 115 | jsPlugins: [{ name: "t3code", specifier: pluginPath }], |
| 114 | rules: { [ruleName]: "error" }, | 116 | rules: { [ruleName]: ["error", ...(options.ruleOptions ?? [])] }, |
| 115 | }), | 117 | }), |
| 116 | ); | 118 | ); |
| 117 | yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true }); | 119 | yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true }); |
packages/client-runtime/src/relay/managedRelayState.test.ts+30-22| ... | @@ -243,30 +243,38 @@ describe("createManagedRelayQueryManager", () => { | ... | @@ -243,30 +243,38 @@ describe("createManagedRelayQueryManager", () => { |
| 243 | }), | 243 | }), |
| 244 | ); | 244 | ); |
| 245 | 245 | ||
| 246 | it("emits credential changes only when the managed relay account changes", async () => { | 246 | it.effect("emits credential changes only when the managed relay account changes", () => |
| 247 | setManagedRelaySession(registry, { | 247 | Effect.gen(function* () { |
| 248 | accountId: "account-1", | 248 | setManagedRelaySession(registry, { |
| 249 | readClerkToken: () => Promise.resolve("first-token"), | 249 | accountId: "account-1", |
| 250 | }); | 250 | readClerkToken: () => Promise.resolve("first-token"), |
| 251 | const changes = Effect.runPromise( | 251 | }); |
| 252 | managedRelayAccountChanges(registry).pipe(Stream.take(2), Stream.runCollect), | 252 | const changes = yield* managedRelayAccountChanges(registry).pipe( |
| 253 | ); | 253 | Stream.take(2), |
| 254 | await vi.waitFor(() => { | 254 | Stream.runCollect, |
| 255 | expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan(0); | 255 | Effect.forkChild, |
| 256 | }); | 256 | ); |
| 257 | yield* Effect.promise(() => | ||
| 258 | vi.waitFor(() => { | ||
| 259 | expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan( | ||
| 260 | 0, | ||
| 261 | ); | ||
| 262 | }), | ||
| 263 | ); | ||
| 257 | 264 | ||
| 258 | setManagedRelaySession(registry, { | 265 | setManagedRelaySession(registry, { |
| 259 | accountId: "account-1", | 266 | accountId: "account-1", |
| 260 | readClerkToken: () => Promise.resolve("refreshed-token"), | 267 | readClerkToken: () => Promise.resolve("refreshed-token"), |
| 261 | }); | 268 | }); |
| 262 | setManagedRelaySession(registry, { | 269 | setManagedRelaySession(registry, { |
| 263 | accountId: "account-2", | 270 | accountId: "account-2", |
| 264 | readClerkToken: () => Promise.resolve("second-token"), | 271 | readClerkToken: () => Promise.resolve("second-token"), |
| 265 | }); | 272 | }); |
| 266 | setManagedRelaySession(registry, null); | 273 | setManagedRelaySession(registry, null); |
| 267 | 274 | ||
| 268 | expect(Array.from(await changes)).toEqual(["account-2", null]); | 275 | expect(Array.from(yield* Fiber.join(changes))).toEqual(["account-2", null]); |
| 269 | }); | 276 | }), |
| 277 | ); | ||
| 270 | 278 | ||
| 271 | it("shares one Clerk token read across concurrent relay list and status queries", async () => { | 279 | it("shares one Clerk token read across concurrent relay list and status queries", async () => { |
| 272 | const secondEnvironment = { | 280 | const secondEnvironment = { |
scripts/build-desktop-artifact.test.ts-1| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off - packaged-archive fixtures compute the sidecar digest with the same Node primitive as the builder. | ||
| 2 | import * as NodeCrypto from "node:crypto"; | 1 | import * as NodeCrypto from "node:crypto"; |
| 3 | 2 | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
vite.config.ts+66-3| ... | @@ -26,7 +26,6 @@ export default defineConfig({ | ... | @@ -26,7 +26,6 @@ export default defineConfig({ |
| 26 | }, | 26 | }, |
| 27 | fmt: { | 27 | fmt: { |
| 28 | ignorePatterns: [ | 28 | ignorePatterns: [ |
| 29 | ".reference", | ||
| 30 | ".repos/**", | 29 | ".repos/**", |
| 31 | ".alchemy", | 30 | ".alchemy", |
| 32 | "dist", | 31 | "dist", |
| ... | @@ -37,7 +36,6 @@ export default defineConfig({ | ... | @@ -37,7 +36,6 @@ export default defineConfig({ |
| 37 | "**/routeTree.gen.ts", | 36 | "**/routeTree.gen.ts", |
| 38 | "apps/mobile/android/**", | 37 | "apps/mobile/android/**", |
| 39 | "apps/mobile/ios/**", | 38 | "apps/mobile/ios/**", |
| 40 | "apps/web/src/lib/vendor/qrcodegen.ts", | ||
| 41 | "apps/mobile/uniwind-types.d.ts", | 39 | "apps/mobile/uniwind-types.d.ts", |
| 42 | "*.icon/**", | 40 | "*.icon/**", |
| 43 | ], | 41 | ], |
| ... | @@ -119,11 +117,76 @@ export default defineConfig({ | ... | @@ -119,11 +117,76 @@ export default defineConfig({ |
| 119 | "t3code/no-global-process-runtime": "error", | 117 | "t3code/no-global-process-runtime": "error", |
| 120 | "t3code/no-inline-schema-compile": "warn", | 118 | "t3code/no-inline-schema-compile": "warn", |
| 121 | "t3code/no-manual-effect-runtime-in-tests": "error", | 119 | "t3code/no-manual-effect-runtime-in-tests": "error", |
| 122 | "t3code/no-mobile-uniwind-theme-escape-hatches": "error", | ||
| 123 | "t3code/no-native-title-tooltip": "error", | 120 | "t3code/no-native-title-tooltip": "error", |
| 124 | "t3code/namespace-node-imports": "error", | 121 | "t3code/namespace-node-imports": "error", |
| 125 | }, | 122 | }, |
| 123 | overrides: [ | ||
| 124 | { | ||
| 125 | // The one place that reads the host platform to seed the injected references. | ||
| 126 | files: ["packages/shared/src/hostProcess.ts"], | ||
| 127 | rules: { "t3code/no-global-process-runtime": "off" }, | ||
| 128 | }, | ||
| 129 | { | ||
| 130 | files: ["apps/mobile/src/**"], | ||
| 131 | rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" }, | ||
| 132 | }, | ||
| 133 | { | ||
| 134 | // Reviewed native and third-party interop boundaries that cannot consume a className. | ||
| 135 | files: [ | ||
| 136 | "apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx", | ||
| 137 | "apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx", | ||
| 138 | "apps/mobile/src/features/files/FileMarkdownPreview.tsx", | ||
| 139 | "apps/mobile/src/features/files/SourceFileSurface.tsx", | ||
| 140 | "apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx", | ||
| 141 | "apps/mobile/src/features/files/thread-file-navigator-pane.tsx", | ||
| 142 | "apps/mobile/src/features/home/HomeHeader.tsx", | ||
| 143 | "apps/mobile/src/features/review/ReviewSheet.tsx", | ||
| 144 | "apps/mobile/src/features/review/useNativeReviewDiffBridge.ts", | ||
| 145 | "apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx", | ||
| 146 | "apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx", | ||
| 147 | "apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx", | ||
| 148 | "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx", | ||
| 149 | "apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx", | ||
| 150 | "apps/mobile/src/features/threads/NewTaskDraftScreen.tsx", | ||
| 151 | "apps/mobile/src/features/threads/ThreadComposer.tsx", | ||
| 152 | "apps/mobile/src/features/threads/ThreadFeed.tsx", | ||
| 153 | "apps/mobile/src/features/threads/ThreadSettingsSheet.tsx", | ||
| 154 | "apps/mobile/src/features/threads/git/GitOverviewSheet.tsx", | ||
| 155 | "apps/mobile/src/features/threads/thread-list-items.tsx", | ||
| 156 | "apps/mobile/src/features/threads/thread-list-v2-items.tsx", | ||
| 157 | "apps/mobile/src/lib/useMobileNavigationTheme.ts", | ||
| 158 | "apps/mobile/src/native/T3ComposerEditor.ios.tsx", | ||
| 159 | "apps/mobile/src/native/T3ComposerEditor.native.tsx", | ||
| 160 | ], | ||
| 161 | rules: { | ||
| 162 | "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }], | ||
| 163 | }, | ||
| 164 | }, | ||
| 165 | // Legacy manual Effect runners tracked as debt: no net-new occurrences. | ||
| 166 | // Lower a ceiling when you migrate a file, and delete its entry at zero. | ||
| 167 | ...Object.entries({ | ||
| 168 | "apps/server/src/orchestration/Layers/CheckpointReactor.test.ts": 42, | ||
| 169 | "apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts": 5, | ||
| 170 | "apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts": 4, | ||
| 171 | "apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts": 66, | ||
| 172 | "apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts": 29, | ||
| 173 | "apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts": 2, | ||
| 174 | "apps/server/src/orchestration/commandInvariants.test.ts": 5, | ||
| 175 | "apps/server/src/orchestration/projector.test.ts": 20, | ||
| 176 | "apps/server/src/provider/Layers/CodexAdapter.test.ts": 1, | ||
| 177 | "apps/server/src/provider/Layers/CodexSessionRuntime.test.ts": 5, | ||
| 178 | "apps/server/src/provider/Layers/CursorAdapter.test.ts": 1, | ||
| 179 | "apps/server/src/provider/Layers/CursorProvider.test.ts": 1, | ||
| 180 | "apps/server/src/provider/Layers/ProviderService.test.ts": 2, | ||
| 181 | "apps/server/src/provider/Layers/ProviderSessionReaper.test.ts": 12, | ||
| 182 | "apps/server/src/provider/acp/CursorAcpSupport.test.ts": 1, | ||
| 183 | }).map(([file, maxOccurrences]) => { | ||
| 184 | const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }]; | ||
| 185 | return { files: [file], rules: { "t3code/no-manual-effect-runtime-in-tests": rule } }; | ||
| 186 | }), | ||
| 187 | ], | ||
| 126 | options: { | 188 | options: { |
| 189 | reportUnusedDisableDirectives: "error", | ||
| 127 | // Revisit once Oxlint's tsgolint path can integrate with @effect/tsgo diagnostics. | 190 | // Revisit once Oxlint's tsgolint path can integrate with @effect/tsgo diagnostics. |
| 128 | typeAware: false, | 191 | typeAware: false, |
| 129 | typeCheck: false, | 192 | typeCheck: false, |