@colixsystems/widget-sdk 0.18.0 → 0.21.1
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/README.md +129 -29
- package/dist/contract.cjs +228 -113
- package/dist/contract.js +238 -122
- package/dist/hooks.js +781 -570
- package/dist/index.d.ts +257 -65
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +157 -2
- package/dist/manifest.js +157 -2
- package/package.json +2 -2
package/dist/manifest.cjs
CHANGED
|
@@ -5,11 +5,154 @@
|
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
7
|
const VALID_CATEGORIES = new Set([
|
|
8
|
-
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
9
|
-
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
8
|
+
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
|
+
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
10
10
|
]);
|
|
11
11
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
12
12
|
|
|
13
|
+
// REQ-WIDGET-ACTION — structural validation for manifest-declared server
|
|
14
|
+
// actions. Mirrors the backend action.service caps; the runner re-validates
|
|
15
|
+
// the cron expression (node-cron) and binds the tenant-local API key + table
|
|
16
|
+
// at enable time, so we only check shape + size here (cross-env: no Buffer).
|
|
17
|
+
const VALID_ACTION_TRIGGERS = new Set([
|
|
18
|
+
"schedule",
|
|
19
|
+
"record_created",
|
|
20
|
+
"record_updated",
|
|
21
|
+
"record_deleted",
|
|
22
|
+
]);
|
|
23
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
24
|
+
const ACTION_TIMEOUT_MIN_MS = 100;
|
|
25
|
+
const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
// REQ-L10N-WIDGET — caps + patterns for manifest-declared translations.
|
|
28
|
+
// The relative key pattern is deliberately tighter than the dictionary's
|
|
29
|
+
// own KEY_RE: once the host namespaces it (`widget.<id>.<key>`) the result
|
|
30
|
+
// must still satisfy the dictionary cap (128 chars / KEY_RE in
|
|
31
|
+
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
32
|
+
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
33
|
+
// would reject.
|
|
34
|
+
const TRANSLATIONS_MAX_KEYS = 100;
|
|
35
|
+
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
36
|
+
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
37
|
+
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
|
38
|
+
const TRANSLATION_FULL_KEY_MAX = 128;
|
|
39
|
+
|
|
40
|
+
function utf8ByteLength(s) {
|
|
41
|
+
return new TextEncoder().encode(s).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateManifestTranslations(translations, manifestId, errors) {
|
|
45
|
+
if (
|
|
46
|
+
translations === null ||
|
|
47
|
+
typeof translations !== "object" ||
|
|
48
|
+
Array.isArray(translations)
|
|
49
|
+
) {
|
|
50
|
+
errors.push(
|
|
51
|
+
"manifest.translations must be an object mapping key -> { en, <locale>? }",
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const keys = Object.keys(translations);
|
|
56
|
+
if (keys.length > TRANSLATIONS_MAX_KEYS) {
|
|
57
|
+
errors.push(
|
|
58
|
+
`manifest.translations may declare at most ${TRANSLATIONS_MAX_KEYS} keys`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (!TRANSLATION_KEY_RE.test(key)) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`manifest.translations key "${key}" must match /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/`,
|
|
65
|
+
);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (isNonEmptyString(manifestId)) {
|
|
69
|
+
const fullKey = `widget.${manifestId}.${key}`;
|
|
70
|
+
if (fullKey.length > TRANSLATION_FULL_KEY_MAX) {
|
|
71
|
+
errors.push(
|
|
72
|
+
`manifest.translations key "${key}" is too long once namespaced (${fullKey.length} > ${TRANSLATION_FULL_KEY_MAX} chars)`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const entry = translations[key];
|
|
77
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
78
|
+
errors.push(
|
|
79
|
+
`manifest.translations["${key}"] must be an object of locale -> string`,
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!isNonEmptyString(entry.en)) {
|
|
84
|
+
errors.push(
|
|
85
|
+
`manifest.translations["${key}"].en is required and must be a non-empty string`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
for (const [locale, value] of Object.entries(entry)) {
|
|
89
|
+
if (!TRANSLATION_LOCALE_RE.test(locale)) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`manifest.translations["${key}"] has invalid locale "${locale}"`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
errors.push(
|
|
96
|
+
`manifest.translations["${key}"]["${locale}"] must be a string`,
|
|
97
|
+
);
|
|
98
|
+
} else if (utf8ByteLength(value) > TRANSLATION_VALUE_MAX_BYTES) {
|
|
99
|
+
errors.push(
|
|
100
|
+
`manifest.translations["${key}"]["${locale}"] exceeds 1 KB`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateManifestActions(actions, errors) {
|
|
108
|
+
if (!Array.isArray(actions)) {
|
|
109
|
+
errors.push("manifest.actions must be an array (omit it or use [] for none)");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const seenKeys = new Set();
|
|
113
|
+
for (const a of actions) {
|
|
114
|
+
if (a === null || typeof a !== "object") {
|
|
115
|
+
errors.push("manifest.actions entries must be objects");
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!isNonEmptyString(a.key)) {
|
|
119
|
+
errors.push("manifest.actions[].key must be a non-empty string");
|
|
120
|
+
} else if (seenKeys.has(a.key)) {
|
|
121
|
+
errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
122
|
+
} else {
|
|
123
|
+
seenKeys.add(a.key);
|
|
124
|
+
}
|
|
125
|
+
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
126
|
+
if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
|
|
127
|
+
errors.push(
|
|
128
|
+
`manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
129
|
+
);
|
|
130
|
+
} else if (a.triggerType === "schedule") {
|
|
131
|
+
pushIf(
|
|
132
|
+
errors,
|
|
133
|
+
isNonEmptyString(a.scheduleCron),
|
|
134
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!isNonEmptyString(a.scriptSource)) {
|
|
138
|
+
errors.push("manifest.actions[].scriptSource must be a non-empty string");
|
|
139
|
+
} else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
|
|
140
|
+
errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
141
|
+
}
|
|
142
|
+
if (a.timeoutMs !== undefined) {
|
|
143
|
+
const t = Number(a.timeoutMs);
|
|
144
|
+
if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
|
|
145
|
+
errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
149
|
+
errors.push(
|
|
150
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
13
156
|
function canonicalCategory(c) {
|
|
14
157
|
if (typeof c !== "string") return null;
|
|
15
158
|
const upper = c.toUpperCase();
|
|
@@ -20,6 +163,7 @@ function canonicalCategory(c) {
|
|
|
20
163
|
"DATA",
|
|
21
164
|
"MEDIA",
|
|
22
165
|
"COMMUNICATION",
|
|
166
|
+
"ADMINISTRATION",
|
|
23
167
|
"CUSTOM",
|
|
24
168
|
].includes(upper)
|
|
25
169
|
? upper
|
|
@@ -135,10 +279,21 @@ function validateManifest(m) {
|
|
|
135
279
|
}
|
|
136
280
|
}
|
|
137
281
|
|
|
282
|
+
// `actions` is optional (additive in SDK 1.8.0) — only validate when present.
|
|
283
|
+
if (manifest.actions !== undefined) {
|
|
284
|
+
validateManifestActions(manifest.actions, errors);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// `translations` is optional (additive in SDK 1.10.0 / REQ-L10N-WIDGET).
|
|
288
|
+
if (manifest.translations !== undefined) {
|
|
289
|
+
validateManifestTranslations(manifest.translations, manifest.id, errors);
|
|
290
|
+
}
|
|
291
|
+
|
|
138
292
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
139
293
|
}
|
|
140
294
|
|
|
141
295
|
module.exports = {
|
|
142
296
|
validateManifest,
|
|
297
|
+
validateManifestTranslations,
|
|
143
298
|
canonicalCategory,
|
|
144
299
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -5,11 +5,154 @@
|
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
7
|
const VALID_CATEGORIES = new Set([
|
|
8
|
-
"input", "display", "layout", "data", "media", "communication", "custom",
|
|
9
|
-
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "CUSTOM",
|
|
8
|
+
"input", "display", "layout", "data", "media", "communication", "administration", "custom",
|
|
9
|
+
"INPUT", "DISPLAY", "LAYOUT", "DATA", "MEDIA", "COMMUNICATION", "ADMINISTRATION", "CUSTOM",
|
|
10
10
|
]);
|
|
11
11
|
const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
12
12
|
|
|
13
|
+
// REQ-WIDGET-ACTION — structural validation for manifest-declared server
|
|
14
|
+
// actions. Mirrors the backend action.service caps; the runner re-validates
|
|
15
|
+
// the cron expression (node-cron) and binds the tenant-local API key + table
|
|
16
|
+
// at enable time, so we only check shape + size here (cross-env: no Buffer).
|
|
17
|
+
const VALID_ACTION_TRIGGERS = new Set([
|
|
18
|
+
"schedule",
|
|
19
|
+
"record_created",
|
|
20
|
+
"record_updated",
|
|
21
|
+
"record_deleted",
|
|
22
|
+
]);
|
|
23
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
24
|
+
const ACTION_TIMEOUT_MIN_MS = 100;
|
|
25
|
+
const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
// REQ-L10N-WIDGET — caps + patterns for manifest-declared translations.
|
|
28
|
+
// The relative key pattern is deliberately tighter than the dictionary's
|
|
29
|
+
// own KEY_RE: once the host namespaces it (`widget.<id>.<key>`) the result
|
|
30
|
+
// must still satisfy the dictionary cap (128 chars / KEY_RE in
|
|
31
|
+
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
32
|
+
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
33
|
+
// would reject.
|
|
34
|
+
const TRANSLATIONS_MAX_KEYS = 100;
|
|
35
|
+
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
36
|
+
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
37
|
+
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
|
38
|
+
const TRANSLATION_FULL_KEY_MAX = 128;
|
|
39
|
+
|
|
40
|
+
function utf8ByteLength(s) {
|
|
41
|
+
return new TextEncoder().encode(s).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateManifestTranslations(translations, manifestId, errors) {
|
|
45
|
+
if (
|
|
46
|
+
translations === null ||
|
|
47
|
+
typeof translations !== "object" ||
|
|
48
|
+
Array.isArray(translations)
|
|
49
|
+
) {
|
|
50
|
+
errors.push(
|
|
51
|
+
"manifest.translations must be an object mapping key -> { en, <locale>? }",
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const keys = Object.keys(translations);
|
|
56
|
+
if (keys.length > TRANSLATIONS_MAX_KEYS) {
|
|
57
|
+
errors.push(
|
|
58
|
+
`manifest.translations may declare at most ${TRANSLATIONS_MAX_KEYS} keys`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (!TRANSLATION_KEY_RE.test(key)) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`manifest.translations key "${key}" must match /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/`,
|
|
65
|
+
);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (isNonEmptyString(manifestId)) {
|
|
69
|
+
const fullKey = `widget.${manifestId}.${key}`;
|
|
70
|
+
if (fullKey.length > TRANSLATION_FULL_KEY_MAX) {
|
|
71
|
+
errors.push(
|
|
72
|
+
`manifest.translations key "${key}" is too long once namespaced (${fullKey.length} > ${TRANSLATION_FULL_KEY_MAX} chars)`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const entry = translations[key];
|
|
77
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
78
|
+
errors.push(
|
|
79
|
+
`manifest.translations["${key}"] must be an object of locale -> string`,
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!isNonEmptyString(entry.en)) {
|
|
84
|
+
errors.push(
|
|
85
|
+
`manifest.translations["${key}"].en is required and must be a non-empty string`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
for (const [locale, value] of Object.entries(entry)) {
|
|
89
|
+
if (!TRANSLATION_LOCALE_RE.test(locale)) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`manifest.translations["${key}"] has invalid locale "${locale}"`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
errors.push(
|
|
96
|
+
`manifest.translations["${key}"]["${locale}"] must be a string`,
|
|
97
|
+
);
|
|
98
|
+
} else if (utf8ByteLength(value) > TRANSLATION_VALUE_MAX_BYTES) {
|
|
99
|
+
errors.push(
|
|
100
|
+
`manifest.translations["${key}"]["${locale}"] exceeds 1 KB`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateManifestActions(actions, errors) {
|
|
108
|
+
if (!Array.isArray(actions)) {
|
|
109
|
+
errors.push("manifest.actions must be an array (omit it or use [] for none)");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const seenKeys = new Set();
|
|
113
|
+
for (const a of actions) {
|
|
114
|
+
if (a === null || typeof a !== "object") {
|
|
115
|
+
errors.push("manifest.actions entries must be objects");
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!isNonEmptyString(a.key)) {
|
|
119
|
+
errors.push("manifest.actions[].key must be a non-empty string");
|
|
120
|
+
} else if (seenKeys.has(a.key)) {
|
|
121
|
+
errors.push(`manifest.actions[].key "${a.key}" is duplicated`);
|
|
122
|
+
} else {
|
|
123
|
+
seenKeys.add(a.key);
|
|
124
|
+
}
|
|
125
|
+
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
126
|
+
if (!VALID_ACTION_TRIGGERS.has(a.triggerType)) {
|
|
127
|
+
errors.push(
|
|
128
|
+
`manifest.actions[].triggerType must be one of ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
129
|
+
);
|
|
130
|
+
} else if (a.triggerType === "schedule") {
|
|
131
|
+
pushIf(
|
|
132
|
+
errors,
|
|
133
|
+
isNonEmptyString(a.scheduleCron),
|
|
134
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!isNonEmptyString(a.scriptSource)) {
|
|
138
|
+
errors.push("manifest.actions[].scriptSource must be a non-empty string");
|
|
139
|
+
} else if (utf8ByteLength(a.scriptSource) > ACTION_SCRIPT_MAX_BYTES) {
|
|
140
|
+
errors.push("manifest.actions[].scriptSource exceeds 200 KiB");
|
|
141
|
+
}
|
|
142
|
+
if (a.timeoutMs !== undefined) {
|
|
143
|
+
const t = Number(a.timeoutMs);
|
|
144
|
+
if (!Number.isFinite(t) || t < ACTION_TIMEOUT_MIN_MS || t > ACTION_TIMEOUT_MAX_MS) {
|
|
145
|
+
errors.push("manifest.actions[].timeoutMs must be between 100 and 300000");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
149
|
+
errors.push(
|
|
150
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
13
156
|
function canonicalCategory(c) {
|
|
14
157
|
if (typeof c !== "string") return null;
|
|
15
158
|
const upper = c.toUpperCase();
|
|
@@ -20,6 +163,7 @@ function canonicalCategory(c) {
|
|
|
20
163
|
"DATA",
|
|
21
164
|
"MEDIA",
|
|
22
165
|
"COMMUNICATION",
|
|
166
|
+
"ADMINISTRATION",
|
|
23
167
|
"CUSTOM",
|
|
24
168
|
].includes(upper)
|
|
25
169
|
? upper
|
|
@@ -135,10 +279,21 @@ function validateManifest(m) {
|
|
|
135
279
|
}
|
|
136
280
|
}
|
|
137
281
|
|
|
282
|
+
// `actions` is optional (additive in SDK 1.8.0) — only validate when present.
|
|
283
|
+
if (manifest.actions !== undefined) {
|
|
284
|
+
validateManifestActions(manifest.actions, errors);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// `translations` is optional (additive in SDK 1.10.0 / REQ-L10N-WIDGET).
|
|
288
|
+
if (manifest.translations !== undefined) {
|
|
289
|
+
validateManifestTranslations(manifest.translations, manifest.id, errors);
|
|
290
|
+
}
|
|
291
|
+
|
|
138
292
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
139
293
|
}
|
|
140
294
|
|
|
141
295
|
export {
|
|
142
296
|
validateManifest,
|
|
297
|
+
validateManifestTranslations,
|
|
143
298
|
canonicalCategory,
|
|
144
299
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|