fix: restore helper subpath exports (#402)

* fix: restore helper subpath exports

* fix: polish helper subpath typings
This commit is contained in:
Michał Pierzchała
2026-04-11 22:16:32 +02:00
committed by GitHub
parent b0f52a9905
commit f4f143ced7
11 changed files with 256 additions and 0 deletions
+16
View File
@@ -12,6 +12,10 @@
"import": "./dist/src/index.js",
"types": "./dist/src/index.d.ts"
},
"./artifacts": {
"import": "./dist/src/artifacts.js",
"types": "./dist/src/artifacts.d.ts"
},
"./metro": {
"import": "./dist/src/metro.js",
"types": "./dist/src/metro.d.ts"
@@ -20,9 +24,21 @@
"import": "./dist/src/remote-config.js",
"types": "./dist/src/remote-config.d.ts"
},
"./install-source": {
"import": "./dist/src/install-source.js",
"types": "./dist/src/install-source.d.ts"
},
"./contracts": {
"import": "./dist/src/contracts.js",
"types": "./dist/src/contracts.d.ts"
},
"./selectors": {
"import": "./dist/src/selectors.js",
"types": "./dist/src/selectors.d.ts"
},
"./finders": {
"import": "./dist/src/finders.js",
"types": "./dist/src/finders.d.ts"
}
},
"engines": {
+4
View File
@@ -17,9 +17,13 @@ export default defineConfig({
source: {
entry: {
index: 'src/index.ts',
artifacts: 'src/artifacts.ts',
metro: 'src/metro.ts',
'remote-config': 'src/remote-config.ts',
'install-source': 'src/install-source.ts',
contracts: 'src/contracts.ts',
selectors: 'src/selectors.ts',
finders: 'src/finders.ts',
},
tsconfigPath: 'tsconfig.lib.json',
},
+11
View File
@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { resolveAndroidArchivePackageName } from '../artifacts.ts';
const resolver: (archivePath: string) => Promise<string | undefined> =
resolveAndroidArchivePackageName;
test('package subpath exports android archive package resolver', () => {
assert.equal(typeof resolver, 'function');
});
+36
View File
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
findBestMatchesByLocator,
normalizeRole,
normalizeText,
parseFindArgs,
} from '../finders.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';
function makeNode(ref: string, label?: string): SnapshotNode {
return {
index: Number(ref.replace('e', '')) || 0,
ref,
type: 'XCUIElementTypeButton',
label,
};
}
test('public finders entrypoint re-exports pure helpers', () => {
const nodes: SnapshotNode[] = [makeNode('e1', 'Continue')];
const parsed = parseFindArgs(['label', 'Continue', 'click']);
const best = findBestMatchesByLocator(nodes, 'label', 'Continue');
const requireRectLegacy = findBestMatchesByLocator(nodes, 'label', 'Continue', true);
const requireRectOptions = findBestMatchesByLocator(nodes, 'label', 'Continue', {
requireRect: true,
});
assert.equal(normalizeText(' Continue\nNow '), 'continue now');
assert.equal(normalizeRole('XCUIElementTypeApplication.XCUIElementTypeButton'), 'button');
assert.equal(parsed.action, 'click');
assert.equal(best.matches.length, 1);
assert.equal(requireRectLegacy.matches.length, 0);
assert.equal(requireRectOptions.matches.length, 0);
});
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
ARCHIVE_EXTENSIONS,
isTrustedInstallSourceUrl,
materializeInstallablePath,
validateDownloadSourceUrl,
} from '../install-source.ts';
test('public install-source entrypoint re-exports pure helpers', () => {
assert.deepEqual(ARCHIVE_EXTENSIONS, ['.zip', '.tar', '.tar.gz', '.tgz']);
assert.equal(
isTrustedInstallSourceUrl('https://api.github.com/repos/acme/app/actions/artifacts/1/zip'),
true,
);
assert.equal(typeof materializeInstallablePath, 'function');
assert.equal(typeof validateDownloadSourceUrl, 'function');
});
+70
View File
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
findSelectorChainMatch,
formatSelectorFailure,
isNodeEditable,
isNodeVisible,
isSelectorToken,
parseSelectorChain,
resolveSelectorChain,
tryParseSelectorChain,
type Selector,
type SelectorChain,
type SelectorDiagnostics,
type SelectorResolution,
} from '../selectors.ts';
import type { SnapshotNode } from '../utils/snapshot.ts';
const nodes: SnapshotNode[] = [
{
ref: 'e1',
index: 0,
type: 'android.widget.Button',
label: 'Continue',
rect: { x: 0, y: 0, width: 120, height: 48 },
enabled: true,
},
{
ref: 'e2',
index: 1,
type: 'android.widget.EditText',
label: 'Email',
rect: { x: 0, y: 64, width: 200, height: 48 },
enabled: true,
},
];
test('public selector subpath exposes platform-aware matching helpers', () => {
const chain: SelectorChain = parseSelectorChain('role=button label="Continue" visible=true');
const firstSelector: Selector = chain.selectors[0];
assert.equal(firstSelector.raw, 'role=button label="Continue" visible=true');
assert.equal(tryParseSelectorChain(chain.raw)?.raw, chain.raw);
assert.equal(isSelectorToken('visible=true'), true);
const match = findSelectorChainMatch(nodes, chain, {
platform: 'android',
requireRect: true,
});
assert.ok(match);
assert.equal(match.matches, 1);
const resolved: SelectorResolution | null = resolveSelectorChain(nodes, chain, {
platform: 'android',
requireRect: true,
});
assert.equal(resolved?.node.ref, 'e1');
assert.equal(isNodeVisible(nodes[0]), true);
assert.equal(isNodeEditable(nodes[1], 'android'), true);
});
test('public selector diagnostics format failures', () => {
const chain = parseSelectorChain('label=Missing');
const diagnostics: SelectorDiagnostics[] = [{ selector: 'label=Missing', matches: 0 }];
assert.equal(
formatSelectorFailure(chain, diagnostics, { unique: false }),
'Selector did not match (label=Missing -> 0)',
);
});
+1
View File
@@ -0,0 +1 @@
export { resolveAndroidArchivePackageName } from './platforms/android/manifest.ts';
+23
View File
@@ -0,0 +1,23 @@
export type { FindLocator } from './utils/finders.ts';
export type { SnapshotNode } from './utils/snapshot.ts';
export { normalizeRole, normalizeText, parseFindArgs } from './utils/finders.ts';
import {
findBestMatchesByLocator as findBestMatchesByLocatorInternal,
type FindLocator,
} from './utils/finders.ts';
import type { SnapshotNode } from './utils/snapshot.ts';
export type FindMatchOptions = {
requireRect?: boolean;
};
export function findBestMatchesByLocator(
nodes: SnapshotNode[],
locator: FindLocator,
query: string,
options?: boolean | FindMatchOptions,
) {
const matchOptions = typeof options === 'boolean' ? { requireRect: options } : options;
return findBestMatchesByLocatorInternal(nodes, locator, query, matchOptions);
}
+13
View File
@@ -0,0 +1,13 @@
export {
ARCHIVE_EXTENSIONS,
isBlockedIpAddress,
isBlockedSourceHostname,
isTrustedInstallSourceUrl,
materializeInstallablePath,
validateDownloadSourceUrl,
} from './platforms/install-source.ts';
export type {
MaterializeInstallSource,
MaterializedInstallable,
} from './platforms/install-source.ts';
+18
View File
@@ -0,0 +1,18 @@
export type {
Selector,
SelectorChain,
SelectorDiagnostics,
SelectorResolution,
} from './daemon/selectors.ts';
export type { SnapshotNode } from './utils/snapshot.ts';
export {
findSelectorChainMatch,
formatSelectorFailure,
isNodeEditable,
isNodeVisible,
isSelectorToken,
parseSelectorChain,
resolveSelectorChain,
tryParseSelectorChain,
} from './daemon/selectors.ts';
+45
View File
@@ -23,6 +23,32 @@ Public subpath API exposed for Node consumers:
- types: `RemoteConfigProfile`, `RemoteConfigProfileOptions`, `ResolvedRemoteConfigProfile`
- `agent-device/contracts`
- types: `SessionRuntimeHints`, `DaemonInstallSource`, `DaemonLockPolicy`, `DaemonRequestMeta`, `DaemonRequest`, `DaemonArtifact`, `DaemonResponseData`, `DaemonError`, `DaemonResponse`
- `agent-device/selectors`
- `parseSelectorChain(expression)`
- `tryParseSelectorChain(expression)`
- `resolveSelectorChain(nodes, chain, options)`
- `findSelectorChainMatch(nodes, chain, options)`
- `formatSelectorFailure(chain, diagnostics, options)`
- `isNodeVisible(node)`
- `isSelectorToken(token)`
- `isNodeEditable(node, platform)`
- types: `Selector`, `SelectorChain`, `SelectorDiagnostics`, `SelectorResolution`, `SnapshotNode`
- `agent-device/finders`
- `findBestMatchesByLocator(nodes, locator, query, requireRectOrOptions)`
- `normalizeRole(value)`
- `normalizeText(value)`
- `parseFindArgs(args)`
- types: `FindLocator`, `FindMatchOptions`, `SnapshotNode`
- `agent-device/install-source`
- `ARCHIVE_EXTENSIONS`
- `isBlockedIpAddress(address)`
- `isBlockedSourceHostname(hostname)`
- `isTrustedInstallSourceUrl(sourceUrl)`
- `materializeInstallablePath(options)`
- `validateDownloadSourceUrl(url)`
- types: `MaterializeInstallSource`, `MaterializedInstallable`
- `agent-device/artifacts`
- `resolveAndroidArchivePackageName(archivePath)`
## Basic usage
@@ -189,3 +215,22 @@ await stopMetroTunnel({
```
Use `agent-device/remote-config` for profile loading and path resolution, `agent-device/metro` for Metro preparation and tunnel lifecycle, and `agent-device/contracts` when a server consumer needs daemon request or runtime contract types.
## Selector helpers
Use `agent-device/selectors` when a remote daemon or bridge needs to parse and match selector expressions without deep-importing daemon internals. Matching is platform-aware because role normalization and editability checks differ by backend.
```ts
import { findSelectorChainMatch, parseSelectorChain } from 'agent-device/selectors';
const chain = parseSelectorChain('role=button label="Continue" visible=true');
const match = findSelectorChainMatch(snapshot.nodes, chain, {
platform: 'android',
requireRect: true,
});
if (!match) {
// Build a daemon-shaped error with formatSelectorFailure(...) if needed.
}
```