Skip to content

Commit e16df94

Browse files
authored
Merge branch 'main' into fix/local-git-status-upstream
2 parents 91d904d + 034b506 commit e16df94

176 files changed

Lines changed: 6632 additions & 1380 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/scripts/build-preview-annotation-css.mjs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ const compilerInput = [
2929
annotationSource.replace('@import "tailwindcss";', "@tailwind utilities;"),
3030
].join("\n");
3131
const compiler = await compile(compilerInput, { base: appRoot });
32-
const css = compiler.build([...candidates]);
32+
// Shadow-root styles cannot register @property defaults. Make Tailwind's
33+
// fallback unconditional so border, shadow, and ring utilities have defaults.
34+
const compiledCss = compiler.build([...candidates]);
35+
const css = compiledCss.replace(
36+
/(@layer properties \{\n) @supports [^\n]*\{\n([\s\S]*?)\n \}\n(\})/,
37+
"$1$2\n$3",
38+
);
39+
if (css === compiledCss || /@layer properties\s*\{\s*@supports\b/.test(css)) {
40+
throw new Error("Tailwind @property fallback was not unwrapped; update the pattern above.");
41+
}
3342
const encodedCss = `'${css
3443
.replaceAll("\\", "\\\\")
3544
.replaceAll("'", "\\'")

‎apps/desktop/src/backend/DesktopServerExposure.test.ts‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const tailnetNetworkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces = {
4040
],
4141
};
4242

43-
function layerMockSpawner(statusJson = "{}", onSpawn?: () => void) {
43+
function layerMockSpawner(statusJson: string | (() => string) = "{}", onSpawn?: () => void) {
4444
return Layer.succeed(
4545
ChildProcessSpawner.ChildProcessSpawner,
4646
ChildProcessSpawner.make(() =>
@@ -53,7 +53,9 @@ function layerMockSpawner(statusJson = "{}", onSpawn?: () => void) {
5353
kill: () => Effect.void,
5454
unref: Effect.succeed(Effect.void),
5555
stdin: Sink.drain,
56-
stdout: Stream.make(encoder.encode(statusJson)),
56+
stdout: Stream.make(
57+
encoder.encode(typeof statusJson === "string" ? statusJson : statusJson()),
58+
),
5759
stderr: Stream.empty,
5860
all: Stream.empty,
5961
getInputFd: () => Sink.drain,
@@ -460,6 +462,66 @@ describe("DesktopServerExposure", () => {
460462
),
461463
);
462464

465+
it.effect(
466+
"refreshes Tailscale status when addresses change and caches unchanged addresses",
467+
() => {
468+
const networkInterfaces: Record<
469+
string,
470+
readonly DesktopNetworkInterfaces.DesktopNetworkInterfaceInfo[] | undefined
471+
> = { ...lanNetworkInterfaces };
472+
let statusJson = "{}";
473+
let statusReads = 0;
474+
475+
return withHarness(
476+
networkInterfaces,
477+
Effect.gen(function* () {
478+
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
479+
yield* serverExposure.configureFromSettings({ port: 4173 });
480+
yield* serverExposure.setMode("network-accessible");
481+
482+
const initial = yield* serverExposure.getAdvertisedEndpoints;
483+
assert.deepEqual(
484+
initial.map((endpoint) => endpoint.httpBaseUrl),
485+
["http://127.0.0.1:4173/", "http://192.168.1.20:4173/"],
486+
);
487+
assert.equal(statusReads, 1);
488+
489+
for (const [address, dnsName] of [
490+
["100.90.1.2", "desktop.tail.ts.net"],
491+
["100.90.1.3", "desktop.other-tail.ts.net"],
492+
] as const) {
493+
networkInterfaces.vpn0 = [{ address, family: "IPv4", internal: false }];
494+
statusJson = JSON.stringify({
495+
Self: { DNSName: `${dnsName}.`, TailscaleIPs: [address] },
496+
});
497+
const previousReads = statusReads;
498+
499+
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
500+
assert.deepEqual(
501+
endpoints.map((endpoint) => endpoint.httpBaseUrl),
502+
[
503+
"http://127.0.0.1:4173/",
504+
"http://192.168.1.20:4173/",
505+
`http://${address}:4173/`,
506+
`https://${dnsName}/`,
507+
],
508+
);
509+
assert.equal(statusReads, previousReads + 1);
510+
assert.deepEqual(yield* serverExposure.getAdvertisedEndpoints, endpoints);
511+
assert.equal(statusReads, previousReads + 1);
512+
}
513+
}),
514+
{},
515+
layerMockSpawner(
516+
() => statusJson,
517+
() => {
518+
statusReads += 1;
519+
},
520+
),
521+
);
522+
},
523+
);
524+
463525
it.effect("does not spawn the tailscale CLI while server exposure is local-only", () =>
464526
withHarness(
465527
lanNetworkInterfaces,

‎apps/desktop/src/backend/DesktopServerExposure.ts‎

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type DesktopServerExposureState,
1111
} from "@t3tools/contracts";
1212
import { isTailscaleIpv4Address, readTailscaleStatus } from "@t3tools/tailscale";
13+
import * as Cache from "effect/Cache";
1314
import * as Context from "effect/Context";
1415
import * as Duration from "effect/Duration";
1516
import * as Effect from "effect/Effect";
@@ -416,6 +417,25 @@ function resolveRuntimeState(input: {
416417
};
417418
}
418419

420+
const tailscaleIpv4AddressKey = (
421+
networkInterfaces: DesktopNetworkInterfaces.NetworkInterfaces,
422+
): string =>
423+
[
424+
...new Set(
425+
Object.values(networkInterfaces)
426+
.flatMap((addresses) => addresses ?? [])
427+
.filter(
428+
(address) =>
429+
!address.internal &&
430+
address.family === "IPv4" &&
431+
isTailscaleIpv4Address(address.address),
432+
)
433+
.map((address) => address.address),
434+
),
435+
]
436+
.toSorted()
437+
.join(",");
438+
419439
const requiresBackendRelaunch = (previous: RuntimeState, next: RuntimeState): boolean =>
420440
previous.port !== next.port ||
421441
previous.bindHost !== next.bindHost ||
@@ -434,16 +454,17 @@ export const make = Effect.gen(function* () {
434454
// with values it read before that change persisted.
435455
const changePermit = yield* Semaphore.make(1);
436456

437-
// Cache the `tailscale status` spawn for the TTL. On macOS, the Mac App
438-
// Store Tailscale CLI lives inside Tailscale's sandbox container, so each
439-
// spawn re-triggers the "Other apps" TCC prompt.
440-
const cachedReadTailscaleStatus = yield* Effect.cachedWithTTL(
441-
readTailscaleStatus.pipe(
442-
Effect.orElseSucceed(() => null),
443-
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
444-
),
445-
TAILSCALE_STATUS_CACHE_TTL,
446-
);
457+
// Key status by interface addresses so network changes bypass stale DNS and
458+
// identity. Cache unchanged addresses to avoid repeated macOS TCC prompts.
459+
const tailscaleStatusCache = yield* Cache.make({
460+
capacity: 1,
461+
timeToLive: TAILSCALE_STATUS_CACHE_TTL,
462+
lookup: (_addresses: string) =>
463+
readTailscaleStatus.pipe(
464+
Effect.orElseSucceed(() => null),
465+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
466+
),
467+
});
447468

448469
const readNetworkInterfaces = networkInterfaces.read;
449470

@@ -564,7 +585,10 @@ export const make = Effect.gen(function* () {
564585
serveEnabled: state.tailscaleServeEnabled,
565586
servePort: state.tailscaleServePort,
566587
networkInterfaces: currentNetworkInterfaces,
567-
readStatus: cachedReadTailscaleStatus,
588+
readStatus: Cache.get(
589+
tailscaleStatusCache,
590+
tailscaleIpv4AddressKey(currentNetworkInterfaces),
591+
),
568592
}).pipe(
569593
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
570594
Effect.provideService(HttpClient.HttpClient, httpClient),

‎apps/desktop/src/preview/AnnotationStyles.generated.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)