frontacles 0.1.0 → 0.2.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.
- package/CHANGELOG.md +29 -0
- package/README.md +55 -1
- package/package.json +44 -17
- package/src/index.js +2 -1
- package/src/math/index.js +4 -1
- package/src/url/email.js +71 -0
- package/src/url/test-utils/zod-suite.js +117 -0
- package/types/index.d.ts +29 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
Nothing for now.
|
|
8
|
+
|
|
9
|
+
<!-- ⚠️ Before a new release, make sure the documentation doesn't contain any **unreleased** mention. -->
|
|
10
|
+
|
|
11
|
+
Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.2.0...main).
|
|
12
|
+
|
|
13
|
+
## v1.4.0 (2025-02-28)
|
|
14
|
+
|
|
15
|
+
Compare with [previous version](https://github.com/frontacles/frontacles/compare/dda10c3...0.2).
|
|
16
|
+
|
|
17
|
+
### New
|
|
18
|
+
|
|
19
|
+
- Add [`Email`](https://github.com/frontacles/frontacles#email), a class to validate emails the same way browsers do, which is rock solid.
|
|
20
|
+
|
|
21
|
+
## v0.1.1 (2021-03-13)
|
|
22
|
+
|
|
23
|
+
Compare with [previous version](https://github.com/frontacles/frontacles/compare/986c759...dda10c3).
|
|
24
|
+
|
|
25
|
+
First published version.
|
|
26
|
+
|
|
27
|
+
## v0.1.0 (2021-03-13)
|
|
28
|
+
|
|
29
|
+
Almost published version.
|
package/README.md
CHANGED
|
@@ -1 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
# Frontacles
|
|
2
|
+
|
|
3
|
+
Cool utilities for front-end development (and potentially for Node).
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Under heavy development. We are only starting!
|
|
7
|
+
|
|
8
|
+
## Email
|
|
9
|
+
|
|
10
|
+
A class to instantiate an `Email` object or validate emails.
|
|
11
|
+
|
|
12
|
+
Unlike most libraries using [RegEx for emails](https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/src/types.ts#L648-L663) (and prone to [bugs](https://github.com/colinhacks/zod/issues/3913)), Frontacles `Email` class relies on the same mechanism as your browser to validate email addresses, making it robust, and very likely RFC compliant.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { Email } from 'frontacles/url/email'
|
|
16
|
+
|
|
17
|
+
const email = new Email('someone@domain.tld')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Get the username and the host separately.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
email.username // 'someone'
|
|
24
|
+
email.host // 'domain.tld'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Turn the email object into a string by using it along another string, or use `toString`.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
console.log(`email: ${email}`) // 'email: someone@domain.tld'
|
|
31
|
+
console.log(email.toString()) // 'someone@domain.tld'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Validate that a string is a valid email address. It passes the complete Zod test suites, and beyond.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
Email.canParse('someone@domain.tld') // true
|
|
38
|
+
Email.canParse('invalid@email.com:3000') // false
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Trying to instantiate an Email with an invalid address will throw. This behaviour is similar to the [`URL` constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), since `Email` relies on it under the hood.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
new Email('double@at@sign.com') // ❌ throw TypeError
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Another behaviour from the `URL` class: you can pass an `Email` object to the `Email` constructor or to `Email.canParse`.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const email = new Email('someone@domain.tld')
|
|
51
|
+
|
|
52
|
+
const alsoEmail = new Email(email) // ✅ a new Email object!
|
|
53
|
+
|
|
54
|
+
Email.canParse(email) // ✅ true
|
|
55
|
+
```
|
package/package.json
CHANGED
|
@@ -1,41 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontacles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Front-end utilities for artisans",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"types": "./types/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"types": "./types/index.d.ts",
|
|
7
14
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
15
|
+
"node": ">=20.18.1"
|
|
9
16
|
},
|
|
10
17
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
"test
|
|
13
|
-
"
|
|
18
|
+
"types": "tsc && dts-bundle-generator --silent --no-banner=true -o types/index.d.ts types-transitive/index.d.ts",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:types": "npm exec tsd",
|
|
21
|
+
"test:ui": "vitest --ui --coverage.enabled --coverage.exclude=types",
|
|
22
|
+
"coverage": "vitest run --coverage --coverage.exclude=types",
|
|
23
|
+
"watch": "vitest watch",
|
|
24
|
+
"build": "echo \"Nothing to build, this command is only here to please size-limit GitHub action\" && exit 0",
|
|
25
|
+
"size": "size-limit",
|
|
26
|
+
"lint": "eslint",
|
|
27
|
+
"lint-fix": "eslint --fix"
|
|
14
28
|
},
|
|
15
29
|
"files": [
|
|
16
|
-
"
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"src/**/*.js",
|
|
32
|
+
"!src/**/*.test.js",
|
|
33
|
+
"types/index.d.ts"
|
|
17
34
|
],
|
|
18
35
|
"keywords": [
|
|
19
36
|
"utilities",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
37
|
+
"email",
|
|
38
|
+
"math",
|
|
39
|
+
"round"
|
|
22
40
|
],
|
|
23
41
|
"homepage": "https://github.com/frontacles/frontacles",
|
|
24
42
|
"author": "Mehdi Merah",
|
|
25
43
|
"license": "WTFPL",
|
|
26
|
-
"repository":
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/frontacles/frontacles.git"
|
|
47
|
+
},
|
|
27
48
|
"bugs": {
|
|
28
49
|
"url": "https://github.com/frontacles/frontacles/issues"
|
|
29
50
|
},
|
|
30
51
|
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
"cache": "~/.npm"
|
|
52
|
+
"access": "public"
|
|
33
53
|
},
|
|
34
54
|
"devDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
55
|
+
"@eslint/js": "^9.9.0",
|
|
56
|
+
"@size-limit/preset-small-lib": "^11.1.4",
|
|
57
|
+
"@vitest/coverage-v8": "^3",
|
|
58
|
+
"@vitest/eslint-plugin": "^1.0.1",
|
|
59
|
+
"@vitest/ui": "^3",
|
|
60
|
+
"dts-bundle-generator": "^9.5.1",
|
|
61
|
+
"eslint": "^9.9.0",
|
|
62
|
+
"size-limit": "^11.1.4",
|
|
63
|
+
"tsd": "^0.31.1",
|
|
64
|
+
"typescript": "^5.5.4",
|
|
65
|
+
"typescript-eslint": "^8.0.1",
|
|
66
|
+
"vitest": "^3"
|
|
40
67
|
}
|
|
41
68
|
}
|
package/src/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './math'
|
|
1
|
+
export * from './math/index.js'
|
|
2
|
+
export * from './url/email.js'
|
package/src/math/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Round a number to provided precision.
|
|
2
|
+
* Round a number to the (optionnally) provided precision.
|
|
3
3
|
*
|
|
4
4
|
* Examples:
|
|
5
5
|
* - round(687.3456, 2) // 687.35
|
|
6
6
|
* - round(687.3456, 0) // 687
|
|
7
7
|
* - round(687.3456) // 687
|
|
8
8
|
* - round(687.3456, -1) // 690
|
|
9
|
+
*
|
|
10
|
+
* @param {number} number
|
|
11
|
+
* @param {number} [precision=0] Rounding precision
|
|
9
12
|
*/
|
|
10
13
|
export const round = (number, precision = 0) => {
|
|
11
14
|
precision = 10 ** precision
|
package/src/url/email.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// eslint-disable-next-line no-undef
|
|
2
|
+
export class Email extends URL {
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param {string|Email} address An email address like `someone@domain.tld`.
|
|
7
|
+
*/
|
|
8
|
+
constructor(address) {
|
|
9
|
+
super(`ftp://${address}`)
|
|
10
|
+
|
|
11
|
+
if (!this.#validate(address)) {
|
|
12
|
+
throw new TypeError(`Email constructor: ${address} is not a valid Email.`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The domain name and extension of the email.
|
|
18
|
+
*
|
|
19
|
+
* In `username@domain.tld`, it is `domain.tld`.
|
|
20
|
+
*/
|
|
21
|
+
get host() {
|
|
22
|
+
return super.host
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The username of the email.
|
|
27
|
+
*
|
|
28
|
+
* In `username@domain.tld`, it is `username`.
|
|
29
|
+
*/
|
|
30
|
+
get username() {
|
|
31
|
+
return super.username
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toJSON () {
|
|
35
|
+
return `${this.username}@${this.host}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replace the string representation of the top-level class (`URL`) to be an
|
|
40
|
+
* email address instead of `ftp://username@domain.tld`. It is needed for
|
|
41
|
+
* situation where type casting to string is involved (`console.log`…).
|
|
42
|
+
*/
|
|
43
|
+
toString () {
|
|
44
|
+
return `${this.username}@${this.host}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recompose the email address and compare it to the input. We also make sure
|
|
49
|
+
* there is no port since `someone@domain.tld:3000` is valid for the `URL`
|
|
50
|
+
* constructor but adds `:{port}` to `this.host` (`domain.tld:3000`).
|
|
51
|
+
*
|
|
52
|
+
* @param {string} address
|
|
53
|
+
*/
|
|
54
|
+
#validate (address) {
|
|
55
|
+
return `${this.username}@${this.host}` == address && !this.port
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether or not an email address is parsable and valid.
|
|
60
|
+
*
|
|
61
|
+
* @param {any|Email} address
|
|
62
|
+
*/
|
|
63
|
+
static canParse(address) {
|
|
64
|
+
try {
|
|
65
|
+
new this(address)
|
|
66
|
+
return true
|
|
67
|
+
} catch {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* eslint-disable no-useless-escape */
|
|
2
|
+
// https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/src/types.ts#L648-L663
|
|
3
|
+
// https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/deno/lib/__tests__/string.test.ts#L42-L154
|
|
4
|
+
|
|
5
|
+
export const validEmailsFromZod = [
|
|
6
|
+
`email@domain.com`,
|
|
7
|
+
`firstname.lastname@domain.com`,
|
|
8
|
+
`email@subdomain.domain.com`,
|
|
9
|
+
`firstname+lastname@domain.com`,
|
|
10
|
+
`1234567890@domain.com`,
|
|
11
|
+
`email@domain-one.com`,
|
|
12
|
+
`_______@domain.com`,
|
|
13
|
+
`email@domain.name`,
|
|
14
|
+
`email@domain.co.jp`,
|
|
15
|
+
`firstname-lastname@domain.com`,
|
|
16
|
+
`very.common@example.com`,
|
|
17
|
+
`disposable.style.email.with+symbol@example.com`,
|
|
18
|
+
`other.email-with-hyphen@example.com`,
|
|
19
|
+
`fully-qualified-domain@example.com`,
|
|
20
|
+
`user.name+tag+sorting@example.com`,
|
|
21
|
+
`x@example.com`,
|
|
22
|
+
`mojojojo@asdf.example.com`,
|
|
23
|
+
`example-indeed@strange-example.com`,
|
|
24
|
+
`example@s.example`,
|
|
25
|
+
`user-@example.org`,
|
|
26
|
+
`user@my-example.com`,
|
|
27
|
+
`a@b.cd`,
|
|
28
|
+
`work+user@mail.com`,
|
|
29
|
+
`tom@test.te-st.com`,
|
|
30
|
+
`something@subdomain.domain-with-hyphens.tld`,
|
|
31
|
+
`common'name@domain.com`,
|
|
32
|
+
`francois@etu.inp-n7.fr`,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export const invalidEmailsFromZod = [
|
|
36
|
+
// no "printable characters"
|
|
37
|
+
// `user%example.com@example.org`,
|
|
38
|
+
// `mailhost!username@example.org`,
|
|
39
|
+
// `test/test@test.com`,
|
|
40
|
+
|
|
41
|
+
// double @
|
|
42
|
+
`francois@@etu.inp-n7.fr`,
|
|
43
|
+
// do not support quotes
|
|
44
|
+
`"email"@domain.com`,
|
|
45
|
+
`"e asdf sadf ?<>ail"@domain.com`,
|
|
46
|
+
`" "@example.org`,
|
|
47
|
+
`"john..doe"@example.org`,
|
|
48
|
+
`"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com`,
|
|
49
|
+
// do not support comma
|
|
50
|
+
`a,b@domain.com`,
|
|
51
|
+
|
|
52
|
+
// do not support IPv4
|
|
53
|
+
`email@123.123.123.123`,
|
|
54
|
+
`email@[123.123.123.123]`,
|
|
55
|
+
`postmaster@123.123.123.123`,
|
|
56
|
+
`user@[68.185.127.196]`,
|
|
57
|
+
`ipv4@[85.129.96.247]`,
|
|
58
|
+
`valid@[79.208.229.53]`,
|
|
59
|
+
`valid@[255.255.255.255]`,
|
|
60
|
+
`valid@[255.0.55.2]`,
|
|
61
|
+
`valid@[255.0.55.2]`,
|
|
62
|
+
|
|
63
|
+
// do not support ipv6
|
|
64
|
+
`hgrebert0@[IPv6:4dc8:ac7:ce79:8878:1290:6098:5c50:1f25]`,
|
|
65
|
+
`bshapiro4@[IPv6:3669:c709:e981:4884:59a3:75d1:166b:9ae]`,
|
|
66
|
+
`jsmith@[IPv6:2001:db8::1]`,
|
|
67
|
+
`postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]`,
|
|
68
|
+
`postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:192.168.1.1]`,
|
|
69
|
+
|
|
70
|
+
// microsoft test cases
|
|
71
|
+
`plainaddress`,
|
|
72
|
+
`#@%^%#$@#$@#.com`,
|
|
73
|
+
`@domain.com`,
|
|
74
|
+
`Joe Smith <email@domain.com>`,
|
|
75
|
+
`email.domain.com`,
|
|
76
|
+
`email@domain@domain.com`,
|
|
77
|
+
`.email@domain.com`,
|
|
78
|
+
`email.@domain.com`,
|
|
79
|
+
`email..email@domain.com`,
|
|
80
|
+
`あいうえお@domain.com`,
|
|
81
|
+
`email@domain.com (Joe Smith)`,
|
|
82
|
+
`email@domain`,
|
|
83
|
+
`email@-domain.com`,
|
|
84
|
+
`email@111.222.333.44444`,
|
|
85
|
+
`email@domain..com`,
|
|
86
|
+
`Abc.example.com`,
|
|
87
|
+
`A@b@c@example.com`,
|
|
88
|
+
`colin..hacks@domain.com`,
|
|
89
|
+
`a"b(c)d,e:f;g<h>i[j\k]l@example.com`,
|
|
90
|
+
`just"not"right@example.com`,
|
|
91
|
+
`this is"not\allowed@example.com`,
|
|
92
|
+
`this\ still\"not\\allowed@example.com`,
|
|
93
|
+
|
|
94
|
+
// random
|
|
95
|
+
`i_like_underscore@but_its_not_allowed_in_this_part.example.com`,
|
|
96
|
+
`QA[icon]CHOCOLATE[icon]@test.com`,
|
|
97
|
+
`invalid@-start.com`,
|
|
98
|
+
`invalid@end.com-`,
|
|
99
|
+
`a.b@c.d`,
|
|
100
|
+
`invalid@[1.1.1.-1]`,
|
|
101
|
+
`invalid@[68.185.127.196.55]`,
|
|
102
|
+
`temp@[192.168.1]`,
|
|
103
|
+
`temp@[9.18.122.]`,
|
|
104
|
+
`double..point@test.com`,
|
|
105
|
+
`asdad@test..com`,
|
|
106
|
+
`asdad@hghg...sd...au`,
|
|
107
|
+
`asdad@hghg........au`,
|
|
108
|
+
`invalid@[256.2.2.48]`,
|
|
109
|
+
`invalid@[256.2.2.48]`,
|
|
110
|
+
`invalid@[999.465.265.1]`,
|
|
111
|
+
`jkibbey4@[IPv6:82c4:19a8::70a9:2aac:557::ea69:d985:28d]`,
|
|
112
|
+
`mlivesay3@[9952:143f:b4df:2179:49a1:5e82:b92e:6b6]`,
|
|
113
|
+
`gbacher0@[IPv6:bc37:4d3f:5048:2e26:37cc:248e:df8e:2f7f:af]`,
|
|
114
|
+
`invalid@[IPv6:5348:4ed3:5d38:67fb:e9b:acd2:c13:192.168.256.1]`,
|
|
115
|
+
`test@.com`,
|
|
116
|
+
`aaaaaaaaaaaaaaalongemailthatcausesregexDoSvulnerability@test.c`,
|
|
117
|
+
]
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function round(number: number, precision?: number): number;
|
|
2
|
+
export class Email extends URL {
|
|
3
|
+
/**
|
|
4
|
+
* Whether or not an email address is parsable and valid.
|
|
5
|
+
*
|
|
6
|
+
* @param {any|Email} address
|
|
7
|
+
*/
|
|
8
|
+
static canParse(address: any | Email): boolean;
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {string|Email} address An email address like `someone@domain.tld`.
|
|
12
|
+
*/
|
|
13
|
+
constructor(address: string | Email);
|
|
14
|
+
/**
|
|
15
|
+
* The domain name and extension of the email.
|
|
16
|
+
*
|
|
17
|
+
* In `username@domain.tld`, it is `domain.tld`.
|
|
18
|
+
*/
|
|
19
|
+
get host(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The username of the email.
|
|
22
|
+
*
|
|
23
|
+
* In `username@domain.tld`, it is `username`.
|
|
24
|
+
*/
|
|
25
|
+
get username(): string;
|
|
26
|
+
#private;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {};
|