@deneb-ui/cli 2.0.22 → 2.0.24

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.
Files changed (52) hide show
  1. package/README.md +62 -114
  2. package/bin/index.js +55 -12
  3. package/package.json +25 -5
  4. package/src/arc/__fixtures__/next-app-basic/package.json +10 -0
  5. package/src/arc/__fixtures__/next-app-basic/src/app/globals.css +3 -0
  6. package/src/arc/__fixtures__/next-app-basic/src/app/layout.tsx +9 -0
  7. package/src/arc/__fixtures__/next-app-basic/src/app/page.tsx +11 -0
  8. package/src/arc/__fixtures__/next-app-basic/src/components/Header.tsx +13 -0
  9. package/src/arc/__fixtures__/next-app-basic/src/components/Hero.tsx +12 -0
  10. package/src/arc/__fixtures__/next-app-basic/src/components/PromoBanner.tsx +10 -0
  11. package/src/arc/__fixtures__/next-app-basic/tsconfig.json +12 -0
  12. package/src/arc/__fixtures__/next-app-storefront/components.json +14 -0
  13. package/src/arc/__fixtures__/next-app-storefront/package.json +22 -0
  14. package/src/arc/__fixtures__/next-app-storefront/src/app/about/page.tsx +13 -0
  15. package/src/arc/__fixtures__/next-app-storefront/src/app/globals.css +5 -0
  16. package/src/arc/__fixtures__/next-app-storefront/src/app/layout.tsx +19 -0
  17. package/src/arc/__fixtures__/next-app-storefront/src/app/page.tsx +13 -0
  18. package/src/arc/__fixtures__/next-app-storefront/src/components/Features.tsx +31 -0
  19. package/src/arc/__fixtures__/next-app-storefront/src/components/Hero.tsx +45 -0
  20. package/src/arc/__fixtures__/next-app-storefront/src/components/ProductGrid.tsx +49 -0
  21. package/src/arc/__fixtures__/next-app-storefront/src/components/SiteFooter.tsx +22 -0
  22. package/src/arc/__fixtures__/next-app-storefront/src/components/SiteHeader.tsx +21 -0
  23. package/src/arc/__fixtures__/next-app-storefront/src/components/ui/button.tsx +36 -0
  24. package/src/arc/__fixtures__/next-app-storefront/tsconfig.json +15 -0
  25. package/src/arc/__fixtures__/next-pages-basic/package.json +10 -0
  26. package/src/arc/__fixtures__/next-pages-basic/pages/_app.jsx +5 -0
  27. package/src/arc/__fixtures__/next-pages-basic/pages/contact.jsx +9 -0
  28. package/src/arc/__fixtures__/next-pages-basic/pages/index.jsx +11 -0
  29. package/src/arc/__fixtures__/next-pages-basic/styles/globals.css +9 -0
  30. package/src/arc/__tests__/arc.test.cjs +498 -0
  31. package/src/arc/adapters.cjs +184 -0
  32. package/src/arc/ast.cjs +339 -0
  33. package/src/arc/field-paths.cjs +165 -0
  34. package/src/arc/fivora-contract.cjs +521 -0
  35. package/src/arc/font-plan.cjs +65 -0
  36. package/src/arc/fs-utils.cjs +170 -0
  37. package/src/arc/index.cjs +627 -0
  38. package/src/arc/learning.cjs +211 -0
  39. package/src/arc/manifest.cjs +438 -0
  40. package/src/arc/next-config.cjs +279 -0
  41. package/src/arc/planner.cjs +252 -0
  42. package/src/arc/printer.cjs +183 -0
  43. package/src/arc/recipes-v2.cjs +49 -0
  44. package/src/arc/scanner.cjs +613 -0
  45. package/src/arc/semantic.cjs +651 -0
  46. package/src/arc/style-candidates.cjs +72 -0
  47. package/src/arc/transformer.cjs +677 -0
  48. package/src/arc/validator.cjs +173 -0
  49. package/src/arc/version.cjs +22 -0
  50. package/src/common/style-validation.ts +30 -0
  51. package/src/tools/deneb-fonts.cjs +114 -0
  52. package/src/tools/template-preview-focus-bridge.cjs +3 -3
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fivora hosts storefronts as static exports served from merchant sub-paths.
5
+ * A converted project therefore needs `output: 'export'`, unoptimized images
6
+ * and a NEXT_PUBLIC_SITE_BASE_PATH-driven basePath, or `deneb package` fails
7
+ * its sandbox build. ARC configures this via AST so an existing config keeps
8
+ * its plugins, comments and formatting.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const recast = require('recast');
14
+ const { parseSource, printSource, b } = require('./ast.cjs');
15
+
16
+ const CONFIG_FILENAMES = [
17
+ 'next.config.ts',
18
+ 'next.config.mjs',
19
+ 'next.config.js',
20
+ 'next.config.cjs',
21
+ ];
22
+
23
+ const BASE_PATH_ENV = 'NEXT_PUBLIC_SITE_BASE_PATH';
24
+
25
+ function findNextConfig(projectDir) {
26
+ for (const name of CONFIG_FILENAMES) {
27
+ const abs = path.join(projectDir, name);
28
+ if (fs.existsSync(abs)) return { abs, name };
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function createNextConfig(projectDir, isTypeScript) {
34
+ const name = isTypeScript ? 'next.config.ts' : 'next.config.mjs';
35
+ const abs = path.join(projectDir, name);
36
+ const typed = isTypeScript
37
+ ? `import type { NextConfig } from 'next';\n\n`
38
+ : '';
39
+ const annotation = isTypeScript ? ': NextConfig' : '';
40
+
41
+ const code = `${typed}const basePath = process.env.${BASE_PATH_ENV} || '';
42
+
43
+ const nextConfig${annotation} = {
44
+ output: 'export',
45
+ basePath: basePath || undefined,
46
+ assetPrefix: basePath ? \`\${basePath}/\` : undefined,
47
+ images: {
48
+ unoptimized: true,
49
+ },
50
+ };
51
+
52
+ export default nextConfig;
53
+ `;
54
+
55
+ fs.writeFileSync(abs, code, 'utf8');
56
+ return { file: name, created: true, updated: true, warnings: [] };
57
+ }
58
+
59
+ /** Locates the object literal that describes the Next.js config. */
60
+ function findConfigObject(ast) {
61
+ let found = null;
62
+
63
+ recast.types.visit(ast, {
64
+ visitExportDefaultDeclaration(pathNode) {
65
+ const declaration = pathNode.node.declaration;
66
+ if (declaration?.type === 'ObjectExpression') {
67
+ found = declaration;
68
+ return false;
69
+ }
70
+ this.traverse(pathNode);
71
+ },
72
+ visitAssignmentExpression(pathNode) {
73
+ const node = pathNode.node;
74
+ const left = recast.print(node.left).code;
75
+ if (!found && /^module\.exports$/.test(left) && node.right?.type === 'ObjectExpression') {
76
+ found = node.right;
77
+ return false;
78
+ }
79
+ this.traverse(pathNode);
80
+ },
81
+ });
82
+
83
+ if (found) return found;
84
+
85
+ // `const nextConfig = {...}` possibly wrapped by a plugin on export.
86
+ recast.types.visit(ast, {
87
+ visitVariableDeclarator(pathNode) {
88
+ const node = pathNode.node;
89
+ if (
90
+ !found &&
91
+ node.id?.type === 'Identifier' &&
92
+ /config/i.test(node.id.name) &&
93
+ node.init?.type === 'ObjectExpression'
94
+ ) {
95
+ found = node.init;
96
+ return false;
97
+ }
98
+ this.traverse(pathNode);
99
+ },
100
+ });
101
+
102
+ return found;
103
+ }
104
+
105
+ function hasProperty(objectExpression, key) {
106
+ return (objectExpression.properties || []).some(
107
+ (property) =>
108
+ (property.type === 'ObjectProperty' || property.type === 'Property') &&
109
+ (property.key?.name === key || property.key?.value === key)
110
+ );
111
+ }
112
+
113
+ function propertyValue(objectExpression, key) {
114
+ const property = (objectExpression.properties || []).find(
115
+ (candidate) =>
116
+ (candidate.type === 'ObjectProperty' || candidate.type === 'Property') &&
117
+ (candidate.key?.name === key || candidate.key?.value === key)
118
+ );
119
+ return property ? property.value : null;
120
+ }
121
+
122
+ function hasBasePathDeclaration(ast) {
123
+ let declared = false;
124
+ recast.types.visit(ast, {
125
+ visitVariableDeclarator(pathNode) {
126
+ if (pathNode.node.id?.type === 'Identifier' && pathNode.node.id.name === 'basePath') {
127
+ declared = true;
128
+ return false;
129
+ }
130
+ this.traverse(pathNode);
131
+ },
132
+ });
133
+ return declared;
134
+ }
135
+
136
+ function insertBasePathDeclaration(ast) {
137
+ const program = ast.program || ast;
138
+ const declaration = b.variableDeclaration('const', [
139
+ b.variableDeclarator(
140
+ b.identifier('basePath'),
141
+ b.logicalExpression(
142
+ '||',
143
+ b.memberExpression(
144
+ b.memberExpression(b.identifier('process'), b.identifier('env')),
145
+ b.identifier(BASE_PATH_ENV)
146
+ ),
147
+ b.stringLiteral('')
148
+ )
149
+ ),
150
+ ]);
151
+
152
+ const body = program.body || [];
153
+ let insertAt = 0;
154
+ for (let index = 0; index < body.length; index += 1) {
155
+ const node = body[index];
156
+ const isDirective =
157
+ node.type === 'ExpressionStatement' &&
158
+ (node.expression?.type === 'StringLiteral' || node.expression?.type === 'Literal');
159
+ if (node.type === 'ImportDeclaration' || isDirective) insertAt = index + 1;
160
+ else break;
161
+ }
162
+ body.splice(insertAt, 0, declaration);
163
+ }
164
+
165
+ /**
166
+ * Adds the static-export settings a converted project needs, leaving any
167
+ * setting the developer already chose untouched.
168
+ */
169
+ function ensureStaticExportConfig(projectDir, profile) {
170
+ const existing = findNextConfig(projectDir);
171
+ if (!existing) {
172
+ return createNextConfig(projectDir, profile?.language !== 'javascript');
173
+ }
174
+
175
+ const original = fs.readFileSync(existing.abs, 'utf8');
176
+ let ast;
177
+ try {
178
+ ast = parseSource(original, existing.name);
179
+ } catch (err) {
180
+ return {
181
+ file: existing.name,
182
+ created: false,
183
+ updated: false,
184
+ warnings: [
185
+ `${existing.name} could not be parsed (${err.message}). Add output: 'export', images.unoptimized and a ${BASE_PATH_ENV} basePath manually.`,
186
+ ],
187
+ };
188
+ }
189
+
190
+ const configObject = findConfigObject(ast);
191
+ if (!configObject) {
192
+ return {
193
+ file: existing.name,
194
+ created: false,
195
+ updated: false,
196
+ warnings: [
197
+ `${existing.name} does not expose a plain config object, so ARC left it unchanged. Add output: 'export', images.unoptimized and a ${BASE_PATH_ENV} basePath manually.`,
198
+ ],
199
+ };
200
+ }
201
+
202
+ const warnings = [];
203
+ let updated = false;
204
+
205
+ if (!hasProperty(configObject, 'output')) {
206
+ configObject.properties.push(
207
+ b.objectProperty(b.identifier('output'), b.stringLiteral('export'))
208
+ );
209
+ updated = true;
210
+ } else {
211
+ const value = propertyValue(configObject, 'output');
212
+ if (value?.value !== 'export') {
213
+ warnings.push(
214
+ `${existing.name} sets output: '${value?.value}'. Fivora requires output: 'export'; ARC left your value in place.`
215
+ );
216
+ }
217
+ }
218
+
219
+ if (!hasProperty(configObject, 'images')) {
220
+ configObject.properties.push(
221
+ b.objectProperty(
222
+ b.identifier('images'),
223
+ b.objectExpression([
224
+ b.objectProperty(b.identifier('unoptimized'), b.booleanLiteral(true)),
225
+ ])
226
+ )
227
+ );
228
+ updated = true;
229
+ } else {
230
+ const images = propertyValue(configObject, 'images');
231
+ if (images?.type === 'ObjectExpression' && !hasProperty(images, 'unoptimized')) {
232
+ images.properties.push(
233
+ b.objectProperty(b.identifier('unoptimized'), b.booleanLiteral(true))
234
+ );
235
+ updated = true;
236
+ }
237
+ }
238
+
239
+ if (!hasProperty(configObject, 'basePath')) {
240
+ if (!hasBasePathDeclaration(ast)) insertBasePathDeclaration(ast);
241
+ configObject.properties.push(
242
+ b.objectProperty(
243
+ b.identifier('basePath'),
244
+ b.logicalExpression('||', b.identifier('basePath'), b.identifier('undefined'))
245
+ )
246
+ );
247
+ configObject.properties.push(
248
+ b.objectProperty(
249
+ b.identifier('assetPrefix'),
250
+ b.conditionalExpression(
251
+ b.identifier('basePath'),
252
+ b.templateLiteral(
253
+ [
254
+ b.templateElement({ raw: '', cooked: '' }, false),
255
+ b.templateElement({ raw: '/', cooked: '/' }, true),
256
+ ],
257
+ [b.identifier('basePath')]
258
+ ),
259
+ b.identifier('undefined')
260
+ )
261
+ )
262
+ );
263
+ updated = true;
264
+ }
265
+
266
+ if (!updated) {
267
+ return { file: existing.name, created: false, updated: false, warnings };
268
+ }
269
+
270
+ const code = printSource(ast, original);
271
+ fs.writeFileSync(existing.abs, code, 'utf8');
272
+ return { file: existing.name, created: false, updated: true, warnings };
273
+ }
274
+
275
+ module.exports = {
276
+ ensureStaticExportConfig,
277
+ findNextConfig,
278
+ BASE_PATH_ENV,
279
+ };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { CONFIDENCE } = require('./version.cjs');
4
+ const { inferSection, inferFieldName, buildFieldPath, classifyFieldType, uniquePath } = require('./field-paths.cjs');
5
+ const { classifyHref } = require('./adapters.cjs');
6
+ const { loadFingerprintBoost } = require('./learning.cjs');
7
+ const { appendStyleBindTransforms } = require('./style-candidates.cjs');
8
+
9
+ function recipeBoost(candidate, recipe) {
10
+ if (!recipe) return 0;
11
+ let boost = 0;
12
+ if (recipe.actionRules?.splitActionAndLabel && candidate.operation === 'split-action-contract') boost += 0.03;
13
+ const keywords = recipe.signatures?.keywords || [];
14
+ const hay = `${candidate.tag} ${candidate.value || ''} ${candidate.label || ''} ${candidate.file || ''}`.toLowerCase();
15
+ if (keywords.some((kw) => hay.includes(String(kw).toLowerCase()))) boost += 0.02;
16
+ return Math.min(boost, 0.08);
17
+ }
18
+
19
+ function decideThreshold(confidence, candidate) {
20
+ if (candidate.skip) return 'skip';
21
+ if (confidence >= CONFIDENCE.AUTO) return 'auto';
22
+ if (confidence >= CONFIDENCE.VALIDATE) return 'validate';
23
+ return 'skip';
24
+ }
25
+
26
+ function planTransformations({ profile, analyses, recipe }) {
27
+ const usedPaths = new Set();
28
+ const filePlans = [];
29
+ const skipped = [];
30
+ const explanations = [];
31
+
32
+ for (const analysis of analyses) {
33
+ const transformations = [];
34
+ for (const candidate of analysis.candidates || []) {
35
+ const fingerprintHint = loadFingerprintBoost(candidate.fingerprint);
36
+ if (fingerprintHint.skip) {
37
+ skipped.push({
38
+ file: candidate.file,
39
+ loc: candidate.loc,
40
+ reason: 'fingerprint-deprecated',
41
+ confidence: candidate.confidence || 0,
42
+ kind: candidate.kind,
43
+ });
44
+ continue;
45
+ }
46
+ const confidence = Math.min(
47
+ 0.99,
48
+ (candidate.confidence || 0) + recipeBoost(candidate, recipe) + (fingerprintHint.boost || 0),
49
+ );
50
+ const decision = decideThreshold(confidence, candidate);
51
+ const section = inferSection({
52
+ componentName: candidate.componentName,
53
+ fileName: candidate.file,
54
+ className: candidate.className,
55
+ parentName: candidate.parentName,
56
+ tag: candidate.tag,
57
+ role: candidate.role,
58
+ });
59
+
60
+ let scope = candidate.ownerScope || 'home';
61
+ if (candidate.extra && ['instagram', 'facebook', 'tiktok', 'twitter', 'youtube', 'linkedin', 'pinterest'].includes(candidate.extra.action || candidate.extra.platform)) {
62
+ scope = 'common';
63
+ }
64
+ if (section === 'header' || section === 'footer' || section === 'navigation' || section === 'announcement') {
65
+ scope = 'common';
66
+ }
67
+
68
+ const extra = { ...(candidate.extra || {}) };
69
+ if (candidate.operation === 'split-action-contract') {
70
+ extra.action = extra.action || classifyHref(candidate.value);
71
+ extra.paired = true;
72
+ }
73
+
74
+ let transform = {
75
+ loc: candidate.loc,
76
+ operation: candidate.operation || candidate.kind,
77
+ tag: candidate.tag,
78
+ file: candidate.file,
79
+ confidence,
80
+ decision,
81
+ reason: candidate.reason,
82
+ recipeId: recipe?.name || recipe?.id || null,
83
+ fingerprint: candidate.fingerprint,
84
+ fallback: candidate.value,
85
+ labelFallback: candidate.label,
86
+ fieldType: classifyFieldType(candidate.kind, candidate.value || candidate.label),
87
+ section,
88
+ scope,
89
+ explain: {
90
+ detected: `${candidate.tag} ${candidate.kind}`,
91
+ why: candidate.reason,
92
+ recipe: recipe?.name || null,
93
+ confidence,
94
+ fingerprintBoost: fingerprintHint.boost || 0,
95
+ fingerprintState: fingerprintHint.state || null,
96
+ },
97
+ };
98
+
99
+ if (decision === 'skip' || candidate.skip || candidate.kind === 'already-editable' || candidate.kind === 'decoration') {
100
+ skipped.push({
101
+ file: candidate.file,
102
+ loc: candidate.loc,
103
+ reason: candidate.reason || 'low-confidence',
104
+ confidence,
105
+ kind: candidate.kind,
106
+ });
107
+ continue;
108
+ }
109
+
110
+ if (candidate.operation === 'split-action-contract') {
111
+ const urlName = inferFieldName('url', candidate.tag, candidate.label, extra);
112
+ const labelName = inferFieldName('label', candidate.tag, candidate.label, { ...extra, paired: true });
113
+ const actionSection = sectionForAction(scope, section, extra);
114
+ transform.urlField = buildFieldPath({ scope, section: actionSection, field: urlName, used: usedPaths });
115
+ const sibling = transform.urlField.split('.');
116
+ sibling[sibling.length - 1] = labelName;
117
+ transform.labelField = uniquePath(usedPaths, sibling.join('.'));
118
+ transform.fieldType = 'url';
119
+ transform.labelFieldType = 'text';
120
+ } else if (candidate.operation === 'extract-url') {
121
+ transform.field = buildFieldPath({
122
+ scope,
123
+ section: sectionForAction(scope, section, extra),
124
+ field: inferFieldName('url', candidate.tag, candidate.value, extra),
125
+ used: usedPaths,
126
+ });
127
+ transform.fieldType = 'url';
128
+ } else if (candidate.operation === 'extract-image') {
129
+ transform.field = buildFieldPath({
130
+ scope,
131
+ section,
132
+ field: inferFieldName('image', candidate.tag, extra.alt, extra),
133
+ used: usedPaths,
134
+ });
135
+ transform.fieldType = 'image';
136
+ } else if (candidate.operation === 'extract-alt') {
137
+ transform.field = buildFieldPath({
138
+ scope,
139
+ section,
140
+ field: inferFieldName('alt', candidate.tag, candidate.value, extra),
141
+ used: usedPaths,
142
+ });
143
+ transform.fieldType = 'text';
144
+ } else if (candidate.operation === 'wrap-text-span') {
145
+ transform.field = buildFieldPath({
146
+ scope,
147
+ section,
148
+ field: inferFieldName('text', candidate.tag, candidate.value, extra),
149
+ used: usedPaths,
150
+ });
151
+ transform.fieldType = 'text';
152
+ } else if (candidate.operation === 'extract-placeholder') {
153
+ transform.field = buildFieldPath({
154
+ scope,
155
+ section,
156
+ field: inferFieldName('placeholder', candidate.tag, candidate.value, extra),
157
+ used: usedPaths,
158
+ });
159
+ } else if (candidate.operation === 'collection-conversion') {
160
+ // A collection is named after the developer's own array variable so the
161
+ // merchant sees "products", not "items2".
162
+ transform.listField = uniquePath(usedPaths, `${scope}.${extra.binding || 'items'}`);
163
+ transform.field = undefined;
164
+ transform.fieldType = 'list';
165
+ transform.itemFields = (extra.itemFields || []).map((field) => ({
166
+ key: field.key,
167
+ type: field.role === 'image' ? 'image' : field.role === 'url' ? 'url' : 'text',
168
+ }));
169
+ transform.items = candidate.value.map((item) => item.value);
170
+ transform.itemParam = extra.itemParam;
171
+ transform.indexParam = extra.indexParam;
172
+ if (!transform.itemFields.length || !extra.objectItems) transform.decision = 'skip';
173
+ } else {
174
+ transform.field = buildFieldPath({
175
+ scope,
176
+ section,
177
+ field: inferFieldName(candidate.kind, extra.tag || candidate.tag, candidate.value, extra),
178
+ used: usedPaths,
179
+ });
180
+ }
181
+
182
+ if (transform.decision === 'skip') {
183
+ skipped.push({
184
+ file: candidate.file,
185
+ loc: candidate.loc,
186
+ reason: 'collection-not-safe',
187
+ confidence,
188
+ kind: candidate.kind,
189
+ });
190
+ continue;
191
+ }
192
+
193
+ transformations.push(transform);
194
+ explanations.push(transform.explain);
195
+ }
196
+
197
+ filePlans.push({
198
+ file: analysis.relativeFile,
199
+ alreadyEditable: analysis.alreadyEditable,
200
+ parseError: analysis.reason === 'parse-error' ? analysis.error : null,
201
+ skippedFile: analysis.skipped,
202
+ skipReason: analysis.skipped ? analysis.reason : null,
203
+ transformations,
204
+ originalCode: analysis.code,
205
+ designSnapshot: analysis.designSnapshot,
206
+ });
207
+ }
208
+
209
+ appendStyleBindTransforms(filePlans);
210
+
211
+ return {
212
+ files: filePlans,
213
+ skipped,
214
+ explanations,
215
+ usedPaths: [...usedPaths],
216
+ stats: summarizePlan(filePlans, skipped),
217
+ };
218
+ }
219
+
220
+ function sectionForAction(scope, section, extra) {
221
+ if (scope === 'common' && (extra.social || ['instagram', 'facebook', 'twitter', 'tiktok', 'youtube', 'linkedin'].includes(extra.action))) {
222
+ return 'footer';
223
+ }
224
+ if (extra.action === 'whatsapp' || extra.action === 'phone' || extra.action === 'email') {
225
+ return section || 'contact';
226
+ }
227
+ return section;
228
+ }
229
+
230
+ function summarizePlan(filePlans, skipped) {
231
+ const planned = filePlans.reduce((n, f) => n + f.transformations.length, 0);
232
+ const auto = filePlans.reduce((n, f) => n + f.transformations.filter((t) => t.decision === 'auto').length, 0);
233
+ const validate = filePlans.reduce((n, f) => n + f.transformations.filter((t) => t.decision === 'validate').length, 0);
234
+ const filesAffected = filePlans.filter((f) => f.transformations.length > 0).length;
235
+ const styleBinds = filePlans.reduce(
236
+ (n, f) => n + f.transformations.filter((t) => t.operation === 'style-bind').length,
237
+ 0,
238
+ );
239
+ return {
240
+ planned,
241
+ auto,
242
+ validate,
243
+ skipped: skipped.length,
244
+ filesAffected,
245
+ styleBinds,
246
+ };
247
+ }
248
+
249
+ module.exports = {
250
+ planTransformations,
251
+ decideThreshold,
252
+ };