@unbound-app/api 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -205,7 +205,7 @@ export declare function filter<A extends FilterArgument[]>(
205
205
  * @description Finds a module matching a filter. Lazy unless `{ lazy: false }` is passed, or `all` is set: the search runs on first access.
206
206
  * @param filter The filter to match modules against: a built-in, a combination, or a predicate you write, which the build keys for the cache.
207
207
  * @param options Search options controlling interop, caching, esModule unwrapping, and `all`.
208
- * @returns The matched module, an array of matches when `all` is set, or `null`.
208
+ * @returns The matched module, an array of matches when `all` is set, `undefined` when the cache knows it is missing, or `null`.
209
209
  */
210
210
  export declare function find(filter: MetroFilter, options?: MetroSearchOptions): any;
211
211
  /**
@@ -249,10 +249,10 @@ export declare function findByName<T extends MetroLookupArgs<string>>(
249
249
  export declare function findByProps<
250
250
  K extends MetroLookupFirst<MetroPropName>,
251
251
  T extends MetroLookupArgs<string>,
252
- >(first: K, ...rest: T): MetroPropertyRecordOrArray<[K, ...T], string> & {};
252
+ >(first: K, ...rest: T): MetroPropertyRecordOrArray<[K, ...T], string>;
253
253
  export declare function findByProps<T extends MetroLookupArgs<string>>(
254
254
  ...args: T
255
- ): MetroPropertyRecordOrArray<T, string> & {};
255
+ ): MetroPropertyRecordOrArray<T, string>;
256
256
  /**
257
257
  * @description Finds a module whose prototype exposes all of the given method names. Lazy unless `{ lazy: false }` is passed: the search runs on first access.
258
258
  * @template U The union of requested prototype method names.
@@ -302,16 +302,16 @@ export declare function onModuleLoad(listener: MetroModuleLoadListener): () => v
302
302
  /**
303
303
  * @description Calls `onLoad` with the module matching `filter` once it initializes, without ever initializing it.
304
304
  * A loaded module is passed at once; otherwise `onLoad` runs inside the module's factory, before its importer reads it.
305
- * A module a full search of this Discord build found missing never loads, so `onLoad` never runs for it.
305
+ * A module a full search of this Discord build found missing never loads, so `onLoad` never runs for it; `onMissing` runs instead.
306
306
  * @param filter The filter to match against.
307
307
  * @param onLoad Receives the matched module, once.
308
- * @param options Whether to also match the `default` export (`esModules`) and return it on such a match (`interop`).
308
+ * @param waitOptions Whether to also match the `default` export (`esModules`) and return it on such a match (`interop`), and `onMissing`.
309
309
  * @returns A function that stops waiting; it does nothing once `onLoad` has run.
310
310
  */
311
311
  export declare function onModuleLoaded(
312
312
  filter: MetroFilter,
313
313
  onLoad: (mdl: MetroExport) => void,
314
- options?: MetroModuleLoadedOptions,
314
+ waitOptions?: MetroWaitOptions,
315
315
  ): () => void;
316
316
  export type AlertActionsComponent = React$1.ComponentType<AlertActionsProps>;
317
317
  /** An object of shape `T` widened with an arbitrary property index. */
@@ -26006,12 +26006,12 @@ export type KeysExposingAll<K> = (
26006
26006
  ? Keys
26007
26007
  : never;
26008
26008
  export type LoadingButtonPillComponent = React$1.ComponentType<LoadingButtonPillProps>;
26009
- /** Widens a single result to an array when the search was made with `all: true`. */
26009
+ /** Widens a single result to an array when the search was made with `all: true`; a single one is `undefined` when the cache knows it is missing. */
26010
26010
  export type MetroAllValues<T extends MetroSearchOptions, U> = T extends {
26011
26011
  all: true;
26012
26012
  }
26013
26013
  ? U[]
26014
- : U;
26014
+ : U | undefined;
26015
26015
  /** Tuple of param bags followed by a `{ bulk: true }` marker, describing a bulk find. */
26016
26016
  export type MetroBulkFind<T extends string> = [
26017
26017
  ...AnyProps<MetroParamsBag<T>>[],
@@ -26048,7 +26048,7 @@ export type MetroFilter = ((mdl: any, id: number | string) => PredicateResult) &
26048
26048
  };
26049
26049
  /** A function or class exported by a Discord module, whose signature Discord does not document. */
26050
26050
  export type MetroFunction = DiscordFunction & (new (...args: any[]) => any);
26051
- /** Dispatches to the bulk or single by-name result shape based on the argument tuple. */
26051
+ /** Dispatches to the bulk or single by-name result shape. */
26052
26052
  export type MetroFunctionSignatureOrArray<T extends any[], U extends string> =
26053
26053
  T extends MetroBulkFind<U> ? MetroBulkModuleByName<T> : MetroSingleModuleByName<T>;
26054
26054
  /** Options that control internal search behaviour shared across every `findBy*` helper. */
@@ -26102,7 +26102,7 @@ export type MetroModuleLoadListener = (mdl: any, id: number) => void;
26102
26102
  export type MetroModuleLoadedOptions = Pick<MetroSearchOptions, 'esModules' | 'interop'>;
26103
26103
  /** A name `findByName` takes: a default export's runtime name, or any other string. */
26104
26104
  export type MetroModuleName = LiteralUnion<Extract<keyof DiscordModulesByName, string>>;
26105
- /** Dispatches to the bulk or single by-path result shape based on the argument tuple. */
26105
+ /** Dispatches to the bulk or single by-path result shape. */
26106
26106
  export type MetroModuleOrArray<T extends any[], U extends string> =
26107
26107
  T extends MetroBulkFind<U> ? AnyProps : MetroSingleModuleByPath<T>;
26108
26108
  /** A source path `findByFilePath` takes: a Discord module's path, or any other string. */
@@ -26130,9 +26130,7 @@ export type MetroParamsBag<T extends string> = {
26130
26130
  };
26131
26131
  /** A prop name `findByProps` takes: an export name of a Discord module, or any other string. */
26132
26132
  export type MetroPropName = LiteralUnion<DiscordExportName>;
26133
- /**
26134
- * Dispatches to the bulk or single by-property result shape based on the argument tuple.
26135
- */
26133
+ /** Dispatches to the bulk or single by-property result shape. */
26136
26134
  export type MetroPropertyRecordOrArray<T extends any[], U extends string> =
26137
26135
  T extends MetroBulkFind<U> ? MetroBulkModuleByProperty<T> : MetroSingleModuleByProperty<T>;
26138
26136
  /** A record keyed by the requested prop names. */
@@ -26175,21 +26173,21 @@ export type MetroSingleModuleByName<T extends any[]> = T extends [
26175
26173
  }
26176
26174
  : MetroNamedExport<Exclude<T[number], object>>
26177
26175
  >
26178
- : MetroNamedExport<Exclude<T[number], object>>;
26176
+ : MetroNamedExport<Exclude<T[number], object>> | undefined;
26179
26177
  /** Result shape of a by-path search: the module's exports. */
26180
26178
  export type MetroSingleModuleByPath<T extends any[]> = T extends [
26181
26179
  ...any,
26182
26180
  infer O extends MetroSearchOptions,
26183
26181
  ]
26184
26182
  ? MetroAllValues<O, MetroModuleAtPath<Exclude<T[number], object>>>
26185
- : MetroModuleAtPath<Exclude<T[number], object>>;
26183
+ : MetroModuleAtPath<Exclude<T[number], object>> | undefined;
26186
26184
  /** Result shape of a by-property search: a record keyed by the requested props. */
26187
26185
  export type MetroSingleModuleByProperty<T extends any[]> = T extends [
26188
26186
  ...any,
26189
26187
  infer O extends MetroSearchOptions,
26190
26188
  ]
26191
26189
  ? MetroAllValues<O, MetroModuleWithProps<Exclude<T[number], object>>>
26192
- : MetroModuleWithProps<Exclude<T[number], object>>;
26190
+ : MetroModuleWithProps<Exclude<T[number], object>> | undefined;
26193
26191
  /** Search options for `flux.getStore`, adding the `short` name-matching toggle. */
26194
26192
  export type MetroStoreOptions = {
26195
26193
  short?: boolean;
@@ -26199,6 +26197,10 @@ export type MetroStringFindWithOptions<T extends string, Options = MetroSearchOp
26199
26197
  ...T[],
26200
26198
  Options,
26201
26199
  ];
26200
+ /** Options for {@link onModuleLoaded}'s wait, adding `onMissing`, run at once instead of waiting when the cache knows the module is missing. */
26201
+ export type MetroWaitOptions = MetroModuleLoadedOptions & {
26202
+ onMissing?: () => void;
26203
+ };
26202
26204
  /** `T`, or `Fallback` when `T` is `never`. */
26203
26205
  export type Otherwise<T, Fallback> = [T] extends [never] ? Fallback : T;
26204
26206
  export type PressableHighlightComponent = React$1.ComponentType<
package/src/native.d.ts CHANGED
@@ -4,6 +4,12 @@ export type {
4
4
  NativeAssociationKey,
5
5
  NativeBatchReference,
6
6
  NativeBatchStep,
7
+ NativeBridge,
8
+ NativeBridgeCapability,
9
+ NativeBridgeError,
10
+ NativeBridgeErrorCode,
11
+ NativeBridgeMethod,
12
+ NativeBridgePlatform,
7
13
  NativeCallOptions,
8
14
  NativeCancelablePromise,
9
15
  NativeClassHandle,
@@ -14,6 +20,7 @@ export type {
14
20
  NativeFFISignature,
15
21
  NativeFFIType,
16
22
  NativeFFITypeName,
23
+ NativeFabricBatchMutation,
17
24
  NativeFabricBridge,
18
25
  NativeFabricFrame,
19
26
  NativeFabricMountOptions,
@@ -45,11 +52,6 @@ export type {
45
52
  NativeObjCCallPlan,
46
53
  NativeObjectHandle,
47
54
  NativePlatformBridge,
48
- NativePluginBridge,
49
- NativePluginCapability,
50
- NativePluginError,
51
- NativePluginErrorCode,
52
- NativePluginPlatform,
53
55
  NativePointer,
54
56
  NativeRuntimeBridge,
55
57
  NativeRuntimeEventName,
@@ -67,9 +69,10 @@ export type {
67
69
  PluginContext,
68
70
  } from './_internal';
69
71
  import type {
72
+ NativeBridgeCapability,
73
+ NativeBridgeMethod,
74
+ NativeBridgePlatform,
70
75
  NativePlatformBridge,
71
- NativePluginCapability,
72
- NativePluginPlatform,
73
76
  NativeResourceCounts,
74
77
  } from './_internal';
75
78
  import * as React$1 from 'react';
@@ -78,39 +81,39 @@ import { ComponentType } from 'react';
78
81
  declare const NativePlatform$1: NativePlatformBridge | undefined;
79
82
 
80
83
  /** Thrown when a plugin uses a Capability its manifest does not declare, or declares one that is unknown or the native bridge lacks. */
81
- export declare class NativePluginCapabilityError extends Error {
84
+ export declare class NativeBridgeCapabilityError extends Error {
82
85
  /** A stable code to match on instead of the message. */
83
86
  readonly code = 'NATIVE_PLUGIN_CAPABILITY_DENIED';
84
87
  /** The Capability that was missing or refused. */
85
- readonly capability: NativePluginCapability;
86
- constructor(capability: NativePluginCapability);
88
+ readonly capability: NativeBridgeCapability;
89
+ constructor(capability: NativeBridgeCapability);
87
90
  }
88
91
  /** Thrown when native work is called through a plugin's scope after the scope was disposed, such as after the plugin stopped. */
89
- export declare class NativePluginDisposedError extends Error {
92
+ export declare class NativeBridgeDisposedError extends Error {
90
93
  /** A stable code to match on instead of the message. */
91
94
  readonly code = 'NATIVE_PLUGIN_SCOPE_DISPOSED';
92
95
  constructor();
93
96
  }
94
- /** Thrown when the loader does not provide the native plugin bridge, or the part of it a call needs. */
95
- export declare class NativePluginUnavailableError extends Error {
97
+ /** Thrown when the loader does not provide the native bridge, or the part of it a call needs. */
98
+ export declare class NativeBridgeUnavailableError extends Error {
96
99
  /** A stable code to match on instead of the message. */
97
100
  readonly code = 'NATIVE_PLUGIN_UNAVAILABLE';
98
101
  }
99
102
  /** Thrown when the host platform is not one the addon runs on, by its manifest or the handlers given to `definePlatform`. */
100
- export declare class NativePluginUnsupportedPlatformError extends Error {
103
+ export declare class NativeBridgeUnsupportedPlatformError extends Error {
101
104
  /** A stable code to match on instead of the message. */
102
105
  readonly code = 'NATIVE_PLUGIN_PLATFORM_EXCLUDED';
103
106
  /** The platforms the addon does run on. */
104
- readonly platforms: readonly NativePluginPlatform[];
105
- constructor(platforms: readonly NativePluginPlatform[], platform: string);
107
+ readonly platforms: readonly NativeBridgePlatform[];
108
+ constructor(platforms: readonly NativeBridgePlatform[], platform: string);
106
109
  }
107
- /** Thrown when the native plugin API is older than the manifest's `minNativePluginApi`. */
108
- export declare class NativePluginVersionError extends Error {
110
+ /** Thrown when the native bridge API is older than the manifest's `minNativeBridgeApi`. */
111
+ export declare class NativeBridgeVersionError extends Error {
109
112
  /** A stable code to match on instead of the message. */
110
113
  readonly code = 'NATIVE_PLUGIN_API_VERSION_UNSUPPORTED';
111
- /** The native plugin API version the loader provides. */
114
+ /** The native bridge API version the loader provides. */
112
115
  readonly installedApi: string;
113
- /** The manifest's `minNativePluginApi`. */
116
+ /** The manifest's `minNativeBridgeApi`. */
114
117
  readonly requiredApi: string;
115
118
  constructor(requiredApi: string, installedApi: string);
116
119
  }
@@ -150,11 +153,17 @@ export declare function getRuntimeProperties(): HermesRuntimeProperties;
150
153
  * @param capability The capability to look for.
151
154
  * @returns Whether the bridge provides it.
152
155
  */
153
- export declare function has(capability: NativePluginCapability): boolean;
156
+ export declare function has(capability: NativeBridgeCapability): boolean;
154
157
  /**
155
158
  * @description Persists pending settings to storage, then reloads the native bundle.
156
159
  */
157
160
  export declare function reload(): Promise<void>;
161
+ /**
162
+ * @description Checks whether the loader on this device implements a bridge method, for methods newer than the capability that gates them.
163
+ * @param method The method's path, such as `fabric.batchAsync`.
164
+ * @returns Whether the loader implements it.
165
+ */
166
+ export declare function supports(method: NativeBridgeMethod): boolean;
158
167
  /** Build and release metadata reported by the native client info module. */
159
168
  export type BundleInfoType = {
160
169
  /** Discord's app version, shown in Unbound's settings beside `Build`. */
@@ -257,7 +266,7 @@ export type DeviceInfoType = {
257
266
  /** What `HermesInternal.getRuntimeProperties()` reports; empty when the engine is not Hermes. */
258
267
  export type HermesRuntimeProperties = ReturnType<HermesInternalObject['getRuntimeProperties']>;
259
268
  /** One Platform handler per platform an addon runs on. */
260
- export type PlatformHandlers<Handler> = Partial<Record<NativePluginPlatform, Handler>>;
269
+ export type PlatformHandlers<Handler> = Partial<Record<NativeBridgePlatform, Handler>>;
261
270
  /** A native method Discord exposes without documenting what its promise resolves to. */
262
271
  export type UndocumentedNativeMethod = () => Promise<unknown>;
263
272
  export { NativePlatform$1 as NativePlatform };
package/src/plugins.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  // Auto-generated by generate-plugin-sdk.ts - DO NOT EDIT
2
2
 
3
- import type { AddonHandle, PushedManifest } from './_internal';
3
+ import type { AddonHandle, PluginSelfTestResult, PushedManifest } from './_internal';
4
4
  /** Stops a running plugin without changing its enabled state. */
5
5
  declare const stop$1: (id: string) => void | Promise<void>;
6
+ /** Every installed plugin, keyed by id. */
7
+ export declare const addons: ReadonlyMap<PropertyKey, AddonHandle>;
6
8
  /** Disables a plugin and stops it. */
7
9
  export declare const disable: (id: string) => void;
8
10
  /** Enables a plugin and starts it. */
9
11
  export declare const enable: (id: string) => void | Promise<void>;
10
- /** Every installed plugin, keyed by id. */
11
- export declare const entities: ReadonlyMap<PropertyKey, AddonHandle>;
12
12
  /** The error each plugin that failed to load or start threw, keyed by id. */
13
13
  export declare const errors: ReadonlyMap<PropertyKey, Error>;
14
14
  /** Reads one installed plugin by id. */
@@ -34,6 +34,12 @@ export declare const startDurations: ReadonlyMap<string, number>;
34
34
  export declare const toggle: (id: string) => void;
35
35
  /** Stops and removes an installed plugin. */
36
36
  export declare const uninstall: (id: string) => Promise<void>;
37
+ /**
38
+ * @description Runs a running plugin's self-test, as the device matrix does after restarting it.
39
+ * @param id The plugin's id.
40
+ * @returns Its report, or `undefined` when it is not running or has no self-test; rejects with what the self-test threw.
41
+ */
42
+ export declare function selfTest(id: string): Promise<PluginSelfTestResult | undefined>;
37
43
  /**
38
44
  * @description Checks for updates, then applies this plugin's pending update.
39
45
  * @param id The plugin's id.
package/src/storage.d.ts CHANGED
@@ -39,7 +39,11 @@ export declare function persist(): Promise<void>;
39
39
  * @param options Set `includeSecrets` to read `displayAs: 'secret'` settings as they are; otherwise they are masked.
40
40
  * @returns Every Bucket by name.
41
41
  */
42
- export declare function snapshot({ includeSecrets }?: { includeSecrets?: boolean }): SettingsStores;
42
+ export declare function snapshot({
43
+ includeSecrets,
44
+ }?: {
45
+ includeSecrets?: boolean;
46
+ }): SettingsByBucket;
43
47
  /**
44
48
  * @description React hook that re-renders the component whenever a matching change occurs in the Bucket.
45
49
  * @param name The Bucket to subscribe to.
@@ -90,6 +94,8 @@ export type DefinedSettings = {
90
94
  /** React hook that re-renders on any change to the settings while `enabled`, and returns a number that changes with them. */
91
95
  useVersion: (enabled?: boolean) => number;
92
96
  };
97
+ /** Every Bucket's settings, keyed by Bucket name, as the loader persists them. */
98
+ export type SettingsByBucket = Record<string, SettingsObject>;
93
99
  /** Payload describing a settings change, passed to `onChange` listeners and predicates. */
94
100
  export type SettingsPayload = {
95
101
  /** The name of the Bucket that changed. */
@@ -101,8 +107,6 @@ export type SettingsPayload = {
101
107
  };
102
108
  /** Decides whether a settings change should reach a listener. */
103
109
  export type SettingsPredicate = (payload: SettingsPayload) => PredicateResult;
104
- /** Every Bucket's settings, keyed by Bucket name, as the loader persists them. */
105
- export type SettingsStores = Record<string, SettingsObject>;
106
110
  /** A plugin's settings as an object: reads apply defaults, writes are validated and saved. Sections are nested objects. */
107
111
  export type SettingsValues = {
108
112
  [key: string]: SettingValue | SettingsValues;
package/src/sync.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Auto-generated by generate-plugin-sdk.ts - DO NOT EDIT
2
2
 
3
- /** The settings store Sync keeps its mode and last agreed document in. Never synced. */
4
- export declare const SYNC_STORE = 'unbound::sync';
3
+ /** The Bucket Sync keeps its mode and last agreed document in. Never synced. */
4
+ export declare const SYNC_BUCKET = 'unbound::sync';
5
5
  /**
6
6
  * @description Deletes the stored document and turns syncing off on this device, so it is not
7
7
  * uploaded again. Other devices keep what they have installed.
@@ -20,7 +20,7 @@ export declare function isEnabled(): boolean;
20
20
  * @description Syncs now: reads the stored document, merges, applies remote changes here and saves
21
21
  * the result. A call while a sync runs queues one more run after it, in this device's mode.
22
22
  * @param strategy How to reconcile the two sides; this device's mode by default.
23
- * @returns Whether the sync succeeded. Failures are recorded as `lastError` in {@link SYNC_STORE}.
23
+ * @returns Whether the sync succeeded. Failures are recorded as `lastError` in {@link SYNC_BUCKET}.
24
24
  */
25
25
  export declare function now(strategy?: SyncStrategy): Promise<boolean>;
26
26
  /**
@@ -2,7 +2,7 @@ import type { AlertDialogProps, DialogHandle } from '../src/dialogs';
2
2
  import type { ToastHandle, ToastOptions } from '../src/toasts';
3
3
  import type { IcuVars } from './icu-format';
4
4
 
5
- import { parseIcu } from './icu-parse';
5
+ import { parseIcu } from '../src/_conventions/icu-parse';
6
6
 
7
7
  /** How the fakes resolve a plugin's message keys, as the client's scoped `resolveMessage` does. */
8
8
  export type FakeMessageLookup = {
@@ -1,13 +1,11 @@
1
1
  import { useSyncExternalStore } from 'react';
2
2
 
3
+ import { SOURCE_LOCALE } from '../src/_conventions/addon-locales';
3
4
  import { formatIcu, type IcuVars } from './icu-format';
4
5
 
5
6
  /** One locale's messages, keyed by message key, as a plugin's `i18n/<code>.json` holds them. */
6
7
  export type LocaleTable = Record<string, string>;
7
8
 
8
- /** The locale every plugin ships, and the one its messages fall back to. */
9
- export const SOURCE_LOCALE = 'en-US';
10
-
11
9
  /** The fake `@unbound-app/i18n`, over a plugin's real tables, and the locale a test switches with `setLocale`. */
12
10
  export type FakeLocales = {
13
11
  /** Each message in the active locale, falling back to en-US, then the key itself. */
@@ -2,14 +2,19 @@ import * as React from 'react';
2
2
 
3
3
  import type { AnyObject } from '../src/patcher';
4
4
 
5
+ import {
6
+ API_PROPS as CLIENT_API_PROPS,
7
+ COMMON_PROPS,
8
+ LAZY_MARKER,
9
+ } from '../src/_conventions/metro';
5
10
  import { createFakeReactNative } from './fake-react-native';
6
11
  import { createFakeComponents } from './fake-components';
7
12
 
8
13
  /** A module the fake metro serves: the exports a plugin finds. */
9
14
  export type FakeModule = AnyObject;
10
15
 
11
- /** A fake metro filter. `path` marks a file-path filter, which matches the path a module was registered under. */
12
- export type FakeFilter = ((module: FakeModule) => boolean) & { path?: string };
16
+ /** A fake metro filter. `path` marks a file-path filter, which matches the path a module was registered under; `terms` are the props, path or name it searches by. */
17
+ export type FakeFilter = ((module: FakeModule) => boolean) & { path?: string; terms?: string[] };
13
18
 
14
19
  type LazyOptions = {
15
20
  signal?: AbortSignal;
@@ -24,40 +29,8 @@ type FindAll = { all: true };
24
29
 
25
30
  type Presence = 'loaded' | 'missing';
26
31
 
27
- const LAZY_MARKER = '__METRO_LAZY__';
28
-
29
- // The props each `metro.common` member is found by, as in the client's metro/common.ts.
30
- const COMMON_PROPS = {
31
- Reanimated: ['useAnimatedStyle', 'useSharedValue'],
32
- Gestures: ['PanGestureHandler'],
33
- Clipboard: ['setString', 'getString', 'setImage', 'getImage'],
34
- MarkdownParser: ['parse', 'parseToAST', 'reactParserFor'],
35
- Screens: ['FullWindowOverlay'],
36
- SVG: ['Svg', 'Path'],
37
- Commands: ['getBuiltInCommands'],
38
- Moment: ['isMoment'],
39
- Util: ['inspect'],
40
- Assets: ['registerAsset'],
41
- ImageSources: ['getImageSourcesFromImageProps'],
42
- Clyde: ['createBotMessage'],
43
- Constants: ['Fonts', 'Endpoints'],
44
- Radius: ['Radius'],
45
- Theme: ['colors', 'internal'],
46
- REST: ['getAPIBaseURL'],
47
- i18n: ['getLanguages', 'getSystemLocale', 'intl'],
48
- };
49
-
50
- // The props each `metro.api` member is found by, as in the client's metro/api.ts.
51
- const API_PROPS = {
52
- Messages: ['sendMessage', 'receiveMessage'],
53
- Linking: ['openURL', 'openSettings'],
54
- Profiles: ['showUserProfile'],
55
- AsyncUsers: ['fetchProfile'],
56
- Invites: ['acceptInviteAndTransitionToInviteChannel'],
57
- OAuth2: ['openOAuth2Modal'],
58
- ActionSheets: ['openLazy', 'hideActionSheet'],
59
- UserProfiles: ['getUserProfileActionSheetKey', 'showUserProfileActionSheetPostConnection'],
60
- };
32
+ // On device `metro.api.Linking` is React Native's; here a test registers a module with its methods.
33
+ const API_PROPS = { ...CLIENT_API_PROPS, Linking: ['openURL', 'openSettings'] };
61
34
 
62
35
  /** What a lazy handle looks its module up by. */
63
36
  type LazyLookup = {
@@ -70,12 +43,16 @@ type Wait = {
70
43
  onLoad: (module: FakeModule) => void;
71
44
  };
72
45
 
46
+ type WaitOptions = { onMissing?: () => void };
47
+
73
48
  /** A metro over modules a test registers, with the finders and filters plugins call. */
74
49
  export type FakeMetro = {
75
50
  /** Adds a module, settling any `findLazy` or `onModuleLoaded` waiting for it and calling `onModuleLoad` listeners. */
76
51
  register: (module: FakeModule, path?: string) => void;
77
- /** Forgets every registered module and waiting `findLazy` or `onModuleLoaded`, so a lazy handle made earlier finds only what is registered next. */
52
+ /** Forgets every registered module, missing mark and waiting `findLazy` or `onModuleLoaded`, so a lazy handle made earlier finds only what is registered next. */
78
53
  reset: () => void;
54
+ /** Marks a prop name, file path or export name as missing from this build, as the device's cache knows after a full search: a lazy lookup by it returns `undefined`, and `onModuleLoaded` calls `onMissing`. */
55
+ markMissing: (term: string) => void;
79
56
  /** Discord's Flux stores, by the name `metro.stores` gives them, such as `Users`; the preset binds each to the seeded store. */
80
57
  stores: Record<string, FakeModule>;
81
58
  /** Discord's components: each module a bag of fake components that draw their children and are found by name, such as `Discord.Text`. */
@@ -112,7 +89,11 @@ export type FakeMetro = {
112
89
  filter: FakeFilter,
113
90
  options?: LazyOptions,
114
91
  ) => FakeModule | Promise<FakeModule | undefined>;
115
- onModuleLoaded: (filter: FakeFilter, onLoad: (module: FakeModule) => void) => () => void;
92
+ onModuleLoaded: (
93
+ filter: FakeFilter,
94
+ onLoad: (module: FakeModule) => void,
95
+ options?: WaitOptions,
96
+ ) => () => void;
116
97
  /** Calls `onLoad` with the module a lazy handle finds, at once when one is registered, else when it registers; a plain object is passed at once. Returns what stops waiting. */
117
98
  whenLoaded: (target: FakeModule, onLoad: (module: FakeModule) => void) => () => void;
118
99
  onModuleLoad: (listener: (module: FakeModule) => void) => () => void;
@@ -178,17 +159,28 @@ export function createFakeMetro(): FakeMetro {
178
159
  const paths = new Map<FakeModule, string>();
179
160
  const waits = new Set<Wait>();
180
161
  const listeners = new Set<(module: FakeModule) => void>();
162
+ const missing = new Set<string>();
163
+
164
+ function isMissing(filter: FakeFilter): boolean {
165
+ return Boolean(filter.terms?.some((term) => missing.has(term)));
166
+ }
181
167
 
182
168
  const filters: FakeMetro['filters'] = {
183
- byProps:
184
- (...props) =>
185
- (module) =>
186
- props.every((prop) => prop in module),
187
- byName: (name) => (module) => 'name' in module && module.name === name,
169
+ byProps: (...props) =>
170
+ Object.assign((module: FakeModule) => props.every((prop) => prop in module), {
171
+ terms: props,
172
+ }),
173
+ byName: (name) =>
174
+ Object.assign((module: FakeModule) => 'name' in module && module.name === name, {
175
+ terms: [name],
176
+ }),
188
177
  byDisplayName: (name) => (module) => module.displayName === name,
189
178
  byTypeName: (name) => (module) => module.type?.name === name,
190
179
  byFilePath: (path) =>
191
- Object.assign((module: FakeModule) => paths.get(module) === path, { path }),
180
+ Object.assign((module: FakeModule) => paths.get(module) === path, {
181
+ path,
182
+ terms: [path],
183
+ }),
192
184
  all: (...parts) => {
193
185
  for (const part of parts) assertCombinable('all', part);
194
186
 
@@ -227,6 +219,8 @@ export function createFakeMetro(): FakeMetro {
227
219
  return first(filter);
228
220
  }
229
221
 
222
+ if (isMissing(filter)) return;
223
+
230
224
  const resolve = () => first(filter);
231
225
  const handle = lazyHandle(resolve, description);
232
226
 
@@ -244,7 +238,17 @@ export function createFakeMetro(): FakeMetro {
244
238
  ) as Record<keyof T, FakeModule>;
245
239
  }
246
240
 
247
- function onModuleLoaded(filter: FakeFilter, onLoad: (module: FakeModule) => void) {
241
+ function onModuleLoaded(
242
+ filter: FakeFilter,
243
+ onLoad: (module: FakeModule) => void,
244
+ { onMissing }: WaitOptions = {},
245
+ ) {
246
+ if (isMissing(filter)) {
247
+ onMissing?.();
248
+
249
+ return () => void 0;
250
+ }
251
+
248
252
  const found = first(filter);
249
253
 
250
254
  if (found) {
@@ -325,6 +329,10 @@ export function createFakeMetro(): FakeMetro {
325
329
  paths.clear();
326
330
  waits.clear();
327
331
  listeners.clear();
332
+ missing.clear();
333
+ },
334
+ markMissing(term) {
335
+ missing.add(term);
328
336
  },
329
337
  register(module, path) {
330
338
  modules.push(module);