diff --git a/.depcheckrc.json b/.depcheckrc.json new file mode 100644 index 00000000..b161776f --- /dev/null +++ b/.depcheckrc.json @@ -0,0 +1,12 @@ +{ + "ignores": [ + "@lavamoat/allow-scripts", + "@lavamoat/preinstall-always-fail", + "@metamask/auto-changelog", + "@types/*", + "prettier-plugin-packagejson", + "superstruct", + "ts-node", + "typedoc" + ] +} diff --git a/.editorconfig b/.editorconfig index 5c165c38..c6c8b362 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,4 +7,3 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -max_line_length = 80 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 78f1a4e2..00000000 --- a/.eslintrc +++ /dev/null @@ -1,117 +0,0 @@ -{ - "extends": [ - "plugin:import/typescript", - "prettier", - "prettier/@typescript-eslint" - ], - "plugins": ["@typescript-eslint", "import", "prettier"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2020, - "ecmaFeatures": { - "jsx": true - } - }, - "env": { - "browser": true, - "es6": true, - "mocha": true, - "node": true - }, - "settings": { - "import/extensions": [".js", ".ts"], - "import/resolver": { - "typescript": { - "project": ["./tsconfig.json"] - } - } - }, - "ignorePatterns": ["/dist/**"], - "rules": { - "@typescript-eslint/no-unused-vars": [ - "error", - { "vars": "all", "args": "none" } - ], - "constructor-super": "error", - "dot-notation": ["error", { "allowKeywords": true }], - "eqeqeq": ["error", "smart"], - "import/default": "error", - "import/export": "error", - "import/first": "error", - "import/named": "error", - "import/namespace": "error", - "import/newline-after-import": "error", - "import/no-deprecated": "error", - "import/no-extraneous-dependencies": [ - "error", - { "peerDependencies": true } - ], - "import/no-mutable-exports": "error", - "import/no-named-as-default": "error", - "import/no-named-as-default-member": "error", - "import/no-unresolved": "error", - "linebreak-style": "error", - "new-parens": "error", - "no-array-constructor": "error", - "no-class-assign": "error", - "no-console": "error", - "no-const-assign": "error", - "no-debugger": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-ex-assign": "error", - "no-extend-native": "error", - "no-extra-boolean-cast": "error", - "no-func-assign": "error", - "no-invalid-regexp": "error", - "no-lonely-if": "error", - "no-native-reassign": "error", - "no-negated-in-lhs": "error", - "no-new-object": "error", - "no-new-symbol": "error", - "no-path-concat": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-sequences": "error", - "no-shadow": "error", - "no-shadow-restricted-names": "error", - "no-tabs": "error", - "no-this-before-super": "error", - "no-throw-literal": "error", - "no-unneeded-ternary": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unused-expressions": "error", - "no-unused-vars": "off", - "no-useless-call": "error", - "no-useless-computed-key": "error", - "no-useless-constructor": "error", - "no-useless-rename": "error", - "no-var": "error", - "no-void": "error", - "no-with": "error", - "object-shorthand": ["error", "always"], - "prefer-arrow-callback": "error", - "prefer-const": [ - "error", - { "destructuring": "all", "ignoreReadBeforeAssign": true } - ], - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - "prettier/prettier": "error", - "radix": "error", - "spaced-comment": ["error", "always", { "exceptions": ["-"] }], - "use-isnan": "error", - "valid-typeof": "error", - "yield-star-spacing": ["error", "after"], - "yoda": ["error", "never"] - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..ba223309 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + + extends: ['@metamask/eslint-config'], + + overrides: [ + { + files: ['*.ts'], + extends: ['@metamask/eslint-config-typescript'], + }, + + { + files: ['*.js'], + parserOptions: { + sourceType: 'script', + }, + extends: ['@metamask/eslint-config-nodejs'], + }, + + { + files: ['*.test.ts', '*.test.js'], + extends: [ + '@metamask/eslint-config-jest', + '@metamask/eslint-config-nodejs', + ], + rules: { + '@typescript-eslint/no-shadow': [ + 'error', + { + allow: ['describe', 'it'], + }, + ], + }, + }, + ], + + ignorePatterns: [ + '!.eslintrc.js', + '!.prettierrc.js', + 'dist/', + 'docs/', + '.yarn/', + 'examples/', + ], +}; diff --git a/.gitignore b/.gitignore index 196b6bdf..704e833d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ npm-debug.log package-lock.json tmp/ +# Optional eslint cache +.eslintcache + # yarn v3 (w/o zero-install) # See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored .pnp.* diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 1eae0cf6..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -node_modules/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0b495108..00000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "semi": false, - "trailingComma": "es5" -} diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..b2d98d2e --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +// All of these are defaults except singleQuote, but we specify them +// for explicitness +module.exports = { + quoteProps: 'as-needed', + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', +}; diff --git a/Changelog.md b/Changelog.md index c6fa396b..26ca50a4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,7 +9,7 @@ This document maintains a list of major changes to Superstruct with each new rel **Added an optional `message` argument to override error messages.** You can now pass in a `message` argument to all of the error checking functions which will override any error message with your own message. If you do, Superstruct's original descriptive message will still be accessible via [`error.cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause). ```ts -assert(data, User, 'The user is invalid!') +assert(data, User, 'The user is invalid!'); // StructError: The user is invalid! ``` @@ -65,14 +65,14 @@ assert(data, User, 'The user is invalid!') ```ts type User = { - id: number - name: string -} + id: number; + name: string; +}; const User: Describe = object({ id: string(), // This mistake will fail to pass type checking! name: string(), -}) +}); ``` ###### BREAKING @@ -82,13 +82,13 @@ const User: Describe = object({ ```ts // Previously const MyNumber = coerce(number(), (value) => { - return typeof value === 'string' ? parseFloat(value) : value -}) + return typeof value === 'string' ? parseFloat(value) : value; +}); // Now const MyNumber = coerce(number(), string(), (value) => { - return parseFloat(value) -}) + return parseFloat(value); +}); ``` ### `0.11.0` — November 20, 2020 @@ -99,15 +99,15 @@ const MyNumber = coerce(number(), string(), (value) => { ```ts // Combine two structs with `assign`: -const a = object({ id: number() }) -const b = object({ name: string() }) -const c = assign([a, b]) +const a = object({ id: number() }); +const b = object({ name: string() }); +const c = assign([a, b]); // Pick out specific properties with `pick`: -const a2 = pick(c, ['id']) +const a2 = pick(c, ['id']); // Omit specific properties with `omit`: -const a3 = omit(c, ['name']) +const a3 = omit(c, ['name']); ``` **New `unknown` struct.** This is the same as the existing `any` struct, but it will ensure that in TypeScript the value is of the more restrictive `unknown` type so it encourages better type safety. @@ -117,7 +117,7 @@ const Shape = type({ id: number(), name: string(), other: unknown(), -}) +}); ``` **New `integer`, `regexp`, and `func` structs.** These are just simple additions for common use cases of ensuring a value is an integer, a regular expression object (not a string!), or a function. @@ -127,15 +127,15 @@ const Shape = type({ id: integer(), matches: regexp(), send: func(), -}) +}); ``` **New `max/min` refinements.** For refining `number` (or `integer`) or `date` structs to ensure they are greater than or less than a specific threshold. The third argument can indicate whether to make the threshold exclusive (instead of the default inclusive). ```ts -const Index = min(number(), 0) -const PastOrPresent = max(date(), new Date()) -const Past = max(date(), new Date(), { exclusive: true }) +const Index = min(number(), 0); +const PastOrPresent = max(date(), new Date()); +const Past = max(date(), new Date(), { exclusive: true }); ``` **Even more information on errors.** Errors now expose the `error.refinement` property when the failure originated in a refinement validation. And they also now have an `error.key` property which is the key for the failure in the case of complex values like arrays/objects. (Previously the key was retrievable by checking `error.path`, but this will make the 90% case easier.) @@ -146,10 +146,10 @@ const Past = max(date(), new Date(), { exclusive: true }) ```ts // Previously -const user = coerce(data, User) +const user = coerce(data, User); // Now -const user = create(data, User) +const user = create(data, User); ``` **The `struct`, `refinement` and `coercion` factories have been renamed.** This renaming is purely for keeping things slightly cleaner and easier to understand. The new names are `define`, `refine`, and `coerce`. Separating them slightly from the noun-based names used for the types themselves. @@ -172,38 +172,38 @@ _Note that the order of `refine` arguments has changed to be slightly more natur ```ts // Previously -const Name = length(string(), 1, 100) -const MyArray = length(array(string()), 3, 3) +const Name = length(string(), 1, 100); +const MyArray = length(array(string()), 3, 3); // Now -const Name = size(string(), 1, 100) -const MyArray = size(array(string()), 3) -const Id = size(integer(), 1, Infinity) -const MySet = size(set(), 1, 9) +const Name = size(string(), 1, 100); +const MyArray = size(array(string()), 3); +const Id = size(integer(), 1, Infinity); +const MySet = size(set(), 1, 9); ``` **The `StructType` inferring helper has been renamed to `Infer`.** This just makes it slightly easier to read what's going on when you're inferring a type. ```ts // Previously -type User = StructType +type User = StructType; // Now -type User = Infer +type User = Infer; ``` **The `error.type` property has been standardized.** Previously it was a human-readable description that sort of incorporated the schema. Now it is simple the plain lowercase name of the struct in question, making it something you can use programmatically when formatting errors. ```ts // Previously -'Array' -'[string,number]' -'Map' +'Array'; +'[string,number]'; +'Map'; // Now -'array' -'tuple' -'map' +'array'; +'tuple'; +'map'; ``` ### `0.10.0` — June 6, 2020 @@ -219,23 +219,23 @@ This makes it much more powerful, however the core architecture has had to chang For example, previously: ```ts -import { struct } from 'superstruct' +import { struct } from 'superstruct'; const User = struct.object({ name: 'string', age: 'number', -}) +}); ``` Now becomes: ```ts -import { object, string, number } from 'superstruct' +import { object, string, number } from 'superstruct'; const User = object({ name: string(), age: number(), -}) +}); ``` **Custom scalars are no longer pre-defined as strings.** Previously, you would define all of your "custom" types in a single place in your codebase and then refer to them in structs later on with a string value. This worked, but added a layer of unnecessary indirection, and made it impossible to accomodate runtime type signatures. @@ -245,25 +245,25 @@ In the new version, custom types are defined extremely similarly to non-custom t Here's how it used to work: ```ts -import { superstruct } from 'superstruct' -import isEmail from 'is-email' +import { superstruct } from 'superstruct'; +import isEmail from 'is-email'; const struct = superstruct({ types: { email: isEmail, }, -}) +}); -const Email = struct('email') +const Email = struct('email'); ``` And here's what it would look like now: ```ts -import { struct } from 'superstruct' -import isEmail from 'is-email' +import { struct } from 'superstruct'; +import isEmail from 'is-email'; -const Email = struct('email', isEmail) +const Email = struct('email', isEmail); ``` **Validation logic has been moved to helper functions.** Previously the `assert` and `is` helpers lived on the struct objects themselves. Now, these functions have been extracted into separate helpers. This was unfortunately necessary to work around limitations in TypeScript's `asserts` keyword. @@ -271,15 +271,15 @@ const Email = struct('email', isEmail) For example, before: ```ts -User.assert(data) +User.assert(data); ``` Now would be: ```ts -import { assert } from 'superstruct' +import { assert } from 'superstruct'; -assert(data, User) +assert(data, User); ``` **Coercion is now separate from validation.** Previously there was native logic for handling default values for structs when validating them. This has been abstracted into the ability to define _any_ custom coercion logic for structs, and it has been separate from validation to make it very clear when data can change and when it cannot. @@ -287,13 +287,13 @@ assert(data, User) For example, previously: ```ts -const output = User.assert(input) +const output = User.assert(input); ``` Would now be: ```ts -const input = coerce(input, User) +const input = coerce(input, User); ``` The `coerce` step is the only time that data will be transformed at all by coercion logic, and the `assert` step no longer needs to return any values. This makes it easy to do things like: @@ -319,8 +319,8 @@ const Article = struct.object( }, { title: 'Untitled', - } -) + }, +); ``` Whereas now you'd do: @@ -332,8 +332,8 @@ const Article = defaulted( }), { title: 'Untitled', - } -) + }, +); ``` **Optional arguments are now defined with a seperate factory.** Similarly to defaults, there is a new `optional` factory for defined values that can also be `undefined`. @@ -341,13 +341,13 @@ const Article = defaulted( Previously you'd do: ```ts -const Flag = struct('string?') +const Flag = struct('string?'); ``` Now you'd do: ```ts -const Flag = optional(string()) +const Flag = optional(string()); ``` **Several structs have been renamed.** This was necessary because structs are now exposed directly as variables, which runs afoul of reserved words. So the following renames have been applied: @@ -371,7 +371,7 @@ Hopefully this will make them easier to understand at a glance! **The `enums` struct has been removed!** This was special-cased in the API previously, but you can get the exact same behavior by creating an using the `array` and `enum` structs: ```js -struct.array(struct.enum(['red', 'blue', 'green'])) +struct.array(struct.enum(['red', 'blue', 'green'])); ``` **The `any` struct has been removed! (Not the scalar though.)** Previously `struct.any()` was exposed that did the same thing as `struct()`, allowing you to use shorthands for common structs. But this was confusingly named because it has nothing to do with the `'any'` scalar type. And since it was redundant it has been removed. @@ -382,12 +382,12 @@ struct.array(struct.enum(['red', 'blue', 'green'])) ```js struct.dynamic((value, branch, path) => { - value === branch[branch.length - 1] // you can get the value... - const parent = branch[branch.length - 2] // ...and the parent... - const key = path[path.length - 1] // ...and the key... - value === parent[key] - const root = branch[0] // ...and the root! -}) + value === branch[branch.length - 1]; // you can get the value... + const parent = branch[branch.length - 2]; // ...and the parent... + const key = path[path.length - 1]; // ...and the key... + value === parent[key]; + const root = branch[0]; // ...and the root! +}); ``` The `path` is an array of keys representing the nested value's location in the root value. And the `branch` is an array of all of the sub values along the path to get to the current one. This allows you to always be able to receive both the **parent** and the **root** values from any location—as well as any value in between. diff --git a/Readme.md b/Readme.md index a5264cde..a66e77bd 100644 --- a/Readme.md +++ b/Readme.md @@ -45,7 +45,7 @@ But Superstruct is designed for validating data at runtime, so it throws (or ret Superstruct allows you to define the shape of data you want to validate: ```js -import { assert, object, number, string, array } from 'superstruct' +import { assert, object, number, string, array } from 'superstruct'; const Article = object({ id: number(), @@ -54,7 +54,7 @@ const Article = object({ author: object({ id: number(), }), -}) +}); const data = { id: 34, @@ -63,9 +63,9 @@ const data = { author: { id: 1, }, -} +}; -assert(data, Article) +assert(data, Article); // This will throw an error when the data is invalid. // If you'd rather not throw, you can use `is()` or `validate()`. ``` @@ -73,24 +73,24 @@ assert(data, Article) Superstruct ships with validators for all the common JavaScript data types, and you can define custom ones too: ```js -import { is, define, object, string } from 'superstruct' -import isUuid from 'is-uuid' -import isEmail from 'is-email' +import { is, define, object, string } from 'superstruct'; +import isUuid from 'is-uuid'; +import isEmail from 'is-email'; -const Email = define('Email', isEmail) -const Uuid = define('Uuid', isUuid.v4) +const Email = define('Email', isEmail); +const Uuid = define('Uuid', isUuid.v4); const User = object({ id: Uuid, email: Email, name: string(), -}) +}); const data = { id: 'c8d63140-a1f7-45e0-bfc6-df72973fea86', email: 'jane@example.com', name: 'Jane', -} +}; if (is(data, User)) { // Your data is guaranteed to be valid in this block. @@ -100,21 +100,21 @@ if (is(data, User)) { Superstruct can also handle coercion of your data before validating it, for example to mix in default values: ```ts -import { create, object, number, string, defaulted } from 'superstruct' +import { create, object, number, string, defaulted } from 'superstruct'; -let i = 0 +let i = 0; const User = object({ id: defaulted(number(), () => i++), name: string(), -}) +}); const data = { name: 'Jane', -} +}; // You can apply the defaults to your data while validating. -const user = create(data, User) +const user = create(data, User); // { // id: 0, // name: 'Jane', diff --git a/docs/guides/01-getting-started.md b/docs/guides/01-getting-started.md index 61442b08..30e753d0 100644 --- a/docs/guides/01-getting-started.md +++ b/docs/guides/01-getting-started.md @@ -11,23 +11,23 @@ npm install --save superstruct And then you can import it: ```ts -import { object, string, number } from 'superstruct' +import { object, string, number } from 'superstruct'; const User = object({ id: number(), name: string(), -}) +}); ``` If you'd like, you can use a wildcard import: ```ts -import * as s from 'superstruct' +import * as s from 'superstruct'; const User = s.object({ id: s.number(), name: s.string(), -}) +}); ``` If you'd rather use a `