Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All notable changes to the **`@reticlehq/*`** packages are documented here (each

### Fixed

- **`@reticlehq/browser` now ships a build `react-scripts` 4 / webpack 4 can actually parse.** `dist/index.js` compiled to the monorepo's shared ES2022 target, which meant optional chaining, nullish coalescing, and nullish-coalescing assignment (`??=`) went out untranspiled. Webpack 4 excludes `node_modules` from Babel, so any app on `react-scripts` 4 failed to compile the moment it imported the SDK — before a dev session could ever connect, and with no diagnostic pointing at why. This package's own `tsconfig.json` now overrides `target` to `ES2019`, downleveling all three; the rest of the monorepo (running on a Node version that never had this problem) is untouched. A new test compiles a snippet through this package's actual tsconfig and asserts none of the three operators survive, so a future bump of the target back up fails in CI instead of in a user's silent build. Closes [#680](https://github.com/reticlehq/reticle/issues/680).
- **`@reticlehq/server`: an ambiguous target is the caller's selector to fix, not a bug to report.** `target matched N elements and an action must not guess between them` arrived with the feedback ask attached, telling the caller their own underspecified query might be a defect in Reticle. The two halves contradicted each other: the first named both matches and the fix, the second called the error unrecognised. It fell through for a punctuation reason, since the text ends `pass an explicit ref from reticle_query.` and the catch-all excludes a tool name followed by a dot so that a module path in a stack trace stays an unanticipated crash. Ambiguity now carries the same `not a Reticle defect` framing as a miss, and the same `no_match` refusal reason. The guard is the part worth keeping: this default has been patched five times, each time by pinning one more string, so the new test drives `resolveTargetRef` itself and asserts every refusal it writes is recognised. `matched an element with no usable ref` deliberately stays unrecognised, because a candidate with no ref is a shape Reticle did not anticipate rather than anything the caller can narrow. Closes [#795](https://github.com/reticlehq/reticle/issues/795).
- **`@reticlehq/core` + `@reticlehq/browser` + `@reticlehq/server` — the split-text recovery hint is now a valid predicate.** A failed text assertion correctly detected prose split across child elements, then suggested `{ scope, self: true }`, which the text predicate schema rejected. The schema now accepts scoped `self`, the browser checks the root's combined subtree text, and the emitted retry is parseable JSON validated against the public predicate parser. Closes [#794](https://github.com/reticlehq/reticle/issues/794).
- **server:** A `{net}` assertion whose named request is still in flight is `unknown`, not `no` — the window closed early, the request had not finished (`#669`).
Expand Down
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"react": "^19.2.8",
"recoil": "^0.7.7",
"svelte": "^5.56.10",
"typescript": "^5.7.3",
"valtio": "^2.3.2",
"vue": "^3.5.41",
"xstate": "^5.32.5"
Expand Down
50 changes: 50 additions & 0 deletions packages/browser/src/build-target.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* `@reticlehq/browser` ships straight `tsc` output with no bundler downlevel step, so this
* package's own `target` IS what a consuming app's bundler receives. `react-scripts` 4 / webpack 4
* excludes `node_modules` from Babel, so any ES2020+ syntax this package emits (optional chaining,
* nullish coalescing, nullish-coalescing assignment) fails to PARSE before a dev session can ever
* connect — with no diagnostic (#680).
*
* Compiles a snippet using each operator through this package's real tsconfig (read once, not
* duplicated here) so a future bump of `target` back up is caught by this test rather than by a
* user's silent build failure.
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import ts from 'typescript';
import { describe, expect, it } from 'vitest';

// vitest runs with cwd at this package's root (`packages/browser`), same as every other test file
// here that reads a fixture off disk — no import.meta.url needed.
const basePath = process.cwd();
const tsconfigPath = join(basePath, 'tsconfig.json');

function compilerOptions(): ts.CompilerOptions {
const configFile = ts.readConfigFile(tsconfigPath, (path) => readFileSync(path, 'utf8'));
if (configFile.error !== undefined) {
throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n'));
}
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(tsconfigPath));
return parsed.options;
}

describe('packages/browser build target (#680)', () => {
it('downlevels optional chaining, nullish coalescing, and nullish-coalescing assignment', () => {
const source = [
'export function readIt(a: { b?: { c: number } } | undefined, store: Record<string, number>): number {',
' const viaOptionalChain = a?.b?.c;',
' const viaNullishCoalescing = viaOptionalChain ?? 0;',
' store.count ??= 0;',
' return viaNullishCoalescing + store.count;',
'}',
].join('\n');

const output = ts.transpileModule(source, { compilerOptions: compilerOptions() }).outputText;

// A syntax check, not a substring-of-comment check: webpack 4's parser (acorn) fails on these
// tokens appearing anywhere in the emitted source, regardless of context.
expect(output).not.toContain('?.');
expect(output).not.toContain('??=');
expect(output).not.toMatch(/[^?]\?\?[^=]/); // bare `??`, excluding the `??=` case already checked
});
});
2 changes: 1 addition & 1 deletion packages/browser/src/registry/stores.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('store registry', () => {
});

it('returns redacted, JSON-safe state for secrets, BigInt, and cycles', () => {
const state: Record<string, unknown> = { password: 'secret', count: 2n };
const state: Record<string, unknown> = { password: 'secret', count: BigInt(2) };
state['self'] = state;
registerStore('ws_safe', () => state);
const out = readStores('ws_safe');
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/security/serialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('transport serialization', () => {
});

it('handles BigInt and cycles without throwing', () => {
const value: Record<string, unknown> = { count: 2n };
const value: Record<string, unknown> = { count: BigInt(2) };
value['self'] = value;
expect(() => safeStringify(value)).not.toThrow();
expect(JSON.parse(safeStringify(value))).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('Transport security', () => {
});

it('redacts and serializes arbitrary command results', async () => {
const value: Record<string, unknown> = { password: 'secret', count: 2n };
const value: Record<string, unknown> = { password: 'secret', count: BigInt(2) };
value['self'] = value;
const transport = new Transport({
url: 'ws://localhost/reticle',
Expand Down
8 changes: 7 additions & 1 deletion packages/browser/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2023", "DOM", "DOM.Iterable"]
"lib": ["ES2023", "DOM", "DOM.Iterable"],
// Everything else in this monorepo targets ES2022 (see tsconfig.base.json), but this package's
// `dist/` is the one output an end user's OWN bundler parses, not just Node — and react-scripts
// 4 / webpack 4 excludes node_modules from Babel, so ES2020+ syntax (optional chaining, nullish
// coalescing, `??=`) fails to even PARSE, with no diagnostic (#680). Downleveled here, and only
// here: the rest of the monorepo runs on a Node version that never had this problem.
"target": "ES2019"
},
"include": ["src/**/*"],
"references": [{ "path": "../core" }]
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.