Skip to content
Open
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
9 changes: 7 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ updates:
- minor
- patch
# The published SDK carries zod as a real dependency; treat its majors as deliberate, reviewed changes.
# Pinned exact at 3.22.4 (not a range) because zod 3.23.0+ ships `??`/`?.` in its own dist —
# the same webpack-4-parse failure issue #680 fixed in this package's own code (see
# legacy-syntax-guard.test.ts's zod-dist case). A minor/patch bump would silently reopen that
# hole through this dependency's own build, so it is blocked here rather than left to the
# semver-major-only ignore below, which a routine "minor-and-patch" grouped PR would sail past.
ignore:
- dependency-name: zod
update-types:
- version-update:semver-major
versions:
- '> 3.22.4'
# astro 7.2 stops the Astro example connecting to the bridge; 7.0.3 works, and the SDK is not at
# fault (the connect script runs and dials). Measured and written up in #347. Held here rather
# than pinned in each grouped PR, because pinning inside the group makes Dependabot supersede
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/package-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,18 @@ jobs:
# margin 2.9.0 left, and the same rule: the next 5% stops here and gets explained. A raise
# to make a red check green is the thing this comment exists to prevent, so if the reason
# above is not in the diff, do not move the number.
#
# 1100KB follows the same protocol. The cause is in this diff: packages/browser and
# packages/core now emit ES2017 so webpack 4 (react-scripts 4) can parse them — issue
# #680 — and below ES2022 tsc must downlevel class fields and private members from their
# native form into WeakMap/assign helpers. That downlevel is the growth: the ES2022 build
# of the same source packs at ~774KB; the ES2017 build packs at ~1063KB, so the exact
# field-downlevel price is ~289KB and nothing configurable avoids it (ES2018 is the same
# size). The budget buys back every token the issue reports (`??`, `?.`, `??=` and their
# siblings are ES2020+) while keeping the SDK's embeddable size honest.
bytes=$(cd packages/browser && npm pack --dry-run --json 2>/dev/null | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d)[0].unpackedSize))")
echo "browser SDK unpacked: $bytes bytes"
if [ "$bytes" -gt 1000000 ]; then
echo "::error::@reticlehq/browser unpacked size $bytes exceeds the 1000KB budget"
if [ "$bytes" -gt 1100000 ]; then
echo "::error::@reticlehq/browser unpacked size $bytes exceeds the 1100KB budget"
exit 1
fi
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ All notable changes to the **`@reticlehq/*`** packages are documented here (each

- **`@reticlehq/server` — a second `reticle_lease` acquire reuses a live lease whose app named its own session, instead of releasing it.** The reuse branch looked the pool's lease id up with `sessions.get` directly, while the mint path beside it resolves the id through `resolveLeasedSessionId` — because an app that names its own session registers under that name, so the id the pool holds is not always one the other tools accept. The two paths disagreed, and the reuse branch fell through to `pool.release`, freeing a **live** lease and minting a second context: the exact tab-poisoning the branch was added to prevent, reached through the branch itself. It bites after an acquire that returned `ready: false`, since the mint path only aliases once its wait resolved, so nothing had recorded the app's own name. `reused: true` was therefore unreachable for precisely the apps the resolver's second clause exists for. The reuse path now resolves and aliases exactly as the mint path does; a genuinely dead lease is still released and re-minted. Closes [#692](https://github.com/reticlehq/reticle/issues/692).

### Fixed

- **`@reticlehq/browser` + `@reticlehq/core` — the published SDK parses on webpack 4 again.** Both packages compiled to ES2022, which emits nullish coalescing, optional chaining and logical assignment verbatim, and `react-scripts` 4 excludes `node_modules` from Babel — so an affected app failed to compile before a session could connect. Both packages now compile to ES2017, and a guard test fails if newer syntax reaches `dist` again — now also parsing `dist` with a real ES-grammar parser at the actual webpack 4 ceiling, not just a grep for known tokens. The one construct the target change cannot move, a unicode property escape in the appeared-text detector (regex bodies are never downleveled), is now built from a string with a range fallback for engines without it — extended to Thai, Georgian and every astral-plane script. `lib` now matches `target` (ES2017, plus an explicit, documented `WeakRef` exception), so `Array.prototype.at` and `Object.hasOwn` — ES2022 runtime APIs that parse fine but throw on an older engine — are caught at compile time instead of shipping silently. `@reticlehq/core`'s `zod` dependency is pinned to the last version whose own published `dist` is free of this same syntax (3.23.0+ reintroduces it upstream), with a guard test and a dependabot ignore so the pin cannot regress unnoticed. Closes [#680](https://github.com/reticlehq/reticle/issues/680).

## [2.13.1] — 2026-09-02

### Fixed
Expand Down
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@iconify-json/heroicons-outline": "^1.2.1",
"@iconify-json/heroicons-solid": "^1.2.1",
"@tanstack/query-core": "^5.101.4",
"acorn": "^8.15.0",
"jotai": "^2.20.2",
"jsdom": "^30.0.1",
"mobx": "^7.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import { describe, expect, it, beforeEach } from 'vitest';
import { dragElement } from './actions-dom.js';
import { at } from '../test-support/array-at.js';

interface Captured {
type: string;
Expand Down Expand Up @@ -90,7 +91,7 @@ describe('drag carries geometry', () => {
await dragElement(source, target, undefined);
const xs = of('pointermove').map((e) => e.clientX);
expect(xs).toEqual([...xs].sort((a, b) => a - b));
expect(xs.at(-1)).toBe(450);
expect(at(xs, -1)).toBe(450);
});

it('holds the primary button down through every move', async () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/browser/src/actions/actions.effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ describe('action effect: valueChanged', () => {
'<select><option value="en">English</option><option value="fr">French</option></select>';
const sel = document.querySelector('select') as HTMLSelectElement;
await expect(executeAction(refs.refFor(sel), 'select', { value: 'English' })).rejects.toThrow(
/en.*fr|fr.*en/s,
// No /s flag: the browser package compiles to ES2017 for webpack 4 (issue #680), and the
// dotAll flag needs ES2018. [\s\S] matches the same newlines here.
/en[\s\S]*fr|fr[\s\S]*en/,
);
});

Expand Down
72 changes: 72 additions & 0 deletions packages/browser/src/actions/appeared-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { AppearedText, HAS_LETTER_FALLBACK } from './appeared-text.js';

describe('HAS_LETTER_FALLBACK: the non-\\p{L} range fallback', () => {
// Exercised directly: on every engine this suite runs on, `\p{L}` is supported, so
// `AppearedText` alone never takes this branch and would never prove these ranges work.

it.each([
['Latin', 'hello'],
['Greek', 'γειά'],
['Cyrillic', 'привет'],
['Armenian', 'բարև'],
['Hebrew', 'שלום'],
['Arabic', 'مرحبا'],
['Thai', 'สวัสดี'],
['Georgian', 'გამარჯობა'],
['Hiragana', 'こんにちは'],
['Katakana', 'コンニチハ'],
['CJK', '你好'],
['Hangul', '안녕하세요'],
])('matches %s text', (_script, text) => {
expect(HAS_LETTER_FALLBACK.test(text)).toBe(true);
});

it('matches an astral-plane letter (Deseret, outside every named BMP range)', () => {
expect(HAS_LETTER_FALLBACK.test('𐐔𐐯𐑅𐐨𐑉𐐯𐐻')).toBe(true);
});

it('matches an astral-plane symbol too — the documented over-inclusive trade-off', () => {
expect(HAS_LETTER_FALLBACK.test('😀')).toBe(true);
});

it('does not match a bare digit run', () => {
expect(HAS_LETTER_FALLBACK.test('409')).toBe(false);
});

it('does not match whitespace and punctuation alone', () => {
expect(HAS_LETTER_FALLBACK.test('— · ')).toBe(false);
});
});

/** A minimal characterData mutation record — the only shape `collect` reads. */
function charDataRecord(text: string): MutationRecord {
return {
type: 'characterData',
target: { textContent: text, parentElement: null } as unknown as Node,
} as unknown as MutationRecord;
}

function collected(text: string): string | undefined {
const at = new AppearedText();
at.collect([charDataRecord(text)]);
return at.effect().appeared;
}

describe('AppearedText: end-to-end script coverage (primary \\p{L} path)', () => {
it('reports Thai text', () => {
expect(collected('ไม่พบข้อมูล')).toBe('ไม่พบข้อมูล');
});

it('reports Georgian text', () => {
expect(collected('ვერ მოიძებნა')).toBe('ვერ მოიძებნა');
});

it('reports an astral-plane letter', () => {
expect(collected('𐐔𐐯𐑅𐐨𐑉𐐯𐐻')).toBe('𐐔𐐯𐑅𐐨𐑉𐐯𐐻');
});

it('still drops a bare digit run with no letters', () => {
expect(collected('409')).toBeUndefined();
});
});
45 changes: 43 additions & 2 deletions packages/browser/src/actions/appeared-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,49 @@ const JOIN = ' | ';

const TEXT_NODE = 3;

/** Any letter, in any script — Latin, Cyrillic, CJK, Arabic. */
const HAS_LETTER = /\p{L}/u;
/**
* Any letter, in any script — Latin, Cyrillic, CJK, Arabic.
*
* Built from a string, not a literal: a `/\p{L}/u` literal is ES2018 syntax, and tsc never
* downlevels regex bodies, so the literal would ride the lowered target straight into dist and
* break webpack 4 parsing (issue #680). Construction from a string parses under any grammar;
* engines without unicode property escapes take the range fallback instead of throwing. The
* fallback covers the letter blocks the product's locales need (Latin, Greek, Cyrillic,
* Armenian, Hebrew, Arabic, Thai, Georgian, Hiragana, Katakana, CJK, Hangul); scripts still
* outside it behave as letterless on those engines only, where the previous bundle did not parse
* at all. Any astral-plane character (a surrogate pair, \uD800-\uDBFF followed by
* \uDC00-\uDFFF) counts as a letter too: matching a supplementary-plane symbol that is not a
* letter (rare outside emoji) is the same over-inclusive direction saysSomething already takes
* everywhere else — a false "this counts as a message" costs less than silently dropping real
* feedback in a script this fallback has no range for.
*/
/**
* The range fallback, exported separately so a test can exercise it directly — on every engine
* this suite runs on, `\p{L}` is supported, so `HAS_LETTER` below never takes this branch and a
* test written against `HAS_LETTER` alone would never prove these ranges actually work.
*/
export const HAS_LETTER_FALLBACK: RegExp = new RegExp(
'[A-Za-zªµºÀ-ÖØ-öø-ʯ' +
'\\u0370-\\u0373\\u0376-\\u0377\\u037B-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C' +
'\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F' +
'\\u0531-\\u0556\\u0559\\u0560-\\u0588' +
'\\u05D0-\\u05EA\\u05F0-\\u05F2' +
'\\u0621-\\u064A\\u066E-\\u06D3\\u06D5\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF' +
'\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46' +
'\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u10FF' +
'\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u2D00-\\u2D25\\u2D27\\u2D2D' +
'\\u3041-\\u3096\\u309B-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF' +
'\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uAC00-\\uD7A3' +
']|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]',
);

const HAS_LETTER: RegExp = (() => {
try {
return new RegExp('\\p{L}', 'u');
} catch {
return HAS_LETTER_FALLBACK;
}
})();

/**
* A fragment with no letters at all is not the app saying something.
Expand Down
6 changes: 6 additions & 0 deletions packages/browser/src/dom/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ function readBase(store: Storage | undefined): number {
}
}

/**
* `WeakRef` (ES2021) is a deliberate, minimal exception to the ES2017 target/lib pin (issue #680):
* unlike `.at()` or `Object.hasOwn`, there is no downlevel-able equivalent — a strong reference here
* would leak every detached element this registry has ever seen. Universally supported in evergreen
* browsers since 2021, well ahead of the webpack-4-parseable syntax floor this pin targets.
*/
export class RefRegistry {
readonly #toRef = new WeakMap<Element, Ref>();
readonly #fromRef = new Map<string, WeakRef<Element>>();
Expand Down
Loading
Loading