Skip to content

Commit

Permalink
refactor!: add retyped-ses package
Browse files Browse the repository at this point in the history
  • Loading branch information
SMotaal committed Dec 28, 2022
1 parent 0cf51d7 commit 261bf0e
Show file tree
Hide file tree
Showing 30 changed files with 1,516 additions and 0 deletions.
2 changes: 2 additions & 0 deletions retyped/.gitignore
@@ -0,0 +1,2 @@
node_modules/
**/node_modules/
38 changes: 38 additions & 0 deletions retyped/README.md
@@ -0,0 +1,38 @@
# Retyped SES

This folder contains retyped versions of some of the SES modules to aid with the discussions in deciding on the best path forward.

This is all still a work in progress just to get some discussions going.

## What to keep in mind…

- **Do not merge**… I butchered your code.
- JavaScript is the source of truth and `jsconfig.json` ensures this.
- Type generation is necessary since for consumer-facing types.
- TypeScript-based language services are the immediate focus.
- JSDoc-based language services are not the immediate focus.

## What to do…

Open `endo/retyped` in a new vscode window and run `yarn`, then manually inspect the files inside the packages and modules subfolders in `endo/retyped/packages/*/test`.

The `build:types` package script must be used to update the `types.d.ts` in the root of each package which can theoretically be run as a `post-install` hook by consumers, but it is not that simple.

## What to look for…

### endo/packages/ses

- endo/packages/ses/package.json
- scripts
- lint:types
- devDependencies
- tsd
- typescript
- endo/packages/ses/index.d.ts
- endo/packages/ses/index-test.d.ts

### endo/retyped/packages/retyped-ses

- endo/retypes/packages/retyped-ses/src/error/internal/*
- endo/retypes/packages/retyped-ses/test/modules/*
- endo/retypes/packages/retyped-ses/test/packages/*/*
9 changes: 9 additions & 0 deletions retyped/package.json
@@ -0,0 +1,9 @@
{
"private": true,
"npmClient": "yarn",
"workspaces": [
"packages/*",
"packages/*/test",
"packages/*/test/packages/*"
]
}
3 changes: 3 additions & 0 deletions retyped/packages/retyped-ses/globals.d.ts
@@ -0,0 +1,3 @@
declare var assert: typeof globalThis extends { null: any, assert: infer T }

This comment has been minimized.

Copy link
@SMotaal

SMotaal Dec 29, 2022

Author Owner

This pattern is borrowed and refined upon from existing modules which I cannot trace back to a clear origin but the oldest mention I found was by @forivall (thanks 🙏) in DefinitelyTyped/DefinitelyTyped#34960 (comment)

This comment was marked as resolved.

Copy link
@forivall

forivall Dec 30, 2022

glad to see my old kludge helped!

This comment was marked as resolved.

Copy link
@SMotaal

SMotaal Dec 30, 2022

Author Owner

@forivall Certainly, credit where it is due, thank you 👍

For the sake of completeness, cloning DefinitelyTyped/DefinitelyTyped locally and doing a git log -S "typeof globalThis extends" --source --all led DefinitelyTyped/DefinitelyTyped#56163 (comment) from 2021-10-19 which followed DefinitelyTyped/DefinitelyTyped#34960 (comment) from 2020-01-21.

I wanted to make sure since a lot of mainstream types adopted additions on this pattern that I as gained a better understanding of how it would play out in the wild, that I also gave credit where it is due.

So, please let me know if you know more, thank you 👍

This comment has been minimized.

Copy link
@SMotaal

SMotaal Jan 2, 2023

Author Owner

My goal here is to document this pattern to ensure it will be stable moving forward:

// @filename: node_modules/y/globals.d.ts
declare var y: typeof globalThis extends { x: any; y: infer T; } ? T : Y;

// @filename: src/index.ts
import 'y';

