diff --git a/packages/pass-style/NEWS.md b/packages/pass-style/NEWS.md index 05055190f1..c7c888fab9 100644 --- a/packages/pass-style/NEWS.md +++ b/packages/pass-style/NEWS.md @@ -2,11 +2,16 @@ User-visible changes in `@endo/pass-style`: # Next release -- deprecates `assertChecker`. Use `Fail` in the confirm/reject pattern instead, as supported by `@endo/errors/rejector.js`. +- Deprecates `assertChecker`. Use `Fail` in the confirm/reject pattern instead, + as supported by `@endo/errors/rejector.js`. - Enables `passStyleOf` to make errors passable as a side-effect when SES locks down with `hardenTaming` set to `unsafe`, which impacts errors on V8 starting with Node.js 21, and similar engines, that own a `stack` getter and setter that would otherwise be repaired as a side-effect of `harden`. +- Updates `CopyArray` to be a `ReadonlyArray`. We know dynamically + that the CopyArray is hardened, so this typing statically reveals where + mutation is attempted. Use `Passable[]` if you want to have a mutable array + that contains only passable values. # 1.6.3 (2025-07-11) diff --git a/packages/pass-style/src/types.d.ts b/packages/pass-style/src/types.d.ts index 00183b18ba..ffde05c74a 100644 --- a/packages/pass-style/src/types.d.ts +++ b/packages/pass-style/src/types.d.ts @@ -116,11 +116,11 @@ export type Container = | CopyArrayInterface | CopyRecordInterface | CopyTaggedInterface; -interface CopyArrayInterface +export interface CopyArrayInterface extends CopyArray> {} -interface CopyRecordInterface +export interface CopyRecordInterface extends CopyRecord> {} -interface CopyTaggedInterface +export interface CopyTaggedInterface extends CopyTagged> {} export type PassStyleOf = { @@ -134,7 +134,7 @@ export type PassStyleOf = { (p: Promise): 'promise'; (p: Error): 'error'; (p: CopyTagged): 'tagged'; - (p: any[]): 'copyArray'; + (p: readonly any[]): 'copyArray'; (p: Iterable): 'remotable'; (p: Iterator): 'remotable'; >(p: T): ExtractStyle; @@ -199,10 +199,15 @@ export type PassableCap = | RemotableObject | RemotableBrand; +/** + * Types you would get from Awaited + */ +export type AwaitedPassableCap = RemotableObject | RemotableBrand; + /** * A Passable sequence of Passable values. */ -export type CopyArray = Array; +export type CopyArray = ReadonlyArray; /** * A hardened immutable ArrayBuffer. diff --git a/packages/pass-style/src/types.test-d.ts b/packages/pass-style/src/types.test-d.ts index f6d52dc861..9b718aafca 100644 --- a/packages/pass-style/src/types.test-d.ts +++ b/packages/pass-style/src/types.test-d.ts @@ -33,6 +33,28 @@ expectType(passStyleOf(someUnknown)); const expectPassable = (val: Passable) => {}; +() => { + const arr = ['hello'] as string[]; + expectPassable(arr); + arr.push('world'); + const slice = arr.slice(); + expectPassable(slice); + arr.shift(); + slice.shift(); +}; + +() => { + const roArr = ['hello'] as Readonly; + expectPassable(roArr); + // @ts-expect-error immutable + roArr.push('world'); + const roSlice = roArr.slice(); + expectPassable(roSlice); + // @ts-expect-error immutable + roArr.shift(); + roSlice.shift(); +}; + const fn = () => {}; expectPassable(1); @@ -44,6 +66,12 @@ expectPassable(fn()); expectPassable({}); expectPassable({ a: {} }); +expectPassable({ a: { b: {} } }); +expectPassable(['car', 'cdr']); +expectPassable(['car', 'cdr'] as string[]); +expectPassable([['a'], ['b']] as const); +expectPassable(['car', 'cdr'] as Readonly); +expectPassable(['car', 'cdr'] as Readonly<[string, string]>); // @ts-expect-error not passable expectPassable(fn); // FIXME promise for a non-Passable is not Passable