@deneb-ui/cli 2.0.88 → 2.0.90

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 (55) hide show
  1. package/bin/index.js +31 -5
  2. package/package.json +2 -2
  3. package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
  4. package/src/arc/__tests__/arc.test.cjs +922 -0
  5. package/src/arc/__tests__/canonical.test.cjs +57 -0
  6. package/src/arc/__tests__/classification.test.cjs +54 -0
  7. package/src/arc/__tests__/corpus.test.cjs +144 -0
  8. package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
  9. package/src/arc/__tests__/fuzz.test.cjs +106 -0
  10. package/src/arc/__tests__/interaction.test.cjs +85 -0
  11. package/src/arc/__tests__/visual-regression.test.cjs +99 -0
  12. package/src/arc/acceptance-gates.cjs +119 -0
  13. package/src/arc/canonical-paths.cjs +142 -23
  14. package/src/arc/corpus-verifier.cjs +295 -0
  15. package/src/arc/data-classification.cjs +182 -0
  16. package/src/arc/data-flow.cjs +62 -2
  17. package/src/arc/explain-blocked.cjs +388 -0
  18. package/src/arc/fuzz-engine.cjs +604 -0
  19. package/src/arc/index.cjs +148 -2
  20. package/src/arc/interaction-verifier.cjs +185 -0
  21. package/src/arc/ir-builder.cjs +170 -12
  22. package/src/arc/planner.cjs +16 -1
  23. package/src/arc/residual.cjs +22 -0
  24. package/src/arc/rsc-boundary.cjs +105 -4
  25. package/src/arc/runtime-validator.cjs +65 -0
  26. package/src/arc/semantic.cjs +12 -1
  27. package/src/arc/transformer.cjs +14 -2745
  28. package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
  29. package/src/arc/transforms/collections/list.cjs +474 -0
  30. package/src/arc/transforms/components/child-card.cjs +268 -0
  31. package/src/arc/transforms/components/compound-content.cjs +49 -0
  32. package/src/arc/transforms/components/reusable-component.cjs +173 -0
  33. package/src/arc/transforms/element-transform.cjs +192 -0
  34. package/src/arc/transforms/healing/conditionals.cjs +295 -0
  35. package/src/arc/transforms/healing/sanitizer.cjs +401 -0
  36. package/src/arc/transforms/index.cjs +195 -0
  37. package/src/arc/transforms/primitives/action.cjs +76 -0
  38. package/src/arc/transforms/primitives/text.cjs +65 -0
  39. package/src/arc/transforms/routing/page-key.cjs +84 -0
  40. package/src/arc/transforms/routing/shared-layout.cjs +294 -0
  41. package/src/arc/transforms/runtime/provider.cjs +230 -0
  42. package/src/arc/transforms/transform-context.cjs +118 -0
  43. package/src/arc/types/acceptance.d.ts +50 -0
  44. package/src/arc/types/canonical.d.ts +50 -0
  45. package/src/arc/types/classification.d.ts +42 -0
  46. package/src/arc/types/corpus.d.ts +52 -0
  47. package/src/arc/types/explain-blocked.d.ts +41 -0
  48. package/src/arc/types/fuzz.d.ts +74 -0
  49. package/src/arc/types/interaction.d.ts +43 -0
  50. package/src/arc/types/ir.d.ts +114 -0
  51. package/src/arc/types/rsc.d.ts +39 -0
  52. package/src/arc/types/visual-regression.d.ts +49 -0
  53. package/src/arc/visual-regression.cjs +195 -0
  54. package/src/arc/workspace.cjs +233 -0
  55. package/src/common/template-visual-edit-contract.ts +4 -1
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ parseSource,
5
+ printSource,
6
+ hasDirective,
7
+ ensureImport,
8
+ ensureDefaultImport,
9
+ sanitizeDuplicateBindings,
10
+ } = require('../ast.cjs');
11
+ const {
12
+ hasMetadataExport,
13
+ hasClientDirective,
14
+ needsClientDirective,
15
+ canSafelyInjectClientDirective,
16
+ ensureClientDirective,
17
+ } = require('../rsc-boundary.cjs');
18
+
19
+ // Context & primitives
20
+ const { findElementByLoc } = require('./transform-context.cjs');
21
+ const { applyTransformToElement } = require('./element-transform.cjs');
22
+ const { instrumentChildCardComponent } = require('./components/child-card.cjs');
23
+ const { instrumentReusableComponent } = require('./components/reusable-component.cjs');
24
+ const { applyCollectionTransform } = require('./collections/list.cjs');
25
+ const { instrumentPageKey, inferPageKey } = require('./routing/page-key.cjs');
26
+ const { instrumentLayoutSource } = require('./routing/shared-layout.cjs');
27
+ const {
28
+ resolveSiteDataSpecifier,
29
+ resolveSiteDataRuntimeSpecifier,
30
+ rewriteRecursiveSiteDataContext,
31
+ ensureJsonModule,
32
+ injectSiteDataHook,
33
+ } = require('./runtime/provider.cjs');
34
+ const {
35
+ sanitizeContradictoryMarkers,
36
+ healBroadContainerMarkers,
37
+ healHiddenPreviewMarkers,
38
+ healLegacyProductDetailLinks,
39
+ healSectionOverflowHidden,
40
+ healDecorativeOverlays,
41
+ healMissingSiteDataHooks,
42
+ } = require('./healing/sanitizer.cjs');
43
+ const {
44
+ healEmptyStateConditionals,
45
+ healUnguardedModalConditionals,
46
+ healUnguardedModalConditionalsInSource,
47
+ sanitizeContradictoryMarkersInSource,
48
+ } = require('./healing/conditionals.cjs');
49
+
50
+ function applyFilePlan(filePlan, profile) {
51
+ if (!filePlan.originalCode || !filePlan.transformations.length) {
52
+ return { code: filePlan.originalCode, changed: false };
53
+ }
54
+
55
+ const ast = parseSource(filePlan.originalCode, filePlan.file);
56
+ let isClient = hasDirective(ast, 'use client') || hasClientDirective(ast) || /['"]use client['"]/.test(filePlan.originalCode.slice(0, 400));
57
+ const hasMetadata = hasMetadataExport(ast);
58
+ let applied = 0;
59
+ const failures = [];
60
+
61
+ const supported = new Set([
62
+ 'split-action-contract',
63
+ 'form-submit-action',
64
+ 'extract-url',
65
+ 'extract-image',
66
+ 'extract-picture',
67
+ 'extract-alt',
68
+ 'extract-placeholder',
69
+ 'extract-text',
70
+ 'extract-prop',
71
+ 'wrap-text-span',
72
+ 'collection-conversion',
73
+ 'style-bind',
74
+ 'extract-tailwind-bg',
75
+ 'instrument-child-card',
76
+ 'bind-button-with-icon',
77
+ 'bind-highlighted-heading',
78
+ 'prop-flow-callsite',
79
+ 'instrument-reusable-component',
80
+ ]);
81
+
82
+ const isTypeScript = Boolean(filePlan.file && /\.(tsx|ts)$/.test(filePlan.file));
83
+
84
+ // Collections run first: they rewrite the array declaration and add an index
85
+ // parameter, and later per-element edits must observe that shape.
86
+ const ordered = [...filePlan.transformations].sort(
87
+ (left, right) =>
88
+ Number(right.operation === 'collection-conversion') -
89
+ Number(left.operation === 'collection-conversion')
90
+ );
91
+
92
+ for (const transform of ordered) {
93
+ if (transform.decision === 'skip') continue;
94
+ if (!supported.has(transform.operation)) continue;
95
+
96
+ if (transform.operation === 'instrument-child-card') {
97
+ try {
98
+ if (instrumentChildCardComponent(ast, transform, isTypeScript)) applied++;
99
+ } catch (err) {
100
+ failures.push({ loc: transform.loc, reason: err.message });
101
+ }
102
+ continue;
103
+ }
104
+
105
+ if (transform.operation === 'instrument-reusable-component') {
106
+ try {
107
+ if (instrumentReusableComponent(ast, transform, isTypeScript)) applied++;
108
+ } catch (err) {
109
+ failures.push({ loc: transform.loc, reason: err.message });
110
+ }
111
+ continue;
112
+ }
113
+
114
+ if (transform.operation === 'collection-conversion') {
115
+ try {
116
+ if (applyCollectionTransform(ast, transform, isClient, isTypeScript)) applied++;
117
+ else failures.push({ loc: transform.loc, reason: 'collection-not-bindable' });
118
+ } catch (err) {
119
+ failures.push({ loc: transform.loc, reason: err.message });
120
+ }
121
+ continue;
122
+ }
123
+
124
+ const pathNode = findElementByLoc(ast, transform.loc, transform.componentName || transform.tag);
125
+ if (!pathNode) {
126
+ failures.push({ loc: transform.loc, reason: 'node-not-found' });
127
+ continue;
128
+ }
129
+ try {
130
+ applyTransformToElement(pathNode, transform);
131
+ applied++;
132
+ } catch (err) {
133
+ failures.push({ loc: transform.loc, reason: err.message });
134
+ }
135
+ }
136
+
137
+ if (applied === 0) {
138
+ return { code: filePlan.originalCode, changed: false, applied, failures };
139
+ }
140
+
141
+ if (!isClient && profile?.router === 'next-app' && !hasMetadata && canSafelyInjectClientDirective(ast, filePlan.file, profile)) {
142
+ if (needsClientDirective(ast, filePlan.originalCode)) {
143
+ ensureClientDirective(ast);
144
+ isClient = true;
145
+ }
146
+ }
147
+
148
+ const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
149
+ if (isClient) {
150
+ const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
151
+ injectSiteDataHook(ast);
152
+ ensureImport(ast, runtimeSpecifier, ['useSiteData']);
153
+ } else {
154
+ ensureDefaultImport(ast, siteDataImport, 'siteData');
155
+ }
156
+ sanitizeDuplicateBindings(ast);
157
+
158
+ // Sanitize any conflicting data-preview-static on elements with editable markers
159
+ sanitizeContradictoryMarkers(ast);
160
+ healBroadContainerMarkers(ast);
161
+ healEmptyStateConditionals(ast);
162
+ healHiddenPreviewMarkers(ast);
163
+ healLegacyProductDetailLinks(ast);
164
+ healSectionOverflowHidden(ast);
165
+ healDecorativeOverlays(ast);
166
+
167
+ const code = printSource(ast, filePlan.originalCode);
168
+ return { code, changed: true, applied, failures, usedClientHook: isClient };
169
+ }
170
+
171
+ module.exports = {
172
+ applyFilePlan,
173
+ applyCollectionTransform,
174
+ instrumentLayoutSource,
175
+ instrumentPageKey,
176
+ resolveSiteDataSpecifier,
177
+ resolveSiteDataRuntimeSpecifier,
178
+ rewriteRecursiveSiteDataContext,
179
+ ensureJsonModule,
180
+ inferPageKey,
181
+ sanitizeContradictoryMarkers,
182
+ sanitizeContradictoryMarkersInSource,
183
+ healBroadContainerMarkers,
184
+ healEmptyStateConditionals,
185
+ healHiddenPreviewMarkers,
186
+ healSectionOverflowHidden,
187
+ healDecorativeOverlays,
188
+ healLegacyProductDetailLinks,
189
+ healMissingSiteDataHooks,
190
+ healUnguardedModalConditionals,
191
+ healUnguardedModalConditionalsInSource,
192
+ injectSiteDataHook,
193
+ instrumentChildCardComponent,
194
+ instrumentReusableComponent,
195
+ };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getJsxName,
5
+ hasJsxAttribute,
6
+ jsxPreviewAttr,
7
+ wrapTextInEditableSpan,
8
+ ensureStyleAttrs,
9
+ siteDataBinding,
10
+ b,
11
+ } = require('../../ast.cjs');
12
+ const { ensurePreviewPath } = require('../transform-context.cjs');
13
+ const { replaceTextChildren } = require('./text.cjs');
14
+
15
+ function splitActionChildren(node, labelField, labelFallback) {
16
+ const nextChildren = [];
17
+ let wrapped = false;
18
+ for (const child of node.children || []) {
19
+ if (!child) continue;
20
+ if (child.type === 'JSXElement' && getJsxName(child) === 'span' && !hasJsxAttribute(child, 'data-preview-field-path')) {
21
+ ensurePreviewPath(child, labelField);
22
+ ensureStyleAttrs(child, labelField, 'text');
23
+ replaceTextChildren(child, labelField, labelFallback, 'text');
24
+ nextChildren.push(child);
25
+ wrapped = true;
26
+ } else if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
27
+ const leading = child.value.match(/^\s*/)?.[0] || '';
28
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
29
+ if (leading) nextChildren.push(b.jsxText(leading));
30
+ nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
31
+ if (trailing && trailing !== leading) nextChildren.push(b.jsxText(trailing));
32
+ wrapped = true;
33
+ } else if (
34
+ child.type === 'JSXExpressionContainer' &&
35
+ child.expression &&
36
+ (child.expression.type === 'StringLiteral' || child.expression.type === 'Literal')
37
+ ) {
38
+ nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
39
+ wrapped = true;
40
+ } else {
41
+ nextChildren.push(child);
42
+ }
43
+ }
44
+ if (!wrapped) {
45
+ nextChildren.push(wrapTextInEditableSpan(labelField, labelFallback, 'text'));
46
+ }
47
+ node.children = nextChildren;
48
+ }
49
+
50
+ function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
51
+ const urlParts = urlField.split('.');
52
+ const hiddenUrl = b.jsxElement(
53
+ b.jsxOpeningElement(
54
+ b.jsxIdentifier('span'),
55
+ [
56
+ b.jsxAttribute(b.jsxIdentifier('hidden')),
57
+ b.jsxAttribute(b.jsxIdentifier('aria-hidden'), b.stringLiteral('true')),
58
+ jsxPreviewAttr(urlField),
59
+ ],
60
+ false
61
+ ),
62
+ b.jsxClosingElement(b.jsxIdentifier('span')),
63
+ [b.jsxExpressionContainer(siteDataBinding(urlParts, fallback, 'url'))],
64
+ false
65
+ );
66
+ if (pathNode.parent && Array.isArray(pathNode.parent.node?.children)) {
67
+ pathNode.insertAfter(hiddenUrl);
68
+ } else if (pathNode.node && Array.isArray(pathNode.node.children)) {
69
+ pathNode.node.children = [hiddenUrl, ...(pathNode.node.children || [])];
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ splitActionChildren,
75
+ wrapHiddenUrlSibling,
76
+ };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ siteDataBinding,
5
+ wrapTextInEditableSpan,
6
+ b,
7
+ } = require('../../ast.cjs');
8
+
9
+ function replaceTextChildren(node, fieldPath, fallback, fieldType) {
10
+ const nextChildren = [];
11
+ let replaced = false;
12
+ for (const child of node.children || []) {
13
+ if (!child) continue;
14
+ if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
15
+ nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
16
+ replaced = true;
17
+ } else if (
18
+ child.type === 'JSXExpressionContainer' &&
19
+ child.expression &&
20
+ (child.expression.type === 'StringLiteral' ||
21
+ child.expression.type === 'Literal' ||
22
+ child.expression.type === 'Identifier')
23
+ ) {
24
+ nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
25
+ replaced = true;
26
+ } else {
27
+ nextChildren.push(child);
28
+ }
29
+ }
30
+ if (!replaced) {
31
+ nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
32
+ }
33
+ node.children = nextChildren;
34
+ }
35
+
36
+ /**
37
+ * Replaces literal text children with an editable <span>, leaving the container
38
+ * element and every one of its classes untouched.
39
+ */
40
+ function wrapLiteralTextChildren(node, fieldPath, fallback) {
41
+ const nextChildren = [];
42
+ let wrapped = false;
43
+
44
+ for (const child of node.children || []) {
45
+ if (!child) continue;
46
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
47
+ const leading = child.value.match(/^\s*/)?.[0] || '';
48
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
49
+ if (leading) nextChildren.push(b.jsxText(leading));
50
+ nextChildren.push(wrapTextInEditableSpan(fieldPath, fallback, 'text'));
51
+ if (trailing) nextChildren.push(b.jsxText(trailing));
52
+ wrapped = true;
53
+ continue;
54
+ }
55
+ if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) continue;
56
+ nextChildren.push(child);
57
+ }
58
+
59
+ if (wrapped) node.children = nextChildren;
60
+ }
61
+
62
+ module.exports = {
63
+ replaceTextChildren,
64
+ wrapLiteralTextChildren,
65
+ };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const recast = require('recast');
4
+ const {
5
+ parseSource,
6
+ printSource,
7
+ getJsxName,
8
+ b,
9
+ } = require('../../ast.cjs');
10
+ const { toPosix } = require('../../fs-utils.cjs');
11
+
12
+ function inferPageKey(relativeFile) {
13
+ const posix = toPosix(relativeFile);
14
+ if (/(^|\/)page\.(tsx|jsx|js)$/.test(posix)) {
15
+ const parts = posix.split('/');
16
+ const idx = parts.lastIndexOf('app');
17
+ const segs = parts.slice(idx + 1, -1).filter((s) => !(s.startsWith('(') && s.endsWith(')')));
18
+ if (!segs.length) return 'home';
19
+ return segs.join('_').replace(/[^a-z0-9_-]/gi, '_').toLowerCase();
20
+ }
21
+ return 'home';
22
+ }
23
+
24
+ /**
25
+ * Stamps a route's page component with data-preview-page-key. Fivora requires
26
+ * exactly one per exported route, so the key comes from the scanned route id
27
+ * rather than being guessed from the filename — Pages Router uses
28
+ * `pages/contact.jsx`, App Router uses `app/contact/page.tsx`.
29
+ *
30
+ * `<main>` is preferred, but any project shape is supported by falling back to
31
+ * the outermost element the page component returns.
32
+ */
33
+ function instrumentPageKey(code, relativeFile, pageKey) {
34
+ if (code.includes('data-preview-page-key')) return { code, updated: false };
35
+ const key = pageKey || inferPageKey(relativeFile);
36
+
37
+ let ast;
38
+ try {
39
+ ast = parseSource(code, relativeFile);
40
+ } catch {
41
+ return { code, updated: false };
42
+ }
43
+
44
+ const attribute = () =>
45
+ b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key));
46
+
47
+ let target = null;
48
+ recast.types.visit(ast, {
49
+ visitJSXOpeningElement(pathNode) {
50
+ const name = pathNode.node.name && pathNode.node.name.name;
51
+ if (!target && name === 'main') {
52
+ target = pathNode.node;
53
+ return false;
54
+ }
55
+ this.traverse(pathNode);
56
+ },
57
+ });
58
+
59
+ if (!target) {
60
+ // No <main>: use the first host element of the returned tree so the marker
61
+ // still lands inside this route's exported HTML.
62
+ recast.types.visit(ast, {
63
+ visitJSXElement(pathNode) {
64
+ if (target) return false;
65
+ const name = getJsxName(pathNode.node);
66
+ if (typeof name === 'string' && /^[a-z]/.test(name)) {
67
+ target = pathNode.node.openingElement;
68
+ return false;
69
+ }
70
+ this.traverse(pathNode);
71
+ },
72
+ });
73
+ }
74
+
75
+ if (!target) return { code, updated: false };
76
+ target.attributes = target.attributes || [];
77
+ target.attributes.unshift(attribute());
78
+ return { code: printSource(ast, code), updated: true };
79
+ }
80
+
81
+ module.exports = {
82
+ instrumentPageKey,
83
+ inferPageKey,
84
+ };