As far as respecting the module importing order which is lacking at this moment, I think first TypeScript needs to settle on a way to define globalThis properties within modules and there needs to be mechanisms to model this behaviour at runtime when transpiling across module formats that would be deterministic.

That said, imho, projects with JavaScript sources that are configured to explicitly express this intent are the ideal candidate as far as flagging offending aspects of the configuration and module code that violates the module loading order, which would be a tenable proposal to make today.

See: DefinitelyTyped/DefinitelyTyped#62782 (comment)

? T
: typeof import('src/error/internal/assert').assert;
2 changes: 2 additions & 0 deletions retyped/packages/retyped-ses/index.d.ts
@@ -0,0 +1,2 @@
/// <reference path="types.d.ts" />
/// <reference path="globals.d.ts" />
5 changes: 5 additions & 0 deletions retyped/packages/retyped-ses/index.js
@@ -0,0 +1,5 @@
// @ts-check
import { assert } from './src/error/assert.js';

// @ts-ignore
globalThis.assert = assert;
12 changes: 12 additions & 0 deletions retyped/packages/retyped-ses/jsconfig.json
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"noEmit": true,
"rootDir": "./",
"downlevelIteration": true,
"strictNullChecks": true,
"moduleResolution": "node",
},
"extends": "../../../packages/ses/tsconfig.json",
"include": ["index.js", "src/**/*.js"],
"exclude": ["./types"]
}
21 changes: 21 additions & 0 deletions retyped/packages/retyped-ses/modules.log
@@ -0,0 +1,21 @@
This file is manually updated with `tsc -p ./tsconfig.json --explainFiles` output.

src/error/internal/details.js
Imported via "./details.js" from file 'src/error/internal/assert.js'
Imported via "./details.js" from file 'src/error/internal/errors.js'
Imported via "./details.js" from file 'src/error/internal/assert.1.js'
Matched by include pattern 'src/**/*.js' in 'tsconfig.json'
src/error/internal/errors.js
Imported via "./errors.js" from file 'src/error/internal/assert.js'
Imported via "./errors.js" from file 'src/error/internal/assert.1.js'
Matched by include pattern 'src/**/*.js' in 'tsconfig.json'
src/error/internal/assert.js
Imported via './internal/assert.js' from file 'src/error/assert.js'
Matched by include pattern 'src/**/*.js' in 'tsconfig.json'
src/error/assert.js
Imported via './src/error/assert.js' from file 'index.js'
Matched by include pattern 'src/**/*.js' in 'tsconfig.json'
index.js
Matched by include pattern 'index.js' in 'tsconfig.json'
src/error/internal/assert.1.js
Matched by include pattern 'src/**/*.js' in 'tsconfig.json'
15 changes: 15 additions & 0 deletions retyped/packages/retyped-ses/package.json
@@ -0,0 +1,15 @@
{
"private": true,
"name": "retyped-ses",
"version": "0.0.1",
"type": "module",
"devDependencies": {
"@types/node": "^18.11.18",
"typescript": "^4.9.2"
},
"npmClient": "yarn",
"types": "index.d.ts",
"scripts": {
"build:types": "tsc -p ./tsconfig.json"
}
}
1 change: 1 addition & 0 deletions retyped/packages/retyped-ses/src/error/assert.js
@@ -0,0 +1 @@
export { assert, makeAssert, fail, error, details } from './internal/assert.js';
87 changes: 87 additions & 0 deletions retyped/packages/retyped-ses/src/error/internal/assert.1.js
@@ -0,0 +1,87 @@
// @ts-check

import { makeDetailsToken, DetailsToken } from "./details.js";
import { makeError } from "./errors.js";

/** @typedef { DetailsToken | { toString(): string } | string } Details */

/**
* Asserts that the flag is truthy or throws an error if it is falsy.
*
* @param {*} flag Truthy/falsy value
* @param {Details} failedDetails The details to throw if flag is falsy
* @param {ErrorConstructor} [errorConstructor] The error type to be thrown if flag is falsy
*/
function assert(
flag,
failedDetails = assert.details`Check failed`,
errorConstructor = globalThis.Error,
) {
if (!flag) assert.fail(failedDetails, errorConstructor);
};

