@deneb-ui/cli 2.0.87 → 2.0.89
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/bin/index.js +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/tools/deneb-template-validator.cjs +17 -17
package/src/arc/residual.cjs
CHANGED
|
@@ -449,7 +449,29 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
|
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
const RESIDUAL_REASONS = {
|
|
453
|
+
STATIC_INTENTIONAL: 'STATIC_INTENTIONAL',
|
|
454
|
+
STATIC_DECORATIVE: 'STATIC_DECORATIVE',
|
|
455
|
+
PLATFORM_CONTROLLED: 'PLATFORM_CONTROLLED',
|
|
456
|
+
RUNTIME_DATA: 'RUNTIME_DATA',
|
|
457
|
+
UNSUPPORTED_SAFE: 'UNSUPPORTED_SAFE',
|
|
458
|
+
CONVERSION_FAILED: 'CONVERSION_FAILED',
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
function normalizeResidualReason(rawReason) {
|
|
462
|
+
if (!rawReason) return RESIDUAL_REASONS.STATIC_DECORATIVE;
|
|
463
|
+
const lower = String(rawReason).toLowerCase();
|
|
464
|
+
if (lower.includes('fail') || lower.includes('error')) return RESIDUAL_REASONS.CONVERSION_FAILED;
|
|
465
|
+
if (lower.includes('platform') || lower.includes('control')) return RESIDUAL_REASONS.PLATFORM_CONTROLLED;
|
|
466
|
+
if (lower.includes('runtime') || lower.includes('session') || lower.includes('cart')) return RESIDUAL_REASONS.RUNTIME_DATA;
|
|
467
|
+
if (lower.includes('intentional') || lower.includes('brand')) return RESIDUAL_REASONS.STATIC_INTENTIONAL;
|
|
468
|
+
if (lower.includes('unsupported') || lower.includes('complex')) return RESIDUAL_REASONS.UNSUPPORTED_SAFE;
|
|
469
|
+
return RESIDUAL_REASONS.STATIC_DECORATIVE;
|
|
470
|
+
}
|
|
471
|
+
|
|
452
472
|
module.exports = {
|
|
453
473
|
applyResidualPass,
|
|
454
474
|
isMeaningfulVisibleText,
|
|
475
|
+
RESIDUAL_REASONS,
|
|
476
|
+
normalizeResidualReason,
|
|
455
477
|
};
|
package/src/arc/rsc-boundary.cjs
CHANGED
|
@@ -4,8 +4,9 @@ const recast = require('recast');
|
|
|
4
4
|
const b = recast.types.builders;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* RSC (React Server Components) Boundary
|
|
8
|
-
* Ensures proper placement of 'use client' directives
|
|
7
|
+
* Deneb ARC v3 — Conservative RSC (React Server Components) Boundary Optimizer.
|
|
8
|
+
* Ensures proper placement of 'use client' directives, minimal boundary isolation,
|
|
9
|
+
* metadata export protection, and RSC metrics tracking.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -54,6 +55,37 @@ function hasMetadataExport(ast) {
|
|
|
54
55
|
return hasMetadata;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Detects if the AST defines an async component (async function Page).
|
|
60
|
+
* Async components in Next.js are strictly server-side and cannot have 'use client'.
|
|
61
|
+
*/
|
|
62
|
+
function isAsyncServerComponent(ast) {
|
|
63
|
+
let isAsync = false;
|
|
64
|
+
|
|
65
|
+
recast.types.visit(ast, {
|
|
66
|
+
visitExportDefaultDeclaration(pathNode) {
|
|
67
|
+
const decl = pathNode.node.declaration;
|
|
68
|
+
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
69
|
+
if (decl.async) {
|
|
70
|
+
isAsync = true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.traverse(pathNode);
|
|
75
|
+
},
|
|
76
|
+
visitFunctionDeclaration(pathNode) {
|
|
77
|
+
const name = pathNode.node.id?.name;
|
|
78
|
+
if (name && /^(Page|Layout|Template|[A-Z])/.test(name) && pathNode.node.async) {
|
|
79
|
+
isAsync = true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
this.traverse(pathNode);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return isAsync;
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
/**
|
|
58
90
|
* Detects if the AST has a 'use client' directive.
|
|
59
91
|
*/
|
|
@@ -105,7 +137,7 @@ function needsClientDirective(ast, code = '') {
|
|
|
105
137
|
* Determines whether 'use client' can be safely injected into the file.
|
|
106
138
|
* Returns false if:
|
|
107
139
|
* 1. File exports metadata or generateMetadata.
|
|
108
|
-
* 2. File is
|
|
140
|
+
* 2. File is an async server component.
|
|
109
141
|
*/
|
|
110
142
|
function canSafelyInjectClientDirective(ast, relativeFile = '', profile = {}) {
|
|
111
143
|
const isAppRouter = profile.router === 'next-app' || (profile.appDir && relativeFile.startsWith(profile.appDir));
|
|
@@ -113,7 +145,7 @@ function canSafelyInjectClientDirective(ast, relativeFile = '', profile = {}) {
|
|
|
113
145
|
return true;
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
if (hasMetadataExport(ast)) {
|
|
148
|
+
if (hasMetadataExport(ast) || isAsyncServerComponent(ast)) {
|
|
117
149
|
return false;
|
|
118
150
|
}
|
|
119
151
|
|
|
@@ -145,10 +177,79 @@ function ensureClientDirective(ast) {
|
|
|
145
177
|
return true;
|
|
146
178
|
}
|
|
147
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Calculates the smallest client-editable boundary.
|
|
182
|
+
* Prevents turning entire server page trees into client components.
|
|
183
|
+
*/
|
|
184
|
+
function calculateMinimalClientBoundary(componentInfo = {}) {
|
|
185
|
+
if (componentInfo.isAsync || componentInfo.hasMetadata) {
|
|
186
|
+
return 'server';
|
|
187
|
+
}
|
|
188
|
+
if (componentInfo.hasHooks || componentInfo.hasEvents) {
|
|
189
|
+
return 'client';
|
|
190
|
+
}
|
|
191
|
+
return componentInfo.isLeaf ? 'client' : 'server';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Computes RSC metrics across the compilation run.
|
|
196
|
+
*/
|
|
197
|
+
function computeRscMetrics(files = []) {
|
|
198
|
+
let clientBoundariesBefore = 0;
|
|
199
|
+
let clientBoundariesAfter = 0;
|
|
200
|
+
let serverComponentsConverted = 0;
|
|
201
|
+
let serverComponentsPreserved = 0;
|
|
202
|
+
|
|
203
|
+
for (const f of files) {
|
|
204
|
+
if (f.hadClientDirective) clientBoundariesBefore++;
|
|
205
|
+
if (f.hasClientDirective) clientBoundariesAfter++;
|
|
206
|
+
|
|
207
|
+
if (!f.hadClientDirective && f.hasClientDirective) {
|
|
208
|
+
serverComponentsConverted++;
|
|
209
|
+
} else if (!f.hasClientDirective) {
|
|
210
|
+
serverComponentsPreserved++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const total = files.length || 1;
|
|
215
|
+
const minimalBoundarySuccessRate = Number(((serverComponentsPreserved / total) * 100).toFixed(2));
|
|
216
|
+
const conversionExceededThreshold = serverComponentsConverted > 10;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
clientBoundariesBefore,
|
|
220
|
+
clientBoundariesAfter,
|
|
221
|
+
serverComponentsConverted,
|
|
222
|
+
serverComponentsPreserved,
|
|
223
|
+
minimalBoundarySuccessRate,
|
|
224
|
+
conversionExceededThreshold,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validates RSC metrics against threshold limits.
|
|
230
|
+
*/
|
|
231
|
+
function validateRscBoundaryIntegrity(metrics, maxAllowedConversions = 10) {
|
|
232
|
+
if (metrics.serverComponentsConverted > maxAllowedConversions) {
|
|
233
|
+
return {
|
|
234
|
+
valid: false,
|
|
235
|
+
error: `Excessive RSC conversions: ${metrics.serverComponentsConverted} server components converted to client (max allowed: ${maxAllowedConversions})`,
|
|
236
|
+
metrics,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
valid: true,
|
|
241
|
+
metrics,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
148
245
|
module.exports = {
|
|
149
246
|
hasMetadataExport,
|
|
150
247
|
hasClientDirective,
|
|
248
|
+
isAsyncServerComponent,
|
|
151
249
|
needsClientDirective,
|
|
152
250
|
canSafelyInjectClientDirective,
|
|
153
251
|
ensureClientDirective,
|
|
252
|
+
calculateMinimalClientBoundary,
|
|
253
|
+
computeRscMetrics,
|
|
254
|
+
validateRscBoundaryIntegrity,
|
|
154
255
|
};
|
|
@@ -269,6 +269,70 @@ class RuntimeValidator {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function validateCollectionOperations(siteData = {}) {
|
|
273
|
+
const content = siteData.content || siteData;
|
|
274
|
+
const results = [];
|
|
275
|
+
let allPassed = true;
|
|
276
|
+
|
|
277
|
+
function findCollections(obj, prefix = '') {
|
|
278
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
279
|
+
const fullPath = prefix ? `${prefix}.${k}` : k;
|
|
280
|
+
if (Array.isArray(v)) {
|
|
281
|
+
// Test 1: Add Item
|
|
282
|
+
const cloned = JSON.parse(JSON.stringify(v));
|
|
283
|
+
const sampleItem = cloned[0] && typeof cloned[0] === 'object' ? { ...cloned[0], id: 'new-999' } : 'new-item';
|
|
284
|
+
cloned.push(sampleItem);
|
|
285
|
+
const addPassed = cloned.length === v.length + 1;
|
|
286
|
+
|
|
287
|
+
// Test 2: Remove Item
|
|
288
|
+
const removeCloned = JSON.parse(JSON.stringify(v));
|
|
289
|
+
removeCloned.pop();
|
|
290
|
+
const removePassed = removeCloned.length === Math.max(0, v.length - 1);
|
|
291
|
+
|
|
292
|
+
// Test 3: Reorder Items
|
|
293
|
+
const reorderCloned = JSON.parse(JSON.stringify(v));
|
|
294
|
+
reorderCloned.reverse();
|
|
295
|
+
const reorderPassed = reorderCloned.length === v.length;
|
|
296
|
+
|
|
297
|
+
// Test 4: Mutate Item Field
|
|
298
|
+
let mutatePassed = true;
|
|
299
|
+
if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null) {
|
|
300
|
+
const firstKey = Object.keys(v[0])[0];
|
|
301
|
+
if (firstKey) {
|
|
302
|
+
const mutateCloned = JSON.parse(JSON.stringify(v));
|
|
303
|
+
mutateCloned[0][firstKey] = '__MUTATED__';
|
|
304
|
+
mutatePassed = mutateCloned[0][firstKey] === '__MUTATED__';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const colPassed = addPassed && removePassed && reorderPassed && mutatePassed;
|
|
309
|
+
if (!colPassed) allPassed = false;
|
|
310
|
+
|
|
311
|
+
results.push({
|
|
312
|
+
path: fullPath,
|
|
313
|
+
itemCount: v.length,
|
|
314
|
+
operations: {
|
|
315
|
+
addItem: addPassed,
|
|
316
|
+
removeItem: removePassed,
|
|
317
|
+
reorderItems: reorderPassed,
|
|
318
|
+
mutateItemField: mutatePassed,
|
|
319
|
+
},
|
|
320
|
+
passed: colPassed,
|
|
321
|
+
});
|
|
322
|
+
} else if (typeof v === 'object' && v !== null) {
|
|
323
|
+
findCollections(v, fullPath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
findCollections(content);
|
|
329
|
+
return {
|
|
330
|
+
passed: allPassed,
|
|
331
|
+
collections: results,
|
|
332
|
+
totalCollections: results.length,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
272
336
|
function validateRuntimeEditabilitySync(options) {
|
|
273
337
|
const validator = new RuntimeValidator(options);
|
|
274
338
|
return validator.runSync();
|
|
@@ -283,4 +347,5 @@ module.exports = {
|
|
|
283
347
|
RuntimeValidator,
|
|
284
348
|
validateRuntimeEditability,
|
|
285
349
|
validateRuntimeEditabilitySync,
|
|
350
|
+
validateCollectionOperations,
|
|
286
351
|
};
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -530,7 +530,16 @@ function collectLooseText(code) {
|
|
|
530
530
|
return /<(h[1-6]|p|Button|span)[^>]*>\s*[A-Za-z]/.test(code);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
function analyzeFile(
|
|
533
|
+
function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
534
|
+
let code, relativeFile, profile, graph, ownerScope, componentMeta, ir;
|
|
535
|
+
if (typeof optionsOrFile === 'string') {
|
|
536
|
+
relativeFile = optionsOrFile;
|
|
537
|
+
code = codeArg;
|
|
538
|
+
profile = profileArg;
|
|
539
|
+
ir = profile?.ir || null;
|
|
540
|
+
} else {
|
|
541
|
+
({ code, relativeFile, profile, graph, ownerScope, componentMeta, ir = profile?.ir || null } = optionsOrFile || {});
|
|
542
|
+
}
|
|
534
543
|
const adapters = activeAdapters(profile);
|
|
535
544
|
const fileSkip = skipReasonForFile(relativeFile, code);
|
|
536
545
|
if (fileSkip) {
|
|
@@ -1165,6 +1174,8 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
1165
1174
|
alreadyEditable: fileAlreadyEditable(code),
|
|
1166
1175
|
bindings: Object.fromEntries([...bindings.entries()].filter(([, v]) => typeof v === 'string')),
|
|
1167
1176
|
imports,
|
|
1177
|
+
code,
|
|
1178
|
+
relativeFile,
|
|
1168
1179
|
};
|
|
1169
1180
|
}
|
|
1170
1181
|
|