diff --git a/.changeset/docs-state-what-is-true.md b/.changeset/docs-state-what-is-true.md new file mode 100644 index 0000000..2bea55f --- /dev/null +++ b/.changeset/docs-state-what-is-true.md @@ -0,0 +1,28 @@ +--- +'@openinf/assert': patch +'@openinf/gh-file-importer': patch +'@openinf/util': patch +'@openinf/util-array': patch +'@openinf/util-core': patch +'@openinf/util-date': patch +'@openinf/util-errors': patch +'@openinf/util-md-table': patch +'@openinf/util-number': patch +'@openinf/util-object': patch +'@openinf/util-string': patch +'@openinf/util-text': patch +'@openinf/util-types': patch +--- + +Made the READMEs and API reference say what is true. Each README names +TypeScript 6 as the release CI type-checks the published declarations with, and +the ones that need it say which `lib` entries or `@types/node` a consumer has to +supply for `Temporal`, `Float16Array` and `Buffer`, and that a CommonJS consumer +of `@openinf/gh-file-importer` cannot use `node16` resolution. The +`@openinf/util-errors` README says which codes Node.js also defines and where +their meanings differ, and its doc comments link only to codes Node.js has. The +`@openinf/util-types` README names every guard that checks shape rather than an +internal slot. The object helpers say they skip `__proto__`, `constructor` and +`prototype` and copy no symbol keys, `deepMerge` says when it shares a plain +object and when it throws on a cycle, `create` says it copies deeply, and `type` +lists the names it reports. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7315b11..3ec9e2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,11 +35,11 @@ shows up then. The packages build with TypeScript **7**. The root pins TypeScript **6**, and that is not an oversight: TypeScript 7 dropped the JavaScript compiler API, and -both TypeDoc and `type-coverage` are built on it. TypeDoc's peer range stops at -`6.0.x`. +TypeDoc is built on it, as are the checkers in `tools/` that import +`typescript`. TypeDoc's peer range stops at `6.0.x`. So `pnpm outdated` will report the root's `typescript` as behind for as long as -that holds, and Dependabot is configured to leave it alone. Revisit when TypeDoc +that holds, and Renovate is configured to leave it alone. Revisit when TypeDoc accepts 7.x. ## The layout @@ -66,10 +66,10 @@ without analyzing them, and it is worth a lot: importing a single function from `@openinf/util` costs 109 bytes against 13.5 KB for the whole module. Adding a bundler to the build looks appealing, because -`require('@openinf/util')` loads 182 modules and pre-bundling each package cuts -that to 7, taking about 25 ms down to 5.5 ms. Measured in August 2026, it is the -wrong trade: the same import then costs **954 bytes instead of 109**, roughly -nine times worse. +`require('@openinf/util')` loads every module of every package it reaches, one +file each, and pre-bundling each package would load one file per package and +start several times faster. Measured in August 2026, it is the wrong trade: the +same import then costs **954 bytes instead of 109**, roughly nine times worse. The reason is specific to this codebase. Every guard is followed by `fn.expectation = '...'`, and once those functions share a module scope, a @@ -92,9 +92,11 @@ Conventions worth knowing before your first PR: - **Named exports only.** No `export default` anywhere, including internal modules. Imports between modules use the named form. - **One symbol per file**, named in kebab-case after the symbol it exports. -- **Type guards carry an `expectation`.** A guard is not just a predicate; the - assertion helpers and argument validators read `.expectation` to build their - error messages, so a guard doubles as the specification for its own check. +- **Type guards carry an `expectation`.** A function that narrows `unknown` to a + type is not just a predicate; the assertion helpers and argument validators + read `.expectation` to build their error messages, so a guard doubles as the + specification for its own check. A predicate that returns a plain `boolean` is + not a guard, and carries none. - Internal-only modules live in `_internal/` and are prefixed with `_`. ## Before you open a PR @@ -108,7 +110,7 @@ pnpm run lint:knip # unused files, exports, and dependencies pnpm run lint:commits # commit message format and sign-off pnpm run lint:examples # README and doc comment examples compile and claim true results pnpm run lint:md # markdown, in files and in doc comments -pnpm run lint:types # the build tasks and checkers type-check +pnpm run lint:types # the .mts build tasks and checkers type-check pnpm run lint:spelling # cspell, en-US pnpm run lint:packages # publint + arethetypeswrong, against real tarballs ``` @@ -116,12 +118,13 @@ pnpm run lint:packages # publint + arethetypeswrong, against real tarballs `pnpm run format` fixes anything `lint:format` reports. It covers the whole repository, not just TypeScript -- Markdown, JSON, and YAML included. -CI runs exactly these. Four of them are the ones people forget: `lint:knip` -catches an export you added but never wired into the barrel, `lint:commits` -catches a message the commit queue would refuse to land, `lint:packages` catches -a `package.json` change that breaks resolution for CommonJS or ESM consumers, -and `lint:spelling` covers prose along with the strings that ship -- error -messages and package descriptions reach users as surely as the code does. +CI runs all of these, and more besides. Four of them are the ones people forget: +`lint:knip` catches an export you added but never wired into the barrel, +`lint:commits` catches a message the commit queue would refuse to land, +`lint:packages` catches a `package.json` change that breaks resolution for +CommonJS or ESM consumers, and `lint:spelling` covers prose along with the +strings that ship -- error messages and package descriptions reach users as +surely as the code does. **This project uses American English.** The dictionary is `en-US` rather than plain `en` precisely because `en` accepts both spellings and lets them drift. If diff --git a/README.md b/README.md index 54d7df8..c09c25a 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ publishing is one idea, carried consistently: **A type guard here is also the specification for its own check.** -Every guard carries an `expectation` describing what it tests. Assertion helpers -and argument validators read it to build their error messages, so the -description and the check cannot drift apart -- there is only one of them. +Every type guard -- a function that narrows `unknown` to a type -- carries an +`expectation` describing what it tests. Assertion helpers and argument +validators read it to build their error messages, so the description and the +check cannot drift apart -- there is only one of them. A predicate that returns +a plain `boolean` narrows nothing, and carries none. ```ts import { and, isInteger, isPositive } from '@openinf/util'; @@ -42,8 +44,8 @@ import { validateString } from '@openinf/util'; import { InvalidArgTypeError } from '@openinf/util-errors'; validateString(42, 'name'); -// ↪ InvalidArgTypeError: The "name" argument must be of type "string". -// Received type "number" ("42") +// ↪ InvalidArgTypeError [ERR_INVALID_ARG_TYPE]: The "name" argument must be +// of type "string". Received type "number" ("42") ``` Errors are classes, not string codes, so callers can catch by type. @@ -88,13 +90,15 @@ testing before removal. bottom with no dependencies at all. It holds the language types and testing operations of ECMAScript sections 6 and 7.2, and the vocabulary everything else is written in -- `Guard`, `Validator` and `Tagged`. A package for a chapter of -the specification builds on it: `util-number` for Number, BigInt and Math, and -`util-date` for Date, `util-string` for String and RegExp. Depend on any of them -alone if that is all you need. +the specification builds on it: `util-number` for Number and BigInt, `util-date` +for Date, and `util-string` for String and RegExp. Depend on any of them alone +if that is all you need. -Each package builds only on the ones listed above it. A package names only the -dependencies it does not already reach through another, so `util-errors` names -`assert`, which brings `util-text` and the rest with it: +Each package builds only on the ones listed above it. The table names the +nearest packages each one builds on, so `util-errors` shows `assert`, which +brings `util-text` and the rest with it. Its `package.json` still lists every +package it imports, because an install that keeps packages apart, as pnpm's +does, lets a package reach only what it declares: | Package | Builds on | | ------------------ | -------------------------------------------------------- | @@ -142,8 +146,9 @@ the full test suite, plus: Releases are published from a manually triggered workflow using npm [trusted publishing](https://docs.npmjs.com/trusted-publishers), with [provenance](https://docs.npmjs.com/generating-provenance-statements) -attestations. No long-lived tokens exist in CI. Third-party actions are pinned -to commit SHAs. +attestations, so no npm token exists in CI. The one long-lived secret is the +commit queue's GitHub App key, which it exchanges for an installation token that +expires within the hour. Third-party actions are pinned to commit SHAs. ## Requirements @@ -160,9 +165,12 @@ other ten today, but nothing holds them to it. TypeScript 6 knows `Temporal` but does not load it by default. A project that uses those three packages needs `esnext.temporal`, or all of `esnext`, in its `lib`; otherwise the compiler reports `Cannot find namespace 'Temporal'` from -inside the package. `@openinf/util` also refers to Node's `Buffer`, so it needs -`@types/node` installed and `node` in `types`, which TypeScript 6 no longer -fills in by itself. Both are type checks of the package's own declarations, so +inside the package. `@openinf/util-array` names `Float16Array`, as do +`@openinf/util-types` and `@openinf/util` through it; that needs `es2025` or +later in `lib`, or `@types/node` 24 or later installed and `node` in `types`. +`@openinf/util` also refers to Node's `Buffer`, so it needs `@types/node` +installed and `node` in `types`, which TypeScript 6 no longer fills in by +itself. These are all type checks of the package's own declarations, so `skipLibCheck` silences them instead, at the cost of reading those types as `any`. diff --git a/RELEASING.md b/RELEASING.md index 53d32ff..224efdb 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,10 +12,10 @@ mode, configured in [`.changeset/config.json`](.changeset/config.json) as Practically, that means: - A release bumps **every** package, including ones with no changes. That is - intentional: `@openinf/util-core` has eight transitive dependents here, and a - breaking change in it can leak through re-exports to consumers of a package - that didn't itself change. Independent versioning would signal that as a patch - bump, which understates the risk. + intentional: nearly every other package here builds on `@openinf/util-core`, + and a breaking change in it can leak through re-exports to consumers of a + package that didn't itself change. Independent versioning would signal that as + a patch bump, which understates the risk. - The whole group lands on the highest version any member needs. One `major` changeset anywhere makes it a major release for all thirteen. - A package with no changeset naming it still gets bumped, but its generated @@ -173,11 +173,7 @@ The `release` script also passes `--provenance`, so each package ships a signed linking the tarball to the commit and workflow run that produced it. npm shows a verified badge for it on the package page. -Three things to know about that flag. - -It is undocumented in `pnpm publish --help` but genuinely supported -- pnpm -rejects flags it does not recognize, so it would fail loudly rather than -silently skip. +Two things to know about that flag. **Provenance requires a public repository.** From a private one the publish fails outright rather than degrading to an unsigned publish. diff --git a/packages/assert/README.md b/packages/assert/README.md index 91767bf..af86ace 100644 --- a/packages/assert/README.md +++ b/packages/assert/README.md @@ -26,8 +26,8 @@ port.toFixed(); ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/gh-file-importer/README.md b/packages/gh-file-importer/README.md index aa9258f..bacab45 100644 --- a/packages/gh-file-importer/README.md +++ b/packages/gh-file-importer/README.md @@ -66,13 +66,19 @@ const ghFileImporter = new GhFileImporter({ destDir: './tmp', log }); ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` - `commonjs`, `esnext`, or `preserve` with `bundler` +`node16` with `node16` works only from an ES module: `@octokit/rest` is +ESM-only, and under `node16` TypeScript does not let a CommonJS file import it. +A CommonJS project uses `nodenext` or `bundler` instead. The declarations also +name the fetch types `AbortSignal`, `Blob` and `File`, so the project needs +`@types/node` or the `dom` library. + ## Documentation The API reference for `@openinf/gh-file-importer` and the other OpenINF packages diff --git a/packages/util-array/README.md b/packages/util-array/README.md index 7f16c43..6547271 100644 --- a/packages/util-array/README.md +++ b/packages/util-array/README.md @@ -22,13 +22,18 @@ arraysEqual([1, 2], [1, 2]); // ↪ true ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` - `commonjs`, `esnext`, or `preserve` with `bundler` +The declarations name `Float16Array`, so `lib` needs `es2025` or later, or +`@types/node` 24 or later installed and `node` in `types`. Node.js has +`Float16Array` from release 24; on an earlier one, `isFloat16Array` returns +`false` for everything. + ## Documentation The API reference for `@openinf/util-array` and the other OpenINF packages is on diff --git a/packages/util-array/src/guards/is-float16-array.ts b/packages/util-array/src/guards/is-float16-array.ts index cb4097e..d7ddc82 100644 --- a/packages/util-array/src/guards/is-float16-array.ts +++ b/packages/util-array/src/guards/is-float16-array.ts @@ -6,7 +6,8 @@ import { _tagTester, isObjectLike } from '@openinf/util-core'; /** * Detects whether `value` is classified as a * [`Float16Array`](https://mdn.io/Global_Objects/Float16Array), the typed - * array ES2025 added. + * array ES2025 added. Node.js has it from release 24; on an earlier one, + * nothing is a `Float16Array`, so this returns `false`. * @since 3.0.0 * @category Indexed Collections * @param value The value to identify. diff --git a/packages/util-core/README.md b/packages/util-core/README.md index 21cdbad..08686ce 100644 --- a/packages/util-core/README.md +++ b/packages/util-core/README.md @@ -52,8 +52,8 @@ everything here alongside its full set of type guards. ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-core/src/types.ts b/packages/util-core/src/types.ts index 32e5c3d..5c3d0a7 100644 --- a/packages/util-core/src/types.ts +++ b/packages/util-core/src/types.ts @@ -10,7 +10,8 @@ /** * Safer version of `Function` which should not be called. - * Every function should be assignable to this, but this should not be assignable to every function. + * Every callable function is assignable to this, but a class, which can only be + * constructed, is not, and this is not assignable to every function. * @category Data Types and Values */ export type AnyFunction = (...args: never[]) => void; diff --git a/packages/util-date/README.md b/packages/util-date/README.md index adaff90..f77fd6c 100644 --- a/packages/util-date/README.md +++ b/packages/util-date/README.md @@ -1,12 +1,12 @@ # @openinf/util-date -Guards for ECMAScript dates, following section 21.4 of the specification: the -`Date` object, and the `Temporal` objects finished for ES2027. +Guards for ECMAScript dates: the `Date` object of section 21.4 of the +specification, and the `Temporal` objects finished for ES2027. `isDate` checks for a real `Date`, one with the internal time value only the `Date` constructor gives an object, so a look-alike carrying `Symbol.toStringTag` does not pass. `isValidDate` also requires that time value -to be a number, which an Invalid Date's is not. +not be `NaN`, which an Invalid Date's is. There is a guard for each Temporal type: `isTemporalInstant`, `isTemporalZonedDateTime`, `isTemporalPlainDate`, `isTemporalPlainTime`, @@ -42,13 +42,18 @@ isTemporalInstant(new Date()); // ↪ false ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` - `commonjs`, `esnext`, or `preserve` with `bundler` +The declarations name `Temporal`, which TypeScript 6 does not load by default, +so `lib` needs `esnext.temporal`, or all of `esnext`; otherwise the compiler +reports `Cannot find namespace 'Temporal'` from inside the package, whichever +guard you import. + ## Documentation The API reference for `@openinf/util-date` and the other OpenINF packages is on diff --git a/packages/util-errors/README.md b/packages/util-errors/README.md index 6256219..9626676 100644 --- a/packages/util-errors/README.md +++ b/packages/util-errors/README.md @@ -1,8 +1,13 @@ # @openinf/util-errors Error classes modeled on the ones Node.js core throws, so that a caller can -catch by class rather than by matching a message. Each carries the same `code` -its Node.js counterpart does. +catch by class rather than by matching a message. Each carries a `code` in the +form Node.js uses. Where Node.js defines that code, it is the same error, except +that `ERR_INVALID_RETURN_PROPERTY_VALUE` and `ERR_INVALID_RETURN_VALUE` here +mean an invalid value, and Node.js uses them for a value of the wrong type. +`ERR_INVALID_ARGS_NUMBER`, `ERR_INVALID_PROPERTY_TYPE`, +`ERR_INVALID_PROPERTY_VALUE`, `ERR_INVALID_RETURN_PROPERTY_TYPE` and +`ERR_INVALID_RETURN_TYPE` are not Node.js codes. ## Installation @@ -32,8 +37,8 @@ try { ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-errors/src/exceptions/invalid-return-property-type-error.ts b/packages/util-errors/src/exceptions/invalid-return-property-type-error.ts index 196e7e6..2d39f7f 100644 --- a/packages/util-errors/src/exceptions/invalid-return-property-type-error.ts +++ b/packages/util-errors/src/exceptions/invalid-return-property-type-error.ts @@ -12,7 +12,6 @@ import { NodeTypeError } from '../abstractions/node-type-error'; /** * Thrown in case a function does not provide an expected value type for * one of its returned object properties on execution. - * @see https://nodejs.org/api/errors.html#ERR_INVALID_RETURN_PROPERTY_VALUE */ export class InvalidReturnPropertyTypeError extends NodeTypeError { /** diff --git a/packages/util-errors/src/exceptions/invalid-return-property-value-error.ts b/packages/util-errors/src/exceptions/invalid-return-property-value-error.ts index 79032a8..6d823ee 100644 --- a/packages/util-errors/src/exceptions/invalid-return-property-value-error.ts +++ b/packages/util-errors/src/exceptions/invalid-return-property-value-error.ts @@ -11,8 +11,10 @@ import { NodeTypeError } from '../abstractions/node-type-error'; /** * Thrown in case a function does not provide a valid value for one of - * its returned object properties on execution. - * @see https://nodejs.org/api/errors.html#ERR_INVALID_RETURN_PROPERTY + * its returned object properties on execution. Node.js throws the same code + * for a returned property of the wrong type, which is + * {@link InvalidReturnPropertyTypeError} here. + * @see https://nodejs.org/api/errors.html#ERR_INVALID_RETURN_PROPERTY_VALUE */ export class InvalidReturnPropertyValueError extends NodeTypeError { /** diff --git a/packages/util-errors/src/exceptions/invalid-return-type-error.ts b/packages/util-errors/src/exceptions/invalid-return-type-error.ts index f9fb76a..eb26428 100644 --- a/packages/util-errors/src/exceptions/invalid-return-type-error.ts +++ b/packages/util-errors/src/exceptions/invalid-return-type-error.ts @@ -12,7 +12,6 @@ import { NodeTypeError } from '../abstractions/node-type-error'; /** * Thrown in case a function does not return an expected value type on * execution, such as when a function is expected to return a promise. - * @see https://nodejs.org/api/errors.html#ERR_INVALID_RETURN_VALUE */ export class InvalidReturnTypeError extends NodeTypeError { /** diff --git a/packages/util-errors/src/exceptions/invalid-return-value-error.ts b/packages/util-errors/src/exceptions/invalid-return-value-error.ts index f9e9980..6d9982e 100644 --- a/packages/util-errors/src/exceptions/invalid-return-value-error.ts +++ b/packages/util-errors/src/exceptions/invalid-return-value-error.ts @@ -11,7 +11,8 @@ import { NodeTypeError } from '../abstractions/node-type-error'; /** * Thrown in case a function does not return an expected valid value on - * execution. + * execution. Node.js throws the same code for a return value of the wrong + * type, which is {@link InvalidReturnTypeError} here. * @see https://nodejs.org/api/errors.html#ERR_INVALID_RETURN_VALUE */ export class InvalidReturnValueError extends NodeTypeError { diff --git a/packages/util-md-table/README.md b/packages/util-md-table/README.md index 4e082de..8e4c6be 100644 --- a/packages/util-md-table/README.md +++ b/packages/util-md-table/README.md @@ -33,8 +33,8 @@ console.log(mdTbl2json(table, (value) => value.toLowerCase())); ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-number/README.md b/packages/util-number/README.md index 6a5a49a..c405aeb 100644 --- a/packages/util-number/README.md +++ b/packages/util-number/README.md @@ -1,10 +1,10 @@ # @openinf/util-number -Guards for ECMAScript numbers, following sections 21.1 to 21.3 of the -specification, Number, BigInt and Math, and the numeric conversions of section -7.1. That covers integers and their sign, the 32-bit ranges `ToInt32` and -`ToUint32` produce, the lengths `ToLength` produces, and the boxed `Number` and -`BigInt` objects. +Guards for ECMAScript numbers, following sections 21.1 and 21.2 of the +specification, Number and BigInt, and the numeric conversions of section 7.1. +That covers integers and their sign, the 32-bit ranges `ToInt32` and `ToUint32` +produce, the lengths `ToLength` produces, and the boxed `Number` and `BigInt` +objects. The number guards narrow to branded types such as `Integer` and `Uint32`, so a value checked once carries the check with it. There is a guard for each range a @@ -14,7 +14,7 @@ tell from `0`. It depends only on [`@openinf/util-core`](https://www.npmjs.com/package/@openinf/util-core), which -supplies the `isNumber` and `isBigInt` type guards these refine. +supplies the `isNumber` type guard these refine. ## Installation @@ -34,8 +34,8 @@ isUint32(-1); // ↪ false ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-object/README.md b/packages/util-object/README.md index 23633a2..e3a1830 100644 --- a/packages/util-object/README.md +++ b/packages/util-object/README.md @@ -32,8 +32,8 @@ if (hasOwn(value, 'id')) { ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-object/src/guards/type.ts b/packages/util-object/src/guards/type.ts index 741fa3a..23309d9 100644 --- a/packages/util-object/src/guards/type.ts +++ b/packages/util-object/src/guards/type.ts @@ -7,8 +7,10 @@ import { _tagTester, isObject } from '@openinf/util-core'; /** * Names the kind of value `obj` is, in lower case: a `typeof` result for a - * non-null primitive, and for an object the built-in it was made by, such as - * `'array'`, `'date'` or `'error'`, falling back to `'object'`. Unlike + * non-null primitive or a function, and for any other object `'array'`, + * `'date'`, `'regexp'` or `'error'` when it is one, the primitive's name for a + * `Boolean`, `Number`, `String` or `Symbol` wrapper, and `'object'` for + * everything else, a `Map` or a `BigInt` wrapper included. Unlike * `typeof null`, which is `'object'`, this function reports null as `'null'`. * * Older documentation called this the internal `[[Class]]`, a slot the diff --git a/packages/util-object/src/helpers/clone.ts b/packages/util-object/src/helpers/clone.ts index 09c3930..b4b15b8 100644 --- a/packages/util-object/src/helpers/clone.ts +++ b/packages/util-object/src/helpers/clone.ts @@ -5,7 +5,9 @@ import { _isUnsafeKey } from '../_internal/_is-unsafe-key'; import { hasOwn } from '../guards/has-own'; /** - * Shallowly clones an object's own enumerable properties. + * Shallowly clones an object's own enumerable string-keyed properties into a + * plain object. The keys `__proto__`, `constructor` and `prototype` are + * skipped, since writing them could reach a prototype. * @category Fundamental Objects * @param object The object to clone. * @returns The clone. diff --git a/packages/util-object/src/helpers/create.ts b/packages/util-object/src/helpers/create.ts index 2cd8823..64761de 100644 --- a/packages/util-object/src/helpers/create.ts +++ b/packages/util-object/src/helpers/create.ts @@ -6,7 +6,9 @@ import { deepAssign } from './deep-assign'; /** * Creates an object that inherits from the `prototype` object. If a * `properties` object is given, its own enumerable string keyed properties - * are assigned to the created object. + * are copied onto the created object as {@link deepAssign} copies them: + * nested plain objects and arrays are copied rather than shared, and the keys + * `__proto__`, `constructor` and `prototype` are skipped. * @since 3.0.0 * @category Fundamental Objects * @param prototype The object to inherit from. diff --git a/packages/util-object/src/helpers/deep-assign.ts b/packages/util-object/src/helpers/deep-assign.ts index bfa2093..3c16f50 100644 --- a/packages/util-object/src/helpers/deep-assign.ts +++ b/packages/util-object/src/helpers/deep-assign.ts @@ -61,7 +61,9 @@ export function deepAssign< * Copies the values of all enumerable own properties of one or more source * objects to the target object, recursively copying all nested plain objects * and arrays as well. Any other object (a function, a `Date`, a `Map`, a class - * instance) is assigned by reference. + * instance) is assigned by reference. Only string keys are copied, and + * `__proto__`, `constructor` and `prototype` are skipped, since writing them + * could reach a prototype. * @category Fundamental Objects * @param target The target object to receive values from source objects * @param sources Any number of objects whose enumerable own properties will be diff --git a/packages/util-object/src/helpers/deep-merge.ts b/packages/util-object/src/helpers/deep-merge.ts index 15702c0..d93cd6e 100644 --- a/packages/util-object/src/helpers/deep-merge.ts +++ b/packages/util-object/src/helpers/deep-merge.ts @@ -37,14 +37,21 @@ function sourcePathIncludes( * Only nested plain objects are merged. Anything else on the source (a * primitive, an array, a function, a `Date`, a `Map`, a class instance) * replaces whatever the target holds at that key, and is assigned as it stands - * rather than copied, so the target and the source share it afterwards. + * rather than copied, so the target and the source share it afterwards. A + * plain object is shared the same way wherever the target holds nothing at + * that key to merge it into, such as a key it lacks. The keys `__proto__`, + * `constructor` and `prototype` are skipped, since writing them could reach a + * prototype, and symbol keys are not copied. * @category Fundamental Objects * @param target The object to merge properties into. * @param source The object to merge properties from. * @param depth The maximum merge depth. If exceeded, properties are assigned * without recursively merging them. * @returns The modified `target` object. - * @throws {Error} If source contains a circular reference. + * @throws {Error} If the merge, descending into plain objects the target also + * has, reaches a source object it is already inside. This is not a general + * cycle check: a cycle the merge does not descend into is assigned as it + * stands, and one the target and source already share is left alone. */ export function deepMerge( target: Record, diff --git a/packages/util-object/src/helpers/deep-mixin.ts b/packages/util-object/src/helpers/deep-mixin.ts index 9e96eef..021e50d 100644 --- a/packages/util-object/src/helpers/deep-mixin.ts +++ b/packages/util-object/src/helpers/deep-mixin.ts @@ -63,7 +63,9 @@ export function deepMixin< * Copies the values of all enumerable (own or inherited) properties of one or * more source objects to the target object, recursively copying all nested * plain objects and arrays as well. Any other object (a function, a `Date`, a - * `Map`, a class instance) is assigned by reference. + * `Map`, a class instance) is assigned by reference. Only string keys are + * copied, and `__proto__`, `constructor` and `prototype` are skipped, since + * writing them could reach a prototype. * @category Fundamental Objects * @param target The target object to receive values from source objects * @param sources Any number of objects whose enumerable properties will be copied to the target object diff --git a/packages/util-object/src/helpers/mixin.ts b/packages/util-object/src/helpers/mixin.ts index 5d6339c..273a134 100644 --- a/packages/util-object/src/helpers/mixin.ts +++ b/packages/util-object/src/helpers/mixin.ts @@ -7,7 +7,9 @@ import { _mixin } from '../_internal/_mixin'; /** * Copies the values of all enumerable (own or inherited) properties of one or - * more source objects to the target object. + * more source objects to the target object. Only string keys are copied, and + * `__proto__`, `constructor` and `prototype` are skipped, since writing them + * could reach a prototype. * @category Fundamental Objects * @returns The modified target object */ diff --git a/packages/util-object/src/helpers/omit.ts b/packages/util-object/src/helpers/omit.ts index 31ed185..49c4044 100644 --- a/packages/util-object/src/helpers/omit.ts +++ b/packages/util-object/src/helpers/omit.ts @@ -4,7 +4,9 @@ import { _isUnsafeKey } from '../_internal/_is-unsafe-key'; /** - * Creates an object with the given properties removed. + * Creates an object with the given properties removed. Only own enumerable + * string-keyed properties are kept; symbol keys and the keys `__proto__`, + * `constructor` and `prototype` are always left out. * @category Fundamental Objects * @param obj An object to remove properties from. * @param props A list of properties to remove from the Object. diff --git a/packages/util-string/README.md b/packages/util-string/README.md index fb4a706..52a2f24 100644 --- a/packages/util-string/README.md +++ b/packages/util-string/README.md @@ -32,8 +32,8 @@ mentionsNode('Node.js'); // ↪ true ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-text/README.md b/packages/util-text/README.md index 2110463..dc4c569 100644 --- a/packages/util-text/README.md +++ b/packages/util-text/README.md @@ -23,8 +23,8 @@ console.log(blueify(ellipsify('Deserializing database tables'))); ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` diff --git a/packages/util-types/README.md b/packages/util-types/README.md index 3a99d6e..8d63a8c 100644 --- a/packages/util-types/README.md +++ b/packages/util-types/README.md @@ -34,20 +34,20 @@ where the language exposes a non-mutating internal-slot check. For example, even when the receiver inherits `Map.prototype` or advertises a `Map` tag. The predicate catches that exception and returns `false`. -| Types | Implementation | Guarantee | -| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------- | -| Maps, sets, weak maps, weak sets | Captured `has` methods | Internal-slot check | -| Dates, boxed primitives | Captured `getTime` or `valueOf` | Internal-slot check | -| Regular expressions | Captured `source` getter | Internal-slot check; does not change `lastIndex` | -| Buffers, data views | Captured `byteLength` or `buffer` getter | Internal-slot check; accepts detached and out-of-bounds views | -| Typed arrays | Captured `%TypedArray%` tag getter | Reads the internal element type, not the object's tag | -| Weak refs, finalization registries, disposable stacks | Captured `deref`, `unregister`, `disposed` | Internal-slot check; leaves the value as it found it | -| WebAssembly modules | Captured `WebAssembly.Module.exports` | Host brand check; returns `false` if WebAssembly is absent | -| Errors | `Error.isError`, or Node.js's `types.isNativeError` | Internal brand check; tag fallback on other older engines | -| Arguments, promises, async/generator functions and objects, map/set iterators, module namespaces | Tag, source, and descriptor checks | Best effort; rejects casual spoofs, not deliberate forgeries | -| Buffer state, raw JSON | Captured `detached`, `resizable` and `growable` getters, and `JSON.isRawJSON` | Internal-slot check; reads state without changing it | -| Proxies | Captured `Array.isArray` | Detects revoked proxies only; live proxies remain opaque | -| Native external values | Compatibility stub | Always `false`; detection requires engine support | +| Types | Implementation | Guarantee | +| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------- | +| Maps, sets, weak maps, weak sets | Captured `has` methods | Internal-slot check | +| Dates, boxed primitives | Captured `getTime` or `valueOf` | Internal-slot check | +| Regular expressions | Captured `source` getter | Internal-slot check; does not change `lastIndex` | +| Buffers, data views | Captured `byteLength` or `buffer` getter | Internal-slot check; accepts detached and out-of-bounds views | +| Typed arrays | Captured `%TypedArray%` tag getter | Reads the internal element type, not the object's tag | +| Weak refs, finalization registries, disposable stacks | Captured `deref`, `unregister`, `disposed` | Internal-slot check; leaves the value as it found it | +| WebAssembly modules | Captured `WebAssembly.Module.exports` | Host brand check; returns `false` if WebAssembly is absent | +| Errors | `Error.isError`, or Node.js's `types.isNativeError` | Internal brand check; tag fallback on other older engines | +| Arguments, promises, async/generator functions and objects, iterators, iterator helpers, module namespaces | Tag, source, and descriptor checks | Best effort; an object inheriting the real prototype passes | +| Buffer state, raw JSON | Captured `detached`, `resizable` and `growable` getters, and `JSON.isRawJSON` | Internal-slot check; reads state without changing it | +| Proxies | Captured `Array.isArray` | Detects revoked proxies only; live proxies remain opaque | +| Native external values | Compatibility stub | Always `false`; detection requires engine support | The internal-slot checks accept values from other realms, including subclasses and genuine objects with custom tags or overridden methods. They reject proxies @@ -63,12 +63,16 @@ cross-realm error tag, which only an object with `[[ErrorData]]` reports, and fall back to local `Error.prototype` ancestry when a custom tag hides it. Forged prototypes and proxy traps can still fool that fallback. -For opaque types, the Underscore tag fallback now reads property descriptors -instead of invoking ordinary tag or method getters. It also checks expected -methods and descriptors, uses captured `Function.prototype.toString` for -async/generator function syntax, and validates the observable invariants of -module namespace objects. These filters reject casual tag spoofing, but a -carefully constructed object or proxy can still pass. Thrown proxy traps return +For opaque types, the Underscore tag fallback reads property descriptors instead +of invoking ordinary tag or method getters. It also checks expected methods and +descriptors, uses captured `Function.prototype.toString` for async/generator +function syntax, and validates the observable invariants of module namespace +objects. These filters reject a plain object that only sets a tag, but a +carefully constructed object or proxy can still pass. `isPromise`, +`isGeneratorObject`, `isAsyncGenerator`, `isIteratorHelper` and the array, +string, RegExp string, map and set iterator guards only ask for the methods +their type has, so an object made with `Object.create` from the real prototype +inherits those methods and the tag, and passes. Thrown proxy traps return `false`. `isProxy` can identify revoked proxies because the standard `IsArray` operation exposes revocation without invoking user traps; it cannot identify a live proxy. None of these best-effort checks is a security boundary. @@ -113,13 +117,19 @@ function. ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` - `commonjs`, `esnext`, or `preserve` with `bundler` +It re-exports `@openinf/util-date`, whose declarations name `Temporal`, and +`@openinf/util-array`, whose declarations name `Float16Array`. TypeScript 6 +loads neither by default, so `lib` needs `esnext.temporal`, or all of `esnext`, +and `Float16Array` needs `es2025` or later there, or `@types/node` 24 or later +installed and `node` in `types`. + ## Documentation The API reference for `@openinf/util-types` and the other OpenINF packages is on diff --git a/packages/util/README.md b/packages/util/README.md index 81b269e..2e0576a 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -64,13 +64,20 @@ for the full set of guards, validators, and helpers. ## Requirements -TypeScript 6 or newer, which is what these packages are built and tested with, -and a `module` and `moduleResolution` pair that reads an `exports` map: +TypeScript 6 or newer, which CI type-checks the published declarations with, and +a `module` and `moduleResolution` pair that reads an `exports` map: - `node16` with `node16` - `nodenext` with `nodenext` - `commonjs`, `esnext`, or `preserve` with `bundler` +It re-exports `@openinf/util-date`, whose declarations name `Temporal`, so `lib` +needs `esnext.temporal`, or all of `esnext`. It also refers to Node's `Buffer` +and, through `@openinf/util-array`, to `Float16Array`, so it needs `@types/node` +installed and `node` in `types`, which TypeScript 6 no longer fills in by +itself. Unless `lib` has `es2025` or later, that has to be `@types/node` 24 or +later, since earlier releases do not declare `Float16Array`. + ## Documentation The API reference for `@openinf/util` and the other OpenINF packages is on the diff --git a/tools/finalize-esm-output.js b/tools/finalize-esm-output.js index 0fb12c2..fb6514f 100644 --- a/tools/finalize-esm-output.js +++ b/tools/finalize-esm-output.js @@ -6,8 +6,9 @@ // extensionless relative specifiers, so that output is unloadable as-is. // // This rewrites a tsc ESM output directory in place so it's directly -// importable: relative specifiers get an explicit .mjs (or .d.mts, inside -// declaration files) appended, and every file is renamed to match +// importable: relative specifiers get an explicit .mjs appended, declaration +// files included, since TypeScript pairs a .mjs specifier with its .d.mts, +// and every file is renamed to match // (.js -> .mjs, .d.ts -> .d.mts, plus their .map siblings). The .mjs/.d.mts // extensions are unambiguous to Node/TypeScript by themselves, so no // dist/esm/package.json "type" marker is needed.