/**
* Creates a DetailsToken instance.
*
* @param {TemplateStringsArray} template
* @param {...*} args
* @returns {DetailsToken}
*/
const details = (template, ...args) => makeDetailsToken(String.raw(template, ...args));

Object.freeze(details);
assert.details = details;
export { details };

/**
* Throws an error.
*
* @param {Details} failedDetails The details to throw
* @param {ErrorConstructor} errorConstructor The error type to be thrown
* @returns {InstanceType<ErrorConstructor>}
*/
const fail = (
failedDetails = makeDetailsToken(`Check failed`),
errorConstructor = globalThis.Error
) => {
throw assert.error(failedDetails, errorConstructor);
};

Object.freeze(fail);
assert.fail = fail;
export { fail };

/**
* Creates an error instance.
*
* @param {Details} [details] The details of the error
* @param {ErrorConstructor} [constructor] The error constructor to use
*/
const error = makeError;
assert.error = error;
export { error };

/**
* Creates an `assert` function that throws an error if the flag is falsy.
*
* @param {Function} [raise]
* @returns {typeof assert}
*/
const makeAssert = (raise) => Object.freeze(
Object.assign(
/** @type {typeof assert} */((flag, failedDetails, errorConstructor) => {
if (flag) return;
const error = assert.error(failedDetails, errorConstructor);
if (typeof raise === 'function') raise(error);
throw error;
}),
assert
)
);

Object.freeze(makeAssert);
assert.makeAssert = makeAssert;
export { makeAssert };

Object.freeze(assert);
export { assert };
88 changes: 88 additions & 0 deletions retyped/packages/retyped-ses/src/error/internal/assert.js
@@ -0,0 +1,88 @@
// @ts-check

import { makeDetailsToken, DetailsToken } from "./details.js";
import { makeError } from "./errors.js";

/** @typedef { DetailsToken | { toString(): string } | string } Details */

/**
* Asserts that the flag is truthy or throws an error if it is falsy.
*
* @param {*} flag Truthy/falsy value
* @param {Details} failedDetails The details to throw if flag is falsy
* @param {ErrorConstructor} [errorConstructor] The error type to be thrown if flag is falsy
*/
function assert(
flag,
failedDetails = assert.details`Check failed`,
errorConstructor = globalThis.Error,
) {
if (!flag) assert.fail(failedDetails, errorConstructor);
};

/**
* Creates a DetailsToken instance.
*
* @param {TemplateStringsArray} template
* @param {...*} args
* @returns {DetailsToken}
*/
assert.details = (template, ...args) => makeDetailsToken(String.raw(template, ...args));

Object.freeze(assert.details);


/**
* Throws an error.
*
* @param {Details} failedDetails The details to throw
* @param {ErrorConstructor} errorConstructor The error type to be thrown
* @returns {InstanceType<ErrorConstructor>}
*/
assert.fail = (
failedDetails = makeDetailsToken(`Check failed`),
errorConstructor = globalThis.Error
) => {
throw assert.error(failedDetails, errorConstructor);
};

Object.freeze(assert.fail);

/**
* Creates an error instance.
*
* @param {Details} [details] The details of the error
* @param {ErrorConstructor} [constructor] The error constructor to use
*/
assert.error = makeError;

/**
* Creates an `assert` function that throws an error if the flag is falsy.
*
* @param {Function} [raise]
* @returns {typeof assert}
*/
assert.makeAssert = (raise) => Object.freeze(
Object.assign(
/** @type {typeof assert} */((flag, failedDetails, errorConstructor) => {
if (flag) return;
const error = assert.error(failedDetails, errorConstructor);
if (typeof raise === 'function') raise(error);
throw error;
}),
assert
)
);

Object.freeze(assert.makeAssert);

Object.freeze(assert);

