@protontech/autofill 0.0.35481761 → 0.0.36026908
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/features/v1/abstract.field.d.ts +38 -38
- package/features/v1/field-predicates.d.ts +8 -0
- package/features/v1/field-predicates.js +42 -0
- package/features/v1/field.email.d.ts +1169 -1169
- package/features/v1/field.otp.d.ts +23826 -23826
- package/features/v1/field.password.d.ts +33195 -33195
- package/features/v1/field.username-hidden.d.ts +337 -337
- package/features/v1/field.username.d.ts +14189 -14189
- package/features/v1/fields.sorted.gen.d.ts +482 -482
- package/features/v1/forms.sorted.gen.d.ts +245 -245
- package/features/v1/index.d.ts +1772 -1772
- package/models/perceptron/params/email-model.json +4 -4
- package/models/perceptron/params/login-model.json +84 -84
- package/models/perceptron/params/new-password-model.json +16 -16
- package/models/perceptron/params/otp-model.json +25 -25
- package/models/perceptron/params/password-change-model.json +74 -74
- package/models/perceptron/params/password-model.json +19 -19
- package/models/perceptron/params/recovery-model.json +82 -82
- package/models/perceptron/params/register-model.json +97 -97
- package/models/perceptron/params/username-hidden-model.json +8 -8
- package/models/perceptron/params/username-model.json +6 -6
- package/package.json +3 -3
- package/rules/v1/index.js +3 -3
- package/utils/credit-card.d.ts +0 -1
- package/utils/credit-card.js +2 -9
- package/utils/field.d.ts +4 -5
- package/utils/field.js +6 -17
- package/utils/identity.d.ts +0 -2
- package/utils/identity.js +2 -23
- package/dictionary/generate.d.ts +0 -1
- package/dictionary/generate.js +0 -51
- package/features/feature.spec.d.ts +0 -1
- package/features/feature.spec.js +0 -108
- package/features/v1/index.spec.d.ts +0 -1
- package/features/v1/index.spec.js +0 -44
- package/scripts/gen-sorted-features.d.ts +0 -1
- package/scripts/gen-sorted-features.js +0 -128
- package/utils/credit-card.samples.spec.d.ts +0 -1
- package/utils/credit-card.samples.spec.js +0 -452
- package/utils/credit-card.spec.d.ts +0 -1
- package/utils/credit-card.spec.js +0 -296
- package/utils/identity.samples.spec.d.ts +0 -1
- package/utils/identity.samples.spec.js +0 -28
- package/utils/re.spec.d.ts +0 -1
- package/utils/re.spec.js +0 -62
- package/utils/shadow-dom.spec.d.ts +0 -1
- package/utils/shadow-dom.spec.js +0 -215
package/utils/credit-card.js
CHANGED
|
@@ -7,7 +7,7 @@ import { matchCCExp, matchCCExpMonth, matchCCExpYear, matchCCFirstName, matchCCL
|
|
|
7
7
|
import { sanitizeString, sanitizeStringWithSpaces } from "./text";
|
|
8
8
|
import { isVisible } from "./visible";
|
|
9
9
|
export const CC_ATTRIBUTES = ["autocomplete", "name", "id", "class", "form", "aria-label", "aria-labelledby", "placeholder", "data-testid", "data-stripe", "data-recurly", "data-encrypted-name"];
|
|
10
|
-
export const CC_INPUT_TYPES = ["tel", "
|
|
10
|
+
export const CC_INPUT_TYPES = ["tel", "text", "number", "password"];
|
|
11
11
|
const CC_EXP_YEAR_FORMAT = ["YYYY", "AAAA", "YY", "AA"];
|
|
12
12
|
const CC_EXP_MONTH_FORMAT = ["MM", "LL"];
|
|
13
13
|
const CC_EXP_FULL_RE = /(mmyy|mmaa|yymm|aamm)/;
|
|
@@ -212,8 +212,7 @@ export const getCCFieldType = (field) => {
|
|
|
212
212
|
const cachedSubType = getCachedCCSubtype(field);
|
|
213
213
|
if (cachedSubType)
|
|
214
214
|
return cachedSubType;
|
|
215
|
-
|
|
216
|
-
if (field.tagName === "INPUT" && type && !CC_INPUT_TYPES.includes(type))
|
|
215
|
+
if (field.tagName === "INPUT" && !CC_INPUT_TYPES.includes(field.type))
|
|
217
216
|
return;
|
|
218
217
|
const haystack = getCCHaystack(field);
|
|
219
218
|
const autocompletes = getAutocompletes(field);
|
|
@@ -234,12 +233,6 @@ export const matchCCFieldCandidate = (input, { visible }) => {
|
|
|
234
233
|
setCachedSubType(input, ccType);
|
|
235
234
|
return ccType !== undefined;
|
|
236
235
|
};
|
|
237
|
-
export const isCCInputField = (fnode) => {
|
|
238
|
-
const { isCC, visible } = fnode.noteFor("field");
|
|
239
|
-
if (!visible)
|
|
240
|
-
return false;
|
|
241
|
-
return isCC;
|
|
242
|
-
};
|
|
243
236
|
export const isCCSelectField = (fnode) => {
|
|
244
237
|
const select = fnode.element;
|
|
245
238
|
if (!(select instanceof HTMLSelectElement))
|
package/utils/field.d.ts
CHANGED
|
@@ -7,11 +7,10 @@ export declare const isSelect: (el: HTMLElement) => el is HTMLSelectElement;
|
|
|
7
7
|
export declare const isTextarea: (el: HTMLElement) => el is HTMLTextAreaElement;
|
|
8
8
|
export declare const splitFieldsByVisibility: (els: HTMLElement[]) => [HTMLElement[], HTMLElement[]];
|
|
9
9
|
export declare const fType: (type: FieldType) => (fnode: Fnode) => boolean;
|
|
10
|
-
export declare const
|
|
11
|
-
export declare const
|
|
12
|
-
export declare const
|
|
13
|
-
export declare const
|
|
14
|
-
export declare const maybeHiddenUsername: (value: Fnode) => boolean;
|
|
10
|
+
export declare const fInput: (types: string[]) => (fnode: Fnode) => boolean;
|
|
11
|
+
export declare const fMatch: (selector: string) => (fnode: Fnode) => boolean;
|
|
12
|
+
export declare const fMode: (mode: string) => (fnode: Fnode) => boolean;
|
|
13
|
+
export declare const fList: (fnode: Fnode) => boolean;
|
|
15
14
|
export declare const isUsernameCandidate: (el: HTMLInputElement) => boolean;
|
|
16
15
|
export declare const isEmailCandidate: (el: HTMLInputElement) => boolean;
|
|
17
16
|
export declare const isOAuthCandidate: (el: HTMLElement) => boolean;
|
package/utils/field.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MIN_AREA_SUBMIT_BTN } from "@protontech/autofill/constants/heuristics";
|
|
2
|
-
import { inputCandidateSelector
|
|
3
|
-
import {
|
|
2
|
+
import { inputCandidateSelector } from "@protontech/autofill/constants/selectors";
|
|
3
|
+
import { any } from "./combinators";
|
|
4
4
|
import { getAllFieldHaystacks } from "./extract";
|
|
5
5
|
import { getParentFormFnode } from "./fathom";
|
|
6
6
|
import { isClassifiable, isHidden, isProcessed } from "./flags";
|
|
@@ -14,11 +14,6 @@ export const isInputField = (el) => {
|
|
|
14
14
|
};
|
|
15
15
|
export const isSelect = (el) => el.tagName === "SELECT";
|
|
16
16
|
export const isTextarea = (el) => el.tagName === "TEXTAREA";
|
|
17
|
-
const isActiveFieldFNode = (fnode) => {
|
|
18
|
-
const el = fnode.element;
|
|
19
|
-
const { visible } = fnode.noteFor("field");
|
|
20
|
-
return visible && !el.readOnly && !el.disabled;
|
|
21
|
-
};
|
|
22
17
|
export const splitFieldsByVisibility = (els) => {
|
|
23
18
|
return els.reduce((acc, el) => {
|
|
24
19
|
if (isVisibleField(el))
|
|
@@ -29,16 +24,10 @@ export const splitFieldsByVisibility = (els) => {
|
|
|
29
24
|
}, [[], []]);
|
|
30
25
|
};
|
|
31
26
|
export const fType = (type) => (fnode) => fnode.hasType(type);
|
|
32
|
-
const fInput = (types) => (fnode) => types.includes(fnode.element.type);
|
|
33
|
-
const fMatch = (selector) => (fnode) => fnode.element.matches(selector);
|
|
34
|
-
const fMode = (mode) => (fnode) => fnode.element.inputMode === mode;
|
|
35
|
-
const
|
|
36
|
-
const fList = (fnode) => fnode.element.getAttribute("aria-autocomplete") === "list" || fnode.element.role === "combobox";
|
|
37
|
-
export const maybeEmail = and(not(fList), or(fInput(["email", "text"]), fMode("email")), fActive);
|
|
38
|
-
export const maybePassword = and(not(fList), fMatch(kPasswordSelector), fActive);
|
|
39
|
-
export const maybeOTP = and(fInput(["text", "number", "tel"]), fActive, not(fList));
|
|
40
|
-
export const maybeUsername = and(not(fList), or(and(not(fMode("email")), fInput(["text", "tel"])), fMatch(kUsernameSelector)), fActive);
|
|
41
|
-
export const maybeHiddenUsername = and(not(fList), fInput(["email", "text", "hidden"]), not(fActive));
|
|
27
|
+
export const fInput = (types) => (fnode) => types.includes(fnode.element.type);
|
|
28
|
+
export const fMatch = (selector) => (fnode) => fnode.element.matches(selector);
|
|
29
|
+
export const fMode = (mode) => (fnode) => fnode.element.inputMode === mode;
|
|
30
|
+
export const fList = (fnode) => fnode.element.getAttribute("aria-autocomplete") === "list" || fnode.element.role === "combobox";
|
|
42
31
|
export const isUsernameCandidate = (el) => el.type === "text" || (el.type === "tel" && any(matchUsername)(getAllFieldHaystacks(el)));
|
|
43
32
|
export const isEmailCandidate = (el) => el.type === "email" || (el.type === "text" && any(matchEmail)(getAllFieldHaystacks(el)));
|
|
44
33
|
export const isOAuthCandidate = (el) => any(matchOAuth)(getAllFieldHaystacks(el));
|
package/utils/identity.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Fnode } from "@protontech/fathom";
|
|
2
1
|
import { IdentityFieldType } from "@protontech/autofill/types";
|
|
3
2
|
import type { FormClassification } from "./fathom";
|
|
4
3
|
type IdentityFieldMatchParams = {
|
|
@@ -11,5 +10,4 @@ export declare const getCachedIdentitySubType: (el: HTMLElement) => IdentityFiel
|
|
|
11
10
|
export declare const getIdentityHaystack: (input: HTMLInputElement) => string;
|
|
12
11
|
export declare const getIdentityFieldType: (input: HTMLInputElement) => IdentityFieldType | undefined;
|
|
13
12
|
export declare const matchIdentityField: (input: HTMLInputElement, { visible }: IdentityFieldMatchParams) => boolean;
|
|
14
|
-
export declare const isIdentity: (fnode: Fnode) => boolean;
|
|
15
13
|
export {};
|
package/utils/identity.js
CHANGED
|
@@ -24,7 +24,7 @@ const IDENTITY_MATCHERS = [
|
|
|
24
24
|
[IdentityFieldType.ADDRESS, guard({ autocompletes: ["street-address", "address-line1"] }, matchAddress)],
|
|
25
25
|
];
|
|
26
26
|
const IDENTITY_ATTRIBUTES = ["autocomplete", "name", "id", "data-bhw"];
|
|
27
|
-
const IDENTITY_INPUT_TYPES = ["tel", "
|
|
27
|
+
const IDENTITY_INPUT_TYPES = ["tel", "text", "number"];
|
|
28
28
|
export const getCachedIdentitySubType = (el) => {
|
|
29
29
|
const subType = getCachedSubType(el);
|
|
30
30
|
if (subType && identityFields.has(subType))
|
|
@@ -41,8 +41,7 @@ export const getIdentityFieldType = (input) => {
|
|
|
41
41
|
return cachedSubType;
|
|
42
42
|
if (getCachedSubType(input) !== undefined)
|
|
43
43
|
return;
|
|
44
|
-
|
|
45
|
-
if (type && !IDENTITY_INPUT_TYPES.includes(type))
|
|
44
|
+
if (!IDENTITY_INPUT_TYPES.includes(input.type))
|
|
46
45
|
return;
|
|
47
46
|
if ((_a = input.getAttribute("autocomplete")) === null || _a === void 0 ? void 0 : _a.includes("email"))
|
|
48
47
|
return;
|
|
@@ -55,7 +54,6 @@ export const getIdentityFieldType = (input) => {
|
|
|
55
54
|
return identityType;
|
|
56
55
|
}
|
|
57
56
|
};
|
|
58
|
-
const isAutocompleteListInput = (el) => el.getAttribute("aria-autocomplete") === "list" || el.role === "combobox";
|
|
59
57
|
export const matchIdentityField = (input, { visible }) => {
|
|
60
58
|
if (!visible)
|
|
61
59
|
return false;
|
|
@@ -67,22 +65,3 @@ export const matchIdentityField = (input, { visible }) => {
|
|
|
67
65
|
setCachedSubType(input, identityType);
|
|
68
66
|
return true;
|
|
69
67
|
};
|
|
70
|
-
export const isIdentity = (fnode) => {
|
|
71
|
-
const input = fnode.element;
|
|
72
|
-
const { isIdentity, isCC, searchField, isFormLogin, isFormRecovery, visible } = fnode.noteFor("field");
|
|
73
|
-
if (!visible)
|
|
74
|
-
return false;
|
|
75
|
-
if (isCC || !isIdentity)
|
|
76
|
-
return false;
|
|
77
|
-
const identityType = getIdentityFieldType(input);
|
|
78
|
-
if (!identityType)
|
|
79
|
-
return false;
|
|
80
|
-
const outlierForm = isFormLogin || isFormRecovery;
|
|
81
|
-
if (outlierForm)
|
|
82
|
-
return false;
|
|
83
|
-
if (isAutocompleteListInput(input))
|
|
84
|
-
return [IdentityFieldType.ADDRESS, IdentityFieldType.ZIPCODE].includes(identityType);
|
|
85
|
-
if (searchField)
|
|
86
|
-
return [IdentityFieldType.ADDRESS, IdentityFieldType.ZIPCODE, IdentityFieldType.CITY].includes(identityType);
|
|
87
|
-
return true;
|
|
88
|
-
};
|
package/dictionary/generate.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dictionary/generate.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { sanitizeString, sanitizeStringWithSpaces } from "@protontech/autofill/utils/text";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import regexgen from "regexgen";
|
|
5
|
-
import dictionary from "./source/dictionary";
|
|
6
|
-
const generateRegexes = () => {
|
|
7
|
-
let output = "";
|
|
8
|
-
output += "/* Regular expressions auto-generated using `source/dictionary.ts`\n";
|
|
9
|
-
output += " * Simplified & optimised using `regexgen` for performance. \n";
|
|
10
|
-
output += " * `TYPE_RE`: i18n regexp for text matches\n";
|
|
11
|
-
output += " * `TYPE_ATTR_RE`: regexp for node attributes\n";
|
|
12
|
-
output += " * `TYPE_ATTR_END`: attr ending constraint regexp */\n\n";
|
|
13
|
-
Object.entries(dictionary).forEach(([name, definition]) => {
|
|
14
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
15
|
-
const i18n = Object.values((_a = definition.i18n) !== null && _a !== void 0 ? _a : {})
|
|
16
|
-
.flat()
|
|
17
|
-
.map(sanitizeString);
|
|
18
|
-
const attrs = ((_c = (_b = definition === null || definition === void 0 ? void 0 : definition.attr) === null || _b === void 0 ? void 0 : _b.values) !== null && _c !== void 0 ? _c : []).map(sanitizeString);
|
|
19
|
-
const prefixes = ((_e = (_d = definition === null || definition === void 0 ? void 0 : definition.attr) === null || _d === void 0 ? void 0 : _d.prefixes) !== null && _e !== void 0 ? _e : []).map(sanitizeString);
|
|
20
|
-
const attrsExact = ((_g = (_f = definition === null || definition === void 0 ? void 0 : definition.attr) === null || _f === void 0 ? void 0 : _f.exact) !== null && _g !== void 0 ? _g : []).map(sanitizeStringWithSpaces);
|
|
21
|
-
if (i18n.length > 0) {
|
|
22
|
-
output += `export const ${name}_RE = ${regexgen(i18n, "")}i;\n`;
|
|
23
|
-
}
|
|
24
|
-
if (attrs.length > 0 || attrsExact.length > 0) {
|
|
25
|
-
const regAttrs = attrs.length > 0 ? regexgen(attrs, "").source : null;
|
|
26
|
-
const regExact = attrsExact.length > 0 ? `\\b(${regexgen(attrsExact, "").source})\\b` : null;
|
|
27
|
-
const regPrefixes = prefixes.length > 0 ? regexgen(prefixes, "").source : null;
|
|
28
|
-
const withPrefixes = (re) => (regPrefixes ? `(?:${regPrefixes})${re}` : re);
|
|
29
|
-
const re = (() => {
|
|
30
|
-
if (regAttrs && !regExact)
|
|
31
|
-
return withPrefixes(regAttrs);
|
|
32
|
-
else if (!regAttrs && regExact)
|
|
33
|
-
return regExact;
|
|
34
|
-
else if (regAttrs && regExact)
|
|
35
|
-
return `${withPrefixes(regAttrs)}|${regExact}`;
|
|
36
|
-
return "";
|
|
37
|
-
})();
|
|
38
|
-
output += `export const ${name}_ATTR_RE = /${re}/i;\n`;
|
|
39
|
-
}
|
|
40
|
-
if (definition === null || definition === void 0 ? void 0 : definition.startsWith) {
|
|
41
|
-
const startWithRe = regexgen(definition.startsWith.map(sanitizeString), "");
|
|
42
|
-
output += `export const ${name}_ATTR_START_RE = /\\b(?:${startWithRe.source})\\S+/i;\n`;
|
|
43
|
-
}
|
|
44
|
-
if (definition === null || definition === void 0 ? void 0 : definition.endsWith) {
|
|
45
|
-
const endWordsRe = regexgen(definition.endsWith.map(sanitizeString), "");
|
|
46
|
-
output += `export const ${name}_ATTR_END_RE= /\\b\\S*(?:${endWordsRe.source})\\b/i;\n`;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
fs.writeFileSync(path.resolve(__dirname, "./generated/dictionary.ts"), output);
|
|
50
|
-
};
|
|
51
|
-
generateRegexes();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/features/feature.spec.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { feature, featuresProduct, getComputerForFeatures, topologicalSort, validateComputerInputs } from "./feature";
|
|
2
|
-
function doNothing() {
|
|
3
|
-
}
|
|
4
|
-
describe("topological sort", () => {
|
|
5
|
-
test("it works", () => {
|
|
6
|
-
const A = feature("A", {}, doNothing);
|
|
7
|
-
const B = feature("B", { A }, doNothing);
|
|
8
|
-
const C = feature("C", { A }, doNothing);
|
|
9
|
-
const D = feature("D", { B, C }, doNothing);
|
|
10
|
-
const E = feature("E", { B }, doNothing);
|
|
11
|
-
const F = feature("F", {}, doNothing);
|
|
12
|
-
const G = feature("G", { E, F }, doNothing);
|
|
13
|
-
const H = feature("H", {}, doNothing);
|
|
14
|
-
const features = { D, G, H };
|
|
15
|
-
const sorted = topologicalSort(features);
|
|
16
|
-
const nodeNames = sorted.map((feat) => feat.name);
|
|
17
|
-
expect(nodeNames.slice(0, 3).sort()).toEqual(["A", "F", "H"]);
|
|
18
|
-
expect(nodeNames.slice(3, 5).sort()).toEqual(["B", "C"]);
|
|
19
|
-
expect(nodeNames[5]).toEqual("E");
|
|
20
|
-
expect(nodeNames.slice(6, 8).sort()).toEqual(["D", "G"]);
|
|
21
|
-
});
|
|
22
|
-
test("a node reachable from multiple paths appears only once", () => {
|
|
23
|
-
const X = feature("X", {}, doNothing);
|
|
24
|
-
const Y = feature("Y", { X }, doNothing);
|
|
25
|
-
const A = feature("A", { X, Y }, doNothing);
|
|
26
|
-
const sorted = topologicalSort({ A });
|
|
27
|
-
expect(sorted.length).toBe(new Set(sorted).size);
|
|
28
|
-
const pos = new Map(sorted.map((f, i) => [f, i]));
|
|
29
|
-
expect(pos.get(X)).toBeLessThan(pos.get(Y));
|
|
30
|
-
expect(pos.get(Y)).toBeLessThan(pos.get(A));
|
|
31
|
-
});
|
|
32
|
-
test("it fails if there is a loop", () => {
|
|
33
|
-
const A = feature("A", {}, doNothing);
|
|
34
|
-
const B = feature("B", { A }, doNothing);
|
|
35
|
-
const C = feature("C", { A }, doNothing);
|
|
36
|
-
const D = feature("D", { B, C }, doNothing);
|
|
37
|
-
const E = feature("E", { B }, doNothing);
|
|
38
|
-
const F = feature("F", {}, doNothing);
|
|
39
|
-
const G = feature("G", { E, F }, doNothing);
|
|
40
|
-
const H = feature("H", {}, doNothing);
|
|
41
|
-
A.parents = { D };
|
|
42
|
-
const features = { A, B, C, D, E, F, G, H };
|
|
43
|
-
expect(() => topologicalSort(features)).toThrow(`loop detected with elements:
|
|
44
|
-
A -> {D}
|
|
45
|
-
B -> {A}
|
|
46
|
-
C -> {A}
|
|
47
|
-
D -> {B, C}
|
|
48
|
-
E -> {B}
|
|
49
|
-
G -> {E}`);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
const buildComputer = (outputs) => getComputerForFeatures({
|
|
53
|
-
sorted: topologicalSort(outputs),
|
|
54
|
-
features: Object.values(outputs)
|
|
55
|
-
.filter((f) => !f.private)
|
|
56
|
-
.map((f) => f.name),
|
|
57
|
-
private: Object.values(outputs)
|
|
58
|
-
.filter((f) => f.private)
|
|
59
|
-
.map((f) => f.name),
|
|
60
|
-
});
|
|
61
|
-
describe("Feature engineering", () => {
|
|
62
|
-
test("featureProduct", () => {
|
|
63
|
-
const A = feature("A", {}, () => 2);
|
|
64
|
-
const B = feature("B", { A }, () => 4);
|
|
65
|
-
const C = featuresProduct(A, B);
|
|
66
|
-
expect(buildComputer({ A, B, C }).compute({})).toMatchObject({ A: 2, B: 4, "A,B": 8 });
|
|
67
|
-
});
|
|
68
|
-
test("feature name re-mapping", () => {
|
|
69
|
-
const A = feature("A", {}, () => 2);
|
|
70
|
-
const B = feature("_B", { A }, () => 4);
|
|
71
|
-
const C = feature("C", { A, B }, (p) => p.A + p._B);
|
|
72
|
-
expect(buildComputer({ A, B, C }).compute({})).toMatchObject({ A: 2, _B: 4, C: 6 });
|
|
73
|
-
});
|
|
74
|
-
describe("validateComputerInputs (gen-time guards)", () => {
|
|
75
|
-
const A = feature("A", {}, () => 1);
|
|
76
|
-
const B = feature("B", { A }, () => 2);
|
|
77
|
-
test("throws on a duplicate feature name in sorted", () => {
|
|
78
|
-
const Adup = feature("A", {}, () => 9);
|
|
79
|
-
expect(() => validateComputerInputs({ sorted: [A, Adup], features: ["A"], private: [] })).toThrow("duplicate feature 'A' in sorted DAG");
|
|
80
|
-
});
|
|
81
|
-
test("throws when a parent is ordered after its child", () => {
|
|
82
|
-
expect(() => validateComputerInputs({ sorted: [B, A], features: ["A", "B"], private: [] })).toThrow("feature 'B' precedes its parent 'A' in sorted DAG");
|
|
83
|
-
});
|
|
84
|
-
test("throws when an output name is absent from the DAG", () => {
|
|
85
|
-
expect(() => validateComputerInputs({ sorted: [A], features: ["A", "ghost"], private: [] })).toThrow("output 'ghost' missing from sorted DAG");
|
|
86
|
-
expect(() => validateComputerInputs({ sorted: [A], features: ["A"], private: ["secret"] })).toThrow("output 'secret' missing from sorted DAG");
|
|
87
|
-
});
|
|
88
|
-
test("accepts a well-formed, topologically sorted DAG", () => {
|
|
89
|
-
expect(() => validateComputerInputs({ sorted: [A, B], features: ["A", "B"], private: [] })).not.toThrow();
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe("private outputs", () => {
|
|
93
|
-
const Trans = feature("Trans", {}, () => 10);
|
|
94
|
-
const Priv = feature("Priv", { Trans }, (p) => p.Trans + 1, { private: true });
|
|
95
|
-
const Pub = feature("Pub", { Priv }, (p) => p.Priv + 1);
|
|
96
|
-
const computer = buildComputer({ Pub, Priv });
|
|
97
|
-
const result = computer.compute({});
|
|
98
|
-
test(".features lists only non-private outputs", () => {
|
|
99
|
-
expect(computer.features).toEqual(["Pub"]);
|
|
100
|
-
});
|
|
101
|
-
test("compute() result includes private outputs ", () => {
|
|
102
|
-
expect(result).toEqual({ Pub: 12, Priv: 11 });
|
|
103
|
-
});
|
|
104
|
-
test("compute() result excludes transients ", () => {
|
|
105
|
-
expect(result).not.toHaveProperty("Trans");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { topologicalSort } from "@protontech/autofill/features/feature";
|
|
2
|
-
import { fieldFeatures } from "./abstract.field";
|
|
3
|
-
import { formFeatures } from "./abstract.form";
|
|
4
|
-
import { emailFeatures } from "./field.email";
|
|
5
|
-
import { otpFeatures } from "./field.otp";
|
|
6
|
-
import { passwordFeatures } from "./field.password";
|
|
7
|
-
import { usernameFeatures } from "./field.username";
|
|
8
|
-
import { usernameHiddenFeatures } from "./field.username-hidden";
|
|
9
|
-
const groups = {
|
|
10
|
-
fieldFeatures,
|
|
11
|
-
formFeatures,
|
|
12
|
-
emailFeatures,
|
|
13
|
-
otpFeatures,
|
|
14
|
-
passwordFeatures,
|
|
15
|
-
usernameFeatures,
|
|
16
|
-
usernameHiddenFeatures,
|
|
17
|
-
};
|
|
18
|
-
const walkDAG = (outputs) => {
|
|
19
|
-
const seen = new Set();
|
|
20
|
-
const visit = (f) => {
|
|
21
|
-
if (seen.has(f))
|
|
22
|
-
return;
|
|
23
|
-
seen.add(f);
|
|
24
|
-
Object.values(f.parents).forEach(visit);
|
|
25
|
-
};
|
|
26
|
-
Object.values(outputs).forEach(visit);
|
|
27
|
-
return [...seen];
|
|
28
|
-
};
|
|
29
|
-
const findNameCollisions = (features) => {
|
|
30
|
-
var _a;
|
|
31
|
-
var _b;
|
|
32
|
-
const byName = {};
|
|
33
|
-
for (const f of features)
|
|
34
|
-
((_a = byName[_b = f.name]) !== null && _a !== void 0 ? _a : (byName[_b] = [])).push(f);
|
|
35
|
-
return Object.fromEntries(Object.entries(byName).filter(([, fs]) => fs.length > 1));
|
|
36
|
-
};
|
|
37
|
-
describe.each(Object.entries(groups))("%s", (_name, outputs) => {
|
|
38
|
-
test("topo-sorts without throwing", () => {
|
|
39
|
-
expect(() => topologicalSort(outputs)).not.toThrow();
|
|
40
|
-
});
|
|
41
|
-
test("every Feature object has a unique .name", () => {
|
|
42
|
-
expect(findNameCollisions(walkDAG(outputs))).toEqual({});
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { topologicalSort, validateComputerInputs } from "@protontech/autofill/features/feature";
|
|
2
|
-
import * as abstractField from "@protontech/autofill/features/v1/abstract.field";
|
|
3
|
-
import * as abstractForm from "@protontech/autofill/features/v1/abstract.form";
|
|
4
|
-
import * as fieldEmail from "@protontech/autofill/features/v1/field.email";
|
|
5
|
-
import * as fieldOtp from "@protontech/autofill/features/v1/field.otp";
|
|
6
|
-
import * as fieldPassword from "@protontech/autofill/features/v1/field.password";
|
|
7
|
-
import * as fieldUsername from "@protontech/autofill/features/v1/field.username";
|
|
8
|
-
import * as fieldUsernameHidden from "@protontech/autofill/features/v1/field.username-hidden";
|
|
9
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { dirname } from "node:path";
|
|
11
|
-
const sources = [
|
|
12
|
-
["./abstract.form", abstractForm],
|
|
13
|
-
["./abstract.field", abstractField],
|
|
14
|
-
["./field.email", fieldEmail],
|
|
15
|
-
["./field.otp", fieldOtp],
|
|
16
|
-
["./field.password", fieldPassword],
|
|
17
|
-
["./field.username", fieldUsername],
|
|
18
|
-
["./field.username-hidden", fieldUsernameHidden],
|
|
19
|
-
];
|
|
20
|
-
const isFeature = (v) => typeof v === "object" && v !== null && typeof v.compute === "function";
|
|
21
|
-
const buildFeatureRefs = (sources) => {
|
|
22
|
-
const refs = new Map();
|
|
23
|
-
for (const [src, mod] of sources) {
|
|
24
|
-
for (const [id, val] of Object.entries(mod)) {
|
|
25
|
-
if (isFeature(val) && !refs.has(val))
|
|
26
|
-
refs.set(val, { id, src, path: [] });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
for (const [src, mod] of sources) {
|
|
30
|
-
for (const [id, val] of Object.entries(mod)) {
|
|
31
|
-
if (!isFeature(val))
|
|
32
|
-
continue;
|
|
33
|
-
const walk = (node, path) => {
|
|
34
|
-
for (const [key, parent] of Object.entries(node.parents)) {
|
|
35
|
-
if (refs.has(parent))
|
|
36
|
-
continue;
|
|
37
|
-
const next = [...path, key];
|
|
38
|
-
refs.set(parent, { id, src, path: next });
|
|
39
|
-
walk(parent, next);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
walk(val, []);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return refs;
|
|
46
|
-
};
|
|
47
|
-
const featureRefs = buildFeatureRefs(sources);
|
|
48
|
-
const refOf = (f) => {
|
|
49
|
-
const ref = featureRefs.get(f);
|
|
50
|
-
if (!ref)
|
|
51
|
-
throw new Error(`feature '${f.name}' has no exported declaration.`);
|
|
52
|
-
return ref;
|
|
53
|
-
};
|
|
54
|
-
const buildComputer = (name, recordSrc, record) => {
|
|
55
|
-
const outputs = Object.values(record);
|
|
56
|
-
const computer = {
|
|
57
|
-
name,
|
|
58
|
-
recordSrc,
|
|
59
|
-
sorted: topologicalSort(record),
|
|
60
|
-
features: outputs.filter((f) => !f.private).map((f) => f.name),
|
|
61
|
-
private: outputs.filter((f) => f.private).map((f) => f.name),
|
|
62
|
-
};
|
|
63
|
-
validateComputerInputs(computer);
|
|
64
|
-
return computer;
|
|
65
|
-
};
|
|
66
|
-
const BANNER = `// Generated by \`bazel run //pass_ml/js/autofill:write_sorted_features\`. Do not edit.\n\n`;
|
|
67
|
-
const RUNTIME_IMPORT = `import { getComputerForFeatures } from "@protontech/autofill/features/feature";`;
|
|
68
|
-
const fmtImportSpec = (id, alias) => (alias === id ? id : `${id} as ${alias}`);
|
|
69
|
-
const fmtImportLine = (prefix, src, specs) => `${prefix} { ${specs.join(", ")} } from "${src}";`;
|
|
70
|
-
const fmtNameList = (names) => names.map((n) => `"${n}"`).join(", ");
|
|
71
|
-
const fmtComputerExport = (name, recordType, sortedRefs, features, privateOutputs) => `export const ${name}Computer = getComputerForFeatures<typeof ${recordType}>({
|
|
72
|
-
sorted: [${sortedRefs.map((r) => ` ${r},`).join("\n")}],
|
|
73
|
-
features: [${fmtNameList(features)}],
|
|
74
|
-
private: [${fmtNameList(privateOutputs)}],
|
|
75
|
-
});`;
|
|
76
|
-
const fmtFile = (imports, exports) => `${BANNER}\n${imports}\n\n${exports}\n`;
|
|
77
|
-
const render = (computers) => {
|
|
78
|
-
const valueImports = {};
|
|
79
|
-
const typeImports = {};
|
|
80
|
-
const srcsById = {};
|
|
81
|
-
const track = (bucket, src, id) => {
|
|
82
|
-
var _a, _b;
|
|
83
|
-
((_a = bucket[src]) !== null && _a !== void 0 ? _a : (bucket[src] = new Set())).add(id);
|
|
84
|
-
((_b = srcsById[id]) !== null && _b !== void 0 ? _b : (srcsById[id] = new Set())).add(src);
|
|
85
|
-
};
|
|
86
|
-
for (const c of computers) {
|
|
87
|
-
track(typeImports, c.recordSrc, c.name);
|
|
88
|
-
for (const f of c.sorted) {
|
|
89
|
-
const { src, id } = refOf(f);
|
|
90
|
-
track(valueImports, src, id);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const importAlias = (src, id) => {
|
|
94
|
-
var _a;
|
|
95
|
-
if (srcsById[id].size === 1)
|
|
96
|
-
return id;
|
|
97
|
-
return `${id}_${(_a = src.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(/[.-]/g, "_")}`;
|
|
98
|
-
};
|
|
99
|
-
const importLinesFor = (bucket, prefix) => Object.entries(bucket).map(([src, ids]) => {
|
|
100
|
-
const specs = [...ids].map((id) => fmtImportSpec(id, importAlias(src, id)));
|
|
101
|
-
return fmtImportLine(prefix, src, specs);
|
|
102
|
-
});
|
|
103
|
-
const imports = [RUNTIME_IMPORT, ...importLinesFor(valueImports, "import"), ...importLinesFor(typeImports, "import type")].join("\n");
|
|
104
|
-
const refExpr = (f) => {
|
|
105
|
-
const { src, id, path } = refOf(f);
|
|
106
|
-
return path.reduce((acc, key) => `${acc}.parents.${key}`, importAlias(src, id));
|
|
107
|
-
};
|
|
108
|
-
const exports = computers.map((c) => fmtComputerExport(c.name, importAlias(c.recordSrc, c.name), c.sorted.map(refExpr), c.features, c.private)).join("\n\n");
|
|
109
|
-
return fmtFile(imports, exports);
|
|
110
|
-
};
|
|
111
|
-
const writeOut = (path, content) => {
|
|
112
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
113
|
-
writeFileSync(path, content);
|
|
114
|
-
};
|
|
115
|
-
const [, , formsOut, fieldsOut] = process.argv;
|
|
116
|
-
if (!formsOut || !fieldsOut)
|
|
117
|
-
throw new Error("usage: gen-sorted-features <forms-out.ts> <fields-out.ts>");
|
|
118
|
-
const formComputers = [buildComputer("formFeatures", "./abstract.form", abstractForm.formFeatures)];
|
|
119
|
-
const fieldComputers = [
|
|
120
|
-
buildComputer("fieldFeatures", "./abstract.field", abstractField.fieldFeatures),
|
|
121
|
-
buildComputer("emailFeatures", "./field.email", fieldEmail.emailFeatures),
|
|
122
|
-
buildComputer("otpFeatures", "./field.otp", fieldOtp.otpFeatures),
|
|
123
|
-
buildComputer("passwordFeatures", "./field.password", fieldPassword.passwordFeatures),
|
|
124
|
-
buildComputer("usernameFeatures", "./field.username", fieldUsername.usernameFeatures),
|
|
125
|
-
buildComputer("usernameHiddenFeatures", "./field.username-hidden", fieldUsernameHidden.usernameHiddenFeatures),
|
|
126
|
-
];
|
|
127
|
-
writeOut(formsOut, render(formComputers));
|
|
128
|
-
writeOut(fieldsOut, render(fieldComputers));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|