export const {
details,
fail,
error,
makeAssert
} = assert;

export { assert };
54 changes: 54 additions & 0 deletions retyped/packages/retyped-ses/src/error/internal/details.js
@@ -0,0 +1,54 @@
// @ts-check

/** @type {WeakMap<DetailsToken, string>} */
const detailsTokenStringMap = new WeakMap();

/** @constructor */
function DetailsToken() {
throw Error('Construction of DetailsToken instances is not permitted.');
};

DetailsToken.prototype = {
/** @returns {string} */
toString() {
return detailsTokenStringMap.has(this)
? /** @type {string} */ (detailsTokenStringMap.get(this))
: '[Not a DetailsToken]';
}
};

Object.freeze(DetailsToken.prototype.toString);
Object.freeze(DetailsToken.prototype);
Object.freeze(DetailsToken);

export { DetailsToken };

/**
* @param {string} tokenString
* @returns {DetailsToken}
*/
const makeDetailsToken = (tokenString) => {
if (tokenString === `${tokenString}`)

This comment has been minimized.

Copy link
@SMotaal

SMotaal Dec 29, 2022

Author Owner

This needs to be corrected:

    if (typeof tokenString !== 'string')
        throw TypeError(`makeDetailsToken called with a non-string token`);

Aside from the operator, coercion may have side-effects!

throw TypeError(`makeDetailsToken called with a non-string token`);

const detailsToken = /** @type {DetailsToken} */ ({ __proto__: DetailsToken.prototype });

detailsTokenStringMap.set(detailsToken, `${tokenString}`);

return detailsToken;
}

Object.freeze(makeDetailsToken);

export { makeDetailsToken };

/**
* Asserts that the value is an actual DetailsToken instance.
*
* @returns {value is DetailsToken}
*/
const isDetailsToken = (value) => detailsTokenStringMap.has(value);

Object.freeze(isDetailsToken);

export { isDetailsToken };
24 changes: 24 additions & 0 deletions retyped/packages/retyped-ses/src/error/internal/errors.js
@@ -0,0 +1,24 @@
// @ts-check

import { makeDetailsToken, isDetailsToken, DetailsToken } from "./details.js";

/** @typedef { DetailsToken | { toString(): string } | string } Details */

/**
* Creates an error instance.
*
* @param {Details} [details] The details of the error
* @param {ErrorConstructor} [constructor] The error constructor to use
* @param {{ errorName?: string }} [options] Options
*/
const makeError = (
details = makeDetailsToken(`Assertion failed`),
constructor = globalThis.Error,
// @ts-ignore
{ errorName = undefined } = {},
) => {
return new constructor(`${isDetailsToken(details) ? details : makeDetailsToken(details)}`);
};
Object.freeze(makeError);

export { makeError };
2 changes: 2 additions & 0 deletions retyped/packages/retyped-ses/test/.gitignore
@@ -0,0 +1,2 @@
node_modules/
**/node_modules/

1 comment on commit 261bf0e

@SMotaal
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reexporting makeError as error and assert.error


Option A

  1. Assigning assert.error = makeError with a preceding JSDoc tag in assert.js#L51-L57
    • Exports imported makeError as assert.error without a preceding JSDoc tag in types.d.ts#L94
  2. Exporting const { error } = assert without a preceding JSDoc tag in assert.js#L84
    • Exports assert.error as error from module without a preceding JSDoc tag in types.d.ts#L54-L56

Option B

  1. Defining const error = makeError with a preceding JSDoc tag which is exported further down in assert.1.js#L54-60
  2. Assigning assert.error = error without a preceding JSDoc tag in assert.1.js#L61
    • Exports error as assert.error without a preceding JSDoc tag in types.d.ts#L159

Option C

Create a wrapper function in assert.js and follow the same pattern used by other assert members.


Option A in assert.js appears to be more stable where it allows the JSDoc to propagate from through error.js without overriding it locally.

Please sign in to comment.